Skip to content
Open
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
Expand Up @@ -326,6 +326,143 @@ some-other-external-package@^4.0.1:
tempFs.cleanup();
});

describe('pnpm snapshot dependency changes', () => {
const lockfile = (dependencies: string) => `lockfileVersion: '9.0'
importers:
.: {}
packages:
host@1.0.0:
resolution: {integrity: sha512-host}
dep@1.0.0:
resolution: {integrity: sha512-one}
dep@2.0.0:
resolution: {integrity: sha512-two}
snapshots:
host@1.0.0:
${dependencies}
dep@1.0.0: {}
dep@2.0.0: {}
`;

beforeEach(() => {
graph.externalNodes = {
'npm:host': {
type: 'npm',
name: 'npm:host',
data: { packageName: 'host', version: '1.0.0' },
},
'npm:dep@1.0.0': {
type: 'npm',
name: 'npm:dep@1.0.0',
data: { packageName: 'dep', version: '1.0.0' },
},
'npm:dep@2.0.0': {
type: 'npm',
name: 'npm:dep@2.0.0',
data: { packageName: 'dep', version: '2.0.0' },
},
};
});

const touched = (base: string, head: string, file = 'pnpm-lock.yaml') =>
getTouchedProjectsFromLockFile(
[{ file, getChanges: () => [new LockFileChange(base, head)] }],
graph.nodes,
autoNxJson,
undefined,
graph
);

it.each(['pnpm-lock.yaml', 'pnpm-lock.yml'])(
'detects redirects between existing versions in %s',
(file) => {
expect(
touched(
lockfile(' dependencies:\n dep: 1.0.0'),
lockfile(' dependencies:\n dep: 2.0.0'),
file
)
).toEqual(['npm:host']);
}
);

it('detects optional dependency redirects', () => {
expect(
touched(
lockfile(' optionalDependencies:\n dep: 1.0.0'),
lockfile(' optionalDependencies:\n dep: 2.0.0')
)
).toEqual(['npm:host']);
});

it.each([false, true])(
'detects dependency additions and removals (reverse: %s)',
(reverse) => {
const before = lockfile(' dependencies: {}');
const after = lockfile(' dependencies:\n dep: 1.0.0');
expect(
touched(reverse ? after : before, reverse ? before : after)
).toEqual(['npm:host']);
}
);

it('ignores dependency key ordering', () => {
expect(
touched(
lockfile(
' dependencies:\n first: dep@1.0.0\n second: dep@2.0.0'
),
lockfile(
' dependencies:\n second: dep@2.0.0\n first: dep@1.0.0'
)
)
).toEqual([]);
});

it('keeps dependency edges associated with their peer context', () => {
const base = lockfile(' dependencies:\n dep: 1.0.0').replace(
'snapshots:\n host@1.0.0:\n dependencies:\n dep: 1.0.0',
'snapshots:\n host@1.0.0(dep@1.0.0):\n dependencies:\n dep: 1.0.0\n host@1.0.0(dep@2.0.0):\n dependencies:\n dep: 2.0.0'
);
const head = base
.replace(' dep: 1.0.0', ' dep: temporary')
.replace(' dep: 2.0.0', ' dep: 1.0.0')
.replace(' dep: temporary', ' dep: 2.0.0');
expect(touched(base, head)).toEqual(['npm:host']);
});

it('detects dependency redirects in v6 package snapshots', () => {
const base = `lockfileVersion: '6.0'
importers:
.: {}
packages:
/host@1.0.0:
resolution: {integrity: sha512-host}
dependencies:
dep: 1.0.0
/dep@1.0.0:
resolution: {integrity: sha512-one}
/dep@2.0.0:
resolution: {integrity: sha512-two}
`;
expect(
touched(base, base.replace(' dep: 1.0.0', ' dep: 2.0.0'))
).toEqual(['npm:host']);
});

it('detects redirects alongside an unrelated integrity change', () => {
expect(
touched(
lockfile(' dependencies:\n dep: 1.0.0'),
lockfile(' dependencies:\n dep: 2.0.0').replace(
'sha512-one',
'sha512-changed'
)
).sort()
).toEqual(['npm:dep@1.0.0', 'npm:dep@2.0.0', 'npm:host']);
});
});

AUTO_CASES.forEach(({ lockFile, base, head }) => {
describe(`"${lockFile}"`, () => {
it('should not return changes when the lock file is untouched', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getPnpmLockfileNodes } from '../../lock-file/pnpm-parser';
import { parseAndNormalizePnpmLockfile } from '../../lock-file/utils/pnpm-normalizer';
import { sortObjectByKeys } from '../../../../utils/object-sort';
import { TouchedProjectLocator } from '../../../../project-graph/affected/affected-project-graph-models';
import {
FileChange,
Expand Down Expand Up @@ -134,21 +137,15 @@ function getChangedPackageNames(
// the iteration keeps the contract open in case multiple ranges are ever
// emitted for the same file.
for (const change of changes) {
const baseFingerprints = collectPackageFingerprints(
getLockFileNodesForName(
file,
change.baseContent,
hashArray([change.baseContent]),
packageJson
).nodes
const baseFingerprints = getPackageFingerprints(
file,
change.baseContent,
packageJson
);
const headFingerprints = collectPackageFingerprints(
getLockFileNodesForName(
file,
change.headContent,
hashArray([change.headContent]),
packageJson
).nodes
const headFingerprints = getPackageFingerprints(
file,
change.headContent,
packageJson
);

for (const [name, fingerprints] of headFingerprints) {
Expand All @@ -173,6 +170,43 @@ function getChangedPackageNames(
}
}

/**
* pnpm's external node hashes describe package contents, not which dependency
* versions each snapshot resolves to. Include those edges in affected detection
* without changing the external node hashes used elsewhere in Nx.
*/
function getPackageFingerprints(
file: string,
content: string,
packageJson: PackageJson | undefined
): Map<string, Set<string>> {
const hash = hashArray([content]);
if (file !== 'pnpm-lock.yaml' && file !== 'pnpm-lock.yml') {
return collectPackageFingerprints(
getLockFileNodesForName(file, content, hash, packageJson).nodes
);
}

const { nodes, keyMap } = getPnpmLockfileNodes(content, hash);
const fingerprints = collectPackageFingerprints(nodes);
// The normalizer folds v9 snapshots into packages and also supports older
// pnpm lockfiles. The parser's keyMap handles aliases and peer contexts.
const lockfile = parseAndNormalizePnpmLockfile(content);
for (const [key, snapshot] of Object.entries(lockfile.packages ?? {})) {
const fingerprint = JSON.stringify({
snapshot: key,
dependencies: sortObjectByKeys(snapshot.dependencies ?? {}),
optionalDependencies: sortObjectByKeys(
snapshot.optionalDependencies ?? {}
),
});
for (const node of keyMap.get(key) ?? []) {
fingerprints.get(node.data.packageName)?.add(fingerprint);
}
}
return fingerprints;
}

/**
* Build a map of packageName -> set of versions present in the
* external-node record returned by a lock-file parser. We include both
Expand Down