Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat quicker selection #93

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:[email protected]"`
*/
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. "[email protected]"
*/
Version = 'Version',

/**
* A version range, e.g. "[email protected]"
*/
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
Loading