Skip to content

Commit

Permalink
Merge pull request #93 from tiktok/feat-quicker-selection
Browse files Browse the repository at this point in the history
Feat quicker selection
  • Loading branch information
chengcyber authored Oct 24, 2024
2 parents 90cb8f6 + 97adbfc commit c7f55da
Show file tree
Hide file tree
Showing 10 changed files with 629 additions and 74 deletions.
2 changes: 2 additions & 0 deletions apps/sparo-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@rushstack/terminal": "~0.8.1",
"git-repo-info": "~2.1.1",
"inversify": "~6.0.2",
"npm-package-arg": "~6.1.0",
"reflect-metadata": "~0.2.1",
"semver": "~7.6.0",
"update-notifier": "~5.1.0",
Expand All @@ -32,6 +33,7 @@
"@rushstack/heft-node-rig": "2.4.5",
"@types/heft-jest": "1.0.6",
"@types/node": "20.11.16",
"@types/npm-package-arg": "6.1.0",
"@types/semver": "7.5.7",
"@types/update-notifier": "6.0.8",
"@types/yargs": "17.0.32",
Expand Down
25 changes: 18 additions & 7 deletions apps/sparo-lib/src/cli/commands/list-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,24 @@ export class ListProfilesCommand implements ICommand<IListProfilesCommandOptions
.join(' ')} ${Array.from(fromSelectors)
.map((x) => `--from ${x}`)
.join(' ')} `;
const res: { projects: IProject[] } = JSON.parse(childProcess.execSync(rushListCmd).toString());
for (const project of res.projects) {
if (profileProjects.has(project.name)) {
const profiles: string[] | undefined = profileProjects.get(project.name);
profiles?.push(profileName);
} else {
profileProjects.set(project.name, [profileName]);
let res: { projects: IProject[] } | undefined;
const resultString: string = childProcess.execSync(rushListCmd).toString();
const firstOpenBraceIndex: number = resultString.indexOf('{');
try {
res = JSON.parse(resultString.slice(firstOpenBraceIndex));
} catch (e) {
throw new Error(
`Parse json result from "${rushListCmd}" failed.\nError: ${e.message}\nrush returns:\n${resultString}\n`
);
}
if (res) {
for (const project of res.projects) {
if (profileProjects.has(project.name)) {
const profiles: string[] | undefined = profileProjects.get(project.name);
profiles?.push(profileName);
} else {
profileProjects.set(project.name, [profileName]);
}
}
}
}
Expand Down
178 changes: 178 additions & 0 deletions apps/sparo-lib/src/logic/DependencySpecifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// This is copied from rush.js source code
// https://github.com/microsoft/rushstack/blob/312b8bc554e64d66b586c65499a512dbf1c329ff/libraries/rush-lib/src/logic/DependencySpecifier.ts

import npmPackageArg from 'npm-package-arg';
import { InternalError } from '@rushstack/node-core-library';

/**
* match workspace protocol in dependencies value declaration in `package.json`
* example:
* `"workspace:*"`
* `"workspace:alias@1.2.3"`
*/
const WORKSPACE_PREFIX_REGEX: RegExp = /^workspace:((?<alias>[^._/][^@]*)@)?(?<version>.*)$/;

/**
* resolve workspace protocol(from `@pnpm/workspace.spec-parser`).
* used by pnpm. see [pkgs-graph](https://github.com/pnpm/pnpm/blob/27c33f0319f86c45c1645d064cd9c28aada80780/workspace/pkgs-graph/src/index.ts#L49)
*/
class WorkspaceSpec {
public readonly alias?: string;
public readonly version: string;
public readonly versionSpecifier: string;

public constructor(version: string, alias?: string) {
this.version = version;
this.alias = alias;
this.versionSpecifier = alias ? `${alias}@${version}` : version;
}

public static tryParse(pref: string): WorkspaceSpec | undefined {
const parts: RegExpExecArray | null = WORKSPACE_PREFIX_REGEX.exec(pref);
if (parts?.groups) {
return new WorkspaceSpec(parts.groups.version, parts.groups.alias);
}
}

public toString(): `workspace:${string}` {
return `workspace:${this.versionSpecifier}`;
}
}

/**
* The parsed format of a provided version specifier.
*/
export enum DependencySpecifierType {
/**
* A git repository
*/
Git = 'Git',

/**
* A tagged version, e.g. "example@latest"
*/
Tag = 'Tag',

/**
* A specific version number, e.g. "example@1.2.3"
*/
Version = 'Version',

/**
* A version range, e.g. "example@2.x"
*/
Range = 'Range',

/**
* A local .tar.gz, .tar or .tgz file
*/
File = 'File',

/**
* A local directory
*/
Directory = 'Directory',

/**
* An HTTP url to a .tar.gz, .tar or .tgz file
*/
Remote = 'Remote',

/**
* A package alias, e.g. "npm:other-package@^1.2.3"
*/
Alias = 'Alias',

/**
* A package specified using workspace protocol, e.g. "workspace:^1.2.3"
*/
Workspace = 'Workspace'
}

/**
* An NPM "version specifier" is a string that can appear as a package.json "dependencies" value.
* Example version specifiers: `^1.2.3`, `file:./blah.tgz`, `npm:other-package@~1.2.3`, and so forth.
* A "dependency specifier" is the version specifier information, combined with the dependency package name.
*/
export class DependencySpecifier {
/**
* The dependency package name, i.e. the key from a "dependencies" key/value table.
*/
public readonly packageName: string;

/**
* The dependency version specifier, i.e. the value from a "dependencies" key/value table.
* Example values: `^1.2.3`, `file:./blah.tgz`, `npm:other-package@~1.2.3`
*/
public readonly versionSpecifier: string;

/**
* The type of the `versionSpecifier`.
*/
public readonly specifierType: DependencySpecifierType;

/**
* If `specifierType` is `alias`, then this is the parsed target dependency.
* For example, if version specifier i `"npm:other-package@^1.2.3"` then this is the parsed object for
* `other-package@^1.2.3`.
*/
public readonly aliasTarget: DependencySpecifier | undefined;

public constructor(packageName: string, versionSpecifier: string) {
this.packageName = packageName;
this.versionSpecifier = versionSpecifier;

// Workspace ranges are a feature from PNPM and Yarn. Set the version specifier
// to the trimmed version range.
const workspaceSpecResult: WorkspaceSpec | undefined = WorkspaceSpec.tryParse(versionSpecifier);
if (workspaceSpecResult) {
this.specifierType = DependencySpecifierType.Workspace;
this.versionSpecifier = workspaceSpecResult.versionSpecifier;

if (workspaceSpecResult.alias) {
// "workspace:some-package@^1.2.3" should be resolved as alias
this.aliasTarget = new DependencySpecifier(workspaceSpecResult.alias, workspaceSpecResult.version);
} else {
this.aliasTarget = undefined;
}

return;
}

const result: npmPackageArg.Result = npmPackageArg.resolve(packageName, versionSpecifier);
this.specifierType = DependencySpecifier.getDependencySpecifierType(result.type);

if (this.specifierType === DependencySpecifierType.Alias) {
const aliasResult: npmPackageArg.AliasResult = result as npmPackageArg.AliasResult;
if (!aliasResult.subSpec || !aliasResult.subSpec.name) {
throw new InternalError('Unexpected result from npm-package-arg');
}
this.aliasTarget = new DependencySpecifier(aliasResult.subSpec.name, aliasResult.subSpec.rawSpec);
} else {
this.aliasTarget = undefined;
}
}

public static getDependencySpecifierType(specifierType: string): DependencySpecifierType {
switch (specifierType) {
case 'git':
return DependencySpecifierType.Git;
case 'tag':
return DependencySpecifierType.Tag;
case 'version':
return DependencySpecifierType.Version;
case 'range':
return DependencySpecifierType.Range;
case 'file':
return DependencySpecifierType.File;
case 'directory':
return DependencySpecifierType.Directory;
case 'remote':
return DependencySpecifierType.Remote;
case 'alias':
return DependencySpecifierType.Alias;
default:
throw new InternalError(`Unexpected npm-package-arg result type "${specifierType}"`);
}
}
}
117 changes: 117 additions & 0 deletions apps/sparo-lib/src/logic/RushProjectSlim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as path from 'path';
import * as semver from 'semver';
import { JsonFile } from '@rushstack/node-core-library';
import { DependencySpecifier, DependencySpecifierType } from './DependencySpecifier';

export interface IProjectJson {
packageName: string;
projectFolder: string;
decoupledLocalDependencies?: string[];
cyclicDependencyProjects?: string[];
}

/**
* A slim version of RushConfigurationProject
*/
export class RushProjectSlim {
public packageName: string;
public projectFolder: string;
public relativeProjectFolder: string;
public packageJson: {
name: string;
version: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
public decoupledLocalDependencies: Set<string>;

private _packageNameToRushProjectSlim: Map<string, RushProjectSlim>;
private _dependencyProjects: Set<RushProjectSlim> | undefined;
private _consumingProjects: Set<RushProjectSlim> | undefined;

public constructor(
projectJson: IProjectJson,
repoRootPath: string,
packageNameToRushProjectSlim: Map<string, RushProjectSlim>
) {
this.packageName = projectJson.packageName;
this.projectFolder = path.resolve(repoRootPath, projectJson.projectFolder);
this.relativeProjectFolder = projectJson.projectFolder;
const packageJsonPath: string = path.resolve(this.projectFolder, 'package.json');
this.packageJson = JsonFile.load(packageJsonPath);
this._packageNameToRushProjectSlim = packageNameToRushProjectSlim;

this.decoupledLocalDependencies = new Set<string>();
if (projectJson.cyclicDependencyProjects || projectJson.decoupledLocalDependencies) {
if (projectJson.cyclicDependencyProjects && projectJson.decoupledLocalDependencies) {
throw new Error(
'A project configuration cannot specify both "decoupledLocalDependencies" and "cyclicDependencyProjects". Please use "decoupledLocalDependencies" only -- the other name is deprecated.'
);
}
for (const cyclicDependencyProject of projectJson.cyclicDependencyProjects ||
projectJson.decoupledLocalDependencies ||
[]) {
this.decoupledLocalDependencies.add(cyclicDependencyProject);
}
}
}

public get dependencyProjects(): ReadonlySet<RushProjectSlim> {
if (this._dependencyProjects) {
return this._dependencyProjects;
}
const dependencyProjects: Set<RushProjectSlim> = new Set<RushProjectSlim>();
const { packageJson } = this;
for (const dependencySet of [
packageJson.dependencies,
packageJson.devDependencies,
packageJson.optionalDependencies
]) {
if (dependencySet) {
for (const [dependency, version] of Object.entries(dependencySet)) {
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependency, version);
const dependencyName: string =
dependencySpecifier.aliasTarget?.packageName ?? dependencySpecifier.packageName;
// Skip if we can't find the local project or it's a cyclic dependency
const localProject: RushProjectSlim | undefined =
this._packageNameToRushProjectSlim.get(dependencyName);
if (localProject && !this.decoupledLocalDependencies.has(dependency)) {
// Set the value if it's a workspace project, or if we have a local project and the semver is satisfied
switch (dependencySpecifier.specifierType) {
case DependencySpecifierType.Version:
case DependencySpecifierType.Range:
if (
semver.satisfies(localProject.packageJson.version, dependencySpecifier.versionSpecifier)
) {
dependencyProjects.add(localProject);
}
break;
case DependencySpecifierType.Workspace:
dependencyProjects.add(localProject);
break;
}
}
}
}
}
this._dependencyProjects = dependencyProjects;
return this._dependencyProjects;
}

public get consumingProjects(): ReadonlySet<RushProjectSlim> {
if (!this._consumingProjects) {
// Force initialize all dependencies relationship
for (const project of this._packageNameToRushProjectSlim.values()) {
project._consumingProjects = new Set();
}

for (const project of this._packageNameToRushProjectSlim.values()) {
for (const dependency of project.dependencyProjects) {
dependency._consumingProjects!.add(project);
}
}
}
return this._consumingProjects!;
}
}
Loading

0 comments on commit c7f55da

Please sign in to comment.