@percy/cli-command 1.32.0-beta.8 → 1.32.0

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.
Files changed (2) hide show
  1. package/dist/smartsnap.js +73 -5
  2. package/package.json +6 -6
package/dist/smartsnap.js CHANGED
@@ -115,6 +115,60 @@ function gitProjectRoot() {
115
115
  return git(['rev-parse', '--show-toplevel']).trim();
116
116
  }
117
117
 
118
+ // Parse `git diff --unified=0` hunk headers to find which line ranges are new
119
+ // or changed (on the HEAD side) for each file, then key them by the file's
120
+ // index in the stats `files` array — the same index module/source refs use, so
121
+ // the graph can join them without a path lookup. `--unified=0` drops context
122
+ // lines so every hunk header bounds an actual change; `--no-renames` makes a
123
+ // rename surface as add+delete so the new path is always concrete.
124
+ //
125
+ // Files in the diff that aren't tracked in `files` (e.g. node_modules,
126
+ // .storybook, or anything the stats compactor didn't index) are skipped — the
127
+ // graph can only reason about indexed files. Returns { <fileIndex>: [[start, end], ...] }.
128
+ export function getAffectedFileLocations(baseRef, files) {
129
+ assertSafeRef(baseRef);
130
+ const diff = git(['diff', '--unified=0', '--no-color', '--no-renames', baseRef, 'HEAD', '--']);
131
+
132
+ // The `files` array uses the platform separator (path.relative → back-slash
133
+ // on Windows); git diff always emits forward slashes. Normalize both to
134
+ // forward-slash for the path→index lookup.
135
+ const toPosix = p => p.split(path.sep).join('/');
136
+ const indexByPath = new Map(files.map((f, i) => [toPosix(f), i]));
137
+
138
+ // @@ -a,b +c,d @@ — the new-side `+c,d` is what we want. `d` defaults to 1
139
+ // when omitted; `d === 0` is a pure deletion anchored at line c (no added
140
+ // lines), so it contributes no range.
141
+ const HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
142
+ const locations = {};
143
+ let currentIdx;
144
+ for (const line of diff.split('\n')) {
145
+ // `+++ b/<path>` opens a file's hunks with its new-side path; a pure
146
+ // deletion shows `+++ /dev/null`, so leave currentIdx unset and skip it.
147
+ if (line.startsWith('+++ ')) {
148
+ const p = line.slice(4);
149
+ if (p === '/dev/null') {
150
+ currentIdx = undefined;
151
+ continue;
152
+ }
153
+ /* istanbul ignore next: git always prefixes the new-side path with `b/`
154
+ (anything else is `/dev/null`, handled above), so the bare `: p`
155
+ fallback is defensive and unreachable in real diff output */
156
+ const rel = p.startsWith('b/') ? p.slice(2) : p;
157
+ currentIdx = indexByPath.get(rel);
158
+ continue;
159
+ }
160
+ if (currentIdx === undefined) continue;
161
+ const m = HUNK.exec(line);
162
+ if (!m) continue;
163
+ const start = parseInt(m[1], 10);
164
+ const count = m[2] === undefined ? 1 : parseInt(m[2], 10);
165
+ if (count === 0) continue;
166
+ if (!locations[currentIdx]) locations[currentIdx] = [];
167
+ locations[currentIdx].push([start, start + count - 1]);
168
+ }
169
+ return locations;
170
+ }
171
+
118
172
  // Paths under these directories are dependencies / framework wiring rather
119
173
  // than first-party source, so we don't track them in the file index.
120
174
  const EXCLUDED_DIRS = new Set(['node_modules', '.storybook']);
@@ -156,6 +210,11 @@ function transformModule(m, fileIndex, projectRoot) {
156
210
  if (copy.type === 'src' && typeof copy.source === 'string') {
157
211
  copy.source = resolveAndIndex(copy.source, fileIndex, projectRoot);
158
212
  }
213
+ // `loc` (when present) is an array of `{ start, end }` source spans; the
214
+ // graph expects each span as a `[start, end]` tuple instead.
215
+ if (Array.isArray(copy.loc)) {
216
+ copy.loc = copy.loc.map(l => [l.start, l.end]);
217
+ }
159
218
  return copy;
160
219
  };
161
220
  if (Array.isArray(m.imports)) out.imports = m.imports.map(mapEntry);
@@ -220,7 +279,7 @@ async function pollGraphStatus(percy, buildId, log) {
220
279
  const res = await percy.client.getStatus('smartsnap_graph', [buildId]);
221
280
  const status = res === null || res === void 0 ? void 0 : res.status;
222
281
  log.debug(`SmartSnap: graph status (attempt ${i + 1}) = ${status}`);
223
- if (status === 'done' || status === 'failure') return {
282
+ if (status === 'done' || status === 'failed') return {
224
283
  status,
225
284
  data: res === null || res === void 0 ? void 0 : res.data
226
285
  };
@@ -453,20 +512,25 @@ export async function runGraphGeneration(percy, buildId, payload, log) {
453
512
  files,
454
513
  modules,
455
514
  storybookPaths,
456
- affectedNodes
515
+ affectedNodes,
516
+ affectedFileLocations
457
517
  } = payload;
458
518
  log.debug(`SmartSnap: starting graph generation job ${JSON.stringify({
459
519
  buildId,
460
520
  files,
461
521
  modules,
462
522
  storybookPaths,
463
- affectedNodes
523
+ affectedNodes,
524
+ affectedFileLocations
464
525
  })}`);
526
+ // Pass camelCase to the client, which snake_cases it to `affected_file_locations`
527
+ // for the API (same convention as storybookPaths → storybook_paths).
465
528
  await percy.client.generateSmartsnapGraph(buildId, {
466
529
  files,
467
530
  modules,
468
531
  storybookPaths,
469
- affectedNodes
532
+ affectedNodes,
533
+ affectedFileLocations
470
534
  });
471
535
  const {
472
536
  status,
@@ -621,11 +685,15 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir
621
685
  if (packageAffectedNodes.length) {
622
686
  affectedNodes = [...affectedNodes, ...packageAffectedNodes];
623
687
  }
688
+
689
+ // Line-level diff ranges for the indexed files, keyed by their `files` index.
690
+ const affectedFileLocations = getAffectedFileLocations(baseRef, files);
624
691
  const data = await runGraphGeneration(percy, buildId, {
625
692
  files,
626
693
  modules,
627
694
  storybookPaths,
628
- affectedNodes
695
+ affectedNodes,
696
+ affectedFileLocations
629
697
  }, log);
630
698
  maybeWriteTrace(trace, data, log);
631
699
  return selectAffectedSnapshots(snapshots, data, baseline, baselineSnapshots, normalizeImportPath, log);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/cli-command",
3
- "version": "1.32.0-beta.8",
3
+ "version": "1.32.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "publishConfig": {
11
11
  "access": "public",
12
- "tag": "beta"
12
+ "tag": "latest"
13
13
  },
14
14
  "files": [
15
15
  "dist",
@@ -37,14 +37,14 @@
37
37
  "test:coverage": "yarn test --coverage"
38
38
  },
39
39
  "dependencies": {
40
- "@percy/config": "1.32.0-beta.8",
41
- "@percy/core": "1.32.0-beta.8",
42
- "@percy/logger": "1.32.0-beta.8",
40
+ "@percy/config": "1.32.0",
41
+ "@percy/core": "1.32.0",
42
+ "@percy/logger": "1.32.0",
43
43
  "glob-to-regexp": "^0.4.1",
44
44
  "stream-json": "^1.8.0"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "snyk-nodejs-lockfile-parser": "2.7.1"
48
48
  },
49
- "gitHead": "8e91413b6ed95aeefad697f058709e2a0fe56f0e"
49
+ "gitHead": "87f5058139cf3f9a44cd6649d797e6f95571b749"
50
50
  }