Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4f22391
Add smartsnap endpoints to percy client
RaghavsBrowserStack Apr 30, 2026
9f4b0af
make smartend endpoints pass buildId as parameter
RaghavsBrowserStack May 4, 2026
4f5f806
remove package mapping from ClI expectation
RaghavsBrowserStack May 6, 2026
85cd3fd
add handling for new stats without package file and with buildId
RaghavsBrowserStack May 8, 2026
bf410b9
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack May 14, 2026
204ea03
write test for smartsnap api's
RaghavsBrowserStack May 14, 2026
01c0f80
move smartsnap logic to cli
RaghavsBrowserStack May 14, 2026
0a86165
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack May 14, 2026
88ab9f6
make package lockfile parser optional
RaghavsBrowserStack May 15, 2026
c9ba0e6
Merge remote-tracking branch 'origin/PPLT-5302' into PPLT-5302
RaghavsBrowserStack May 15, 2026
2a4589c
fix lint issue
RaghavsBrowserStack May 15, 2026
0133f82
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack May 21, 2026
6d9ff03
update cli to match new API
RaghavsBrowserStack May 22, 2026
897c32f
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack May 26, 2026
f689ac8
new api contracts
RaghavsBrowserStack May 26, 2026
bc60bb3
delete accidentally generated pnpm lockfile
RaghavsBrowserStack May 28, 2026
7c59f0a
fix major issues
RaghavsBrowserStack May 28, 2026
9be124a
Potential fix for pull request finding 'CodeQL / Bad HTML filtering r…
RaghavsBrowserStack May 28, 2026
0a99751
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack May 29, 2026
f7fdfd5
resolve comments
RaghavsBrowserStack May 29, 2026
e8ee178
resolve claude comments
RaghavsBrowserStack May 29, 2026
bd72fb6
add tests and refactor smartsnap
RaghavsBrowserStack May 29, 2026
628b873
add semgrep ignore statements for approved code
RaghavsBrowserStack Jun 1, 2026
6b4ef70
move semgrep suppressions to same-line trailing comments so they regi…
RaghavsBrowserStack Jun 1, 2026
ac0d479
use bare nosemgrep so semgrep ci honors the suppressions
RaghavsBrowserStack Jun 1, 2026
8387323
bump semgrep image to 1.164.0 so --sarif honors nosemgrep suppressions
RaghavsBrowserStack Jun 1, 2026
b3f22c4
validate lockfile type before loading optional snyk parser (fixes Nod…
RaghavsBrowserStack Jun 1, 2026
5f5c134
coverage-ignore snyk-backed lockfile paths unreachable on Node 14 CI
RaghavsBrowserStack Jun 1, 2026
be3bf0c
disable fail-fast on test matrix so one (often flaky) package can't c…
RaghavsBrowserStack Jun 1, 2026
e5431be
Revert "disable fail-fast on test matrix so one (often flaky) package…
RaghavsBrowserStack Jun 1, 2026
dc16d84
attempt to get 100% coverage
RaghavsBrowserStack Jun 1, 2026
e781d85
attempt to get 100% coverage
RaghavsBrowserStack Jun 1, 2026
4fdc3bb
update test case for windows compatibility
RaghavsBrowserStack Jun 1, 2026
9629722
update test case for windows compatibility
RaghavsBrowserStack Jun 1, 2026
cd141b4
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack Jun 2, 2026
c4bb6bd
fix package.json
RaghavsBrowserStack Jun 2, 2026
90010dc
Merge branch 'master' into PPLT-5302
RaghavsBrowserStack Jun 2, 2026
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
8 changes: 6 additions & 2 deletions .github/workflows/Semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ jobs:
runs-on: ubuntu-latest

container:
# A Docker image with Semgrep installed. Do not change this.
image: returntocorp/semgrep
# A Docker image with Semgrep installed.
# Pinned to >=1.164.0: the old `returntocorp/semgrep:latest` (semgrep 1.162.0)
# has a bug where `semgrep ci --sarif` counts nosemgrep-suppressed findings as
# blocking and exits non-zero (fixed in 1.164.0), so inline `// nosemgrep`
# suppressions were ignored. See https://semgrep.dev/docs CHANGELOG v1.164.0.
image: semgrep/semgrep:1.164.0

# Skip any PR created by dependabot to avoid permission issues:
if: (github.actor != 'dependabot[bot]')
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-command/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
".": "./dist/index.js",
"./flags": "./dist/flags.js",
"./utils": "./dist/utils.js",
"./smartsnap": "./dist/smartsnap.js",
"./test/helpers": "./test/helpers.js"
},
"scripts": {
Expand All @@ -38,6 +39,11 @@
"dependencies": {
"@percy/config": "1.32.0-beta.6",
"@percy/core": "1.32.0-beta.6",
"@percy/logger": "1.32.0-beta.6"
"@percy/logger": "1.32.0-beta.6",
"glob-to-regexp": "^0.4.1",
"stream-json": "^1.8.0"
},
"optionalDependencies": {
"snyk-nodejs-lockfile-parser": "2.7.1"
}
}
154 changes: 154 additions & 0 deletions packages/cli-command/src/graphTrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import fs from 'fs';
import path from 'path';
import url from 'url';

// Template resolution mirrors core/utils.js's secretPatterns.yml lookup:
// resolves relative to this file's URL so it works under src/ (dev) and
// dist/ (installed) without bundler help. The .html file is copied alongside
// by babel's copyFiles when cli-command is built.
const TEMPLATE_PATH = path.resolve(url.fileURLToPath(import.meta.url), '../graphTraceTemplate.html');

// Maps a (raw kind, changed) pair to the kind value the template expects:
// 'package' | 'component' | 'story' | 'is_relevant'. `changed: true` wins
// over the underlying kind so any node touched in the diff renders purple.
function templateKindOf(v) {
if (v.changed) return 'is_relevant';
switch (v.kind) {
case 'dependency': return 'package';
case 'component': return 'component';
case 'story': return 'story';
default: return 'component';
}
}

// Sort order within a column: packages left, components middle, stories right.
// `is_relevant` shares rank with components so a changed node doesn't jump
// out of its own group — it just recolors.
const KIND_RANK = { package: 0, component: 1, is_relevant: 1, story: 2 };

// Layout algorithm (ported from the original Ruby renderer):
// 1. col = longest-path depth reaching the vertex (read from the
// transitive-closure triples the API sends), with dependencies pinned
// to col 0.
// 2. Propagate over edges so col[target] > col[source]. Bounded loop
// guards against degenerate inputs.
// 3. Stories pushed past the rightmost non-story column.
// 4. Within each column, sort by (kind-rank, name) and assign row.
function computeLayout(rawVertices, edges, transitiveClosure) {
const n = rawVertices.length;
const vertices = rawVertices.map((v, i) => ({
index: i,
name: v.file_path,
kind: v.kind,
changed: !!v.changed,
row: 0,
col: 0
}));

// 1. Seed col from incoming transitive-closure lengths.
const incomingMax = new Array(n).fill(0);
for (const triple of transitiveClosure) {
const [u, v, val] = triple;
if (u === v || val <= 0) continue;
if (v < 0 || v >= n) continue;
if (val > incomingMax[v]) incomingMax[v] = val;
}
for (let i = 0; i < n; i++) {
vertices[i].col = vertices[i].kind === 'dependency' ? 0 : incomingMax[i] + 1;
}

// 2. Propagate edge constraint. n+2 iterations is enough for any DAG
// and bounds the work on accidentally-cyclic input.
const iterations = n + 2;
for (let iter = 0; iter < iterations; iter++) {
let changed = false;
for (const [s, t] of edges) {
if (s < 0 || s >= n || t < 0 || t >= n) continue;
if (vertices[s].col < vertices[t].col) continue;
vertices[t].col = vertices[s].col + 1;
changed = true;
}
if (!changed) break;
}

// 3. Stories rightmost. Two passes: max across non-stories first, then
// push every story past that boundary. Folding into one loop would let
// stories visited before the last non-story keep a stale max.
let furthestNonStory = 0;
for (const v of vertices) {
if (v.kind === 'story') continue;
if (v.col > furthestNonStory) furthestNonStory = v.col;
}
for (const v of vertices) {
if (v.kind !== 'story') continue;
if (v.col < furthestNonStory + 1) v.col = furthestNonStory + 1;
}

// 4. Group by column, sort by (kind-rank, name), assign row.
const groups = new Map();
for (const v of vertices) {
let list = groups.get(v.col);
if (!list) groups.set(v.col, list = []);
list.push(v);
}
const rankOf = v => {
const r = KIND_RANK[templateKindOf(v)];
/* istanbul ignore next: templateKindOf always returns a kind present in
KIND_RANK, so the `=== undefined` fallback is defensive */
return r === undefined ? 99 : r;
};
for (const list of groups.values()) {
list.sort((a, b) => {
const ra = rankOf(a);
const rb = rankOf(b);
if (ra !== rb) return ra - rb;
// Byte-wise compare on name to match Ruby's String#<=> behaviour.
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
list.forEach((v, row) => { v.row = row; });
}

// 5. Final shape the template consumes: drop `changed`, fold it into kind.
return vertices.map(v => ({
index: v.index,
name: v.name,
row: v.row,
col: v.col,
kind: templateKindOf(v)
}));
}

// Escapes characters that have meaning inside a <script> block so user-derived
// strings (e.g. a vertex file_path) can't break out of the surrounding tag.
// `</` covers `</script>`; `<!--`/`-->` cover HTML comment confusion; U+2028
// and U+2029 are valid JSON but illegal in JS string literals pre-ES2019 and
// have historically been XSS sinks.
const LS = String.fromCharCode(0x2028);
const PS = String.fromCharCode(0x2029);
function safeJson(obj) {
return JSON.stringify(obj)
.replace(/<\//g, '<\\/')
.replace(/<!--/g, '<\\!--')
.replace(/--(!?)>/g, '--$1\\>')
.split(LS).join('\\u2028')
.split(PS).join('\\u2029');
}

// Populates the trace template with the three JSON payloads the page needs.
// Input shape matches the API's graph data: `vertices` carries `kind`,
// `file_path`, `changed`; `edges` and `transitive_closure_matrix_sparse`
// are arrays of integer tuples.
export function renderGraphTraceHtml({ vertices, edges, transitiveClosureMatrixSparse }) {
const laidOutVertices = computeLayout(
vertices || [],
edges || [],
transitiveClosureMatrixSparse || []
);
const template = fs.readFileSync(TEMPLATE_PATH, 'utf8');
return template
.replace('__VERTICES_JSON__', safeJson(laidOutVertices))
.replace('__EDGES_JSON__', safeJson(edges || []))
.replace('__TRANSITIVE_CLOSURE_JSON__', safeJson(transitiveClosureMatrixSparse || []));
}
Loading
Loading