From 4f22391e961449b994ebc8c370ff68c034e4b868 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 30 Apr 2026 12:21:45 +0530 Subject: [PATCH 01/29] Add smartsnap endpoints to percy client --- packages/client/src/client.js | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 452c60e93..20938132c 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -419,6 +419,60 @@ export class PercyClient { return this.get(`job_status?sync=true&type=${type}&id=${ids.join()}`); } + async getSmartsnapSnapshotNameToCommit(buildId, snapshotNames) { + validateId('build', buildId); + this.log.debug(`Smartsnap: looking up baselines for build ${buildId}...`); + const qs = new URLSearchParams(); + qs.append('build_id', buildId); + for (const name of (snapshotNames || [])) qs.append('snapshot_names[]', name); + return this.get( + `smartsnap/snapshot-name-to-commit?${qs.toString()}`, + { identifier: 'smartsnap.snapshot_name_to_commit' } + ); + } + + async generateSmartsnapGraph(buildId, { + files, + modules, + packageMapping, + storybookPaths, + affectedNodes + } = {}) { + validateId('build', buildId); + this.log.debug(`Smartsnap: enqueueing graph build for build ${buildId}...`); + return this.post('smartsnap/generate-graph', { + build_id: buildId, + files, + modules, + package_mapping: packageMapping, + storybook_paths: storybookPaths, + affected_nodes: affectedNodes + }, { identifier: 'smartsnap.generate_graph' }); + } + + async getSmartsnapGraphStatus(buildId) { + validateId('build', buildId); + this.log.debug(`Smartsnap: polling graph status for build ${buildId}...`); + return this.get( + `smartsnap/generate-graph/${buildId}`, + { identifier: 'smartsnap.graph_status' } + ); + } + + async getSmartsnapGraphData(buildId, { trace = false } = {}) { + validateId('build', buildId); + this.log.debug(`Smartsnap: fetching graph data for build ${buildId}...`); + const qs = new URLSearchParams(); + if (trace) qs.append('trace', 'true'); + const query = qs.toString(); + return this.get( + query + ? `smartsnap/generate-graph/${buildId}/data?${query}` + : `smartsnap/generate-graph/${buildId}/data`, + { identifier: 'smartsnap.graph_data' } + ); + } + // Returns device details enabled on project associated with given token async getDeviceDetails(buildId) { try { From 9f4b0af89d3b3e9ff87c4c13d6387ae04e295585 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Mon, 4 May 2026 20:33:20 +0530 Subject: [PATCH 02/29] make smartend endpoints pass buildId as parameter --- packages/client/src/client.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 20938132c..b91f10765 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -419,11 +419,16 @@ export class PercyClient { return this.get(`job_status?sync=true&type=${type}&id=${ids.join()}`); } - async getSmartsnapSnapshotNameToCommit(buildId, snapshotNames) { - validateId('build', buildId); - this.log.debug(`Smartsnap: looking up baselines for build ${buildId}...`); + // SmartSnap endpoints authenticate against the project attached to the + // current Percy token (via the Authorization header). The three graph + // endpoints additionally take a `build_id` (sourced from the bundler- + // emitted stats file) so multiple concurrent storybook builds for the + // same project don't share Redis state. `snapshot-name-to-commit` is + // project-scoped only and takes no buildId. + + async getSmartsnapSnapshotNameToCommit(snapshotNames) { + this.log.debug('Smartsnap: looking up baselines...'); const qs = new URLSearchParams(); - qs.append('build_id', buildId); for (const name of (snapshotNames || [])) qs.append('snapshot_names[]', name); return this.get( `smartsnap/snapshot-name-to-commit?${qs.toString()}`, @@ -438,7 +443,6 @@ export class PercyClient { storybookPaths, affectedNodes } = {}) { - validateId('build', buildId); this.log.debug(`Smartsnap: enqueueing graph build for build ${buildId}...`); return this.post('smartsnap/generate-graph', { build_id: buildId, @@ -451,24 +455,22 @@ export class PercyClient { } async getSmartsnapGraphStatus(buildId) { - validateId('build', buildId); this.log.debug(`Smartsnap: polling graph status for build ${buildId}...`); + const qs = new URLSearchParams(); + qs.append('build_id', buildId); return this.get( - `smartsnap/generate-graph/${buildId}`, + `smartsnap/generate-graph?${qs.toString()}`, { identifier: 'smartsnap.graph_status' } ); } async getSmartsnapGraphData(buildId, { trace = false } = {}) { - validateId('build', buildId); this.log.debug(`Smartsnap: fetching graph data for build ${buildId}...`); const qs = new URLSearchParams(); + qs.append('build_id', buildId); if (trace) qs.append('trace', 'true'); - const query = qs.toString(); return this.get( - query - ? `smartsnap/generate-graph/${buildId}/data?${query}` - : `smartsnap/generate-graph/${buildId}/data`, + `smartsnap/generate-graph/data?${qs.toString()}`, { identifier: 'smartsnap.graph_data' } ); } From 4f5f806f287471ed14dba58961f02624670171e1 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Wed, 6 May 2026 20:49:51 +0530 Subject: [PATCH 03/29] remove package mapping from ClI expectation --- packages/client/src/client.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client/src/client.js b/packages/client/src/client.js index b91f10765..f1ca56616 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -439,7 +439,6 @@ export class PercyClient { async generateSmartsnapGraph(buildId, { files, modules, - packageMapping, storybookPaths, affectedNodes } = {}) { @@ -448,7 +447,6 @@ export class PercyClient { build_id: buildId, files, modules, - package_mapping: packageMapping, storybook_paths: storybookPaths, affected_nodes: affectedNodes }, { identifier: 'smartsnap.generate_graph' }); From 85cd3fd1a532001e1a8e0bf1a1fa84cc2d9e86a4 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 8 May 2026 15:05:32 +0530 Subject: [PATCH 04/29] add handling for new stats without package file and with buildId --- packages/client/src/client.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/client/src/client.js b/packages/client/src/client.js index f1ca56616..76aa97b7d 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -430,6 +430,18 @@ export class PercyClient { this.log.debug('Smartsnap: looking up baselines...'); const qs = new URLSearchParams(); for (const name of (snapshotNames || [])) qs.append('snapshot_names[]', name); + + // Same git/PR context `createBuild` sends — the API uses these to predict + // the base build that *would* be selected if we called createBuild now, + // without actually creating one. Any missing field means the API falls + // back through the same strategy chain it would on real build creation. + if (this.env.git?.branch) qs.append('branch', this.env.git.branch); + if (this.env.target?.branch) qs.append('target_branch', this.env.target.branch); + if (this.env.git?.sha) qs.append('commit_sha', this.env.git.sha); + if (this.env.target?.commit) qs.append('target_commit_sha', this.env.target.commit); + if (this.env.pullRequest != null) qs.append('pull_request_number', String(this.env.pullRequest)); + if (this.env.partial) qs.append('partial', 'true'); + return this.get( `smartsnap/snapshot-name-to-commit?${qs.toString()}`, { identifier: 'smartsnap.snapshot_name_to_commit' } From 204ea03786f1bbd2eb2e79a5ea874d6c7f136e3d Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 14 May 2026 16:12:22 +0530 Subject: [PATCH 05/29] write test for smartsnap api's --- packages/client/test/client.test.js | 180 ++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 6227423ae..137d8e8f3 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -813,6 +813,186 @@ describe('PercyClient', () => { }); }); + describe('#getSmartsnapSnapshotNameToCommit()', () => { + const stubEnv = (overrides = {}) => { + Object.defineProperty(client.env, 'git', { value: overrides.git ?? {}, configurable: true }); + Object.defineProperty(client.env, 'target', { value: overrides.target ?? {}, configurable: true }); + Object.defineProperty(client.env, 'pullRequest', { value: overrides.pullRequest ?? null, configurable: true }); + Object.defineProperty(client.env, 'partial', { value: overrides.partial ?? false, configurable: true }); + }; + + beforeEach(() => stubEnv()); + + it('issues a GET with snapshot_names[] params and no git/PR context when env is empty', async () => { + const path = '/smartsnap/snapshot-name-to-commit?snapshot_names%5B%5D=foo&snapshot_names%5B%5D=bar'; + api.reply(path, () => [200, { data: { foo: 'sha-foo', bar: 'sha-bar' } }]); + + await expectAsync( + client.getSmartsnapSnapshotNameToCommit(['foo', 'bar']) + ).toBeResolvedTo({ data: { foo: 'sha-foo', bar: 'sha-bar' } }); + + expect(api.requests[path]).toBeDefined(); + expect(api.requests[path][0].method).toBe('GET'); + }); + + it('handles null/undefined snapshotNames without throwing', async () => { + // empty query string is normalized away by URL parsing + api.reply('/smartsnap/snapshot-name-to-commit', () => [200, { data: {} }]); + + await expectAsync(client.getSmartsnapSnapshotNameToCommit()).toBeResolvedTo({ data: {} }); + await expectAsync(client.getSmartsnapSnapshotNameToCommit(null)).toBeResolvedTo({ data: {} }); + }); + + it('appends git/target/PR/partial context when present in env', async () => { + stubEnv({ + git: { branch: 'feature/x', sha: 'commit-sha-1' }, + target: { branch: 'main', commit: 'commit-sha-2' }, + pullRequest: 42, + partial: true + }); + + const expectedPath = '/smartsnap/snapshot-name-to-commit?' + [ + 'snapshot_names%5B%5D=a', + 'branch=feature%2Fx', + 'target_branch=main', + 'commit_sha=commit-sha-1', + 'target_commit_sha=commit-sha-2', + 'pull_request_number=42', + 'partial=true' + ].join('&'); + + api.reply(expectedPath, () => [200, { data: { a: 'sha-a' } }]); + + await expectAsync( + client.getSmartsnapSnapshotNameToCommit(['a']) + ).toBeResolvedTo({ data: { a: 'sha-a' } }); + + expect(api.requests[expectedPath]).toBeDefined(); + expect(api.requests[expectedPath][0].method).toBe('GET'); + }); + + it('includes pull_request_number=0 when env.pullRequest is 0 (not null)', async () => { + stubEnv({ pullRequest: 0 }); + + const expectedPath = '/smartsnap/snapshot-name-to-commit?snapshot_names%5B%5D=a&pull_request_number=0'; + api.reply(expectedPath, () => [200, { data: {} }]); + + await expectAsync( + client.getSmartsnapSnapshotNameToCommit(['a']) + ).toBeResolvedTo({ data: {} }); + + expect(api.requests[expectedPath]).toBeDefined(); + }); + }); + + describe('#generateSmartsnapGraph()', () => { + it('POSTs build_id and graph payload to smartsnap/generate-graph', async () => { + api.reply('/smartsnap/generate-graph', () => [202, { status: 'queued' }]); + + await expectAsync(client.generateSmartsnapGraph('build-1', { + files: ['a.js', 'b.js'], + modules: [{ id: 1, name: 'mod' }], + storybookPaths: ['stories/a.js'], + affectedNodes: ['node-1'] + })).toBeResolvedTo({ status: 'queued' }); + + expect(api.requests['/smartsnap/generate-graph']).toBeDefined(); + expect(api.requests['/smartsnap/generate-graph'][0].method).toBe('POST'); + expect(api.requests['/smartsnap/generate-graph'][0].body).toEqual({ + build_id: 'build-1', + files: ['a.js', 'b.js'], + modules: [{ id: 1, name: 'mod' }], + storybook_paths: ['stories/a.js'], + affected_nodes: ['node-1'] + }); + }); + + it('sends undefined fields when called without payload', async () => { + api.reply('/smartsnap/generate-graph', () => [202, { status: 'queued' }]); + + await expectAsync(client.generateSmartsnapGraph('build-2')).toBeResolvedTo({ status: 'queued' }); + + // JSON.stringify drops undefined values, so only build_id should remain in body + expect(api.requests['/smartsnap/generate-graph'][0].body).toEqual({ build_id: 'build-2' }); + }); + + it('rejects when API returns an error', async () => { + api.reply('/smartsnap/generate-graph', () => [500, { error: 'boom' }]); + + await expectAsync(client.generateSmartsnapGraph('build-3', {})) + .toBeRejected(); + }); + }); + + describe('#getSmartsnapGraphStatus()', () => { + it('GETs the graph status for the given build_id', async () => { + api.reply('/smartsnap/generate-graph?build_id=build-1', () => [ + 200, { status: 'in_progress' } + ]); + + await expectAsync(client.getSmartsnapGraphStatus('build-1')) + .toBeResolvedTo({ status: 'in_progress' }); + + expect(api.requests['/smartsnap/generate-graph?build_id=build-1']).toBeDefined(); + expect(api.requests['/smartsnap/generate-graph?build_id=build-1'][0].method).toBe('GET'); + }); + + it('URL-encodes the build_id query param', async () => { + const path = '/smartsnap/generate-graph?build_id=build+with+spaces'; + api.reply(path, () => [200, { status: 'ready' }]); + + await expectAsync(client.getSmartsnapGraphStatus('build with spaces')) + .toBeResolvedTo({ status: 'ready' }); + + expect(api.requests[path]).toBeDefined(); + }); + + it('rejects when the API returns an error', async () => { + api.reply('/smartsnap/generate-graph?build_id=build-err', () => [500]); + + await expectAsync(client.getSmartsnapGraphStatus('build-err')).toBeRejected(); + }); + }); + + describe('#getSmartsnapGraphData()', () => { + it('GETs graph data for the given build_id without trace by default', async () => { + api.reply('/smartsnap/generate-graph/data?build_id=build-1', () => [ + 200, { graph: { nodes: [] } } + ]); + + await expectAsync(client.getSmartsnapGraphData('build-1')) + .toBeResolvedTo({ graph: { nodes: [] } }); + + expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1']).toBeDefined(); + expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1'][0].method).toBe('GET'); + }); + + it('appends trace=true when trace option is set', async () => { + const path = '/smartsnap/generate-graph/data?build_id=build-1&trace=true'; + api.reply(path, () => [200, { graph: {}, trace: [] }]); + + await expectAsync(client.getSmartsnapGraphData('build-1', { trace: true })) + .toBeResolvedTo({ graph: {}, trace: [] }); + + expect(api.requests[path]).toBeDefined(); + }); + + it('does not append trace when trace option is false', async () => { + api.reply('/smartsnap/generate-graph/data?build_id=build-1', () => [200, { graph: {} }]); + + await expectAsync(client.getSmartsnapGraphData('build-1', { trace: false })) + .toBeResolvedTo({ graph: {} }); + + expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1']).toBeDefined(); + }); + + it('rejects when the API returns an error', async () => { + api.reply('/smartsnap/generate-graph/data?build_id=build-err', () => [500]); + + await expectAsync(client.getSmartsnapGraphData('build-err')).toBeRejected(); + }); + }); + describe('#getDeviceDetails()', () => { it('in case of error return []', async () => { api.reply('/discovery/device-details', () => [500]); From 01c0f80d2d62525b99f9dcefc3115f603deef1da Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 14 May 2026 18:03:18 +0530 Subject: [PATCH 06/29] move smartsnap logic to cli --- packages/cli-command/package.json | 6 +- packages/cli-command/src/index.js | 1 + packages/cli-command/src/lockfileDiff.js | 101 ++ packages/cli-command/src/smartsnap.js | 409 +++++++++ packages/cli/pnpm-lock.yaml | 1068 ++++++++++++++++++++++ yarn.lock | 647 ++++++++++++- 6 files changed, 2226 insertions(+), 6 deletions(-) create mode 100644 packages/cli-command/src/lockfileDiff.js create mode 100644 packages/cli-command/src/smartsnap.js create mode 100644 packages/cli/pnpm-lock.yaml diff --git a/packages/cli-command/package.json b/packages/cli-command/package.json index 2e597bcd2..9e816b9a4 100644 --- a/packages/cli-command/package.json +++ b/packages/cli-command/package.json @@ -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": { @@ -38,6 +39,9 @@ "dependencies": { "@percy/config": "1.31.14-beta.3", "@percy/core": "1.31.14-beta.3", - "@percy/logger": "1.31.14-beta.3" + "@percy/logger": "1.31.14-beta.3", + "glob-to-regexp": "^0.4.1", + "snyk-nodejs-lockfile-parser": "2.7.1", + "stream-json": "^1.8.0" } } diff --git a/packages/cli-command/src/index.js b/packages/cli-command/src/index.js index d84c18762..4efd6e002 100644 --- a/packages/cli-command/src/index.js +++ b/packages/cli-command/src/index.js @@ -1,5 +1,6 @@ export { default, command, _resetShutdownForTest } from './command.js'; export { legacyCommand, legacyFlags as flags } from './legacy.js'; +export { applySmartSnap, SmartSnapBailError } from './smartsnap.js'; // export common packages to avoid dependency resolution issues export { default as PercyConfig } from '@percy/config'; export { default as logger } from '@percy/logger'; diff --git a/packages/cli-command/src/lockfileDiff.js b/packages/cli-command/src/lockfileDiff.js new file mode 100644 index 000000000..9154311e4 --- /dev/null +++ b/packages/cli-command/src/lockfileDiff.js @@ -0,0 +1,101 @@ +import { createRequire } from 'module'; + +// snyk-nodejs-lockfile-parser is CommonJS. Load via createRequire so the +// named imports resolve correctly under "type": "module". +const require = createRequire(import.meta.url); +const { buildDepTree, LockfileType } = require('snyk-nodejs-lockfile-parser'); + +// Map the on-disk filename to snyk's LockfileType enum. yarn.lock could be +// either yarn classic (v1) or berry (v2+); we default to yarn v1 here and +// can refine by sniffing the lockfile header in a follow-up if needed. +const TYPE_BY_FILENAME = { + 'package-lock.json': LockfileType.npm, + 'yarn.lock': LockfileType.yarn, + 'pnpm-lock.yaml': LockfileType.pnpm +}; + +// Walk snyk's PkgTree (a recursive `{ name, version, dependencies: { ... } }` +// structure) into a flat `Map` of every resolved package. +// We dedupe by name on first sight — multiple versions of the same package +// in the tree get collapsed to the first one encountered, which is fine for +// the diff because we only care about whether *something* about the package +// changed between old and new. +function flattenPkgTree(tree) { + const out = new Map(); + const walk = node => { + if (!node?.dependencies) return; + for (const [name, child] of Object.entries(node.dependencies)) { + if (!out.has(name)) out.set(name, child.version); + walk(child); + } + }; + walk(tree); + return out; +} + +// Pull `{ name -> rangeString }` from a raw package.json's `dependencies` +// and `peerDependencies` blocks. devDeps and optionalDeps are intentionally +// excluded — only runtime-relevant top-level packages count. Returns {} on +// parse failure so the diff falls through to lockfile-only signal. +function topLevelDeps(packageJsonContents) { + try { + const pkg = JSON.parse(packageJsonContents); + return { + ...(pkg.dependencies || {}), + ...(pkg.peerDependencies || {}) + }; + } catch { + return {}; + } +} + +// Given the project's package.json plus the old and new lockfile contents, +// return the set of package names that were added, removed, or version-bumped. +// Results are restricted to packages declared at the top level (dependencies +// or peerDependencies) of either old or new package.json — transitives ride +// along on direct-dep bumps and the BE module graph already resolves them +// from stats `imports[]`, so surfacing them here would just be noise. +// +// Two complementary diffs run, both gated by top-level names: +// 1. Resolved-version diff over the snyk PkgTree — catches lockfile entries +// whose version actually changed. +// 2. Range-string diff over package.json — catches the case where a user +// bumped `^5.8.3` to `^5.18.0` but the lockfile already resolved to +// 5.18.0 under the old range, so the resolved tree looks identical. +export async function diffLockfileDeps({ packageJson, oldPackageJson, oldLockfile, newLockfile, lockfileType }) { + const type = TYPE_BY_FILENAME[lockfileType]; + if (!type) { + throw new Error(`Unsupported lockfile type: ${lockfileType}`); + } + + const [oldTree, newTree] = await Promise.all([ + buildDepTree(oldPackageJson, oldLockfile, true, type), + buildDepTree(packageJson, newLockfile, true, type) + ]); + + const oldPkgs = flattenPkgTree(oldTree); + const newPkgs = flattenPkgTree(newTree); + + const oldTopLevel = topLevelDeps(oldPackageJson); + const newTopLevel = topLevelDeps(packageJson); + const topLevelNames = new Set([...Object.keys(oldTopLevel), ...Object.keys(newTopLevel)]); + + const affected = new Set(); + for (const [name, version] of newPkgs) { + if (!topLevelNames.has(name)) continue; + if (!oldPkgs.has(name) || oldPkgs.get(name) !== version) affected.add(name); + } + for (const [name] of oldPkgs) { + if (!topLevelNames.has(name)) continue; + if (!newPkgs.has(name)) affected.add(name); + } + + // Range-string diff is inherently top-level since it iterates package.json + // directly. `!==` between the strings (or undefined) covers added, removed, + // and changed in a single comparison. + for (const name of topLevelNames) { + if (oldTopLevel[name] !== newTopLevel[name]) affected.add(name); + } + + return [...affected]; +} diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js new file mode 100644 index 000000000..04d80d930 --- /dev/null +++ b/packages/cli-command/src/smartsnap.js @@ -0,0 +1,409 @@ +import fs from 'fs'; +import path from 'path'; +import { spawnSync } from 'child_process'; +import { createRequire } from 'module'; +import globToRegExp from 'glob-to-regexp'; +import logger from '@percy/logger'; +import { diffLockfileDeps } from './lockfileDiff.js'; + +// stream-json is CommonJS — load via createRequire to avoid named-import interop issues. +const require = createRequire(import.meta.url); +const { parser } = require('stream-json'); +const { pick } = require('stream-json/filters/Pick'); +const { streamArray } = require('stream-json/streamers/StreamArray'); +const { streamValues } = require('stream-json/streamers/StreamValues'); + +// Webpack/Vite stats prefix loader-resolved virtual modules with a NUL byte +// (e.g. "\u0000/path/to/file?commonjs-es-import"). Strip it so the path lines +// up with the absolute paths in our file index. Built via String.fromCharCode +// to avoid embedding a control char in source / tripping no-control-regex. +const NULL_CHAR = String.fromCharCode(0); +const stripNull = s => (typeof s === 'string' ? s.replaceAll(NULL_CHAR, '') : s); + +// Status poll cadence: 12 attempts × 5s = 1 minute total. +const POLL_INTERVAL_MS = 5000; +const POLL_ATTEMPTS = 12; + +const GLOB_CHARS = /[*?]/; +const MAX_PATTERN_LENGTH = 500; + +function patternToRegex(pattern) { + if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) { + throw new Error('Invalid pattern: must be a string with max length of 500 characters'); + } + return globToRegExp(pattern, { extended: true, globstar: false }); +} + +function matchesPattern(str, pattern) { + if (GLOB_CHARS.test(pattern)) { + try { + return patternToRegex(pattern).test(str); + } catch { + return false; + } + } + return str === pattern; +} + +// Thrown from any pipeline step that wants to fall back to the full snapshot +// set. The caller in snapshots.js downgrades these to log.info — they're +// expected, user-visible bail conditions, not crashes. +export class SmartSnapBailError extends Error { + constructor(message) { + super(message); + this.name = 'SmartSnapBailError'; + } +} + +function git(args) { + const res = spawnSync('git', args, { encoding: 'utf8' }); + if (res.status !== 0) { + throw new Error(`git ${args.join(' ')} failed: ${res.stderr || res.stdout || `exit ${res.status}`}`); + } + return res.stdout; +} + +function gitDiffNames(ref) { + return git(['diff', '--name-only', `${ref}`, 'HEAD']).split('\n').filter(Boolean); +} + +function gitProjectRoot() { + return git(['rev-parse', '--show-toplevel']).trim(); +} + +// Paths under these directories are dependencies / framework wiring rather +// than first-party source, so we don't track them in the file index. +const EXCLUDED_DIRS = new Set(['node_modules', '.storybook']); +const isExcluded = relPath => relPath.split(/[/\\]/).some(seg => EXCLUDED_DIRS.has(seg)); + +// Resolve+index used for `id` and `resolvedFrom` only. Converts absolute paths +// to projectRoot-relative form, then either returns the existing index for that +// path or assigns the next one (= current map size). Paths inside node_modules +// or .storybook are returned as the relative string and *not* indexed; modules +// whose id falls into that bucket are dropped downstream by the +// "id is string → drop" contract in transformModule. +function resolveAndIndex(value, fileIndex, projectRoot) { + const clean = stripNull(value); + if (!path.isAbsolute(clean)) return clean; + const rel = path.relative(projectRoot, clean); + if (isExcluded(rel)) return rel; + let idx = fileIndex.get(rel); + if (idx === undefined) { + idx = fileIndex.size; + fileIndex.set(rel, idx); + } + return idx; +} + +// Transform a stats `modules[]` entry into the indexed shape the SmartSnap graph expects. +// Returns null when the module's id is a string — those entries are dropped (per BE contract). +// Each `imports[i]` / `passThroughExports[i]` carries `{ type, source }` from the +// bundler-plugin: for `type === 'src'` we translate the absolute file path to its +// project-file index when possible; for `type === 'module'` the source is already +// the bare package name, so we leave it alone. +function transformModule(m, fileIndex, projectRoot) { + const out = {}; + if (m.id != null) out.id = resolveAndIndex(m.id, fileIndex, projectRoot); + if (typeof out.id === 'string') return null; + + const mapEntry = (e) => { + const copy = { ...e }; + if (copy.type === 'src' && typeof copy.source === 'string') { + copy.source = resolveAndIndex(copy.source, fileIndex, projectRoot); + } + return copy; + }; + + if (Array.isArray(m.imports)) out.imports = m.imports.map(mapEntry); + if (Array.isArray(m.passThroughExports)) out.passThroughExports = m.passThroughExports.map(mapEntry); + if (Array.isArray(m.nonPassThroughExports)) out.nonPassThroughExports = m.nonPassThroughExports; + + return out; +} + +function streamModules(filePath, onModule) { + return new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(parser()) + .pipe(pick({ filter: 'modules' })) + .pipe(streamArray()) + .on('data', ({ value }) => onModule(value)) + .on('end', resolve) + .on('error', reject); + }); +} + +function readTopLevelKey(filePath, key) { + return new Promise((resolve, reject) => { + let value; + fs.createReadStream(filePath) + .pipe(parser()) + .pipe(pick({ filter: key })) + .pipe(streamValues()) + .on('data', ({ value: v }) => { value = v; }) + .on('end', () => resolve(value)) + .on('error', reject); + }); +} + +async function readStats(statsFile, projectRoot) { + const fileIndex = new Map(); + const modules = []; + await streamModules(statsFile, (m) => { + const t = transformModule(m, fileIndex, projectRoot); + if (t) modules.push(t); + }); + + // Emit `files` ordered by encounter-time index so files[N] corresponds to + // every module/source ref that was assigned index N during streaming. + const files = [...fileIndex.entries()] + .sort((a, b) => a[1] - b[1]) + .map(([p]) => p); + + // The bundler-plugin-smartsnap emits a unique buildId per storybook build + // so concurrent runs against the same project don't share Redis state. + const buildId = await readTopLevelKey(statsFile, 'buildId'); + return { files, modules, buildId }; +} + +async function pollGraphStatus(percy, buildId, log) { + for (let i = 0; i < POLL_ATTEMPTS; i++) { + await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); + const res = await percy.client.getSmartsnapGraphStatus(buildId); + const status = res?.status; + log.debug(`SmartSnap: graph status (attempt ${i + 1}) = ${status}`); + if (status === 'done' || status === 'failure') return status; + } + return null; +} + +// Given the mapped snapshots and storybook.smartSnap config, returns the subset of snapshots +// that the SmartSnap graph reports as affected. On any recoverable failure, returns the input +// list unchanged so the build runs as a full snapshot pass. +export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir) { + const log = logger('storybook:smartsnap'); + const { baseline, untraced, trace, bailOnChanges, statsFile } = smartSnapConfig || {}; + + + if (!buildDir) { + throw new SmartSnapBailError('SmartSnap requires the Storybook build directory (e.g. `percy storybook ./storybook-static`); URL and `start` modes are not supported. Running full snapshot set'); + } + + // Treat statsFile as a flat filename anchored inside the build directory. + // path.basename() strips any traversal segments so the resolved path can't + // escape buildDir even if the config is hostile. + const statsName = path.basename(statsFile || 'enriched-stats.json'); + if (!/^[\w.-]+\.json$/i.test(statsName)) { + throw new SmartSnapBailError(`SmartSnap: invalid statsFile "${statsName}" — must be a .json filename; running full snapshot set`); + } + const resolvedStatsPath = path.join(path.resolve(buildDir), statsName); + let statsStat; + try { + statsStat = fs.statSync(resolvedStatsPath); + } catch { + throw new SmartSnapBailError(`SmartSnap: stats file "${statsName}" not found in build directory ${buildDir}; running full snapshot set`); + } + if (!statsStat.isFile()) { + throw new SmartSnapBailError(`SmartSnap: stats file "${statsName}" in ${buildDir} is not a regular file; running full snapshot set`); + } + + const snapshotNames = snapshots.map(s => s.name); + // New API shape: `{ base_build_commit_sha, snapshots: { : } }`. + // The single base-build commit replaces the previous per-snapshot commit map — + // baseline prediction now happens server-side via `Percy::BaseBuildService`, + // so we just diff against whatever commit it picked. + const baseLookup = await percy.client.getSmartsnapSnapshotNameToCommit(snapshotNames); + log.debug(`SmartSnap: base lookup ${JSON.stringify(baseLookup)}`); + + let affectedNodes; + let baseRef; + + if (baseline) { + log.debug(`SmartSnap: diffing against explicit baseline "${baseline}"`); + baseRef = baseline; + } else if (baseLookup?.base_build_commit_sha) { + log.debug(`SmartSnap: diffing against predicted base build commit "${baseLookup.base_build_commit_sha}"`); + baseRef = baseLookup.base_build_commit_sha; + } else { + throw new SmartSnapBailError('SmartSnap: API could not predict a base build commit and no explicit baseline was set; running full snapshot set'); + } + affectedNodes = gitDiffNames(baseRef); + + // HEAD already matches the base build commit (or the diff is otherwise + // empty) — there's nothing for the dep graph to map. Bail to a full set. + if (affectedNodes.length === 0) { + throw new SmartSnapBailError('SmartSnap: no files changed between HEAD and base build commit; running full snapshot set'); + } + + // A change to anything under `.storybook/` (preview config, addons, manager + // wiring) can affect every story's render, so the dep graph isn't enough. + const dotStorybookHit = affectedNodes.find(p => p.split(/[/\\]/).includes('.storybook')); + if (dotStorybookHit) { + throw new SmartSnapBailError(`SmartSnap: change to "${dotStorybookHit}" inside .storybook affects all stories; running full snapshot set`); + } + + // Manifest/lockfile changes can shift the dependency tree, so resolve the + // diff at the package level via snyk-nodejs-lockfile-parser and feed the + // changed package names back into the graph. Short-circuits below fall + // back to a full snapshot whenever we can't reason about the change. + const MANIFEST_PATHS = new Set(['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']); + const manifestHits = affectedNodes.filter(p => MANIFEST_PATHS.has(path.basename(p))); + const projectRoot = gitProjectRoot(); + + if (manifestHits.length > 0) { + // Locate the changed manifest's directory from affectedNodes (NOT the git + // root) — monorepos keep package.json + lockfile inside a workspace dir. + // If two changes land in different dirs, we'd need per-workspace resolution + // we don't try to do yet, so bail. + const uniqueDirs = [...new Set(manifestHits.map(p => path.dirname(p)))]; + if (uniqueDirs.length > 1) { + throw new SmartSnapBailError(`SmartSnap: manifest changes span multiple directories (${uniqueDirs.join(', ')}); running full snapshot set`); + } + const manifestDir = uniqueDirs[0]; // repo-relative; '.' for root + const absManifestDir = path.resolve(projectRoot, manifestDir); + + // Pick the lockfile that lives next to the changed manifest. If two + // coexist (e.g. a stray package-lock.json next to yarn.lock) we can't + // pick a canonical source, so bail. + const LOCKFILE_NAMES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']; + const presentLockfiles = LOCKFILE_NAMES.filter(n => fs.existsSync(path.join(absManifestDir, n))); + if (presentLockfiles.length === 0) { + throw new SmartSnapBailError(`SmartSnap: manifest changed in "${manifestDir}" but no lockfile present there; running full snapshot set`); + } + if (presentLockfiles.length > 1) { + throw new SmartSnapBailError(`SmartSnap: multiple lockfiles in "${manifestDir}" (${presentLockfiles.join(', ')}); cannot pick canonical; running full snapshot set`); + } + const lockfileName = presentLockfiles[0]; + // git always uses forward slashes for its : spec; build it by + // hand instead of path.join (which would use backslashes on Windows). + const lockfileRepoPath = manifestDir === '.' ? lockfileName : `${manifestDir}/${lockfileName}`; + + // Resolve the lockfile at the base commit. If it wasn't tracked there + // (first SmartSnap run, lockfile renamed, etc.) we can't diff, so bail. + let oldLockfile; + try { + oldLockfile = git(['show', `${baseRef}:${lockfileRepoPath}`]); + } catch { + throw new SmartSnapBailError(`SmartSnap: lockfile "${lockfileRepoPath}" not present at base ref ${baseRef}; running full snapshot set`); + } + + const newLockfile = fs.readFileSync(path.join(absManifestDir, lockfileName), 'utf8'); + + if (oldLockfile !== newLockfile) { + // Lockfile actually shifted — invoke the diff. If byte-identical, only + // package.json's non-dep fields changed and we fall through unchanged. + const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); + const packageJsonRepoPath = manifestDir === '.' ? "package.json" : `${manifestDir}/package.json`; + const oldPackageJson = git(['show', `${baseRef}:${packageJsonRepoPath}`]); + const packageAffected = await diffLockfileDeps({ + packageJson, oldLockfile, newLockfile, lockfileType: lockfileName, + oldPackageJson + }); + log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); + } + } + + if (untraced?.length) { + affectedNodes = affectedNodes.filter(p => !untraced.some(g => matchesPattern(p, g))); + } + + if (bailOnChanges?.length) { + const bailed = affectedNodes.find(p => bailOnChanges.some(g => matchesPattern(p, g))); + if (bailed) { + throw new SmartSnapBailError(`SmartSnap: change to "${bailed}" matched bailOnChanges; running full snapshot set`); + } + } + + log.debug(`SmartSnap: parsing stats file ${resolvedStatsPath}`); + const { files, modules, buildId } = await readStats(resolvedStatsPath, projectRoot); + + if (typeof buildId !== 'string' || !buildId) { + throw new SmartSnapBailError(`SmartSnap: stats file at ${resolvedStatsPath} is missing a top-level "buildId" — running full snapshot set`); + } + + // Storybook's `parameters.fileName` (and v7+ `entries[id].importPath`) + // both come back with a leading `./` (or `.\` on Windows) — e.g. + // `./src/stories/Foo.stories.tsx`. The compactor's `files` array uses + // `path.relative(projectRoot, ...)` which produces `src/stories/Foo.stories.tsx` + // with no prefix. Normalize using the platform's separator (and also strip + // the POSIX form for Storybook builds on Windows that emit `./` regardless). + const dotPosix = './'; + const dotPlatform = `.${path.sep}`; + const normalizeImportPath = p => { + if (typeof p !== 'string') return p; + if (p.startsWith(dotPlatform)) return p.slice(dotPlatform.length); + if (p.startsWith(dotPosix)) return p.slice(dotPosix.length); + return p; + }; + + const storybookPaths = [...new Set(snapshots.map(s => normalizeImportPath(s.importPath)).filter(Boolean))]; + const snapshotsWithImportPath = snapshots.filter(s => s.importPath).length; + log.debug(`SmartSnap: ${snapshotsWithImportPath}/${snapshots.length} snapshots have importPath; ${storybookPaths.length} unique storybookPaths`); + if (storybookPaths.length === 0) { + log.warn(`SmartSnap: no snapshots have importPath set — check Storybook story extraction. Sample snapshot: ${JSON.stringify({ + id: snapshots[0]?.id, name: snapshots[0]?.name, importPath: snapshots[0]?.importPath, keys: snapshots[0] ? Object.keys(snapshots[0]) : [] + })}`); + } else { + log.debug(`SmartSnap: storybookPaths sample: ${storybookPaths.slice(0, 3).join(', ')}`); + } + + log.debug(`SmartSnap: starting graph generation job ${JSON.stringify({ buildId, files, modules, storybookPaths, affectedNodes })}`); + await percy.client.generateSmartsnapGraph(buildId, { + files, modules, storybookPaths, affectedNodes + }); + + const status = await pollGraphStatus(percy, buildId, log); + if (status !== 'done') { + throw new SmartSnapBailError(`SmartSnap: graph generation did not complete (status: ${status ?? 'timed out'}); running full snapshot set`); + } + + const data = await percy.client.getSmartsnapGraphData(buildId, { trace: trace }); + + // Persist the rendered Drawflow visualization next to where the user ran + // percy, so they can open it after the build finishes. + + log.debug(`SmartSnap: affected stories result ${JSON.stringify(data?.affected_stories)}`); + + if (data?.trace_graph_html) { + const tracePath = path.resolve(process.cwd(), 'trace.html'); + try { + fs.writeFileSync(tracePath, data.trace_graph_html); + log.info(`SmartSnap: trace written to ${tracePath}`); + } catch (e) { + log.warn(`SmartSnap: failed to write trace.html: ${e.message}`); + } + } + + const affected = new Set((data?.affected_stories || []).map(s => s.file_path)); + + // Snapshots whose baseline review_state is `failed` or `rejected` have no + // usable baseline image to diff against, and snapshots that don't appear + // in the base build at all are brand-new (no baseline exists yet). In + // both cases SmartSnap can't legitimately skip them — re-snapshot + // unconditionally regardless of what the affected-graph reports. + const baselineSnapshots = baseLookup?.snapshots || {}; + const FORCE_RESNAPSHOT_STATES = new Set(['failed', 'rejected']); + const needsBaselineRefresh = name => { + const state = baselineSnapshots[name]; + return state === undefined || FORCE_RESNAPSHOT_STATES.has(state); + }; + + // Use the same normalization on lookup so a snapshot's `./src/...` matches + // an affected-stories `src/...` from the API. + let forced = 0; + let affectedKept = 0; + const filtered = snapshots.filter(s => { + if (needsBaselineRefresh(s.name)) { + forced += 1; + return true; + } + const p = normalizeImportPath(s.importPath); + if (p && affected.has(p)) { + affectedKept += 1; + return true; + } + return false; + }); + log.info(`SmartSnap: ${filtered.length} of ${snapshots.length} snapshots kept (${affectedKept} via affected-graph, ${forced} via missing/failed/rejected baseline)`); + return filtered; +} diff --git a/packages/cli/pnpm-lock.yaml b/packages/cli/pnpm-lock.yaml new file mode 100644 index 000000000..2aa4888c3 --- /dev/null +++ b/packages/cli/pnpm-lock.yaml @@ -0,0 +1,1068 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@percy/cli-app': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-build': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-command': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-config': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-doctor': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-exec': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-snapshot': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/cli-upload': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/client': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + '@percy/logger': + specifier: 1.31.13-beta.0 + version: 1.31.13-beta.0 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@percy/cli-app@1.31.13-beta.0': + resolution: {integrity: sha512-moDQKT6MI2SMXFOYHYSN8wiTRK6054C4/U87mAjtim7p0DOb9XgDPAuzncngbVy4Url3Eizbwyc7n0ohrpSKHA==} + engines: {node: '>=14'} + + '@percy/cli-build@1.31.13-beta.0': + resolution: {integrity: sha512-4n1/X4iVwXcEg2DBLPjcYhs4uPxd2X7KGV634Lor/S01wO5Hf9Y9XoM8dTMbIzzMZo2soOZBf7jwiS4GQvRTJQ==} + engines: {node: '>=14'} + + '@percy/cli-command@1.31.13-beta.0': + resolution: {integrity: sha512-WYR7KAUwJLvK9EG2jx9z/g+8wgFytuOEuw24c1NNrG9yZdhlffe+OZM8zcoWwV3+2rASl5fG0tuAatApr9LSKA==} + engines: {node: '>=14'} + hasBin: true + + '@percy/cli-config@1.31.13-beta.0': + resolution: {integrity: sha512-reMgjjed7MAzGz579ALxfo0BF8YvJgRtwJbmAr6+9K8i4ogNRWUJ3oH39ewSWEBjHzf1YtpXQJugnhi/4BZMag==} + engines: {node: '>=14'} + + '@percy/cli-doctor@1.31.13-beta.0': + resolution: {integrity: sha512-ertkEz1+lFre+7fFdQshwtakZqVccMAiGPTvXRfahWJ4kn7626KRoAtHW2t7LSEpIqN8fcgLD78xfHpWmaY5SA==} + engines: {node: '>=14'} + + '@percy/cli-exec@1.31.13-beta.0': + resolution: {integrity: sha512-uYozwBnGguu5H7rSRsysG0ICEQ1YjFE0w01+5gKNm18oKHR61UGsvl8jsy0Jf2fXJpwAWWmQe82ChhwvIzvW7g==} + engines: {node: '>=14'} + + '@percy/cli-snapshot@1.31.13-beta.0': + resolution: {integrity: sha512-3hrWZ31O6HVakC/3AmSFiB1tJAAebrP0s5EPYqhm4/tJszFRNVQs1miAN0b/waWK/L+pdEULx2A1Td1qSG/cYQ==} + engines: {node: '>=14'} + + '@percy/cli-upload@1.31.13-beta.0': + resolution: {integrity: sha512-fP5UYHyE9JeUVQ4VA2yax+/88wiVoWa2XK63QQiswETqeH5OIUTu/Sm7uYBH0PVdyw87I6OgknHIV6JRr6g+BA==} + engines: {node: '>=14'} + + '@percy/client@1.31.13-beta.0': + resolution: {integrity: sha512-55ATMx5TDbZRGzoXZBK7s4RdTL8pEWdRw8ziCkg47M3Wx8ETRoiNxM6GIIIZn+aWL5rsvdeJ0Fbygl109YHz4g==} + engines: {node: '>=14'} + + '@percy/config@1.31.13-beta.0': + resolution: {integrity: sha512-7V0ctz9IcHv1+MCqPN5H2WV44m4c5BofwVuWOLCztRZwnTyV0HgEtUFiHzLcGyr6Bn8KnnUB2WC+jBUYB0nKjA==} + engines: {node: '>=14'} + + '@percy/core@1.31.13-beta.0': + resolution: {integrity: sha512-YdK9D+6SRlbNQxS4cJBmizE/NWSMz8OK86CGLipN0lP/9drJUZWGUjt9dpCrj3H40GnBiv7PMmBcWC0UwpS9cQ==} + engines: {node: '>=14'} + + '@percy/dom@1.31.13-beta.0': + resolution: {integrity: sha512-bVMZTlgnwPuHdvBhUAKNTHLj48vhMLVRO6O0jhL+/zfm7QcWxSgwgvEKgNC6a24nWqNVAc2hBno8yRlcC83Sqg==} + + '@percy/env@1.31.13-beta.0': + resolution: {integrity: sha512-2AnqPea92xf5aSm6WH9g4KO2I1FQhVkdsXXIbI+zvfUwlR+vKXO55Tld6UNa88quHIZSABRKS0z8QQeU1FVK3A==} + engines: {node: '>=14'} + + '@percy/logger@1.31.13-beta.0': + resolution: {integrity: sha512-/uvaji1GyAQBvYeyzI9f7HOiuTeLqBNOivCnRa0FEmMem8OVGQjKa3tsyyAAe5OyXFU+/zjqYI1365BZLbiRZQ==} + engines: {node: '>=14'} + + '@percy/monitoring@1.31.13-beta.0': + resolution: {integrity: sha512-6nR9SWH/gR8MvpzpwO0F6kM8K4xsX1DfCg3tNTcOsLGfbnI27hm9dZInCOOmBXwajuEtSwUs54fnN1w8QK3FMA==} + engines: {node: '>=14'} + + '@percy/sdk-utils@1.31.13-beta.0': + resolution: {integrity: sha512-c6EI8lUqG0me7O2Ijr9TB+6zu9kvBAogYmBRj460oq7WiidH8Mr7w6fOMqYGdyfD6y3T2lydJ7pvrm43y//qww==} + engines: {node: '>=14'} + + '@percy/webdriver-utils@1.31.13-beta.0': + resolution: {integrity: sha512-5Z6u+UFBT3ldFANfVGd6mv0awufuwQ1mOsoGNWTPDToNZ1Un8+FPJsHSThbdAnnTJvCT2RcblrdyHgRZkm5sWg==} + engines: {node: '>=14'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/node@25.7.0': + resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + systeminformation@5.31.6: + resolution: {integrity: sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici-types@7.21.0: + resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@percy/cli-app@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + '@percy/cli-exec': 1.31.13-beta.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@percy/cli-build@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + transitivePeerDependencies: + - typescript + + '@percy/cli-command@1.31.13-beta.0': + dependencies: + '@percy/config': 1.31.13-beta.0 + '@percy/core': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@percy/cli-config@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + transitivePeerDependencies: + - typescript + + '@percy/cli-doctor@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + '@percy/client': 1.31.13-beta.0 + '@percy/config': 1.31.13-beta.0 + '@percy/core': 1.31.13-beta.0 + '@percy/env': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + '@percy/monitoring': 1.31.13-beta.0 + minimatch: 9.0.9 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@percy/cli-exec@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + cross-spawn: 7.0.6 + which: 2.0.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@percy/cli-snapshot@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + yaml: 2.9.0 + transitivePeerDependencies: + - typescript + + '@percy/cli-upload@1.31.13-beta.0': + dependencies: + '@percy/cli-command': 1.31.13-beta.0 + fast-glob: 3.3.3 + image-size: 1.2.1 + transitivePeerDependencies: + - typescript + + '@percy/client@1.31.13-beta.0': + dependencies: + '@percy/config': 1.31.13-beta.0 + '@percy/env': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + pac-proxy-agent: 7.2.0 + pako: 2.1.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@percy/config@1.31.13-beta.0': + dependencies: + '@percy/logger': 1.31.13-beta.0 + ajv: 8.20.0 + cosmiconfig: 8.3.6 + yaml: 2.9.0 + transitivePeerDependencies: + - typescript + + '@percy/core@1.31.13-beta.0': + dependencies: + '@percy/client': 1.31.13-beta.0 + '@percy/config': 1.31.13-beta.0 + '@percy/dom': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + '@percy/monitoring': 1.31.13-beta.0 + '@percy/webdriver-utils': 1.31.13-beta.0 + content-disposition: 0.5.4 + cross-spawn: 7.0.6 + extract-zip: 2.0.1 + fast-glob: 3.3.3 + micromatch: 4.0.8 + mime-types: 2.1.35 + pako: 2.1.0 + path-to-regexp: 6.3.0 + rimraf: 3.0.2 + ws: 8.20.1 + yaml: 2.9.0 + optionalDependencies: + '@percy/cli-doctor': 1.31.13-beta.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@percy/dom@1.31.13-beta.0': {} + + '@percy/env@1.31.13-beta.0': + dependencies: + '@percy/logger': 1.31.13-beta.0 + + '@percy/logger@1.31.13-beta.0': {} + + '@percy/monitoring@1.31.13-beta.0': + dependencies: + '@percy/config': 1.31.13-beta.0 + '@percy/logger': 1.31.13-beta.0 + '@percy/sdk-utils': 1.31.13-beta.0 + systeminformation: 5.31.6 + transitivePeerDependencies: + - supports-color + - typescript + + '@percy/sdk-utils@1.31.13-beta.0': + dependencies: + pac-proxy-agent: 7.2.0 + transitivePeerDependencies: + - supports-color + + '@percy/webdriver-utils@1.31.13-beta.0': + dependencies: + '@percy/config': 1.31.13-beta.0 + '@percy/sdk-utils': 1.31.13-beta.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/node@25.7.0': + dependencies: + undici-types: 7.21.0 + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.7.0 + optional: true + + agent-base@7.1.4: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + argparse@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + balanced-match@1.0.2: {} + + basic-ftp@5.3.1: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-crc32@0.2.13: {} + + callsites@3.1.0: {} + + concat-map@0.0.1: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + cosmiconfig@8.3.6: + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@6.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.2: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fs.realpath@1.0.0: {} + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + lines-and-columns@1.2.4: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + ms@2.1.3: {} + + netmask@2.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + queue-microtask@1.2.3: {} + + queue@6.0.2: + dependencies: + inherits: 2.0.4 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.2.0 + smart-buffer: 4.2.0 + + source-map@0.6.1: + optional: true + + systeminformation@5.31.6: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.8.1: {} + + undici-types@7.21.0: + optional: true + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.20.1: {} + + yaml@2.9.0: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/yarn.lock b/yarn.lock index 51af32576..a40179313 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,13 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@arcanis/slice-ansi@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@arcanis/slice-ansi/-/slice-ansi-1.1.1.tgz#0ee328a68996ca45854450033a3d161421dc4f55" + integrity sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w== + dependencies: + grapheme-splitter "^1.0.4" + "@babel/cli@^7.11.6": version "7.24.7" resolved "https://registry.npmjs.org/@babel/cli/-/cli-7.24.7.tgz" @@ -1162,6 +1169,13 @@ resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz" @@ -2237,6 +2251,18 @@ dependencies: esquery "^1.0.1" +"@pnpm/crypto.base32-hash@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@pnpm/crypto.base32-hash/-/crypto.base32-hash-1.0.1.tgz#e0eeff4ae736d2a781e41041206a65fe78704ffd" + integrity sha512-pzAXNn6KxTA3kbcI3iEnYs4vtH51XEVqmK/1EiD18MaPKylhqy8UvMJK3zKG+jeP82cqQbozcTGm4yOQ8i3vNw== + dependencies: + rfc4648 "^1.5.1" + +"@pnpm/types@8.9.0": + version "8.9.0" + resolved "https://registry.yarnpkg.com/@pnpm/types/-/types-8.9.0.tgz#9636d5f0642793432f72609b79458ca9be049b02" + integrity sha512-3MYHYm8epnciApn6w5Fzx6sepawmsNU7l6lvIq+ER22/DPSrr83YMhU/EQWnf4lORn2YyiXFj0FJSyJzEtIGmw== + "@rollup/plugin-alias@^5.1.1": version "5.1.1" resolved "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz" @@ -2302,11 +2328,77 @@ resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + +"@snyk/dep-graph@^2.12.0": + version "2.16.8" + resolved "https://registry.yarnpkg.com/@snyk/dep-graph/-/dep-graph-2.16.8.tgz#d5f5940e6758618a9c963b0966aecba575c766e9" + integrity sha512-vlUCjaV8XE6bJEZ7ZfyMnOb+oUWNDzSMeeuO9ek32JR4tpnIgb1DT6EFJw3zeFDxk0e9cb/PznK/7JxyM1TL7Q== + dependencies: + event-loop-spinner "^2.1.0" + lodash.clone "^4.5.0" + lodash.constant "^3.0.0" + lodash.filter "^4.6.0" + lodash.foreach "^4.5.0" + lodash.isempty "^4.4.0" + lodash.isequal "^4.5.0" + lodash.isfunction "^3.0.9" + lodash.isundefined "^3.0.1" + lodash.map "^4.6.0" + lodash.reduce "^4.6.0" + lodash.size "^4.2.0" + lodash.transform "^4.6.0" + lodash.union "^4.6.0" + lodash.values "^4.3.0" + object-hash "^3.0.0" + packageurl-js "2.0.1" + semver "^7.0.0" + tslib "^2" + +"@snyk/error-catalog-nodejs-public@^5.16.0": + version "5.81.0" + resolved "https://registry.yarnpkg.com/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.81.0.tgz#8ee889eb016de0ef693e8b661f5a42d28a05da9e" + integrity sha512-vR3fVLYTdHXujpLp/u2afrqQwCHNQj7sb3EKwqTNpgdtgJhluetcc/Qrf+PfMWvh/VioKCi17wO3nWaaGVAdyw== + dependencies: + tslib "^2.8.1" + uuid "^11.1.0" + +"@snyk/graphlib@2.1.9-patch.3": + version "2.1.9-patch.3" + resolved "https://registry.yarnpkg.com/@snyk/graphlib/-/graphlib-2.1.9-patch.3.tgz#b8edb2335af1978db7f3cb1f28f5d562960acf23" + integrity sha512-bBY9b9ulfLj0v2Eer0yFYa3syVeIxVKl2EpxSrsVeT4mjA0CltZyHsF0JjoaGXP27nItTdJS5uVsj1NA+3aE+Q== + dependencies: + lodash.clone "^4.5.0" + lodash.constant "^3.0.0" + lodash.filter "^4.6.0" + lodash.foreach "^4.5.0" + lodash.has "^4.5.2" + lodash.isempty "^4.4.0" + lodash.isfunction "^3.0.9" + lodash.isundefined "^3.0.1" + lodash.keys "^4.2.0" + lodash.map "^4.6.0" + lodash.reduce "^4.6.0" + lodash.size "^4.2.0" + lodash.transform "^4.6.0" + lodash.union "^4.6.0" + lodash.values "^4.3.0" + "@socket.io/component-emitter@~3.1.0": version "3.1.2" resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -2322,6 +2414,16 @@ resolved "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz" integrity sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ== +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz" @@ -2332,6 +2434,11 @@ resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/emscripten@^1.39.6": + version "1.41.5" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.41.5.tgz#5670e4b52b098691cb844b84ee48c9176699b68d" + integrity sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q== + "@types/eslint@^7.2.13": version "7.28.1" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz" @@ -2355,6 +2462,11 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/http-cache-semantics@*": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#f6a7788f438cbfde15f29acad46512b4c01913b3" + integrity sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q== + "@types/json-schema@*": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz" @@ -2365,6 +2477,13 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz" @@ -2400,6 +2519,23 @@ resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/responselike@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== + dependencies: + "@types/node" "*" + +"@types/semver@^7.1.0": + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== + +"@types/treeify@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/treeify/-/treeify-1.0.3.tgz#f502e11e851b1464d5e80715d5ce3705ad864638" + integrity sha512-hx0o7zWEUU4R2Amn+pjCBQQt23Khy/Dk56gQU5xi5jtPL1h83ACJCeFaB2M/+WO1AntvWrSoVnnCAfI1AQH4Cg== + "@types/yauzl@^2.9.1": version "2.9.1" resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz" @@ -2407,6 +2543,54 @@ dependencies: "@types/node" "*" +"@yarnpkg/core@^4.4.1": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/core/-/core-4.7.0.tgz#ece83ad51eadb7786df38dbf7314cd77917d72fb" + integrity sha512-/zOgPAcDvwx8NDzLidDFaZDg+3Jgv824C4fudqZFlUuWv/BzOEtjfRTAyi+O0h15FyT7YvlsMnQpmnIQB+LgKw== + dependencies: + "@arcanis/slice-ansi" "^1.1.1" + "@types/semver" "^7.1.0" + "@types/treeify" "^1.0.0" + "@yarnpkg/fslib" "^3.1.5" + "@yarnpkg/libzip" "^3.2.2" + "@yarnpkg/parsers" "^3.0.3" + "@yarnpkg/shell" "^4.1.3" + camelcase "^5.3.1" + chalk "^4.1.2" + ci-info "^4.0.0" + clipanion "^4.0.0-rc.2" + cross-spawn "^7.0.3" + diff "^5.1.0" + dotenv "^16.3.1" + es-toolkit "^1.39.7" + fast-glob "^3.2.2" + got "^11.7.0" + hpagent "^1.2.0" + micromatch "^4.0.2" + p-limit "^2.2.0" + semver "^7.1.2" + strip-ansi "^6.0.0" + tar "^7.5.3" + tinylogic "^2.0.0" + treeify "^1.1.0" + tslib "^2.4.0" + +"@yarnpkg/fslib@^3.1.2", "@yarnpkg/fslib@^3.1.3", "@yarnpkg/fslib@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@yarnpkg/fslib/-/fslib-3.1.5.tgz#e06924ab0bb312abe4e1e23a462cd481c446741e" + integrity sha512-hXaPIWl5GZA+rXcx+yaKWUuePJruZuD+3A5A2X6paEBfFsyCD7oEp88lSMj1ym1ehBWUmhNH/YGOp+SrbmSBPg== + dependencies: + tslib "^2.4.0" + +"@yarnpkg/libzip@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@yarnpkg/libzip/-/libzip-3.2.2.tgz#075a25ff850e898dfb1c68df515ddaf5809bbcbe" + integrity sha512-Kqxgjfy6SwwC4tTGQYToIWtUhIORTpkowqgd9kkMiBixor0eourHZZAggt/7N4WQKt9iCyPSkO3Xvr44vXUBAw== + dependencies: + "@types/emscripten" "^1.39.6" + "@yarnpkg/fslib" "^3.1.3" + tslib "^2.4.0" + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" @@ -2420,6 +2604,28 @@ js-yaml "^3.10.0" tslib "^2.4.0" +"@yarnpkg/parsers@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@yarnpkg/parsers/-/parsers-3.0.3.tgz#624f35f242c1115a48beb1fd12aed610bcd8e823" + integrity sha512-mQZgUSgFurUtA07ceMjxrWkYz8QtDuYkvPlu0ZqncgjopQ0t6CNEo/OSealkmnagSUx8ZD5ewvezUwUuMqutQg== + dependencies: + js-yaml "^3.10.0" + tslib "^2.4.0" + +"@yarnpkg/shell@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@yarnpkg/shell/-/shell-4.1.3.tgz#a99a1bcfb8ca1d896440046b71d2b254d0712948" + integrity sha512-5igwsHbPtSAlLdmMdKqU3atXjwhtLFQXsYAG0sn1XcPb3yF8WxxtWxN6fycBoUvFyIHFz1G0KeRefnAy8n6gdw== + dependencies: + "@yarnpkg/fslib" "^3.1.2" + "@yarnpkg/parsers" "^3.0.3" + chalk "^4.1.2" + clipanion "^4.0.0-rc.2" + cross-spawn "^7.0.3" + fast-glob "^3.2.2" + micromatch "^4.0.2" + tslib "^2.4.0" + "@zkochan/js-yaml@0.0.6": version "0.0.6" resolved "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz" @@ -2711,6 +2917,11 @@ astral-regex@^2.0.0: resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@^3.2.2: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + async@^3.2.3: version "3.2.4" resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" @@ -2968,6 +3179,24 @@ cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: tar "^6.1.11" unique-filename "^1.1.1" +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz" @@ -3038,7 +3267,7 @@ chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3071,11 +3300,21 @@ chownr@^2.0.0: resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" @@ -3103,6 +3342,13 @@ cli-width@^3.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +clipanion@^4.0.0-rc.2: + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-4.0.0-rc.4.tgz#7191a940e47ef197e5f18c9cbbe419278b5f5903" + integrity sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q== + dependencies: + typanion "^3.8.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" @@ -3130,6 +3376,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" @@ -3524,6 +3777,13 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" @@ -3553,6 +3813,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" @@ -3605,6 +3870,16 @@ depd@^1.1.2: resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dependency-path@^9.2.8: + version "9.2.8" + resolved "https://registry.yarnpkg.com/dependency-path/-/dependency-path-9.2.8.tgz#9fe05be8d69ad1943a2084e4d86f3063c4b50c01" + integrity sha512-S0OhIK7sIyAsph8hVH/LMCTDL3jozKtlrPx3dMQrlE2nAlXTquTT+AcOufphDMTQqLkfn4acvfiem9I1IWZ4jQ== + dependencies: + "@pnpm/crypto.base32-hash" "1.0.1" + "@pnpm/types" "8.9.0" + encode-registry "^3.0.0" + semver "^7.3.8" + deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" @@ -3643,6 +3918,11 @@ diff-sequences@^29.4.3: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" + integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -3688,6 +3968,11 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dotenv@^16.3.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + dotenv@~10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" @@ -3729,6 +4014,13 @@ emoji-regex@^8.0.0: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encode-registry@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/encode-registry/-/encode-registry-3.0.1.tgz#cb925d0db14ce59b18882b62c67133721b0846d1" + integrity sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw== + dependencies: + mem "^8.0.0" + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" @@ -3928,6 +4220,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-toolkit@^1.39.7: + version "1.46.1" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.46.1.tgz#38ca27191a98a867fc544b81cf1477a68947fb06" + integrity sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ== + es6-error@^4.0.1: version "4.1.1" resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz" @@ -4194,6 +4491,13 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-loop-spinner@^2.0.0, event-loop-spinner@^2.1.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/event-loop-spinner/-/event-loop-spinner-2.3.2.tgz#cbf92985dccf8ce52c8905b33b8ac909c33f5605" + integrity sha512-O078Lkxi/yZEPPifcizDOGUeK1OFOlPC6sfCCrx10odvqX3tEi9XLaIRt9cIl9TBFcPZzuMaXbJ0b+T6D2Tnjg== + dependencies: + tslib "^2.6.3" + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -4255,7 +4559,7 @@ fast-glob@3.2.7: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.2.9: version "3.3.3" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -4699,6 +5003,11 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.4: version "7.1.4" resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz" @@ -4808,11 +5117,33 @@ gopd@^1.2.0: resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +got@^11.7.0: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6: version "4.2.9" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + handlebars@^4.7.7: version "4.7.9" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.9.tgz#6f139082ab58dc4e5a0e51efe7db5ae890d56a0f" @@ -4927,11 +5258,21 @@ hosted-git-info@^5.0.0: dependencies: lru-cache "^7.5.1" +hpagent@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903" + integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-cache-semantics@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + http-cache-semantics@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" @@ -4974,6 +5315,14 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" @@ -5587,6 +5936,11 @@ jsesc@~0.5.0: resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" @@ -5737,6 +6091,13 @@ karma@^6.0.2: ua-parser-js "^0.7.30" yargs "^16.1.1" +keyv@^4.0.0: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" @@ -5848,36 +6209,126 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.constant@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash.constant/-/lodash.constant-3.0.0.tgz#bfe05cce7e515b3128925d6362138420bd624910" + integrity sha512-X5XMrB+SdI1mFa81162NSTo/YNd23SLdLOLzcXTwS4inDZ5YCL8X67UFzZJAH4CqIa6R8cr56CShfA5K5MFiYQ== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.filter@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + integrity sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ== + +lodash.flatmap@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e" + integrity sha512-/OcpcAGWlrZyoHGeHh3cAoa6nGdX6QYtmzNP84Jqol6UEQQ2gIaU3H+0eICcjcKGl0/XF8LWOujNn9lffsnaOg== + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ== + +lodash.has@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g== + +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isundefined@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" + integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA== + +lodash.keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" + integrity sha512-J79MkJcp7Df5mizHiVNpjoHXLi4HLjh9VLS/M7lQSGoQ+0oQ+lWEigREkqKyizPB1IawvQLLKY8mzEcm1tkyxQ== + +lodash.map@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + integrity sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.reduce@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" + integrity sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw== + +lodash.size@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.size/-/lodash.size-4.2.0.tgz#71fe75ed3eabdb2bcb73a1b0b4f51c392ee27b86" + integrity sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ== + +lodash.topairs@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.topairs/-/lodash.topairs-4.3.0.tgz#3b6deaa37d60fb116713c46c5f17ea190ec48d64" + integrity sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ== + +lodash.transform@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0" + integrity sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + +lodash.values@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + integrity sha512-r0RwvdCv8id9TUblb/O7rYPwVy6lerCbcawrfdo9iC/1t1wsNMJknO79WNBgwkH0hIeJ08jmvvESbFpNb4jH0Q== + lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.10: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -5916,6 +6367,11 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.0.2" +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" @@ -5984,6 +6440,13 @@ make-fetch-happen@^10.0.3, make-fetch-happen@^10.0.6: socks-proxy-agent "^7.0.0" ssri "^9.0.0" +map-age-cleaner@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-obj@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" @@ -6004,6 +6467,14 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" + integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== + dependencies: + map-age-cleaner "^0.1.3" + mimic-fn "^3.1.0" + memfs@^3.4.0: version "3.4.12" resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.12.tgz" @@ -6056,7 +6527,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4, micromatch@^4.0.8: +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6086,6 +6557,21 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" @@ -6216,6 +6702,11 @@ minipass@^5.0.0: resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +minipass@^7.0.4, minipass@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" @@ -6224,6 +6715,13 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" +minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== + dependencies: + minipass "^7.1.2" + mkdirp-infer-owner@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz" @@ -6393,6 +6891,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz" @@ -6580,6 +7083,11 @@ object-assign@^4: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.13.1, object-inspect@^1.9.0: version "1.13.1" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" @@ -6697,6 +7205,16 @@ os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" @@ -6830,6 +7348,11 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" +packageurl-js@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/packageurl-js/-/packageurl-js-2.0.1.tgz#a8fa43a64971b5dd0dca5fb904b950a6cc317a6f" + integrity sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg== + pacote@^13.0.3, pacote@^13.6.1: version "13.6.1" resolved "https://registry.npmjs.org/pacote/-/pacote-13.6.1.tgz" @@ -7171,6 +7694,11 @@ quick-lru@^4.0.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + range-parser@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" @@ -7394,6 +7922,11 @@ reselect@^4.1.7: resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz" integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -7420,6 +7953,13 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.2 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" @@ -7438,6 +7978,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc4648@^1.5.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.4.tgz#1174c0afba72423a0b70c386ecfeb80aa61b05ca" + integrity sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg== + rfdc@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" @@ -7532,6 +8077,11 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semve resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.1.2, semver@^7.3.8, semver@^7.6.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df" + integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA== + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" @@ -7636,6 +8186,40 @@ smart-buffer@^4.2.0: resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +snyk-config@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-5.3.0.tgz#5ea906ad1c9c6151dd87c4e523c1d9659f5cf0e3" + integrity sha512-YPxhYZXBXgnYdvovwlKf5JYcOp+nxB7lel3tWLarYqZ4hwxN118FodIFb8nSqMrepsPdyOaQYKKnrTYqvQeaJA== + dependencies: + async "^3.2.2" + debug "^4.3.4" + lodash.merge "^4.6.2" + minimist "^1.2.6" + +snyk-nodejs-lockfile-parser@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-2.7.1.tgz#e175501c9691f70c6a32b590956e680ce0c0048b" + integrity sha512-ViG434ZhiWXRtAEXVS2yjkHKz6Yk1lj9FxyMYWjRDZN/VplvrTGhkI/6BISgKotkR9h+QPsmVHiwWdoeVoqRog== + dependencies: + "@snyk/dep-graph" "^2.12.0" + "@snyk/error-catalog-nodejs-public" "^5.16.0" + "@snyk/graphlib" "2.1.9-patch.3" + "@yarnpkg/core" "^4.4.1" + "@yarnpkg/lockfile" "^1.1.0" + dependency-path "^9.2.8" + event-loop-spinner "^2.0.0" + js-yaml "^4.1.0" + lodash.clonedeep "^4.5.0" + lodash.flatmap "^4.5.0" + lodash.isempty "^4.4.0" + lodash.topairs "^4.3.0" + micromatch "^4.0.8" + p-map "^4.0.0" + semver "^7.6.0" + snyk-config "^5.2.0" + tslib "^1.9.3" + uuid "^8.3.0" + socket.io-adapter@~2.5.2: version "2.5.5" resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz" @@ -7810,6 +8394,18 @@ statuses@~1.5.0: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stream-chain@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" + integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA== + +stream-json@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.9.1.tgz#e3fec03e984a503718946c170db7d74556c2a187" + integrity sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw== + dependencies: + stream-chain "^2.2.5" + streamroller@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/streamroller/-/streamroller-3.0.2.tgz" @@ -8003,6 +8599,17 @@ tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^7.5.3: + version "7.5.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.15.tgz#afe6d1316cddf614a566e3813e42fe01aed46fee" + integrity sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.1.0" + yallist "^5.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" @@ -8047,6 +8654,11 @@ through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tinylogic@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinylogic/-/tinylogic-2.0.0.tgz#0d2409c492b54c0663082ac1e3f16be64497bb47" + integrity sha512-dljTkiLLITtsjqBvTA1MRZQK/sGP4kI3UJKc3yA9fMzYbMF2RhcN04SeROVqJBIYYOoJMM8u0WDnhFwMSFQotw== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -8083,6 +8695,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +treeify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" + integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== + treeverse@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/treeverse/-/treeverse-2.0.0.tgz" @@ -8121,11 +8738,21 @@ tsd@^0.31.2: path-exists "^4.0.0" read-pkg-up "^7.0.0" -tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.8.1: +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.3, tslib@^2.8.1: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +typanion@^3.8.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.14.0.tgz#a766a91810ce8258033975733e836c43a2929b94" + integrity sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -8334,12 +8961,17 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@^11.1.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad" + integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ== + uuid@^3.3.3: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -8594,6 +9226,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yaml@^1.10.0: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" From 88ab9f6d25a4c6eba141e682ed67d19aee6d599d Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 15 May 2026 11:32:26 +0530 Subject: [PATCH 07/29] make package lockfile parser optional --- packages/cli-command/package.json | 4 +- packages/cli-command/src/lockfileDiff.js | 70 +++++++++++++++++++----- packages/cli-command/src/smartsnap.js | 20 +++++-- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/packages/cli-command/package.json b/packages/cli-command/package.json index 9e816b9a4..7c67dc638 100644 --- a/packages/cli-command/package.json +++ b/packages/cli-command/package.json @@ -41,7 +41,9 @@ "@percy/core": "1.31.14-beta.3", "@percy/logger": "1.31.14-beta.3", "glob-to-regexp": "^0.4.1", - "snyk-nodejs-lockfile-parser": "2.7.1", "stream-json": "^1.8.0" + }, + "optionalDependencies": { + "snyk-nodejs-lockfile-parser": "2.7.1" } } diff --git a/packages/cli-command/src/lockfileDiff.js b/packages/cli-command/src/lockfileDiff.js index 9154311e4..8b2430fec 100644 --- a/packages/cli-command/src/lockfileDiff.js +++ b/packages/cli-command/src/lockfileDiff.js @@ -1,17 +1,35 @@ import { createRequire } from 'module'; +import logger from '@percy/logger'; -// snyk-nodejs-lockfile-parser is CommonJS. Load via createRequire so the -// named imports resolve correctly under "type": "module". +// snyk-nodejs-lockfile-parser is a CommonJS optionalDependency. It requires +// Node >=18 while the CLI supports Node >=14, so we defer the require to call +// time — that way importing this module never throws on older Node versions +// (or when the optional install was skipped for any other reason). Cached on +// first successful load so the require only resolves once per process. const require = createRequire(import.meta.url); -const { buildDepTree, LockfileType } = require('snyk-nodejs-lockfile-parser'); +let _snykModule; +function loadSnyk() { + if (_snykModule) return _snykModule; + try { + _snykModule = require('snyk-nodejs-lockfile-parser'); + return _snykModule; + } catch (e) { + // Surface the underlying require failure so callers can distinguish + // "not installed" (engine mismatch / optional skip) from genuine parse + // failures that happen later inside buildDepTree. + const err = new Error(`snyk-nodejs-lockfile-parser is not available (requires Node >=18, or the optional install was skipped): ${e.message}`); + err.code = 'SNYK_LOCKFILE_PARSER_UNAVAILABLE'; + throw err; + } +} -// Map the on-disk filename to snyk's LockfileType enum. yarn.lock could be -// either yarn classic (v1) or berry (v2+); we default to yarn v1 here and -// can refine by sniffing the lockfile header in a follow-up if needed. -const TYPE_BY_FILENAME = { - 'package-lock.json': LockfileType.npm, - 'yarn.lock': LockfileType.yarn, - 'pnpm-lock.yaml': LockfileType.pnpm +// Map the on-disk filename to snyk's LockfileType enum key. We can't resolve +// the enum value at module-eval time because the snyk import is deferred, so +// we look up the value inside diffLockfileDeps after loadSnyk() runs. +const TYPE_KEY_BY_FILENAME = { + 'package-lock.json': 'npm', + 'yarn.lock': 'yarn', + 'pnpm-lock.yaml': 'pnpm' }; // Walk snyk's PkgTree (a recursive `{ name, version, dependencies: { ... } }` @@ -63,15 +81,37 @@ function topLevelDeps(packageJsonContents) { // bumped `^5.8.3` to `^5.18.0` but the lockfile already resolved to // 5.18.0 under the old range, so the resolved tree looks identical. export async function diffLockfileDeps({ packageJson, oldPackageJson, oldLockfile, newLockfile, lockfileType }) { - const type = TYPE_BY_FILENAME[lockfileType]; + const log = logger('storybook:smartsnap:lockfile'); + const { buildDepTree, LockfileType } = loadSnyk(); + const typeKey = TYPE_KEY_BY_FILENAME[lockfileType]; + const type = typeKey && LockfileType[typeKey]; if (!type) { throw new Error(`Unsupported lockfile type: ${lockfileType}`); } - const [oldTree, newTree] = await Promise.all([ - buildDepTree(oldPackageJson, oldLockfile, true, type), - buildDepTree(packageJson, newLockfile, true, type) - ]); + // The two buildDepTree calls are kept sequential (not Promise.all'd) so that + // when one of them throws the log message identifies *which* side failed — + // old vs. new lockfile parsing behave differently when the user's lockfile + // is in an unexpected state. + let oldTree; + try { + log.debug('buildDepTree: parsing OLD lockfile...'); + oldTree = await buildDepTree(oldPackageJson, oldLockfile, true, type); + log.debug('buildDepTree: OLD lockfile parsed successfully'); + } catch (e) { + log.warn(`buildDepTree: OLD lockfile failed to parse: ${e.message}`); + throw e; + } + + let newTree; + try { + log.debug('buildDepTree: parsing NEW lockfile...'); + newTree = await buildDepTree(packageJson, newLockfile, true, type); + log.debug('buildDepTree: NEW lockfile parsed successfully'); + } catch (e) { + log.warn(`buildDepTree: NEW lockfile failed to parse: ${e.message}`); + throw e; + } const oldPkgs = flattenPkgTree(oldTree); const newPkgs = flattenPkgTree(newTree); diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 04d80d930..ee476983d 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -295,11 +295,21 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); const packageJsonRepoPath = manifestDir === '.' ? "package.json" : `${manifestDir}/package.json`; const oldPackageJson = git(['show', `${baseRef}:${packageJsonRepoPath}`]); - const packageAffected = await diffLockfileDeps({ - packageJson, oldLockfile, newLockfile, lockfileType: lockfileName, - oldPackageJson - }); - log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); + try { + const packageAffected = await diffLockfileDeps({ + packageJson, oldLockfile, newLockfile, lockfileType: lockfileName, + oldPackageJson + }); + log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); + } catch (e) { + // snyk-nodejs-lockfile-parser is an optionalDependency (requires Node >=18). + // When it's missing we can't reason about the manifest change, so we + // conservatively bail to a full snapshot rather than under-snapshotting. + if (e.code === 'SNYK_LOCKFILE_PARSER_UNAVAILABLE') { + throw new SmartSnapBailError(`SmartSnap: ${e.message}; running full snapshot set`); + } + throw e; + } } } From 2a4589c85e06f0dfc6710f52058ab110bef4e4ec Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 15 May 2026 11:45:57 +0530 Subject: [PATCH 08/29] fix lint issue --- packages/cli-command/src/smartsnap.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index ee476983d..6c0722068 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -184,7 +184,6 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir const log = logger('storybook:smartsnap'); const { baseline, untraced, trace, bailOnChanges, statsFile } = smartSnapConfig || {}; - if (!buildDir) { throw new SmartSnapBailError('SmartSnap requires the Storybook build directory (e.g. `percy storybook ./storybook-static`); URL and `start` modes are not supported. Running full snapshot set'); } @@ -293,11 +292,14 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir // Lockfile actually shifted — invoke the diff. If byte-identical, only // package.json's non-dep fields changed and we fall through unchanged. const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); - const packageJsonRepoPath = manifestDir === '.' ? "package.json" : `${manifestDir}/package.json`; + const packageJsonRepoPath = manifestDir === '.' ? 'package.json' : `${manifestDir}/package.json`; const oldPackageJson = git(['show', `${baseRef}:${packageJsonRepoPath}`]); try { const packageAffected = await diffLockfileDeps({ - packageJson, oldLockfile, newLockfile, lockfileType: lockfileName, + packageJson, + oldLockfile, + newLockfile, + lockfileType: lockfileName, oldPackageJson }); log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); From 6d9ff03bcb5cf9cf9c45f3df0211f3da859a07c5 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 22 May 2026 13:58:12 +0530 Subject: [PATCH 09/29] update cli to match new API --- packages/cli-command/src/smartsnap.js | 73 +++++++++++------ packages/client/src/client.js | 46 ++++------- packages/client/test/client.test.js | 110 ++++++-------------------- 3 files changed, 89 insertions(+), 140 deletions(-) diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 6c0722068..2e453bc17 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -166,15 +166,25 @@ async function readStats(statsFile, projectRoot) { return { files, modules, buildId }; } +// Polls `job_status?sync=true&type=smartsnap_graph` — the sync response blocks +// server-side until the job moves off `in_progress`, but the API enforces a +// shorter timeout than the job can take, so we retry up to POLL_ATTEMPTS +// times. On `done` the response also carries the graph payload (affected +// stories, trace HTML); the caller reads it directly, no second fetch needed. +// +// Response shape is `{ : { status, data? } }` — the job_status +// endpoint accepts a comma-separated id list and always keys the response +// by id, even for a single id. async function pollGraphStatus(percy, buildId, log) { for (let i = 0; i < POLL_ATTEMPTS; i++) { await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); - const res = await percy.client.getSmartsnapGraphStatus(buildId); - const status = res?.status; + const res = await percy.client.getStatus('smartsnap_graph', [buildId]); + const entry = res?.[buildId]; + const status = entry?.status; log.debug(`SmartSnap: graph status (attempt ${i + 1}) = ${status}`); - if (status === 'done' || status === 'failure') return status; + if (status === 'done' || status === 'failure') return { status, data: entry?.data }; } - return null; + return { status: null }; } // Given the mapped snapshots and storybook.smartSnap config, returns the subset of snapshots @@ -206,12 +216,13 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir throw new SmartSnapBailError(`SmartSnap: stats file "${statsName}" in ${buildDir} is not a regular file; running full snapshot set`); } - const snapshotNames = snapshots.map(s => s.name); // New API shape: `{ base_build_commit_sha, snapshots: { : } }`. // The single base-build commit replaces the previous per-snapshot commit map — // baseline prediction now happens server-side via `Percy::BaseBuildService`, - // so we just diff against whatever commit it picked. - const baseLookup = await percy.client.getSmartsnapSnapshotNameToCommit(snapshotNames); + // so we just diff against whatever commit it picked. The set of snapshot + // names is no longer sent; the API resolves baselines from the project + + // git/PR context alone. + const baseLookup = await percy.client.getSmartsnapSnapshotNameToCommit(); log.debug(`SmartSnap: base lookup ${JSON.stringify(baseLookup)}`); let affectedNodes; @@ -333,19 +344,34 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir throw new SmartSnapBailError(`SmartSnap: stats file at ${resolvedStatsPath} is missing a top-level "buildId" — running full snapshot set`); } - // Storybook's `parameters.fileName` (and v7+ `entries[id].importPath`) - // both come back with a leading `./` (or `.\` on Windows) — e.g. - // `./src/stories/Foo.stories.tsx`. The compactor's `files` array uses - // `path.relative(projectRoot, ...)` which produces `src/stories/Foo.stories.tsx` - // with no prefix. Normalize using the platform's separator (and also strip - // the POSIX form for Storybook builds on Windows that emit `./` regardless). + // Storybook's `entries[id].importPath` (and v6 `parameters.fileName`) + // is resolved relative to the directory percy was invoked from — for a + // monorepo storybook that's typically the package dir (e.g. + // `frontend/packages/design-stack`), not the git root. Example: + // `./modules/AgentCard/AgentCard.stories.tsx`. + // + // `affectedNodes` from `git diff --name-only` and the `files` array + // built by the stats compactor are both project-root-relative + // (e.g. `packages/design-stack/modules/AgentCard/AgentCard.stories.tsx`). + // Project them into the same frame so the BE matches importPath against + // affectedNodes without a cross-frame translation. const dotPosix = './'; const dotPlatform = `.${path.sep}`; + const invocationDir = process.cwd(); const normalizeImportPath = p => { - if (typeof p !== 'string') return p; - if (p.startsWith(dotPlatform)) return p.slice(dotPlatform.length); - if (p.startsWith(dotPosix)) return p.slice(dotPosix.length); - return p; + if (typeof p !== 'string' || !p) return p; + let rel = p; + if (rel.startsWith(dotPlatform)) rel = rel.slice(dotPlatform.length); + else if (rel.startsWith(dotPosix)) rel = rel.slice(dotPosix.length); + // If the importPath happens to be absolute (older Storybook configs), + // path.resolve treats it as the target directly; otherwise it's joined + // against `invocationDir`. Then re-base against the git project root. + const abs = path.resolve(invocationDir, rel); + const projRel = path.relative(projectRoot, abs); + // path.relative('','') → '' and `path.relative` produces backslashes + // on Windows; the stats `files` array uses the same — leave platform + // sep alone so the two stay byte-identical for the BE match. + return projRel || rel; }; const storybookPaths = [...new Set(snapshots.map(s => normalizeImportPath(s.importPath)).filter(Boolean))]; @@ -364,19 +390,20 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir files, modules, storybookPaths, affectedNodes }); - const status = await pollGraphStatus(percy, buildId, log); + const { status, data } = await pollGraphStatus(percy, buildId, log); if (status !== 'done') { throw new SmartSnapBailError(`SmartSnap: graph generation did not complete (status: ${status ?? 'timed out'}); running full snapshot set`); } - const data = await percy.client.getSmartsnapGraphData(buildId, { trace: trace }); - - // Persist the rendered Drawflow visualization next to where the user ran - // percy, so they can open it after the build finishes. + // The sync status response carries the graph payload directly on completion, + // so there's no second fetch — `data` here is the same response body that + // used to come from `getSmartsnapGraphData`. log.debug(`SmartSnap: affected stories result ${JSON.stringify(data?.affected_stories)}`); - if (data?.trace_graph_html) { + // The API always returns trace_graph_html now; only persist it when the + // user opted in via the `trace` config flag. + if (trace && data?.trace_graph_html) { const tracePath = path.resolve(process.cwd(), 'trace.html'); try { fs.writeFileSync(tracePath, data.trace_graph_html); diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 76aa97b7d..43b21614d 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -413,23 +413,27 @@ export class PercyClient { } // Retrieves snapshot/comparison data by id. Requires a read access token. + // For `smartsnap_graph`, the API blocks (sync=true) until the graph job + // finishes and returns the graph payload directly in the response — there + // is no separate "data" endpoint to fetch after polling. async getStatus(type, ids) { - if (!['snapshot', 'comparison'].includes(type)) throw new Error('Invalid type passed'); + if (!['snapshot', 'comparison', 'smartsnap_graph'].includes(type)) throw new Error('Invalid type passed'); this.log.debug(`Getting ${type} status for ids ${ids}`); return this.get(`job_status?sync=true&type=${type}&id=${ids.join()}`); } // SmartSnap endpoints authenticate against the project attached to the - // current Percy token (via the Authorization header). The three graph - // endpoints additionally take a `build_id` (sourced from the bundler- - // emitted stats file) so multiple concurrent storybook builds for the - // same project don't share Redis state. `snapshot-name-to-commit` is - // project-scoped only and takes no buildId. - - async getSmartsnapSnapshotNameToCommit(snapshotNames) { + // current Percy token (via the Authorization header). `generate-graph` + // takes a `build_id` (sourced from the bundler-emitted stats file) so + // multiple concurrent storybook builds for the same project don't share + // Redis state. `snapshot-name-to-commit` is project-scoped only. + // Graph status is polled through the shared `getStatus()` helper with + // type `smartsnap_graph` — the sync response returns the graph payload + // directly on completion. + + async getSmartsnapSnapshotNameToCommit() { this.log.debug('Smartsnap: looking up baselines...'); const qs = new URLSearchParams(); - for (const name of (snapshotNames || [])) qs.append('snapshot_names[]', name); // Same git/PR context `createBuild` sends — the API uses these to predict // the base build that *would* be selected if we called createBuild now, @@ -442,8 +446,9 @@ export class PercyClient { if (this.env.pullRequest != null) qs.append('pull_request_number', String(this.env.pullRequest)); if (this.env.partial) qs.append('partial', 'true'); + const query = qs.toString(); return this.get( - `smartsnap/snapshot-name-to-commit?${qs.toString()}`, + query ? `smartsnap/snapshot-name-to-commit?${query}` : 'smartsnap/snapshot-name-to-commit', { identifier: 'smartsnap.snapshot_name_to_commit' } ); } @@ -464,27 +469,6 @@ export class PercyClient { }, { identifier: 'smartsnap.generate_graph' }); } - async getSmartsnapGraphStatus(buildId) { - this.log.debug(`Smartsnap: polling graph status for build ${buildId}...`); - const qs = new URLSearchParams(); - qs.append('build_id', buildId); - return this.get( - `smartsnap/generate-graph?${qs.toString()}`, - { identifier: 'smartsnap.graph_status' } - ); - } - - async getSmartsnapGraphData(buildId, { trace = false } = {}) { - this.log.debug(`Smartsnap: fetching graph data for build ${buildId}...`); - const qs = new URLSearchParams(); - qs.append('build_id', buildId); - if (trace) qs.append('trace', 'true'); - return this.get( - `smartsnap/generate-graph/data?${qs.toString()}`, - { identifier: 'smartsnap.graph_data' } - ); - } - // Returns device details enabled on project associated with given token async getDeviceDetails(buildId) { try { diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 137d8e8f3..4d48b4111 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -811,6 +811,22 @@ describe('PercyClient', () => { await expectAsync(client.getStatus('comparison', [3, 4])).toBeResolvedTo({ data: '<>' }); await expectAsync(client.getStatus('comparison', [5])).toBeResolvedTo({ data: '<>' }); }); + + it('gets smartsnap_graph status (sync response carries graph payload on completion)', async () => { + const path = '/job_status?sync=true&type=smartsnap_graph&id=build-1'; + api.reply(path, () => [200, { + status: 'done', + affected_stories: [{ file_path: 'src/Foo.stories.tsx' }], + trace_graph_html: '' + }]); + + await expectAsync(client.getStatus('smartsnap_graph', ['build-1'])).toBeResolvedTo({ + status: 'done', + affected_stories: [{ file_path: 'src/Foo.stories.tsx' }], + trace_graph_html: '' + }); + expect(api.requests[path][0].method).toBe('GET'); + }); }); describe('#getSmartsnapSnapshotNameToCommit()', () => { @@ -823,26 +839,18 @@ describe('PercyClient', () => { beforeEach(() => stubEnv()); - it('issues a GET with snapshot_names[] params and no git/PR context when env is empty', async () => { - const path = '/smartsnap/snapshot-name-to-commit?snapshot_names%5B%5D=foo&snapshot_names%5B%5D=bar'; - api.reply(path, () => [200, { data: { foo: 'sha-foo', bar: 'sha-bar' } }]); + it('issues a GET with no query params when env is empty', async () => { + const path = '/smartsnap/snapshot-name-to-commit'; + api.reply(path, () => [200, { data: { foo: 'sha-foo' } }]); await expectAsync( - client.getSmartsnapSnapshotNameToCommit(['foo', 'bar']) - ).toBeResolvedTo({ data: { foo: 'sha-foo', bar: 'sha-bar' } }); + client.getSmartsnapSnapshotNameToCommit() + ).toBeResolvedTo({ data: { foo: 'sha-foo' } }); expect(api.requests[path]).toBeDefined(); expect(api.requests[path][0].method).toBe('GET'); }); - it('handles null/undefined snapshotNames without throwing', async () => { - // empty query string is normalized away by URL parsing - api.reply('/smartsnap/snapshot-name-to-commit', () => [200, { data: {} }]); - - await expectAsync(client.getSmartsnapSnapshotNameToCommit()).toBeResolvedTo({ data: {} }); - await expectAsync(client.getSmartsnapSnapshotNameToCommit(null)).toBeResolvedTo({ data: {} }); - }); - it('appends git/target/PR/partial context when present in env', async () => { stubEnv({ git: { branch: 'feature/x', sha: 'commit-sha-1' }, @@ -852,7 +860,6 @@ describe('PercyClient', () => { }); const expectedPath = '/smartsnap/snapshot-name-to-commit?' + [ - 'snapshot_names%5B%5D=a', 'branch=feature%2Fx', 'target_branch=main', 'commit_sha=commit-sha-1', @@ -864,7 +871,7 @@ describe('PercyClient', () => { api.reply(expectedPath, () => [200, { data: { a: 'sha-a' } }]); await expectAsync( - client.getSmartsnapSnapshotNameToCommit(['a']) + client.getSmartsnapSnapshotNameToCommit() ).toBeResolvedTo({ data: { a: 'sha-a' } }); expect(api.requests[expectedPath]).toBeDefined(); @@ -874,11 +881,11 @@ describe('PercyClient', () => { it('includes pull_request_number=0 when env.pullRequest is 0 (not null)', async () => { stubEnv({ pullRequest: 0 }); - const expectedPath = '/smartsnap/snapshot-name-to-commit?snapshot_names%5B%5D=a&pull_request_number=0'; + const expectedPath = '/smartsnap/snapshot-name-to-commit?pull_request_number=0'; api.reply(expectedPath, () => [200, { data: {} }]); await expectAsync( - client.getSmartsnapSnapshotNameToCommit(['a']) + client.getSmartsnapSnapshotNameToCommit() ).toBeResolvedTo({ data: {} }); expect(api.requests[expectedPath]).toBeDefined(); @@ -924,75 +931,6 @@ describe('PercyClient', () => { }); }); - describe('#getSmartsnapGraphStatus()', () => { - it('GETs the graph status for the given build_id', async () => { - api.reply('/smartsnap/generate-graph?build_id=build-1', () => [ - 200, { status: 'in_progress' } - ]); - - await expectAsync(client.getSmartsnapGraphStatus('build-1')) - .toBeResolvedTo({ status: 'in_progress' }); - - expect(api.requests['/smartsnap/generate-graph?build_id=build-1']).toBeDefined(); - expect(api.requests['/smartsnap/generate-graph?build_id=build-1'][0].method).toBe('GET'); - }); - - it('URL-encodes the build_id query param', async () => { - const path = '/smartsnap/generate-graph?build_id=build+with+spaces'; - api.reply(path, () => [200, { status: 'ready' }]); - - await expectAsync(client.getSmartsnapGraphStatus('build with spaces')) - .toBeResolvedTo({ status: 'ready' }); - - expect(api.requests[path]).toBeDefined(); - }); - - it('rejects when the API returns an error', async () => { - api.reply('/smartsnap/generate-graph?build_id=build-err', () => [500]); - - await expectAsync(client.getSmartsnapGraphStatus('build-err')).toBeRejected(); - }); - }); - - describe('#getSmartsnapGraphData()', () => { - it('GETs graph data for the given build_id without trace by default', async () => { - api.reply('/smartsnap/generate-graph/data?build_id=build-1', () => [ - 200, { graph: { nodes: [] } } - ]); - - await expectAsync(client.getSmartsnapGraphData('build-1')) - .toBeResolvedTo({ graph: { nodes: [] } }); - - expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1']).toBeDefined(); - expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1'][0].method).toBe('GET'); - }); - - it('appends trace=true when trace option is set', async () => { - const path = '/smartsnap/generate-graph/data?build_id=build-1&trace=true'; - api.reply(path, () => [200, { graph: {}, trace: [] }]); - - await expectAsync(client.getSmartsnapGraphData('build-1', { trace: true })) - .toBeResolvedTo({ graph: {}, trace: [] }); - - expect(api.requests[path]).toBeDefined(); - }); - - it('does not append trace when trace option is false', async () => { - api.reply('/smartsnap/generate-graph/data?build_id=build-1', () => [200, { graph: {} }]); - - await expectAsync(client.getSmartsnapGraphData('build-1', { trace: false })) - .toBeResolvedTo({ graph: {} }); - - expect(api.requests['/smartsnap/generate-graph/data?build_id=build-1']).toBeDefined(); - }); - - it('rejects when the API returns an error', async () => { - api.reply('/smartsnap/generate-graph/data?build_id=build-err', () => [500]); - - await expectAsync(client.getSmartsnapGraphData('build-err')).toBeRejected(); - }); - }); - describe('#getDeviceDetails()', () => { it('in case of error return []', async () => { api.reply('/discovery/device-details', () => [500]); From f689ac801590ecc38db0a43483a4b26b0b8eef83 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Tue, 26 May 2026 17:33:21 +0530 Subject: [PATCH 10/29] new api contracts --- packages/cli-command/src/graphTrace.js | 142 +++++++ .../cli-command/src/graphTraceTemplate.html | 349 ++++++++++++++++++ packages/cli-command/src/smartsnap.js | 42 ++- packages/client/test/client.test.js | 19 +- yarn.lock | 131 ++++++- 5 files changed, 649 insertions(+), 34 deletions(-) create mode 100644 packages/cli-command/src/graphTrace.js create mode 100644 packages/cli-command/src/graphTraceTemplate.html diff --git a/packages/cli-command/src/graphTrace.js b/packages/cli-command/src/graphTrace.js new file mode 100644 index 000000000..671a80f5d --- /dev/null +++ b/packages/cli-command/src/graphTrace.js @@ -0,0 +1,142 @@ +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)]; + 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 `" in a +// vertex name can't terminate the surrounding + + + +
+ + + +
+
+
Package Dependency
+
Component
+
Story
+
Changed file
+
+
+ + + diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 2e453bc17..92bfd6204 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -5,6 +5,7 @@ import { createRequire } from 'module'; import globToRegExp from 'glob-to-regexp'; import logger from '@percy/logger'; import { diffLockfileDeps } from './lockfileDiff.js'; +import { renderGraphTraceHtml } from './graphTrace.js'; // stream-json is CommonJS — load via createRequire to avoid named-import interop issues. const require = createRequire(import.meta.url); @@ -166,23 +167,22 @@ async function readStats(statsFile, projectRoot) { return { files, modules, buildId }; } -// Polls `job_status?sync=true&type=smartsnap_graph` — the sync response blocks -// server-side until the job moves off `in_progress`, but the API enforces a -// shorter timeout than the job can take, so we retry up to POLL_ATTEMPTS -// times. On `done` the response also carries the graph payload (affected -// stories, trace HTML); the caller reads it directly, no second fetch needed. +// Polls `job_status?sync=true&type=smartsnap_graph&id=` — the sync +// response blocks server-side until the job moves off `in_progress`, but the +// API enforces a shorter timeout than the job can take, so we retry up to +// POLL_ATTEMPTS times. On `done` the response also carries the graph payload +// (affected stories + vertices/edges/transitive closure for trace +// rendering), so the caller reads it directly without a second fetch. // -// Response shape is `{ : { status, data? } }` — the job_status -// endpoint accepts a comma-separated id list and always keys the response -// by id, even for a single id. +// Response shape is the unwrapped `{ status, data }` — the smartsnap_graph +// status response no longer keys the result by buildId. async function pollGraphStatus(percy, buildId, log) { for (let i = 0; i < POLL_ATTEMPTS; i++) { await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); - const res = await percy.client.getStatus('smartsnap_graph', [buildId]); - const entry = res?.[buildId]; - const status = entry?.status; + const res = await percy.client.getStatus('smartsnap_graph', [buildId]); + const status = res?.status; log.debug(`SmartSnap: graph status (attempt ${i + 1}) = ${status}`); - if (status === 'done' || status === 'failure') return { status, data: entry?.data }; + if (status === 'done' || status === 'failure') return { status, data: res?.data }; } return { status: null }; } @@ -401,19 +401,26 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir log.debug(`SmartSnap: affected stories result ${JSON.stringify(data?.affected_stories)}`); - // The API always returns trace_graph_html now; only persist it when the - // user opted in via the `trace` config flag. - if (trace && data?.trace_graph_html) { + // Trace rendering moved client-side: the API now returns the raw graph + // (vertices, edges, transitive-closure triples) and we populate the + // bundled HTML template here. Anything missing means the BE couldn't + // produce a graph — skip silently rather than write a broken page. + if (trace && data?.vertices && data?.edges && data?.transitive_closure_matrix_sparse) { const tracePath = path.resolve(process.cwd(), 'trace.html'); try { - fs.writeFileSync(tracePath, data.trace_graph_html); + const html = renderGraphTraceHtml({ + vertices: data.vertices, + edges: data.edges, + transitive_closure_matrix_sparse: data.transitive_closure_matrix_sparse + }); + fs.writeFileSync(tracePath, html); log.info(`SmartSnap: trace written to ${tracePath}`); } catch (e) { log.warn(`SmartSnap: failed to write trace.html: ${e.message}`); } } - const affected = new Set((data?.affected_stories || []).map(s => s.file_path)); + const affected = new Set(data?.affected_stories || []); // Snapshots whose baseline review_state is `failed` or `rejected` have no // usable baseline image to diff against, and snapshots that don't appear @@ -423,6 +430,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir const baselineSnapshots = baseLookup?.snapshots || {}; const FORCE_RESNAPSHOT_STATES = new Set(['failed', 'rejected']); const needsBaselineRefresh = name => { + if(baseline) return false; const state = baselineSnapshots[name]; return state === undefined || FORCE_RESNAPSHOT_STATES.has(state); }; diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 4d48b4111..018050971 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -814,17 +814,18 @@ describe('PercyClient', () => { it('gets smartsnap_graph status (sync response carries graph payload on completion)', async () => { const path = '/job_status?sync=true&type=smartsnap_graph&id=build-1'; - api.reply(path, () => [200, { + const body = { status: 'done', - affected_stories: [{ file_path: 'src/Foo.stories.tsx' }], - trace_graph_html: '' - }]); + data: { + affected_stories: ['src/Foo.stories.tsx'], + vertices: [{ kind: 'story', file_path: 'src/Foo.stories.tsx', changed: true }], + edges: [], + transitive_closure_matrix_sparse: [] + } + }; + api.reply(path, () => [200, body]); - await expectAsync(client.getStatus('smartsnap_graph', ['build-1'])).toBeResolvedTo({ - status: 'done', - affected_stories: [{ file_path: 'src/Foo.stories.tsx' }], - trace_graph_html: '' - }); + await expectAsync(client.getStatus('smartsnap_graph', ['build-1'])).toBeResolvedTo(body); expect(api.requests[path][0].method).toBe('GET'); }); }); diff --git a/yarn.lock b/yarn.lock index f78e77558..38d71d367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2267,6 +2267,118 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" +"@percy/cli-command@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.31.14.tgz#6c08f8841e7bd468026024908ac6d6e88d41e25c" + integrity sha512-rLDR8dqHuUMQf4QfoStcQ1YTjdahGHOLn8IgA4ZC/o0H/9o2ynDIckMfQRgPYwBSwJDZU/pFZmuFGXiC8l/3tw== + dependencies: + "@percy/config" "1.31.14" + "@percy/core" "1.31.14" + "@percy/logger" "1.31.14" + +"@percy/cli-doctor@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/cli-doctor/-/cli-doctor-1.31.14.tgz#6021763fe076244d600b932ecc221c63465f0898" + integrity sha512-UjH6LUvAWoX3HUFg9qcxo7bRVb6Y41/RCFYbAdC+/jZF/IX+FU3wnmGD8s0FZs4L+wlz6PXSJtIzAcyQGRas1w== + dependencies: + "@percy/cli-command" "1.31.14" + "@percy/client" "1.31.14" + "@percy/config" "1.31.14" + "@percy/core" "1.31.14" + "@percy/env" "1.31.14" + "@percy/logger" "1.31.14" + "@percy/monitoring" "1.31.14" + minimatch "^9.0.0" + ws "^8.17.1" + +"@percy/client@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.31.14.tgz#dc850b8e490118194efa9ef428ef971480674ca9" + integrity sha512-HA/1SlYg57L3eNQZivNJ5yKZte8v7ZZ3CT29hhn03sHReVd3NshQuSC1Vm8kchrnoSi4SR5bUilwJ9w8lnDT3A== + dependencies: + "@percy/config" "1.31.14" + "@percy/env" "1.31.14" + "@percy/logger" "1.31.14" + pac-proxy-agent "^7.0.2" + pako "^2.1.0" + +"@percy/config@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.31.14.tgz#6ba0d312b51556e85e07d207a9abccbf5075daa7" + integrity sha512-BRVfkff1j3ATdA8xH600802nhFfE1K4XRsGwOHTEZd/DnFVvGUIagQvfZxpTdHBTZ8G+nwi9Z8CTcVtzgd4AMw== + dependencies: + "@percy/logger" "1.31.14" + ajv "^8.6.2" + cosmiconfig "^8.0.0" + yaml "^2.0.0" + +"@percy/core@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.31.14.tgz#76eb027e6a6df2cb5990196e899d357d5612ae9b" + integrity sha512-c6QTjqaKs7UxL9HK6VJJl5B57hywFNn+l22vES4dFkegrkdE0maAz7rG88X2Xp2sYrsR3qf9n9CKCDy511/KYw== + dependencies: + "@percy/client" "1.31.14" + "@percy/config" "1.31.14" + "@percy/dom" "1.31.14" + "@percy/logger" "1.31.14" + "@percy/monitoring" "1.31.14" + "@percy/webdriver-utils" "1.31.14" + content-disposition "^0.5.4" + cross-spawn "^7.0.3" + extract-zip "^2.0.1" + fast-glob "^3.2.11" + micromatch "^4.0.8" + mime-types "^2.1.34" + pako "^2.1.0" + path-to-regexp "^6.3.0" + rimraf "^3.0.2" + ws "^8.17.1" + yaml "^2.4.1" + optionalDependencies: + "@percy/cli-doctor" "1.31.14" + +"@percy/dom@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.31.14.tgz#bb8506014589d1d76d148f31410f259554389757" + integrity sha512-Ilz9qSf9Z80jHah9b7OfX2M2N9vmY4hkGEhMO953ui3NSQhXZsezr4aIPREYdyZdo11pEWm+JWU1AEZw54CbbA== + +"@percy/env@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.31.14.tgz#035116da85d36bbb14dbe274ac4694d56b301ecf" + integrity sha512-eRW4Ot2Z5WESKPJHcdLc9JFLtjxJaALzRxGa7Z+0R5t5wOlqbEVQytLBzgFWWTVa2XEejR4Sffs5cYqM+ldgEA== + dependencies: + "@percy/logger" "1.31.14" + +"@percy/logger@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.31.14.tgz#07790eb96f46fdc7a25566d2fe2e6cc8922b6d7a" + integrity sha512-e078iCrSDa4qdMfOlX+DXhZ08MzYxNTFfz7HsgLcAXSn6AV+CGZT/BxaHlzm2I5tp1zQV7wHnvvm4RV2qx1FEA== + +"@percy/monitoring@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/monitoring/-/monitoring-1.31.14.tgz#6d0726cf0daf9eb9ea45d10dcc139676d969d3d4" + integrity sha512-5zqOZmkua08pc/lppXeBUCEWCG9xGQwrxI+DlZ6TMLg05Dwn7nPRaMMxYla8A/XsDemcq0TECgCT83uYY0WpeQ== + dependencies: + "@percy/config" "1.31.14" + "@percy/logger" "1.31.14" + "@percy/sdk-utils" "1.31.14" + systeminformation "^5.25.11" + +"@percy/sdk-utils@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.31.14.tgz#f86e0451a5931d5a1d2bf1daa2438f543c70936e" + integrity sha512-I31GM+aCHiME12jX9ac5COThZWOpTBONkR9J6D059wqiFhHtRenA2mWFx6rC+zUpG5un0imkal7tN9y1D4hPBg== + dependencies: + pac-proxy-agent "^7.0.2" + +"@percy/webdriver-utils@1.31.14": + version "1.31.14" + resolved "https://registry.yarnpkg.com/@percy/webdriver-utils/-/webdriver-utils-1.31.14.tgz#486e56ddd34496dad0356d21c7251fe78e51ef30" + integrity sha512-2V4yxf60mXsDAUz+GFOePcfSNF9mSbrV1WH+sUWDItFUjV14QSVNuBcZRKO8Etgi7bFSCPz1Z1jXrxmopBFpbg== + dependencies: + "@percy/config" "1.31.14" + "@percy/sdk-utils" "1.31.14" + "@phenomnomnominal/tsquery@4.1.1": version "4.1.1" resolved "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz" @@ -2285,6 +2397,7 @@ version "8.9.0" resolved "https://registry.yarnpkg.com/@pnpm/types/-/types-8.9.0.tgz#9636d5f0642793432f72609b79458ca9be049b02" integrity sha512-3MYHYm8epnciApn6w5Fzx6sepawmsNU7l6lvIq+ER22/DPSrr83YMhU/EQWnf4lORn2YyiXFj0FJSyJzEtIGmw== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -6314,15 +6427,16 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.clone@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" - integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" @@ -6476,15 +6590,16 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.0.2" -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== long@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" From bc60bb3391dd1bdb4199c17ef20917bbc10e9682 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 28 May 2026 13:46:48 +0530 Subject: [PATCH 11/29] delete accidentally generated pnpm lockfile --- packages/cli/pnpm-lock.yaml | 1068 ----------------------------------- 1 file changed, 1068 deletions(-) delete mode 100644 packages/cli/pnpm-lock.yaml diff --git a/packages/cli/pnpm-lock.yaml b/packages/cli/pnpm-lock.yaml deleted file mode 100644 index 2aa4888c3..000000000 --- a/packages/cli/pnpm-lock.yaml +++ /dev/null @@ -1,1068 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@percy/cli-app': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-build': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-command': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-config': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-doctor': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-exec': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-snapshot': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/cli-upload': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/client': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - '@percy/logger': - specifier: 1.31.13-beta.0 - version: 1.31.13-beta.0 - -packages: - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@percy/cli-app@1.31.13-beta.0': - resolution: {integrity: sha512-moDQKT6MI2SMXFOYHYSN8wiTRK6054C4/U87mAjtim7p0DOb9XgDPAuzncngbVy4Url3Eizbwyc7n0ohrpSKHA==} - engines: {node: '>=14'} - - '@percy/cli-build@1.31.13-beta.0': - resolution: {integrity: sha512-4n1/X4iVwXcEg2DBLPjcYhs4uPxd2X7KGV634Lor/S01wO5Hf9Y9XoM8dTMbIzzMZo2soOZBf7jwiS4GQvRTJQ==} - engines: {node: '>=14'} - - '@percy/cli-command@1.31.13-beta.0': - resolution: {integrity: sha512-WYR7KAUwJLvK9EG2jx9z/g+8wgFytuOEuw24c1NNrG9yZdhlffe+OZM8zcoWwV3+2rASl5fG0tuAatApr9LSKA==} - engines: {node: '>=14'} - hasBin: true - - '@percy/cli-config@1.31.13-beta.0': - resolution: {integrity: sha512-reMgjjed7MAzGz579ALxfo0BF8YvJgRtwJbmAr6+9K8i4ogNRWUJ3oH39ewSWEBjHzf1YtpXQJugnhi/4BZMag==} - engines: {node: '>=14'} - - '@percy/cli-doctor@1.31.13-beta.0': - resolution: {integrity: sha512-ertkEz1+lFre+7fFdQshwtakZqVccMAiGPTvXRfahWJ4kn7626KRoAtHW2t7LSEpIqN8fcgLD78xfHpWmaY5SA==} - engines: {node: '>=14'} - - '@percy/cli-exec@1.31.13-beta.0': - resolution: {integrity: sha512-uYozwBnGguu5H7rSRsysG0ICEQ1YjFE0w01+5gKNm18oKHR61UGsvl8jsy0Jf2fXJpwAWWmQe82ChhwvIzvW7g==} - engines: {node: '>=14'} - - '@percy/cli-snapshot@1.31.13-beta.0': - resolution: {integrity: sha512-3hrWZ31O6HVakC/3AmSFiB1tJAAebrP0s5EPYqhm4/tJszFRNVQs1miAN0b/waWK/L+pdEULx2A1Td1qSG/cYQ==} - engines: {node: '>=14'} - - '@percy/cli-upload@1.31.13-beta.0': - resolution: {integrity: sha512-fP5UYHyE9JeUVQ4VA2yax+/88wiVoWa2XK63QQiswETqeH5OIUTu/Sm7uYBH0PVdyw87I6OgknHIV6JRr6g+BA==} - engines: {node: '>=14'} - - '@percy/client@1.31.13-beta.0': - resolution: {integrity: sha512-55ATMx5TDbZRGzoXZBK7s4RdTL8pEWdRw8ziCkg47M3Wx8ETRoiNxM6GIIIZn+aWL5rsvdeJ0Fbygl109YHz4g==} - engines: {node: '>=14'} - - '@percy/config@1.31.13-beta.0': - resolution: {integrity: sha512-7V0ctz9IcHv1+MCqPN5H2WV44m4c5BofwVuWOLCztRZwnTyV0HgEtUFiHzLcGyr6Bn8KnnUB2WC+jBUYB0nKjA==} - engines: {node: '>=14'} - - '@percy/core@1.31.13-beta.0': - resolution: {integrity: sha512-YdK9D+6SRlbNQxS4cJBmizE/NWSMz8OK86CGLipN0lP/9drJUZWGUjt9dpCrj3H40GnBiv7PMmBcWC0UwpS9cQ==} - engines: {node: '>=14'} - - '@percy/dom@1.31.13-beta.0': - resolution: {integrity: sha512-bVMZTlgnwPuHdvBhUAKNTHLj48vhMLVRO6O0jhL+/zfm7QcWxSgwgvEKgNC6a24nWqNVAc2hBno8yRlcC83Sqg==} - - '@percy/env@1.31.13-beta.0': - resolution: {integrity: sha512-2AnqPea92xf5aSm6WH9g4KO2I1FQhVkdsXXIbI+zvfUwlR+vKXO55Tld6UNa88quHIZSABRKS0z8QQeU1FVK3A==} - engines: {node: '>=14'} - - '@percy/logger@1.31.13-beta.0': - resolution: {integrity: sha512-/uvaji1GyAQBvYeyzI9f7HOiuTeLqBNOivCnRa0FEmMem8OVGQjKa3tsyyAAe5OyXFU+/zjqYI1365BZLbiRZQ==} - engines: {node: '>=14'} - - '@percy/monitoring@1.31.13-beta.0': - resolution: {integrity: sha512-6nR9SWH/gR8MvpzpwO0F6kM8K4xsX1DfCg3tNTcOsLGfbnI27hm9dZInCOOmBXwajuEtSwUs54fnN1w8QK3FMA==} - engines: {node: '>=14'} - - '@percy/sdk-utils@1.31.13-beta.0': - resolution: {integrity: sha512-c6EI8lUqG0me7O2Ijr9TB+6zu9kvBAogYmBRj460oq7WiidH8Mr7w6fOMqYGdyfD6y3T2lydJ7pvrm43y//qww==} - engines: {node: '>=14'} - - '@percy/webdriver-utils@1.31.13-beta.0': - resolution: {integrity: sha512-5Z6u+UFBT3ldFANfVGd6mv0awufuwQ1mOsoGNWTPDToNZ1Un8+FPJsHSThbdAnnTJvCT2RcblrdyHgRZkm5sWg==} - engines: {node: '>=14'} - - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - - '@types/node@25.7.0': - resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} - - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - basic-ftp@5.3.1: - resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} - engines: {node: '>=10.0.0'} - - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-uri@6.0.5: - resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} - engines: {node: '>= 14'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - image-size@1.2.1: - resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} - engines: {node: '>=16.x'} - hasBin: true - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} - engines: {node: '>= 12'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - netmask@2.1.1: - resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} - engines: {node: '>= 0.4.0'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - pac-proxy-agent@7.2.0: - resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - - socks@2.8.9: - resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - systeminformation@5.31.6: - resolution: {integrity: sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==} - engines: {node: '>=8.0.0'} - os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] - hasBin: true - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - undici-types@7.21.0: - resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - -snapshots: - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.28.5': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@percy/cli-app@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - '@percy/cli-exec': 1.31.13-beta.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@percy/cli-build@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - transitivePeerDependencies: - - typescript - - '@percy/cli-command@1.31.13-beta.0': - dependencies: - '@percy/config': 1.31.13-beta.0 - '@percy/core': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@percy/cli-config@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - transitivePeerDependencies: - - typescript - - '@percy/cli-doctor@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - '@percy/client': 1.31.13-beta.0 - '@percy/config': 1.31.13-beta.0 - '@percy/core': 1.31.13-beta.0 - '@percy/env': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - '@percy/monitoring': 1.31.13-beta.0 - minimatch: 9.0.9 - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@percy/cli-exec@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - cross-spawn: 7.0.6 - which: 2.0.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@percy/cli-snapshot@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - yaml: 2.9.0 - transitivePeerDependencies: - - typescript - - '@percy/cli-upload@1.31.13-beta.0': - dependencies: - '@percy/cli-command': 1.31.13-beta.0 - fast-glob: 3.3.3 - image-size: 1.2.1 - transitivePeerDependencies: - - typescript - - '@percy/client@1.31.13-beta.0': - dependencies: - '@percy/config': 1.31.13-beta.0 - '@percy/env': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - pac-proxy-agent: 7.2.0 - pako: 2.1.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@percy/config@1.31.13-beta.0': - dependencies: - '@percy/logger': 1.31.13-beta.0 - ajv: 8.20.0 - cosmiconfig: 8.3.6 - yaml: 2.9.0 - transitivePeerDependencies: - - typescript - - '@percy/core@1.31.13-beta.0': - dependencies: - '@percy/client': 1.31.13-beta.0 - '@percy/config': 1.31.13-beta.0 - '@percy/dom': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - '@percy/monitoring': 1.31.13-beta.0 - '@percy/webdriver-utils': 1.31.13-beta.0 - content-disposition: 0.5.4 - cross-spawn: 7.0.6 - extract-zip: 2.0.1 - fast-glob: 3.3.3 - micromatch: 4.0.8 - mime-types: 2.1.35 - pako: 2.1.0 - path-to-regexp: 6.3.0 - rimraf: 3.0.2 - ws: 8.20.1 - yaml: 2.9.0 - optionalDependencies: - '@percy/cli-doctor': 1.31.13-beta.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@percy/dom@1.31.13-beta.0': {} - - '@percy/env@1.31.13-beta.0': - dependencies: - '@percy/logger': 1.31.13-beta.0 - - '@percy/logger@1.31.13-beta.0': {} - - '@percy/monitoring@1.31.13-beta.0': - dependencies: - '@percy/config': 1.31.13-beta.0 - '@percy/logger': 1.31.13-beta.0 - '@percy/sdk-utils': 1.31.13-beta.0 - systeminformation: 5.31.6 - transitivePeerDependencies: - - supports-color - - typescript - - '@percy/sdk-utils@1.31.13-beta.0': - dependencies: - pac-proxy-agent: 7.2.0 - transitivePeerDependencies: - - supports-color - - '@percy/webdriver-utils@1.31.13-beta.0': - dependencies: - '@percy/config': 1.31.13-beta.0 - '@percy/sdk-utils': 1.31.13-beta.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@tootallnate/quickjs-emscripten@0.23.0': {} - - '@types/node@25.7.0': - dependencies: - undici-types: 7.21.0 - optional: true - - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 25.7.0 - optional: true - - agent-base@7.1.4: {} - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - argparse@2.0.1: {} - - ast-types@0.13.4: - dependencies: - tslib: 2.8.1 - - balanced-match@1.0.2: {} - - basic-ftp@5.3.1: {} - - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.1.0: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - buffer-crc32@0.2.13: {} - - callsites@3.1.0: {} - - concat-map@0.0.1: {} - - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - cosmiconfig@8.3.6: - dependencies: - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - path-type: 4.0.0 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - data-uri-to-buffer@6.0.2: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - - esprima@4.0.1: {} - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-uri@3.1.2: {} - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - fs.realpath@1.0.0: {} - - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - - get-uri@6.0.5: - dependencies: - basic-ftp: 5.3.1 - data-uri-to-buffer: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - image-size@1.2.1: - dependencies: - queue: 6.0.2 - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - ip-address@10.2.0: {} - - is-arrayish@0.2.1: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@1.0.0: {} - - lines-and-columns@1.2.4: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.0 - - ms@2.1.3: {} - - netmask@2.1.1: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - pac-proxy-agent@7.2.0: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.4 - debug: 4.4.3 - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.1.1 - - pako@2.1.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-to-regexp@6.3.0: {} - - path-type@4.0.0: {} - - pend@1.2.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - queue-microtask@1.2.3: {} - - queue@6.0.2: - dependencies: - inherits: 2.0.4 - - require-from-string@2.0.2: {} - - resolve-from@4.0.0: {} - - reusify@1.1.0: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-buffer@5.2.1: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - smart-buffer@4.2.0: {} - - socks-proxy-agent@8.0.5: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.9 - transitivePeerDependencies: - - supports-color - - socks@2.8.9: - dependencies: - ip-address: 10.2.0 - smart-buffer: 4.2.0 - - source-map@0.6.1: - optional: true - - systeminformation@5.31.6: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tslib@2.8.1: {} - - undici-types@7.21.0: - optional: true - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrappy@1.0.2: {} - - ws@8.20.1: {} - - yaml@2.9.0: {} - - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 From 7c59f0aea5c997ad1a5f86603104f0979d0c64cd Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 28 May 2026 16:00:58 +0530 Subject: [PATCH 12/29] fix major issues --- packages/cli-command/src/graphTrace.js | 22 ++++++++++++++++------ packages/cli-command/src/smartsnap.js | 25 +++++++++++++++++++------ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/cli-command/src/graphTrace.js b/packages/cli-command/src/graphTrace.js index 671a80f5d..e02e28dc6 100644 --- a/packages/cli-command/src/graphTrace.js +++ b/packages/cli-command/src/graphTrace.js @@ -118,25 +118,35 @@ function computeLayout(rawVertices, edges, transitiveClosure) { })); } -// Escapes `" in a -// vertex name can't terminate the surrounding `; `` 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, '<\\/'); + return JSON.stringify(obj) + .replace(/<\//g, '<\\/') + .replace(//g, '--\\>') + .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, transitive_closure_matrix_sparse }) { +export function renderGraphTraceHtml({ vertices, edges, transitiveClosureMatrixSparse }) { const laidOutVertices = computeLayout( vertices || [], edges || [], - transitive_closure_matrix_sparse || [] + 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(transitive_closure_matrix_sparse || [])); + .replace('__TRANSITIVE_CLOSURE_JSON__', safeJson(transitiveClosureMatrixSparse || [])); } diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 92bfd6204..f2de3ac84 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -32,7 +32,7 @@ function patternToRegex(pattern) { if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) { throw new Error('Invalid pattern: must be a string with max length of 500 characters'); } - return globToRegExp(pattern, { extended: true, globstar: false }); + return globToRegExp(pattern, { extended: true, globstar: true }); } function matchesPattern(str, pattern) { @@ -64,8 +64,20 @@ function git(args) { return res.stdout; } +// baseRef flows into `git` argv from either user config (`baseline`) or the +// API. Reject anything that could be parsed as an option (leading `-`) or +// contains chars outside the safe ref alphabet — git also accepts `--` as an +// end-of-options separator in `git diff`, but not before the rev in +// `git show :`, so validation is the only universal guard. +function assertSafeRef(ref) { + if (typeof ref !== 'string' || !/^[A-Za-z0-9_./][A-Za-z0-9_./-]*$/.test(ref)) { + throw new SmartSnapBailError(`SmartSnap: unsafe baseline ref "${ref}"; running full snapshot set`); + } +} + function gitDiffNames(ref) { - return git(['diff', '--name-only', `${ref}`, 'HEAD']).split('\n').filter(Boolean); + assertSafeRef(ref); + return git(['diff', '--name-only', ref, 'HEAD', '--']).split('\n').filter(Boolean); } function gitProjectRoot() { @@ -178,11 +190,11 @@ async function readStats(statsFile, projectRoot) { // status response no longer keys the result by buildId. async function pollGraphStatus(percy, buildId, log) { for (let i = 0; i < POLL_ATTEMPTS; i++) { - await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); - const res = await percy.client.getStatus('smartsnap_graph', [buildId]); + const res = await percy.client.getStatus('smartsnap_graph', [buildId]); const status = res?.status; log.debug(`SmartSnap: graph status (attempt ${i + 1}) = ${status}`); if (status === 'done' || status === 'failure') return { status, data: res?.data }; + if (i < POLL_ATTEMPTS - 1) await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); } return { status: null }; } @@ -237,6 +249,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir } else { throw new SmartSnapBailError('SmartSnap: API could not predict a base build commit and no explicit baseline was set; running full snapshot set'); } + assertSafeRef(baseRef); affectedNodes = gitDiffNames(baseRef); // HEAD already matches the base build commit (or the diff is otherwise @@ -411,7 +424,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir const html = renderGraphTraceHtml({ vertices: data.vertices, edges: data.edges, - transitive_closure_matrix_sparse: data.transitive_closure_matrix_sparse + transitiveClosureMatrixSparse: data.transitive_closure_matrix_sparse }); fs.writeFileSync(tracePath, html); log.info(`SmartSnap: trace written to ${tracePath}`); @@ -430,7 +443,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir const baselineSnapshots = baseLookup?.snapshots || {}; const FORCE_RESNAPSHOT_STATES = new Set(['failed', 'rejected']); const needsBaselineRefresh = name => { - if(baseline) return false; + if (baseline) return false; const state = baselineSnapshots[name]; return state === undefined || FORCE_RESNAPSHOT_STATES.has(state); }; From 9be124a5e03169d31ea4797d744df73a5ab0eee2 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Thu, 28 May 2026 16:03:46 +0530 Subject: [PATCH 13/29] Potential fix for pull request finding 'CodeQL / Bad HTML filtering regexp' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- packages/cli-command/src/graphTrace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-command/src/graphTrace.js b/packages/cli-command/src/graphTrace.js index e02e28dc6..3e79d84f6 100644 --- a/packages/cli-command/src/graphTrace.js +++ b/packages/cli-command/src/graphTrace.js @@ -129,7 +129,7 @@ function safeJson(obj) { return JSON.stringify(obj) .replace(/<\//g, '<\\/') .replace(//g, '--\\>') + .replace(/--(!?)>/g, '--$1\\>') .split(LS).join('\\u2028') .split(PS).join('\\u2029'); } From f7fdfd5e92dec7a4690576da5a99546ecf18f85f Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 29 May 2026 11:21:22 +0530 Subject: [PATCH 14/29] resolve comments --- packages/cli-command/src/smartsnap.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index f2de3ac84..72ade5cc8 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -238,6 +238,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir log.debug(`SmartSnap: base lookup ${JSON.stringify(baseLookup)}`); let affectedNodes; + let packageAffectedNodes = []; let baseRef; if (baseline) { @@ -326,6 +327,7 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir lockfileType: lockfileName, oldPackageJson }); + packageAffectedNodes = packageAffected; log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); } catch (e) { // snyk-nodejs-lockfile-parser is an optionalDependency (requires Node >=18). @@ -341,6 +343,9 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir if (untraced?.length) { affectedNodes = affectedNodes.filter(p => !untraced.some(g => matchesPattern(p, g))); + if (affectedNodes.length === 0 && packageAffectedNodes.length === 0) { + throw new SmartSnapBailError('SmartSnap: all changed files matched untraced globs; running full snapshot set'); + } } if (bailOnChanges?.length) { @@ -398,6 +403,10 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir log.debug(`SmartSnap: storybookPaths sample: ${storybookPaths.slice(0, 3).join(', ')}`); } + if (packageAffectedNodes.length) { + affectedNodes = [...affectedNodes, ...packageAffectedNodes]; + } + log.debug(`SmartSnap: starting graph generation job ${JSON.stringify({ buildId, files, modules, storybookPaths, affectedNodes })}`); await percy.client.generateSmartsnapGraph(buildId, { files, modules, storybookPaths, affectedNodes From e8ee178d017bee36f77a495606bb08d31a32fb44 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 29 May 2026 15:08:32 +0530 Subject: [PATCH 15/29] resolve claude comments --- packages/cli-command/src/smartsnap.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 72ade5cc8..99fc631da 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -19,7 +19,9 @@ const { streamValues } = require('stream-json/streamers/StreamValues'); // up with the absolute paths in our file index. Built via String.fromCharCode // to avoid embedding a control char in source / tripping no-control-regex. const NULL_CHAR = String.fromCharCode(0); -const stripNull = s => (typeof s === 'string' ? s.replaceAll(NULL_CHAR, '') : s); +// split/join instead of String.prototype.replaceAll: replaceAll is Node 15+ +// and cli-command supports Node >=14. +const stripNull = s => (typeof s === 'string' ? s.split(NULL_CHAR).join('') : s); // Status poll cadence: 12 attempts × 5s = 1 minute total. const POLL_INTERVAL_MS = 5000; @@ -56,10 +58,20 @@ export class SmartSnapBailError extends Error { } } +// Any git failure is treated as a recoverable bail (full snapshot fallback), +// per this module's contract. A common trigger is CI shallow clones where the +// predicted base commit isn't fetched locally, so `git diff HEAD` fails — +// we must downgrade that to a full snapshot, not crash the build. Callers that +// want a more specific bail message (e.g. lockfile lookup) catch and re-throw. function git(args) { - const res = spawnSync('git', args, { encoding: 'utf8' }); + let res; + try { + res = spawnSync('git', args, { encoding: 'utf8' }); + } catch (e) { + throw new SmartSnapBailError(`SmartSnap: git ${args.join(' ')} failed to spawn: ${e.message}; running full snapshot set`); + } if (res.status !== 0) { - throw new Error(`git ${args.join(' ')} failed: ${res.stderr || res.stdout || `exit ${res.status}`}`); + throw new SmartSnapBailError(`SmartSnap: git ${args.join(' ')} failed: ${res.stderr || res.stdout || `exit ${res.status}`}; running full snapshot set`); } return res.stdout; } From bd72fb6e689119f197f2cdd95784de3585e08182 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 29 May 2026 17:35:57 +0530 Subject: [PATCH 16/29] add tests and refactor smartsnap --- packages/cli-command/src/smartsnap.js | 402 ++++++++++-------- packages/cli-command/test/graphTrace.test.js | 190 +++++++++ .../cli-command/test/lockfileDiff.test.js | 115 +++++ packages/cli-command/test/smartsnap.test.js | 359 ++++++++++++++++ 4 files changed, 899 insertions(+), 167 deletions(-) create mode 100644 packages/cli-command/test/graphTrace.test.js create mode 100644 packages/cli-command/test/lockfileDiff.test.js create mode 100644 packages/cli-command/test/smartsnap.test.js diff --git a/packages/cli-command/src/smartsnap.js b/packages/cli-command/src/smartsnap.js index 99fc631da..cd0991aba 100644 --- a/packages/cli-command/src/smartsnap.js +++ b/packages/cli-command/src/smartsnap.js @@ -211,17 +211,10 @@ async function pollGraphStatus(percy, buildId, log) { return { status: null }; } -// Given the mapped snapshots and storybook.smartSnap config, returns the subset of snapshots -// that the SmartSnap graph reports as affected. On any recoverable failure, returns the input -// list unchanged so the build runs as a full snapshot pass. -export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir) { - const log = logger('storybook:smartsnap'); - const { baseline, untraced, trace, bailOnChanges, statsFile } = smartSnapConfig || {}; - - if (!buildDir) { - throw new SmartSnapBailError('SmartSnap requires the Storybook build directory (e.g. `percy storybook ./storybook-static`); URL and `start` modes are not supported. Running full snapshot set'); - } - +// First component: resolve + validate the statsFile path, stream-read it, and +// assert it carries a usable buildId. Returns { files, modules, buildId }; any +// problem throws a SmartSnapBailError so the caller falls back to a full set. +export async function validateAndReadStats(buildDir, statsFile, projectRoot, log) { // Treat statsFile as a flat filename anchored inside the build directory. // path.basename() strips any traversal segments so the resolved path can't // escape buildDir even if the config is hostile. @@ -240,170 +233,161 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir throw new SmartSnapBailError(`SmartSnap: stats file "${statsName}" in ${buildDir} is not a regular file; running full snapshot set`); } - // New API shape: `{ base_build_commit_sha, snapshots: { : } }`. - // The single base-build commit replaces the previous per-snapshot commit map — - // baseline prediction now happens server-side via `Percy::BaseBuildService`, - // so we just diff against whatever commit it picked. The set of snapshot - // names is no longer sent; the API resolves baselines from the project + - // git/PR context alone. - const baseLookup = await percy.client.getSmartsnapSnapshotNameToCommit(); - log.debug(`SmartSnap: base lookup ${JSON.stringify(baseLookup)}`); - - let affectedNodes; - let packageAffectedNodes = []; + log.debug(`SmartSnap: parsing stats file ${resolvedStatsPath}`); + const { files, modules, buildId } = await readStats(resolvedStatsPath, projectRoot); + + if (typeof buildId !== 'string' || !buildId) { + throw new SmartSnapBailError(`SmartSnap: stats file at ${resolvedStatsPath} is missing a top-level "buildId" — running full snapshot set`); + } + + return { files, modules, buildId }; +} + +// Resolve the diff base ref + the list of changed files. With an explicit +// `baseline` we diff against it directly and skip the API base-build lookup +// entirely (its snapshot map is only consulted on the no-baseline path), so +// `baselineSnapshots` comes back null in that case. +export async function getBaselineAndAffectedNodes(percy, baseline, log) { let baseRef; + let baselineSnapshots; if (baseline) { log.debug(`SmartSnap: diffing against explicit baseline "${baseline}"`); baseRef = baseline; - } else if (baseLookup?.base_build_commit_sha) { + baselineSnapshots = null; + } else { + // New API shape: `{ base_build_commit_sha, snapshots: { : } }`. + // The single base-build commit replaces the previous per-snapshot commit map — + // baseline prediction now happens server-side via `Percy::BaseBuildService`, + // so we just diff against whatever commit it picked. The set of snapshot + // names is no longer sent; the API resolves baselines from the project + + // git/PR context alone. + const baseLookup = await percy.client.getSmartsnapSnapshotNameToCommit(); + log.debug(`SmartSnap: base lookup ${JSON.stringify(baseLookup)}`); + if (!baseLookup?.base_build_commit_sha) { + throw new SmartSnapBailError('SmartSnap: API could not predict a base build commit and no explicit baseline was set; running full snapshot set'); + } log.debug(`SmartSnap: diffing against predicted base build commit "${baseLookup.base_build_commit_sha}"`); baseRef = baseLookup.base_build_commit_sha; - } else { - throw new SmartSnapBailError('SmartSnap: API could not predict a base build commit and no explicit baseline was set; running full snapshot set'); + baselineSnapshots = baseLookup.snapshots || {}; } - assertSafeRef(baseRef); - affectedNodes = gitDiffNames(baseRef); - // HEAD already matches the base build commit (or the diff is otherwise - // empty) — there's nothing for the dep graph to map. Bail to a full set. - if (affectedNodes.length === 0) { - throw new SmartSnapBailError('SmartSnap: no files changed between HEAD and base build commit; running full snapshot set'); - } + assertSafeRef(baseRef); + const affectedNodes = gitDiffNames(baseRef); + return { baseRef, affectedNodes, baselineSnapshots }; +} - // A change to anything under `.storybook/` (preview config, addons, manager - // wiring) can affect every story's render, so the dep graph isn't enough. +// A change to anything under `.storybook/` (preview config, addons, manager +// wiring) can affect every story's render, so the dep graph isn't enough. +export function assertNoDotStorybookChange(affectedNodes) { const dotStorybookHit = affectedNodes.find(p => p.split(/[/\\]/).includes('.storybook')); if (dotStorybookHit) { throw new SmartSnapBailError(`SmartSnap: change to "${dotStorybookHit}" inside .storybook affects all stories; running full snapshot set`); } +} - // Manifest/lockfile changes can shift the dependency tree, so resolve the - // diff at the package level via snyk-nodejs-lockfile-parser and feed the - // changed package names back into the graph. Short-circuits below fall - // back to a full snapshot whenever we can't reason about the change. - const MANIFEST_PATHS = new Set(['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']); - const manifestHits = affectedNodes.filter(p => MANIFEST_PATHS.has(path.basename(p))); - const projectRoot = gitProjectRoot(); - - if (manifestHits.length > 0) { - // Locate the changed manifest's directory from affectedNodes (NOT the git - // root) — monorepos keep package.json + lockfile inside a workspace dir. - // If two changes land in different dirs, we'd need per-workspace resolution - // we don't try to do yet, so bail. - const uniqueDirs = [...new Set(manifestHits.map(p => path.dirname(p)))]; - if (uniqueDirs.length > 1) { - throw new SmartSnapBailError(`SmartSnap: manifest changes span multiple directories (${uniqueDirs.join(', ')}); running full snapshot set`); - } - const manifestDir = uniqueDirs[0]; // repo-relative; '.' for root - const absManifestDir = path.resolve(projectRoot, manifestDir); - - // Pick the lockfile that lives next to the changed manifest. If two - // coexist (e.g. a stray package-lock.json next to yarn.lock) we can't - // pick a canonical source, so bail. - const LOCKFILE_NAMES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']; - const presentLockfiles = LOCKFILE_NAMES.filter(n => fs.existsSync(path.join(absManifestDir, n))); - if (presentLockfiles.length === 0) { - throw new SmartSnapBailError(`SmartSnap: manifest changed in "${manifestDir}" but no lockfile present there; running full snapshot set`); - } - if (presentLockfiles.length > 1) { - throw new SmartSnapBailError(`SmartSnap: multiple lockfiles in "${manifestDir}" (${presentLockfiles.join(', ')}); cannot pick canonical; running full snapshot set`); - } - const lockfileName = presentLockfiles[0]; - // git always uses forward slashes for its : spec; build it by - // hand instead of path.join (which would use backslashes on Windows). - const lockfileRepoPath = manifestDir === '.' ? lockfileName : `${manifestDir}/${lockfileName}`; - - // Resolve the lockfile at the base commit. If it wasn't tracked there - // (first SmartSnap run, lockfile renamed, etc.) we can't diff, so bail. - let oldLockfile; - try { - oldLockfile = git(['show', `${baseRef}:${lockfileRepoPath}`]); - } catch { - throw new SmartSnapBailError(`SmartSnap: lockfile "${lockfileRepoPath}" not present at base ref ${baseRef}; running full snapshot set`); - } - - const newLockfile = fs.readFileSync(path.join(absManifestDir, lockfileName), 'utf8'); - - if (oldLockfile !== newLockfile) { - // Lockfile actually shifted — invoke the diff. If byte-identical, only - // package.json's non-dep fields changed and we fall through unchanged. - const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); - const packageJsonRepoPath = manifestDir === '.' ? 'package.json' : `${manifestDir}/package.json`; - const oldPackageJson = git(['show', `${baseRef}:${packageJsonRepoPath}`]); - try { - const packageAffected = await diffLockfileDeps({ - packageJson, - oldLockfile, - newLockfile, - lockfileType: lockfileName, - oldPackageJson - }); - packageAffectedNodes = packageAffected; - log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); - } catch (e) { - // snyk-nodejs-lockfile-parser is an optionalDependency (requires Node >=18). - // When it's missing we can't reason about the manifest change, so we - // conservatively bail to a full snapshot rather than under-snapshotting. - if (e.code === 'SNYK_LOCKFILE_PARSER_UNAVAILABLE') { - throw new SmartSnapBailError(`SmartSnap: ${e.message}; running full snapshot set`); - } - throw e; - } - } - } - - if (untraced?.length) { - affectedNodes = affectedNodes.filter(p => !untraced.some(g => matchesPattern(p, g))); - if (affectedNodes.length === 0 && packageAffectedNodes.length === 0) { - throw new SmartSnapBailError('SmartSnap: all changed files matched untraced globs; running full snapshot set'); - } - } - +// Bail to a full snapshot set if any changed file matches a user-supplied +// bailOnChanges glob. +export function assertNoBailOnChanges(affectedNodes, bailOnChanges) { if (bailOnChanges?.length) { const bailed = affectedNodes.find(p => bailOnChanges.some(g => matchesPattern(p, g))); if (bailed) { throw new SmartSnapBailError(`SmartSnap: change to "${bailed}" matched bailOnChanges; running full snapshot set`); } } +} - log.debug(`SmartSnap: parsing stats file ${resolvedStatsPath}`); - const { files, modules, buildId } = await readStats(resolvedStatsPath, projectRoot); +// Drop changed files that match a user-supplied `untraced` glob so they don't +// drive snapshot selection. Returns the filtered list (unchanged when no +// untraced patterns are configured). +export function enforceUntraced(affectedNodes, untraced) { + if (untraced?.length) { + return affectedNodes.filter(p => !untraced.some(g => matchesPattern(p, g))); + } + return affectedNodes; +} - if (typeof buildId !== 'string' || !buildId) { - throw new SmartSnapBailError(`SmartSnap: stats file at ${resolvedStatsPath} is missing a top-level "buildId" — running full snapshot set`); +// Manifest/lockfile changes can shift the dependency tree, so resolve the +// diff at the package level via snyk-nodejs-lockfile-parser and return the +// changed package names to feed back into the graph. Returns [] when there's +// nothing to resolve; short-circuits below fall back to a full snapshot +// (via SmartSnapBailError) whenever we can't reason about the change. +export async function getAffectedPackages(affectedNodes, baseRef, projectRoot, log) { + const MANIFEST_PATHS = new Set(['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']); + const manifestHits = affectedNodes.filter(p => MANIFEST_PATHS.has(path.basename(p))); + + if (manifestHits.length === 0) return []; + + // Locate the changed manifest's directory from affectedNodes (NOT the git + // root) — monorepos keep package.json + lockfile inside a workspace dir. + // If two changes land in different dirs, we'd need per-workspace resolution + // we don't try to do yet, so bail. + const uniqueDirs = [...new Set(manifestHits.map(p => path.dirname(p)))]; + if (uniqueDirs.length > 1) { + throw new SmartSnapBailError(`SmartSnap: manifest changes span multiple directories (${uniqueDirs.join(', ')}); running full snapshot set`); + } + const manifestDir = uniqueDirs[0]; // repo-relative; '.' for root + const absManifestDir = path.resolve(projectRoot, manifestDir); + + // Pick the lockfile that lives next to the changed manifest. If two + // coexist (e.g. a stray package-lock.json next to yarn.lock) we can't + // pick a canonical source, so bail. + const LOCKFILE_NAMES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']; + const presentLockfiles = LOCKFILE_NAMES.filter(n => fs.existsSync(path.join(absManifestDir, n))); + if (presentLockfiles.length === 0) { + throw new SmartSnapBailError(`SmartSnap: manifest changed in "${manifestDir}" but no lockfile present there; running full snapshot set`); + } + if (presentLockfiles.length > 1) { + throw new SmartSnapBailError(`SmartSnap: multiple lockfiles in "${manifestDir}" (${presentLockfiles.join(', ')}); cannot pick canonical; running full snapshot set`); + } + const lockfileName = presentLockfiles[0]; + // git always uses forward slashes for its : spec; build it by + // hand instead of path.join (which would use backslashes on Windows). + const lockfileRepoPath = manifestDir === '.' ? lockfileName : `${manifestDir}/${lockfileName}`; + + // Resolve the lockfile at the base commit. If it wasn't tracked there + // (first SmartSnap run, lockfile renamed, etc.) we can't diff, so bail. + let oldLockfile; + try { + oldLockfile = git(['show', `${baseRef}:${lockfileRepoPath}`]); + } catch { + throw new SmartSnapBailError(`SmartSnap: lockfile "${lockfileRepoPath}" not present at base ref ${baseRef}; running full snapshot set`); } - // Storybook's `entries[id].importPath` (and v6 `parameters.fileName`) - // is resolved relative to the directory percy was invoked from — for a - // monorepo storybook that's typically the package dir (e.g. - // `frontend/packages/design-stack`), not the git root. Example: - // `./modules/AgentCard/AgentCard.stories.tsx`. - // - // `affectedNodes` from `git diff --name-only` and the `files` array - // built by the stats compactor are both project-root-relative - // (e.g. `packages/design-stack/modules/AgentCard/AgentCard.stories.tsx`). - // Project them into the same frame so the BE matches importPath against - // affectedNodes without a cross-frame translation. - const dotPosix = './'; - const dotPlatform = `.${path.sep}`; - const invocationDir = process.cwd(); - const normalizeImportPath = p => { - if (typeof p !== 'string' || !p) return p; - let rel = p; - if (rel.startsWith(dotPlatform)) rel = rel.slice(dotPlatform.length); - else if (rel.startsWith(dotPosix)) rel = rel.slice(dotPosix.length); - // If the importPath happens to be absolute (older Storybook configs), - // path.resolve treats it as the target directly; otherwise it's joined - // against `invocationDir`. Then re-base against the git project root. - const abs = path.resolve(invocationDir, rel); - const projRel = path.relative(projectRoot, abs); - // path.relative('','') → '' and `path.relative` produces backslashes - // on Windows; the stats `files` array uses the same — leave platform - // sep alone so the two stay byte-identical for the BE match. - return projRel || rel; - }; + const newLockfile = fs.readFileSync(path.join(absManifestDir, lockfileName), 'utf8'); + // Byte-identical lockfile means only package.json's non-dep fields changed — + // nothing shifted in the dependency tree, so there are no affected packages. + if (oldLockfile === newLockfile) return []; + + const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); + const packageJsonRepoPath = manifestDir === '.' ? 'package.json' : `${manifestDir}/package.json`; + const oldPackageJson = git(['show', `${baseRef}:${packageJsonRepoPath}`]); + try { + const packageAffected = await diffLockfileDeps({ + packageJson, + oldLockfile, + newLockfile, + lockfileType: lockfileName, + oldPackageJson + }); + log.debug(`SmartSnap: lockfile diff produced ${packageAffected.length} affected packages: ${packageAffected.join(', ')}`); + return packageAffected; + } catch (e) { + // snyk-nodejs-lockfile-parser is an optionalDependency (requires Node >=18). + // When it's missing we can't reason about the manifest change, so we + // conservatively bail to a full snapshot rather than under-snapshotting. + if (e.code === 'SNYK_LOCKFILE_PARSER_UNAVAILABLE') { + throw new SmartSnapBailError(`SmartSnap: ${e.message}; running full snapshot set`); + } + throw e; + } +} + +// Project each snapshot's importPath into the project-root frame and return +// the unique set. Logs how many snapshots carried an importPath and warns +// (with a sample) when none do, which usually means broken story extraction. +export function extractStorybookPaths(snapshots, normalizeImportPath, log) { const storybookPaths = [...new Set(snapshots.map(s => normalizeImportPath(s.importPath)).filter(Boolean))]; const snapshotsWithImportPath = snapshots.filter(s => s.importPath).length; log.debug(`SmartSnap: ${snapshotsWithImportPath}/${snapshots.length} snapshots have importPath; ${storybookPaths.length} unique storybookPaths`); @@ -414,11 +398,16 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir } else { log.debug(`SmartSnap: storybookPaths sample: ${storybookPaths.slice(0, 3).join(', ')}`); } + return storybookPaths; +} - if (packageAffectedNodes.length) { - affectedNodes = [...affectedNodes, ...packageAffectedNodes]; - } - +// Kick off the graph generation job and poll it to completion, returning the +// graph payload. The sync status response carries the graph payload directly +// on completion, so there's no second fetch — the returned `data` is the same +// response body that used to come from `getSmartsnapGraphData`. Bails to a +// full snapshot set if the job doesn't reach `done`. +export async function runGraphGeneration(percy, buildId, payload, log) { + const { files, modules, storybookPaths, affectedNodes } = payload; log.debug(`SmartSnap: starting graph generation job ${JSON.stringify({ buildId, files, modules, storybookPaths, affectedNodes })}`); await percy.client.generateSmartsnapGraph(buildId, { files, modules, storybookPaths, affectedNodes @@ -429,16 +418,16 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir throw new SmartSnapBailError(`SmartSnap: graph generation did not complete (status: ${status ?? 'timed out'}); running full snapshot set`); } - // The sync status response carries the graph payload directly on completion, - // so there's no second fetch — `data` here is the same response body that - // used to come from `getSmartsnapGraphData`. - log.debug(`SmartSnap: affected stories result ${JSON.stringify(data?.affected_stories)}`); + return data; +} - // Trace rendering moved client-side: the API now returns the raw graph - // (vertices, edges, transitive-closure triples) and we populate the - // bundled HTML template here. Anything missing means the BE couldn't - // produce a graph — skip silently rather than write a broken page. +// Trace rendering moved client-side: the API now returns the raw graph +// (vertices, edges, transitive-closure triples) and we populate the bundled +// HTML template here. Only runs when `trace` is enabled and the payload is +// complete — anything missing means the BE couldn't produce a graph, so we +// skip silently rather than write a broken page. +export function maybeWriteTrace(trace, data, log) { if (trace && data?.vertices && data?.edges && data?.transitive_closure_matrix_sparse) { const tracePath = path.resolve(process.cwd(), 'trace.html'); try { @@ -453,15 +442,21 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir log.warn(`SmartSnap: failed to write trace.html: ${e.message}`); } } +} +// Filter the snapshots down to those the affected-graph reports plus any that +// need a forced re-snapshot, log the summary, and return the kept list. +// +// Snapshots whose baseline review_state is `failed` or `rejected` have no +// usable baseline image to diff against, and snapshots that don't appear in +// the base build at all are brand-new (no baseline exists yet). In both cases +// SmartSnap can't legitimately skip them — re-snapshot unconditionally +// regardless of what the affected-graph reports. `baselineSnapshots` is null +// when an explicit baseline is set, but `needsBaselineRefresh` short-circuits +// on `baseline` before reading it. +export function selectAffectedSnapshots(snapshots, data, baseline, baselineSnapshots, normalizeImportPath, log) { const affected = new Set(data?.affected_stories || []); - // Snapshots whose baseline review_state is `failed` or `rejected` have no - // usable baseline image to diff against, and snapshots that don't appear - // in the base build at all are brand-new (no baseline exists yet). In - // both cases SmartSnap can't legitimately skip them — re-snapshot - // unconditionally regardless of what the affected-graph reports. - const baselineSnapshots = baseLookup?.snapshots || {}; const FORCE_RESNAPSHOT_STATES = new Set(['failed', 'rejected']); const needsBaselineRefresh = name => { if (baseline) return false; @@ -488,3 +483,76 @@ export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir log.info(`SmartSnap: ${filtered.length} of ${snapshots.length} snapshots kept (${affectedKept} via affected-graph, ${forced} via missing/failed/rejected baseline)`); return filtered; } + +// Given the mapped snapshots and storybook.smartSnap config, returns the subset of snapshots +// that the SmartSnap graph reports as affected. On any recoverable failure, returns the input +// list unchanged so the build runs as a full snapshot pass. +export async function applySmartSnap(percy, snapshots, smartSnapConfig, buildDir) { + const log = logger('storybook:smartsnap'); + const { baseline, untraced, trace, bailOnChanges, statsFile } = smartSnapConfig || {}; + + if (!buildDir) { + throw new SmartSnapBailError('SmartSnap requires the Storybook build directory (e.g. `percy storybook ./storybook-static`); URL and `start` modes are not supported. Running full snapshot set'); + } + + const projectRoot = gitProjectRoot(); + + const { files, modules, buildId } = await validateAndReadStats(buildDir, statsFile, projectRoot, log); + + let { baseRef, affectedNodes, baselineSnapshots } = await getBaselineAndAffectedNodes(percy, baseline, log); + + assertNoDotStorybookChange(affectedNodes); + assertNoBailOnChanges(affectedNodes, bailOnChanges); + affectedNodes = enforceUntraced(affectedNodes, untraced); + + const packageAffectedNodes = await getAffectedPackages(affectedNodes, baseRef, projectRoot, log); + + // With no traced files and no package-level changes there's nothing for the + // graph to reason about — bail to a full snapshot set rather than send an + // empty diff. + if (!affectedNodes.length && !packageAffectedNodes.length) { + throw new SmartSnapBailError('SmartSnap: no affected files or packages detected after filtering; running full snapshot set'); + } + + // Storybook's `entries[id].importPath` (and v6 `parameters.fileName`) + // is resolved relative to the directory percy was invoked from — for a + // monorepo storybook that's typically the package dir (e.g. + // `frontend/packages/design-stack`), not the git root. Example: + // `./modules/AgentCard/AgentCard.stories.tsx`. + // + // `affectedNodes` from `git diff --name-only` and the `files` array + // built by the stats compactor are both project-root-relative + // (e.g. `packages/design-stack/modules/AgentCard/AgentCard.stories.tsx`). + // Project them into the same frame so the BE matches importPath against + // affectedNodes without a cross-frame translation. + const dotPosix = './'; + const dotPlatform = `.${path.sep}`; + const invocationDir = process.cwd(); + const normalizeImportPath = p => { + if (typeof p !== 'string' || !p) return p; + let rel = p; + if (rel.startsWith(dotPlatform)) rel = rel.slice(dotPlatform.length); + else if (rel.startsWith(dotPosix)) rel = rel.slice(dotPosix.length); + // If the importPath happens to be absolute (older Storybook configs), + // path.resolve treats it as the target directly; otherwise it's joined + // against `invocationDir`. Then re-base against the git project root. + const abs = path.resolve(invocationDir, rel); + const projRel = path.relative(projectRoot, abs); + // path.relative('','') → '' and `path.relative` produces backslashes + // on Windows; the stats `files` array uses the same — leave platform + // sep alone so the two stay byte-identical for the BE match. + return projRel || rel; + }; + + const storybookPaths = extractStorybookPaths(snapshots, normalizeImportPath, log); + + if (packageAffectedNodes.length) { + affectedNodes = [...affectedNodes, ...packageAffectedNodes]; + } + + const data = await runGraphGeneration(percy, buildId, { files, modules, storybookPaths, affectedNodes }, log); + + maybeWriteTrace(trace, data, log); + + return selectAffectedSnapshots(snapshots, data, baseline, baselineSnapshots, normalizeImportPath, log); +} diff --git a/packages/cli-command/test/graphTrace.test.js b/packages/cli-command/test/graphTrace.test.js new file mode 100644 index 000000000..4c69735fb --- /dev/null +++ b/packages/cli-command/test/graphTrace.test.js @@ -0,0 +1,190 @@ +import { renderGraphTraceHtml } from '../src/graphTrace.js'; + +// The trace template embeds each payload as `const = ;` on its +// own line (see graphTraceTemplate.html). Pulling that line back out lets us +// assert both the computed layout and the escaping that renderGraphTraceHtml +// applies, without exporting the module-private helpers it delegates to. +function embeddedJson(html, name) { + let match = html.match(new RegExp(`const ${name} = (.*);`)); + if (!match) throw new Error(`could not find embedded "${name}" payload`); + return match[1]; +} + +function vertices(html) { + return JSON.parse(embeddedJson(html, 'vertices')); +} + +describe('graphTrace', () => { + describe('renderGraphTraceHtml() layout', () => { + it('pins dependencies to column 0 and maps their kind to "package"', () => { + let [dep] = vertices(renderGraphTraceHtml({ + vertices: [{ kind: 'dependency', file_path: 'left-pad' }], + edges: [], + transitiveClosureMatrixSparse: [] + })); + + expect(dep.col).toEqual(0); + expect(dep.kind).toEqual('package'); + }); + + it('propagates columns along edges so a target sits right of its source', () => { + // a(dependency) -> b(component) -> c(component), no closure hints, so the + // edge constraint alone drives the columns: 0, 1, 2. + let laidOut = vertices(renderGraphTraceHtml({ + vertices: [ + { kind: 'dependency', file_path: 'pkg-a' }, + { kind: 'component', file_path: 'B.jsx' }, + { kind: 'component', file_path: 'C.jsx' } + ], + edges: [[0, 1], [1, 2]], + transitiveClosureMatrixSparse: [] + })); + + expect(laidOut.map(v => v.col)).toEqual([0, 1, 2]); + }); + + it('pushes every story past the rightmost non-story column', () => { + let laidOut = vertices(renderGraphTraceHtml({ + vertices: [ + { kind: 'dependency', file_path: 'dep' }, + { kind: 'component', file_path: 'Comp.jsx' }, + { kind: 'story', file_path: 'A.stories.jsx' }, + { kind: 'story', file_path: 'B.stories.jsx' } + ], + edges: [[0, 1], [1, 2], [1, 3]], + transitiveClosureMatrixSparse: [[0, 1, 1], [1, 2, 1], [1, 3, 1]] + })); + + let maxNonStory = Math.max(...laidOut + .filter(v => v.kind !== 'story') + .map(v => v.col)); + + for (let v of laidOut.filter(v => v.kind === 'story')) { + expect(v.col).toBeGreaterThan(maxNonStory); + } + }); + + it('lets `changed` win over the underlying kind ("is_relevant")', () => { + let [comp, story] = vertices(renderGraphTraceHtml({ + vertices: [ + { kind: 'component', file_path: 'Comp.jsx', changed: true }, + { kind: 'story', file_path: 'S.stories.jsx', changed: true } + ], + edges: [], + transitiveClosureMatrixSparse: [] + })); + + expect(comp.kind).toEqual('is_relevant'); + expect(story.kind).toEqual('is_relevant'); + }); + + it('maps an unknown kind to "component"', () => { + let [v] = vertices(renderGraphTraceHtml({ + vertices: [{ kind: 'mystery', file_path: 'x' }], + edges: [], + transitiveClosureMatrixSparse: [] + })); + + expect(v.kind).toEqual('component'); + }); + + it('preserves each vertex index and name', () => { + let laidOut = vertices(renderGraphTraceHtml({ + vertices: [ + { kind: 'component', file_path: 'first.jsx' }, + { kind: 'component', file_path: 'second.jsx' } + ], + edges: [], + transitiveClosureMatrixSparse: [] + })); + + expect(laidOut.map(v => v.index)).toEqual([0, 1]); + expect(laidOut.map(v => v.name)).toEqual(['first.jsx', 'second.jsx']); + }); + + it('assigns unique rows within a shared column', () => { + // Two same-column components must not collide on row 0. + let laidOut = vertices(renderGraphTraceHtml({ + vertices: [ + { kind: 'component', file_path: 'A.jsx' }, + { kind: 'component', file_path: 'B.jsx' } + ], + edges: [], + transitiveClosureMatrixSparse: [] + })); + + let col0 = laidOut.filter(v => v.col === laidOut[0].col); + expect(col0.map(v => v.row).sort()).toEqual([0, 1]); + }); + + it('renders empty payloads without throwing', () => { + let html = renderGraphTraceHtml({}); + expect(embeddedJson(html, 'vertices')).toEqual('[]'); + expect(embeddedJson(html, 'edges')).toEqual('[]'); + expect(embeddedJson(html, 'transitive_closure_matrix_sparse')).toEqual('[]'); + }); + + it('tolerates cyclic edges and out-of-range indices', () => { + // Cyclic edges would loop forever without the bounded iteration guard; + // out-of-range edge/closure indices must be ignored, not throw. + let render = () => renderGraphTraceHtml({ + vertices: [ + { kind: 'component', file_path: 'x' }, + { kind: 'component', file_path: 'y' } + ], + edges: [[0, 1], [1, 0], [5, 9]], + transitiveClosureMatrixSparse: [[0, 99, 3]] + }); + + expect(render).not.toThrow(); + expect(vertices(render()).length).toEqual(2); + }); + }); + + describe('renderGraphTraceHtml() escaping', () => { + // Build a name carrying every sequence safeJson must neutralize so a + // malicious file_path can't break out of the surrounding `; + + function hostileLine() { + return embeddedJson(renderGraphTraceHtml({ + vertices: [{ kind: 'component', file_path: hostile }], + edges: [], + transitiveClosureMatrixSparse: [] + }), 'vertices'); + } + + it('escapes " { + let line = hostileLine(); + expect(line).not.toContain(''); + expect(line).toContain('<\\/script>'); + }); + + it('escapes HTML comment open and close markers', () => { + let line = hostileLine(); + expect(line).toContain('<\\!--'); + expect(line).toContain('--\\>'); + }); + + it('escapes U+2028 and U+2029 line/paragraph separators', () => { + let line = hostileLine(); + expect(line).not.toContain(LS); + expect(line).not.toContain(PS); + expect(line).toContain('\\u2028'); + expect(line).toContain('\\u2029'); + }); + + it('escapes only the dangerous sequences, leaving the payload intact', () => { + // The output is embedded in a