Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Deduplicate parsing of dependency specifiers.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
5 changes: 4 additions & 1 deletion libraries/rush-lib/src/api/RushConfigurationProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,10 @@ export class RushConfigurationProject {
]) {
if (dependencySet) {
for (const [dependency, version] of Object.entries(dependencySet)) {
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependency, version);
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
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
Expand Down
2 changes: 1 addition & 1 deletion libraries/rush-lib/src/logic/ApprovedPackagesChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ApprovedPackagesChecker {
// "dependencies": {
// "alias-name": "npm:target-name@^1.2.3"
// }
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
packageName,
dependencies[packageName]
);
Expand Down
35 changes: 33 additions & 2 deletions libraries/rush-lib/src/logic/DependencySpecifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export enum DependencySpecifierType {
Workspace = 'Workspace'
}

const dependencySpecifierParseCache: Map<string, DependencySpecifier> = new Map();

/**
* 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.
Expand Down Expand Up @@ -131,7 +133,10 @@ export class DependencySpecifier {

if (workspaceSpecResult.alias) {
// "workspace:some-package@^1.2.3" should be resolved as alias
this.aliasTarget = new DependencySpecifier(workspaceSpecResult.alias, workspaceSpecResult.version);
this.aliasTarget = DependencySpecifier.parseWithCache(
workspaceSpecResult.alias,
workspaceSpecResult.version
);
} else {
this.aliasTarget = undefined;
}
Expand All @@ -147,12 +152,38 @@ export class DependencySpecifier {
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);
this.aliasTarget = DependencySpecifier.parseWithCache(
aliasResult.subSpec.name,
aliasResult.subSpec.rawSpec
);
} else {
this.aliasTarget = undefined;
}
}

/**
* Clears the dependency specifier parse cache.
*/
public static clearCache(): void {
dependencySpecifierParseCache.clear();
}

/**
* Parses a dependency specifier with caching.
* @param packageName - The name of the package the version specifier corresponds to
* @param versionSpecifier - The version specifier to parse
* @returns The parsed dependency specifier
*/
public static parseWithCache(packageName: string, versionSpecifier: string): DependencySpecifier {
const cacheKey: string = `${packageName}\0${versionSpecifier}`;
let result: DependencySpecifier | undefined = dependencySpecifierParseCache.get(cacheKey);
if (!result) {
result = new DependencySpecifier(packageName, versionSpecifier);
dependencySpecifierParseCache.set(cacheKey, result);
}
return result;
}

public static getDependencySpecifierType(specifierType: string): DependencySpecifierType {
switch (specifierType) {
case 'git':
Expand Down
10 changes: 5 additions & 5 deletions libraries/rush-lib/src/logic/PublishUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export class PublishUtilities {
dependencyName: string,
newProjectVersion: string
): string {
const currentDependencySpecifier: DependencySpecifier = new DependencySpecifier(
const currentDependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependencyName,
dependencies[dependencyName]
);
Expand Down Expand Up @@ -502,7 +502,7 @@ export class PublishUtilities {
// TODO: treat prerelease version the same as non-prerelease version.
// For prerelease, the newVersion needs to be appended with prerelease name.
// And dependency should specify the specific prerelease version.
const currentSpecifier: DependencySpecifier = new DependencySpecifier(
const currentSpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
depName,
dependencies[depName]
);
Expand Down Expand Up @@ -765,7 +765,7 @@ export class PublishUtilities {
dependencies[change.packageName] &&
!PublishUtilities._isCyclicDependency(allPackages, parentPackageName, change.packageName)
) {
const requiredVersion: DependencySpecifier = new DependencySpecifier(
const requiredVersion: DependencySpecifier = DependencySpecifier.parseWithCache(
change.packageName,
dependencies[change.packageName]
);
Expand Down Expand Up @@ -863,7 +863,7 @@ export class PublishUtilities {
// "*", "~", and "^" are special cases for workspace ranges, since it will publish using the exact
// version of the local dependency, so we need to modify what we write for our change
// comment
const currentDependencySpecifier: DependencySpecifier = new DependencySpecifier(
const currentDependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependencyName,
currentDependencyVersion
);
Expand All @@ -873,7 +873,7 @@ export class PublishUtilities {
? undefined
: currentDependencySpecifier.versionSpecifier;

const newDependencySpecifier: DependencySpecifier = new DependencySpecifier(
const newDependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependencyName,
newDependencyVersion
);
Expand Down
2 changes: 1 addition & 1 deletion libraries/rush-lib/src/logic/VersionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export class VersionManager {
oldDependencyVersion: string,
newDependencyVersion: string
): void {
const oldSpecifier: DependencySpecifier = new DependencySpecifier(
const oldSpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
updatedDependentProject.name,
oldDependencyVersion
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ export class RushInstallManager extends BaseInstallManager {
if (shrinkwrapFile) {
// Check any (explicitly) preferred dependencies first
allExplicitPreferredVersions.forEach((version: string, dependency: string) => {
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependency, version);
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependency,
version
);

if (!shrinkwrapFile.hasCompatibleTopLevelDependency(dependencySpecifier)) {
shrinkwrapWarnings.push(
Expand Down Expand Up @@ -230,7 +233,10 @@ export class RushInstallManager extends BaseInstallManager {
Sort.sortMapKeys(tempDependencies);

for (const [packageName, packageVersion] of tempDependencies.entries()) {
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(packageName, packageVersion);
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
packageName,
packageVersion
);

// Is there a locally built Rush project that could satisfy this dependency?
// If so, then we will symlink to the project folder rather than to common/temp/node_modules.
Expand Down Expand Up @@ -391,7 +397,10 @@ export class RushInstallManager extends BaseInstallManager {
}

private _revertWorkspaceNotation(dependency: PackageJsonDependency): boolean {
const specifier: DependencySpecifier = new DependencySpecifier(dependency.name, dependency.version);
const specifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependency.name,
dependency.version
);
if (specifier.specifierType !== DependencySpecifierType.Workspace) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export class WorkspaceInstallManager extends BaseInstallManager {
continue;
}

const dependencySpecifier: DependencySpecifier = new DependencySpecifier(name, version);
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(name, version);

// Is there a locally built Rush project that could satisfy this dependency?
let referencedLocalProject: RushConfigurationProject | undefined =
Expand Down
4 changes: 2 additions & 2 deletions libraries/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile {
return undefined;
}

return new DependencySpecifier(dependencyName, dependencyJson.version);
return DependencySpecifier.parseWithCache(dependencyName, dependencyJson.version);
}

/**
Expand Down Expand Up @@ -121,7 +121,7 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile {
return this.getTopLevelDependencyVersion(dependencySpecifier.packageName);
}

return new DependencySpecifier(dependencySpecifier.packageName, dependencyJson.version);
return DependencySpecifier.parseWithCache(dependencySpecifier.packageName, dependencyJson.version);
}

/** @override */
Expand Down
24 changes: 15 additions & 9 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function parsePnpm9DependencyKey(

// Example: 7.26.0
if (semver.valid(key)) {
return new DependencySpecifier(dependencyName, key);
return DependencySpecifier.parseWithCache(dependencyName, key);
}
}

Expand All @@ -169,16 +169,16 @@ export function parsePnpm9DependencyKey(
// Example: https://github.com/jonschlinkert/pad-left/tarball/2.1.0
// Example: https://codeload.github.com/jonschlinkert/pad-left/tar.gz/7798d648225aa5d879660a37c408ab4675b65ac7
if (/^https?:/.test(version)) {
return new DependencySpecifier(name, version);
return DependencySpecifier.parseWithCache(name, version);
}

// Is it an alias for a different package?
if (name === dependencyName) {
// No, it's a regular dependency
return new DependencySpecifier(name, version);
return DependencySpecifier.parseWithCache(name, version);
} else {
// If the parsed package name is different from the dependencyName, then this is an NPM package alias
return new DependencySpecifier(dependencyName, `npm:${name}@${version}`);
return DependencySpecifier.parseWithCache(dependencyName, `npm:${name}@${version}`);
}
}

Expand Down Expand Up @@ -266,7 +266,10 @@ export function parsePnpmDependencyKey(
// git@bitbucket.com+abc/def/188ed64efd5218beda276e02f2277bf3a6b745b2
// bitbucket.co.in/abc/def/188ed64efd5218beda276e02f2277bf3a6b745b2
if (urlRegex.test(dependencyKey)) {
const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependencyName, dependencyKey);
const dependencySpecifier: DependencySpecifier = DependencySpecifier.parseWithCache(
dependencyName,
dependencyKey
);
return dependencySpecifier;
} else {
return undefined;
Expand All @@ -276,10 +279,13 @@ export function parsePnpmDependencyKey(
// Is it an alias for a different package?
if (parsedPackageName === dependencyName) {
// No, it's a regular dependency
return new DependencySpecifier(parsedPackageName, parsedVersionPart);
return DependencySpecifier.parseWithCache(parsedPackageName, parsedVersionPart);
} else {
// If the parsed package name is different from the dependencyName, then this is an NPM package alias
return new DependencySpecifier(dependencyName, `npm:${parsedPackageName}@${parsedVersionPart}`);
return DependencySpecifier.parseWithCache(
dependencyName,
`npm:${parsedPackageName}@${parsedVersionPart}`
);
}
}

Expand Down Expand Up @@ -673,7 +679,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {

const dependency: IPnpmShrinkwrapDependencyYaml | undefined = this.packages.get(value);
if (dependency?.resolution?.tarball && value.startsWith(dependency.resolution.tarball)) {
return new DependencySpecifier(dependencyName, dependency.resolution.tarball);
return DependencySpecifier.parseWithCache(dependencyName, dependency.resolution.tarball);
}

if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9) {
Expand All @@ -690,7 +696,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
}
}

return new DependencySpecifier(dependencyName, value);
return DependencySpecifier.parseWithCache(dependencyName, value);
}
return undefined;
}
Expand Down
23 changes: 23 additions & 0 deletions libraries/rush-lib/src/logic/test/DependencySpecifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import { DependencySpecifier } from '../DependencySpecifier';

describe(DependencySpecifier.name, () => {
afterEach(() => {
DependencySpecifier.clearCache();
});

it('parses a simple version', () => {
const specifier = new DependencySpecifier('dep', '1.2.3');
expect(specifier).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -135,4 +139,23 @@ DependencySpecifier {
`);
});
});

describe(DependencySpecifier.parseWithCache.name, () => {
it('returns a cached instance for the same input', () => {
const specifier1 = DependencySpecifier.parseWithCache('dep', '1.2.3');
const specifier2 = DependencySpecifier.parseWithCache('dep', '1.2.3');
expect(specifier1).toBe(specifier2);
});
it('returns a cached instance for the same alias', () => {
const specifier1 = DependencySpecifier.parseWithCache('dep1', 'npm:dep@1.2.3');
const specifier2 = DependencySpecifier.parseWithCache('dep2', 'npm:dep@1.2.3');
expect(specifier1.aliasTarget).toBe(specifier2.aliasTarget);
});

it('returns different instances for different inputs', () => {
const specifier1 = DependencySpecifier.parseWithCache('dep', '1.2.3');
const specifier2 = DependencySpecifier.parseWithCache('dep', '1.2.4');
expect(specifier1).not.toBe(specifier2);
});
});
});
Loading