@percy/cli-command 1.32.2 → 1.32.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,184 +0,0 @@
1
- import { createRequire } from 'module';
2
- import logger from '@percy/logger';
3
-
4
- // snyk-nodejs-lockfile-parser is a CommonJS optionalDependency. It requires
5
- // Node >=18 while the CLI supports Node >=14, so we defer the require to call
6
- // time — that way importing this module never throws on older Node versions
7
- // (or when the optional install was skipped for any other reason). Cached on
8
- // first successful load so the require only resolves once per process.
9
- const require = createRequire(import.meta.url);
10
- let _snykModule;
11
- /* istanbul ignore next: snyk-backed path — the parser requires Node >=18 while
12
- CI runs the suite on Node 14, so these lines can't execute there; they're
13
- exercised by the describeSnyk tests on Node >=18 */
14
- function loadSnyk() {
15
- if (_snykModule) return _snykModule;
16
- try {
17
- _snykModule = require('snyk-nodejs-lockfile-parser');
18
- return _snykModule;
19
- } catch (e) {
20
- // Surface the underlying require failure so callers can distinguish
21
- // "not installed" (engine mismatch / optional skip) from genuine parse
22
- // failures that happen later inside buildDepTree.
23
- const err = new Error(`snyk-nodejs-lockfile-parser is not available (requires Node >=18, or the optional install was skipped): ${e.message}`);
24
- err.code = 'SNYK_LOCKFILE_PARSER_UNAVAILABLE';
25
- throw err;
26
- }
27
- }
28
-
29
- // Map the on-disk filename to snyk's LockfileType enum key. We can't resolve
30
- // the enum value at module-eval time because the snyk import is deferred, so
31
- // we look up the value inside diffLockfileDeps after loadSnyk() runs.
32
- const TYPE_KEY_BY_FILENAME = {
33
- 'package-lock.json': 'npm',
34
- 'yarn.lock': 'yarn',
35
- 'pnpm-lock.yaml': 'pnpm'
36
- };
37
-
38
- // Walk snyk's PkgTree (a recursive `{ name, version, dependencies: { ... } }`
39
- // structure) into a flat `Map<name, version>` of every resolved package.
40
- // We dedupe by name on first sight — multiple versions of the same package
41
- // in the tree get collapsed to the first one encountered, which is fine for
42
- // the diff because we only care about whether *something* about the package
43
- // changed between old and new.
44
- /* istanbul ignore next: snyk-backed path — only reachable on Node >=18 (see loadSnyk) */
45
- function flattenPkgTree(tree) {
46
- const out = new Map();
47
- const walk = node => {
48
- if (!(node !== null && node !== void 0 && node.dependencies)) return;
49
- for (const [name, child] of Object.entries(node.dependencies)) {
50
- if (!out.has(name)) out.set(name, child.version);
51
- walk(child);
52
- }
53
- };
54
- walk(tree);
55
- return out;
56
- }
57
-
58
- // Pull `{ name -> rangeString }` from a raw package.json's `dependencies`
59
- // and `peerDependencies` blocks. devDeps and optionalDeps are intentionally
60
- // excluded — only runtime-relevant top-level packages count. Returns {} on
61
- // parse failure so the diff falls through to lockfile-only signal.
62
- /* istanbul ignore next: snyk-backed path — only reachable on Node >=18 (see loadSnyk) */
63
- function topLevelDeps(packageJsonContents) {
64
- try {
65
- const pkg = JSON.parse(packageJsonContents);
66
- return {
67
- ...(pkg.dependencies || {}),
68
- ...(pkg.peerDependencies || {})
69
- };
70
- } catch {
71
- return {};
72
- }
73
- }
74
-
75
- // Given the project's package.json plus the old and new lockfile contents,
76
- // return the set of package names that were added, removed, or version-bumped.
77
- // Results are restricted to packages declared at the top level (dependencies
78
- // or peerDependencies) of either old or new package.json — transitives ride
79
- // along on direct-dep bumps and the BE module graph already resolves them
80
- // from stats `imports[]`, so surfacing them here would just be noise.
81
- //
82
- // Two complementary diffs run, both gated by top-level names:
83
- // 1. Resolved-version diff over the snyk PkgTree — catches lockfile entries
84
- // whose version actually changed.
85
- // 2. Range-string diff over package.json — catches the case where a user
86
- // bumped `^5.8.3` to `^5.18.0` but the lockfile already resolved to
87
- // 5.18.0 under the old range, so the resolved tree looks identical.
88
- export async function diffLockfileDeps({
89
- packageJson,
90
- oldPackageJson,
91
- oldLockfile,
92
- newLockfile,
93
- lockfileType
94
- }) {
95
- const log = logger('storybook:smartsnap:lockfile');
96
-
97
- // Validate the filename maps to a supported lockfile type BEFORE loading the
98
- // optional snyk parser. The parser requires Node >=18, so on older runtimes
99
- // (or when the optional install was skipped) an unsupported type must still
100
- // fail with this clear error rather than the parser-unavailable error from
101
- // loadSnyk(). This filename check needs no parser to run.
102
- const typeKey = TYPE_KEY_BY_FILENAME[lockfileType];
103
- /* istanbul ignore else: on the Node-14 CI only the unsupported-type path is
104
- exercised; the supported-type branch runs in describeSnyk on Node >=18 */
105
- if (!typeKey) {
106
- throw new Error(`Unsupported lockfile type: ${lockfileType}`);
107
- }
108
-
109
- /* istanbul ignore next: supported-type path — only reachable on Node >=18 (see loadSnyk) */
110
- return resolveAffectedDeps({
111
- packageJson,
112
- oldPackageJson,
113
- oldLockfile,
114
- newLockfile,
115
- typeKey,
116
- log
117
- });
118
- }
119
-
120
- // The snyk-backed resolution, split into its own function so a single
121
- // coverage-ignore covers the whole path: the parser requires Node >=18 while CI
122
- // runs the suite on Node 14, so none of this executes there. The describeSnyk
123
- // tests exercise it on Node >=18. `typeKey` is already validated against
124
- // TYPE_KEY_BY_FILENAME by the caller, so it always resolves to a LockfileType.
125
- /* istanbul ignore next: snyk-backed path — only reachable on Node >=18 (see loadSnyk) */
126
- async function resolveAffectedDeps({
127
- packageJson,
128
- oldPackageJson,
129
- oldLockfile,
130
- newLockfile,
131
- typeKey,
132
- log
133
- }) {
134
- const {
135
- buildDepTree,
136
- LockfileType
137
- } = loadSnyk();
138
- const type = LockfileType[typeKey];
139
-
140
- // The two buildDepTree calls are kept sequential (not Promise.all'd) so that
141
- // when one of them throws the log message identifies *which* side failed —
142
- // old vs. new lockfile parsing behave differently when the user's lockfile
143
- // is in an unexpected state.
144
- let oldTree;
145
- try {
146
- log.debug('buildDepTree: parsing OLD lockfile...');
147
- oldTree = await buildDepTree(oldPackageJson, oldLockfile, true, type);
148
- log.debug('buildDepTree: OLD lockfile parsed successfully');
149
- } catch (e) {
150
- log.warn(`buildDepTree: OLD lockfile failed to parse: ${e.message}`);
151
- throw e;
152
- }
153
- let newTree;
154
- try {
155
- log.debug('buildDepTree: parsing NEW lockfile...');
156
- newTree = await buildDepTree(packageJson, newLockfile, true, type);
157
- log.debug('buildDepTree: NEW lockfile parsed successfully');
158
- } catch (e) {
159
- log.warn(`buildDepTree: NEW lockfile failed to parse: ${e.message}`);
160
- throw e;
161
- }
162
- const oldPkgs = flattenPkgTree(oldTree);
163
- const newPkgs = flattenPkgTree(newTree);
164
- const oldTopLevel = topLevelDeps(oldPackageJson);
165
- const newTopLevel = topLevelDeps(packageJson);
166
- const topLevelNames = new Set([...Object.keys(oldTopLevel), ...Object.keys(newTopLevel)]);
167
- const affected = new Set();
168
- for (const [name, version] of newPkgs) {
169
- if (!topLevelNames.has(name)) continue;
170
- if (!oldPkgs.has(name) || oldPkgs.get(name) !== version) affected.add(name);
171
- }
172
- for (const [name] of oldPkgs) {
173
- if (!topLevelNames.has(name)) continue;
174
- if (!newPkgs.has(name)) affected.add(name);
175
- }
176
-
177
- // Range-string diff is inherently top-level since it iterates package.json
178
- // directly. `!==` between the strings (or undefined) covers added, removed,
179
- // and changed in a single comparison.
180
- for (const name of topLevelNames) {
181
- if (oldTopLevel[name] !== newTopLevel[name]) affected.add(name);
182
- }
183
- return [...affected];
184
- }