Skip to content

Commit 153cf60

Browse files
authored
Merge pull request #286 from snyk/fix/handle-npm-aliases-in-overrides-field
fix: handle npm aliases in overrides field
2 parents b7115b3 + db50e7d commit 153cf60

File tree

5 files changed

+1134
-23
lines changed

5 files changed

+1134
-23
lines changed

lib/aliasesPreprocessors/pkgJson.ts

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
1-
import { PackageJsonBase } from '../dep-graph-builders/types';
1+
import { Overrides, PackageJsonBase } from '../dep-graph-builders/types';
22
import { parsePkgJson } from '../dep-graph-builders/util';
33

4+
/**
5+
* Parses a npm alias string (e.g., "npm:[email protected]") and returns the package name and version
6+
*/
7+
export const parseNpmAlias = (
8+
aliasString: string,
9+
): { packageName: string; version: string } | null => {
10+
if (!aliasString.startsWith('npm:')) {
11+
return null;
12+
}
13+
const lastAtIndex = aliasString.lastIndexOf('@');
14+
if (lastAtIndex <= 4) {
15+
// Invalid format: must have content after 'npm:' and before '@'
16+
return null;
17+
}
18+
return {
19+
packageName: aliasString.substring(4, lastAtIndex),
20+
version: aliasString.substring(lastAtIndex + 1),
21+
};
22+
};
23+
24+
/**
25+
* Adds an alias entry to the package.json aliases field
26+
*/
27+
const addAlias = (
28+
pkgJson: PackageJsonBase,
29+
aliasName: string,
30+
targetDepName: string,
31+
semver: string,
32+
): void => {
33+
if (!pkgJson['aliases']) {
34+
pkgJson['aliases'] = {};
35+
}
36+
pkgJson['aliases'][aliasName] = {
37+
aliasName,
38+
aliasTargetDepName: targetDepName,
39+
semver,
40+
version: null,
41+
};
42+
};
43+
444
export const rewriteAliasesPkgJson = (packageJsonContent: string): string => {
545
const pkgJsonPreprocessed = parsePkgJson(packageJsonContent);
646
pkgJsonPreprocessed.dependencies = rewriteAliases(
@@ -19,6 +59,13 @@ export const rewriteAliasesPkgJson = (packageJsonContent: string): string => {
1959
pkgJsonPreprocessed,
2060
pkgJsonPreprocessed.peerDependencies,
2161
);
62+
// Process overrides field to extract aliases
63+
if (pkgJsonPreprocessed.overrides) {
64+
rewriteAliasesInOverrides(
65+
pkgJsonPreprocessed,
66+
pkgJsonPreprocessed.overrides,
67+
);
68+
}
2269
return JSON.stringify(pkgJsonPreprocessed);
2370
};
2471

@@ -32,23 +79,38 @@ export const rewriteAliases = (
3279
const newDependencies: Record<string, string> = {};
3380
for (const key in dependencies) {
3481
const value = dependencies[key];
35-
if (value.startsWith('npm:')) {
36-
if (!pkgJsonPreprocessed['aliases']) {
37-
pkgJsonPreprocessed['aliases'] = {};
38-
}
39-
pkgJsonPreprocessed['aliases'] = {
40-
...pkgJsonPreprocessed['aliases'],
41-
...{
42-
[key]: {
43-
aliasName: key,
44-
aliasTargetDepName: value.substring(4, value.lastIndexOf('@')),
45-
semver: value.substring(value.lastIndexOf('@') + 1, value.length),
46-
version: null,
47-
},
48-
},
49-
};
82+
const parsed = parseNpmAlias(value);
83+
if (parsed) {
84+
addAlias(pkgJsonPreprocessed, key, parsed.packageName, parsed.version);
5085
}
5186
newDependencies[key] = value;
5287
}
5388
return newDependencies;
5489
};
90+
91+
/**
92+
* Recursively processes the overrides object to extract aliases
93+
*/
94+
export const rewriteAliasesInOverrides = (
95+
pkgJsonPreprocessed: PackageJsonBase,
96+
overrides: Overrides,
97+
): void => {
98+
if (typeof overrides === 'string') {
99+
return; // String values are handled at the parent level where we have the key
100+
}
101+
102+
// Recursive case: process each key-value pair in the overrides object
103+
for (const key in overrides) {
104+
const value = overrides[key];
105+
106+
if (typeof value === 'string') {
107+
const parsed = parseNpmAlias(value);
108+
if (parsed) {
109+
addAlias(pkgJsonPreprocessed, key, parsed.packageName, parsed.version);
110+
}
111+
} else if (typeof value === 'object') {
112+
// Recursively process nested overrides
113+
rewriteAliasesInOverrides(pkgJsonPreprocessed, value);
114+
}
115+
}
116+
};

lib/dep-graph-builders/npm-lock-v2/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../util';
1717
import { OutOfSyncError } from '../../errors';
1818
import { LockfileType } from '../../parsers';
19+
import { parseNpmAlias } from '../../aliasesPreprocessors/pkgJson';
1920

2021
import * as semver from 'semver';
2122
import * as micromatch from 'micromatch';
@@ -224,21 +225,33 @@ const getChildNode = (
224225
pruneNpmStrictOutOfSync?: boolean,
225226
) => {
226227
let version = depInfo.version;
228+
let aliasInfo = depInfo.alias;
227229

228230
const override =
229231
overrides &&
230232
checkOverrides([...ancestry, { name, version }] as Ancestry[], overrides);
231233

232234
if (override) {
233235
version = override;
234-
}
235236

236-
if (version.startsWith('npm:')) {
237+
// If the override is an alias (starts with npm:), extract alias information
238+
const parsed = parseNpmAlias(version);
239+
if (parsed) {
240+
aliasInfo = {
241+
aliasName: name,
242+
aliasTargetDepName: parsed.packageName,
243+
semver: parsed.version,
244+
version: parsed.version,
245+
};
246+
version = parsed.version;
247+
}
248+
} else if (version.startsWith('npm:')) {
249+
// Handle non-override aliases
237250
version = version.split('@').pop() || version;
238251
}
239252

240253
let childNodeKey = getChildNodeKey(
241-
depInfo.alias ? depInfo.alias.aliasName : name,
254+
aliasInfo ? aliasInfo.aliasName : name,
242255
version,
243256
ancestry,
244257
pkgs,
@@ -329,7 +342,7 @@ const getChildNode = (
329342

330343
return {
331344
id: `${name}@${depData.version}`,
332-
name: depInfo.alias?.aliasTargetDepName ?? name,
345+
name: aliasInfo?.aliasTargetDepName ?? name,
333346
version: depData.version,
334347
dependencies: {
335348
...dependencies,
@@ -339,9 +352,7 @@ const getChildNode = (
339352
isDev: depInfo.isDev,
340353
inBundle: depData.inBundle,
341354
key: childNodeKey,
342-
...(depInfo.alias
343-
? { alias: { ...depInfo.alias, version: depData.version } }
344-
: {}),
355+
...(aliasInfo ? { alias: { ...aliasInfo, version: depData.version } } : {}),
345356
};
346357
};
347358

test/jest/dep-graph-builders/aliases.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,44 @@ describe('Testing aliases for npm', () => {
525525
expect(JSON.stringify(newDepGraph)).toContain('[email protected]');
526526
expect(JSON.stringify(newDepGraph)).toContain('[email protected]');
527527
});
528+
it('match aliased package in overrides - npm-lock-v2', async () => {
529+
const pkgJsonContent = readFileSync(
530+
join(
531+
__dirname,
532+
`./fixtures/npm-lock-v2/override-with-alias/package.json`,
533+
),
534+
'utf8',
535+
);
536+
const pkgLockContent = readFileSync(
537+
join(
538+
__dirname,
539+
`./fixtures/npm-lock-v2/override-with-alias/package-lock.json`,
540+
),
541+
'utf8',
542+
);
543+
544+
const newDepGraph = await parseNpmLockV2Project(
545+
pkgJsonContent,
546+
pkgLockContent,
547+
{
548+
includeDevDeps: true,
549+
includeOptionalDeps: true,
550+
pruneCycles: true,
551+
strictOutOfSync: true,
552+
honorAliases: true,
553+
},
554+
);
555+
556+
expect(newDepGraph).toBeDefined;
557+
expect(() => JSON.parse(JSON.stringify(newDepGraph))).not.toThrow();
558+
559+
// elliptic should be aliased to dry-uninstall
560+
const depGraphJson = JSON.stringify(newDepGraph);
561+
expect(depGraphJson).toContain('dry-uninstall');
562+
563+
// Verify the alias metadata is present
564+
expect(depGraphJson).toContain('"alias":"elliptic=>[email protected]"');
565+
});
528566
});
529567

530568
describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])(

0 commit comments

Comments
 (0)