@gfxlabs/third-eye-cli 3.23.2 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import path, { join, normalize } from "node:path";
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
@@ -25,6 +26,9 @@ import { createORPCClient } from "@orpc/client";
25
26
  import { RPCLink } from "@orpc/client/fetch";
26
27
  import { execa } from "execa";
27
28
  import { XMLParser } from "fast-xml-parser";
29
+ //#region \0rolldown/runtime.js
30
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
31
+ //#endregion
28
32
  //#region src/log.ts
29
33
  const logMemory = [];
30
34
  const serializeError = (error) => ({
@@ -860,7 +864,7 @@ const collectLadleStories = async (ladleUrl) => {
860
864
  };
861
865
  //#endregion
862
866
  //#region src/crawler/storybook.ts
863
- const kebabCase = (str) => (str ?? "").replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/[\s_/]+/g, "-").toLowerCase();
867
+ const kebabCase$1 = (str) => (str ?? "").replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/[\s_/]+/g, "-").toLowerCase();
864
868
  const getStoryBookUrl = (url) => {
865
869
  if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) return url;
866
870
  if (url.startsWith("/")) return `file://${url}`;
@@ -996,9 +1000,9 @@ const generateStoryUrl = (iframeUrl, storyId, args, breakpoint) => {
996
1000
  const generateFilename = (kind, story, prefix, suffix) => {
997
1001
  return [
998
1002
  prefix,
999
- kebabCase(kind),
1000
- kebabCase(story),
1001
- kebabCase(suffix)
1003
+ kebabCase$1(kind),
1004
+ kebabCase$1(story),
1005
+ kebabCase$1(suffix)
1002
1006
  ].filter(Boolean).join("--");
1003
1007
  };
1004
1008
  const generateStorybookShotItems = (baseUrl, stories, mask, modeBreakpoints, browser) => {
@@ -1496,7 +1500,12 @@ const collectHistoireStories = async (histoireUrl) => {
1496
1500
  };
1497
1501
  //#endregion
1498
1502
  //#region src/createShots.ts
1499
- const createShots = async () => {
1503
+ /**
1504
+ * @param turboSnapFilter - When provided, only stories whose shotName is in this set
1505
+ * will be screenshotted. Stories not in the set are skipped entirely (no Playwright
1506
+ * navigation), which is the main performance win of TurboSnap.
1507
+ */
1508
+ const createShots = async (turboSnapFilter) => {
1500
1509
  const { ladleShots, histoireShots, storybookShots, pageShots, customShots, imagePathCurrent } = config;
1501
1510
  let storybookShotItems = [];
1502
1511
  let ladleShotItems = [];
@@ -1580,10 +1589,12 @@ const createShots = async () => {
1580
1589
  await mapLimit(browsers, 1, async (browser) => {
1581
1590
  const shotItems = generateStorybookShotItems(storybookWebUrl, collection.stories, mask, storybookShots.breakpoints, browsers.length > 1 ? browser : void 0);
1582
1591
  const filterItemsToCheck = "filterItemsToCheck" in config ? config.filterItemsToCheck : void 0;
1583
- const filteredShotItems = filterItemsToCheck ? shotItems.filter((item) => filterItemsToCheck(item)) : shotItems;
1592
+ let filteredShotItems = filterItemsToCheck ? shotItems.filter((item) => filterItemsToCheck(item)) : shotItems;
1593
+ if (turboSnapFilter) filteredShotItems = filteredShotItems.filter((item) => turboSnapFilter.has(item.shotName) || turboSnapFilter.has(`${item.shotMode}/${item.shotName}`));
1584
1594
  storybookShotItems = shotItems;
1585
- log.process("info", "general", `Prepared ${filteredShotItems.length} stories for screenshots on ${browser.name()}`);
1586
- await takeScreenShots(filteredShotItems, browser);
1595
+ const capturedItems = filteredShotItems;
1596
+ log.process("info", "general", turboSnapFilter ? `Prepared ${capturedItems.length}/${shotItems.length} stories for screenshots on ${browser.name()} (TurboSnap: ${shotItems.length - capturedItems.length} skipped)` : `Prepared ${capturedItems.length} stories for screenshots on ${browser.name()}`);
1597
+ await takeScreenShots(capturedItems, browser);
1587
1598
  });
1588
1599
  localServer?.close();
1589
1600
  } catch (error) {
@@ -1626,6 +1637,76 @@ const createShots = async () => {
1626
1637
  ];
1627
1638
  };
1628
1639
  //#endregion
1640
+ //#region src/git.ts
1641
+ const INITIAL_BATCH_SIZE = 20;
1642
+ /**
1643
+ * Execute a git command and return the trimmed output.
1644
+ */
1645
+ const execGit = (command) => {
1646
+ try {
1647
+ return execSync(command, {
1648
+ encoding: "utf-8",
1649
+ timeout: 1e4
1650
+ }).trim();
1651
+ } catch {
1652
+ return "";
1653
+ }
1654
+ };
1655
+ /**
1656
+ * Find the "covering" set of ancestor commits that have builds on the server.
1657
+ *
1658
+ * This mirrors Chromatic's approach:
1659
+ * 1. Walk git history with `git rev-list`
1660
+ * 2. Ask the server which commits have builds
1661
+ * 3. Use `--not` to exclude ancestors of commits with builds
1662
+ * 4. Repeat with exponentially larger batches until no more uncovered commits
1663
+ *
1664
+ * The result is the minimal set of ancestor commits with builds such that
1665
+ * every ancestor of HEAD either has a build or is an ancestor of a commit
1666
+ * with a build.
1667
+ */
1668
+ const getParentCommits = async (hasBuildsWithCommits) => {
1669
+ if (!execGit("git rev-parse HEAD")) {
1670
+ log.process("info", "general", "Not a git repository, skipping ancestor detection");
1671
+ return [];
1672
+ }
1673
+ if (!execGit("git --no-pager log -n 1 --skip=1 --format=\"%H\"")) {
1674
+ log.process("info", "general", "Initial commit, no ancestors");
1675
+ return [];
1676
+ }
1677
+ let commitsWithBuilds = [];
1678
+ let commitsWithoutBuilds = [];
1679
+ let limit = INITIAL_BATCH_SIZE;
1680
+ for (;;) {
1681
+ const notArgs = commitsWithBuilds.map((c) => c.trim()).join(" ");
1682
+ const output = execGit(`git rev-list HEAD -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
1683
+ const candidates = (output ? output.split("\n").filter(Boolean) : []).filter((c) => !commitsWithBuilds.includes(c)).filter((c) => !commitsWithoutBuilds.includes(c)).slice(0, limit);
1684
+ if (candidates.length === 0) break;
1685
+ log.process("info", "general", `🔍 Checking ${candidates.length} commits for builds (batch size ${limit})`);
1686
+ const newCommitsWithBuilds = await hasBuildsWithCommits(candidates);
1687
+ const newCommitsWithoutBuilds = candidates.filter((c) => !newCommitsWithBuilds.includes(c));
1688
+ commitsWithBuilds = [...commitsWithBuilds, ...newCommitsWithBuilds];
1689
+ commitsWithoutBuilds = [...commitsWithoutBuilds, ...newCommitsWithoutBuilds];
1690
+ limit *= 2;
1691
+ if (commitsWithoutBuilds.length > 1e4) {
1692
+ log.process("info", "general", "Reached max history depth (10000 commits)");
1693
+ break;
1694
+ }
1695
+ }
1696
+ if (commitsWithBuilds.length === 0) {
1697
+ log.process("info", "general", "No ancestor builds found — this may be the first build");
1698
+ return [];
1699
+ }
1700
+ if (commitsWithBuilds.length > 1) {
1701
+ const parentArgs = commitsWithBuilds.map((c) => `"${c}^@"`).join(" ");
1702
+ const maxOutput = execGit(`git rev-list ${commitsWithBuilds.join(" ")} --not ${parentArgs}`);
1703
+ const maxCommits = maxOutput ? maxOutput.split("\n").filter(Boolean) : [];
1704
+ if (maxCommits.length > 0) commitsWithBuilds = maxCommits;
1705
+ }
1706
+ log.process("info", "general", `📌 Found ${commitsWithBuilds.length} ancestor build(s): ${commitsWithBuilds.map((c) => c.slice(0, 7)).join(", ")}`);
1707
+ return commitsWithBuilds;
1708
+ };
1709
+ //#endregion
1629
1710
  //#region ../shared/dist/client.js
1630
1711
  /**
1631
1712
  * Typed oRPC client factory.
@@ -1694,7 +1775,7 @@ const getApiToken = async (config) => {
1694
1775
  process.exit(1);
1695
1776
  }
1696
1777
  };
1697
- const sendInitToAPI = async (config, apiToken) => {
1778
+ const sendInitToAPI = async (config, apiToken, parentCommits) => {
1698
1779
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1699
1780
  return withRetry("init", () => client.orgs.projects.builds.init({
1700
1781
  orgId: config.thirdEyeOrgId,
@@ -1703,9 +1784,18 @@ const sendInitToAPI = async (config, apiToken) => {
1703
1784
  branchName: config.commitRefName,
1704
1785
  buildNumber: config.ciBuildNumber,
1705
1786
  baseBranch: config.baseBranch || void 0,
1706
- prNumber: config.prNumber
1787
+ prNumber: config.prNumber,
1788
+ parentCommits
1707
1789
  }));
1708
1790
  };
1791
+ const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
1792
+ const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1793
+ return (await withRetry("hasBuildsWithCommits", () => client.orgs.projects.builds.hasBuildsWithCommits({
1794
+ orgId: config.thirdEyeOrgId,
1795
+ projectId: config.thirdEyeProjectId,
1796
+ commits
1797
+ }))).commits;
1798
+ };
1709
1799
  const sendFinalizeToAPI = async (config, apiToken) => {
1710
1800
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1711
1801
  return withRetry("finalize", () => client.orgs.projects.builds.finalize({
@@ -1795,7 +1885,7 @@ const uploadStorybookArchive = async (config, apiToken, projectId, buildId, arch
1795
1885
  };
1796
1886
  //#endregion
1797
1887
  //#region src/upload.ts
1798
- const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFileHashes, extendedShotItems }) => {
1888
+ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFileHashes, extendedShotItems, dependencyMap }) => {
1799
1889
  if (requiredFileHashes.length > 0) {
1800
1890
  log.process("info", "api", "Uploading shots");
1801
1891
  const uploadStart = process.hrtime();
@@ -1825,7 +1915,8 @@ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFile
1825
1915
  ...shotItem.tags ? { tags: shotItem.tags } : {},
1826
1916
  ...shotItem.viewport ? { viewport: shotItem.viewport } : {},
1827
1917
  ...shotItem.breakpoint !== void 0 ? { breakpoint: shotItem.breakpoint } : {},
1828
- ...domHtml ? { dom_html: domHtml } : {}
1918
+ ...domHtml ? { dom_html: domHtml } : {},
1919
+ ...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {}
1829
1920
  },
1830
1921
  logger
1831
1922
  });
@@ -1836,7 +1927,168 @@ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFile
1836
1927
  return true;
1837
1928
  };
1838
1929
  //#endregion
1930
+ //#region src/turbosnap.ts
1931
+ /**
1932
+ * TurboSnap: static import tracer for determining which stories are affected by code changes.
1933
+ *
1934
+ * Given a list of changed files (from git diff) and the storybook index (importPath per story),
1935
+ * this module traces each story file's transitive imports to determine which stories need
1936
+ * re-screenshotting. Stories whose dependency tree does not overlap with any changed file are
1937
+ * skipped entirely — they never get launched in Playwright, saving the majority of run time.
1938
+ *
1939
+ * The import tracer is intentionally simple: it parses `import`/`require` statements with a
1940
+ * regex, resolves relative and aliased paths, and follows them recursively with cycle detection.
1941
+ * It does not evaluate dynamic imports or re-exports — those are rare in component code and the
1942
+ * fallback is always "capture the story" (safe default).
1943
+ */
1944
+ const RESOLVE_EXTENSIONS = [
1945
+ ".ts",
1946
+ ".tsx",
1947
+ ".js",
1948
+ ".jsx",
1949
+ ".mjs",
1950
+ ".cjs"
1951
+ ];
1952
+ const INDEX_FILES = RESOLVE_EXTENSIONS.map((ext) => `index${ext}`);
1953
+ /**
1954
+ * Regex that captures the string literal from import/require statements.
1955
+ * Handles:
1956
+ * import ... from 'foo'
1957
+ * import ... from "foo"
1958
+ * import 'foo'
1959
+ * export ... from 'foo'
1960
+ * require('foo')
1961
+ */
1962
+ const IMPORT_RE = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+(?:[\s\S]*?\s+from\s+)?|require\s*\()["']([^"']+)["']/g;
1963
+ /**
1964
+ * Parse tsconfig-style path aliases into a simple prefix → directory map.
1965
+ * E.g. `{ "#/*": ["./src/app/*"] }` → `{ "#/": "/abs/path/src/app/" }`
1966
+ */
1967
+ const parsePathAliases = (tsconfigPath, projectRoot) => {
1968
+ const aliases = {};
1969
+ try {
1970
+ const stripped = readFileSync(tsconfigPath, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
1971
+ const paths = JSON.parse(stripped).compilerOptions?.paths;
1972
+ if (paths) {
1973
+ for (const [pattern, targets] of Object.entries(paths)) if (targets.length > 0) {
1974
+ const prefix = pattern.replace(/\*$/, "");
1975
+ const target = targets[0].replace(/\*$/, "");
1976
+ aliases[prefix] = path.resolve(projectRoot, target);
1977
+ }
1978
+ }
1979
+ } catch {}
1980
+ return aliases;
1981
+ };
1982
+ /**
1983
+ * Resolve a single import specifier to an absolute file path.
1984
+ * Returns undefined if the import is external (node_modules) or unresolvable.
1985
+ */
1986
+ const resolveImport = (specifier, fromFile, aliases) => {
1987
+ if (!specifier.startsWith(".") && !specifier.startsWith("/") && !Object.keys(aliases).some((prefix) => specifier.startsWith(prefix))) return;
1988
+ let resolved;
1989
+ const matchingAlias = Object.entries(aliases).find(([prefix]) => specifier.startsWith(prefix));
1990
+ if (matchingAlias) {
1991
+ const [prefix, target] = matchingAlias;
1992
+ resolved = path.join(target, specifier.slice(prefix.length));
1993
+ } else resolved = path.resolve(path.dirname(fromFile), specifier);
1994
+ if (existsSync(resolved) && !isDirectory(resolved)) return resolved;
1995
+ for (const ext of RESOLVE_EXTENSIONS) {
1996
+ const withExt = resolved + ext;
1997
+ if (existsSync(withExt)) return withExt;
1998
+ }
1999
+ for (const indexFile of INDEX_FILES) {
2000
+ const withIndex = path.join(resolved, indexFile);
2001
+ if (existsSync(withIndex)) return withIndex;
2002
+ }
2003
+ };
2004
+ const isDirectory = (p) => {
2005
+ try {
2006
+ const { statSync } = __require("node:fs");
2007
+ return statSync(p).isDirectory();
2008
+ } catch {
2009
+ return false;
2010
+ }
2011
+ };
2012
+ /**
2013
+ * Extract all import specifiers from a source file.
2014
+ */
2015
+ const extractImports = (filePath) => {
2016
+ try {
2017
+ const content = readFileSync(filePath, "utf-8");
2018
+ const imports = [];
2019
+ let match;
2020
+ IMPORT_RE.lastIndex = 0;
2021
+ while ((match = IMPORT_RE.exec(content)) !== null) if (match[1]) imports.push(match[1]);
2022
+ return imports;
2023
+ } catch {
2024
+ return [];
2025
+ }
2026
+ };
2027
+ /**
2028
+ * Trace all transitive dependencies of a file recursively.
2029
+ * Returns a Set of absolute file paths that the file depends on (including itself).
2030
+ */
2031
+ const traceDependencies = (entryFile, projectRoot, aliases, _cache) => {
2032
+ const cache = _cache ?? /* @__PURE__ */ new Map();
2033
+ const absEntry = path.isAbsolute(entryFile) ? entryFile : path.resolve(projectRoot, entryFile);
2034
+ if (cache.has(absEntry)) return cache.get(absEntry);
2035
+ const deps = new Set([absEntry]);
2036
+ cache.set(absEntry, deps);
2037
+ if (!existsSync(absEntry)) return deps;
2038
+ const importSpecifiers = extractImports(absEntry);
2039
+ for (const spec of importSpecifiers) {
2040
+ const resolved = resolveImport(spec, absEntry, aliases);
2041
+ if (resolved && !deps.has(resolved)) {
2042
+ deps.add(resolved);
2043
+ const transitiveDeps = traceDependencies(resolved, projectRoot, aliases, cache);
2044
+ for (const d of transitiveDeps) deps.add(d);
2045
+ }
2046
+ }
2047
+ return deps;
2048
+ };
2049
+ /**
2050
+ * Determine which stories are affected by a set of changed files.
2051
+ *
2052
+ * @param stories - Array of { shotName, importPath } from the storybook index
2053
+ * @param changedFiles - Relative file paths from `git diff --name-only`
2054
+ * @param projectRoot - Absolute path to the project root (for resolving imports)
2055
+ * @returns Which stories to capture and which to skip
2056
+ */
2057
+ const getAffectedStoriesLocal = (stories, changedFiles, projectRoot) => {
2058
+ const aliases = parsePathAliases(path.join(projectRoot, "tsconfig.json"), projectRoot);
2059
+ const changedAbsolute = new Set(changedFiles.map((f) => path.resolve(projectRoot, f)));
2060
+ const affected = [];
2061
+ const skipped = [];
2062
+ const dependencyMap = /* @__PURE__ */ new Map();
2063
+ const depCache = /* @__PURE__ */ new Map();
2064
+ for (const story of stories) {
2065
+ if (!story.importPath) {
2066
+ affected.push(story.shotName);
2067
+ continue;
2068
+ }
2069
+ const deps = traceDependencies(path.resolve(projectRoot, story.importPath), projectRoot, aliases, depCache);
2070
+ const relativeDeps = [...deps].map((d) => path.relative(projectRoot, d));
2071
+ dependencyMap.set(story.importPath, relativeDeps);
2072
+ let isAffected = false;
2073
+ for (const dep of deps) if (changedAbsolute.has(dep)) {
2074
+ isAffected = true;
2075
+ break;
2076
+ }
2077
+ if (isAffected) affected.push(story.shotName);
2078
+ else skipped.push(story.shotName);
2079
+ }
2080
+ return {
2081
+ affected,
2082
+ skipped,
2083
+ total: stories.length,
2084
+ dependencyMap
2085
+ };
2086
+ };
2087
+ //#endregion
1839
2088
  //#region src/runner.ts
2089
+ const kebabCase = (str) => (str ?? "").replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/[\s_/]+/g, "-").toLowerCase();
2090
+ /** Generate the shot name from a storybook title and story name, matching the crawler's logic. */
2091
+ const generateShotName = (title, name) => [kebabCase(title), kebabCase(name)].filter(Boolean).join("--");
1840
2092
  /**
1841
2093
  * Get the list of files changed between the current HEAD and a base ref
1842
2094
  * using git diff. Returns an empty array if git is unavailable or fails.
@@ -1941,30 +2193,46 @@ const platformRunner = async (config, apiToken) => {
1941
2193
  `commitRefName = ${config.commitRefName}`,
1942
2194
  `commitHash = ${config.commitHash}`
1943
2195
  ].join("\n - "));
1944
- await sendInitToAPI(config, apiToken);
2196
+ log.process("info", "general", "🔍 Resolving ancestor builds from git history...");
2197
+ await sendInitToAPI(config, apiToken, await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits)));
1945
2198
  if (!await checkForCachedBuild(config, apiToken)) {
1946
2199
  log.process("info", "general", "📂 Creating shot folders");
1947
2200
  const createShotsStart = process.hrtime();
1948
2201
  createShotsFolders();
1949
- log.process("info", "general", "📸 Creating shots");
1950
- let shotItems = await createShots();
2202
+ let turboSnapFilter;
2203
+ let turboSnapDependencyMap;
1951
2204
  if (config.turboSnap && config.baseBranch) {
1952
2205
  log.process("info", "general", `⚡ TurboSnap enabled, checking changed files against ${config.baseBranch}`);
1953
2206
  const changedFiles = getChangedFiles(config.baseBranch);
1954
2207
  if (changedFiles.length > 0) {
1955
2208
  log.process("info", "general", `Found ${changedFiles.length} changed file(s)`);
1956
2209
  try {
1957
- const turboResult = await getAffectedStories(config, apiToken, changedFiles);
1958
- log.process("info", "general", `TurboSnap: ${turboResult.affectedCount} affected, ${turboResult.skippedCount} skipped`);
1959
- if (turboResult.affected.length > 0) {
1960
- const affectedSet = new Set(turboResult.affected);
1961
- shotItems = shotItems.filter((item) => affectedSet.has(item.shotName) || affectedSet.has(`${item.shotMode}/${item.shotName}`));
2210
+ const storybookPath = config.storybookShots?.storybookUrl ?? "";
2211
+ const indexJsonPath = path.join(storybookPath.startsWith("http") ? "" : storybookPath, "index.json");
2212
+ if (existsSync(indexJsonPath)) {
2213
+ const indexJson = JSON.parse(fs.readFileSync(indexJsonPath, "utf-8"));
2214
+ const entries = indexJson.entries ?? indexJson.stories ?? {};
2215
+ const turboResult = getAffectedStoriesLocal(Object.values(entries).filter((e) => e.type !== "docs").map((e) => ({
2216
+ shotName: generateShotName(e.title, e.name),
2217
+ importPath: e.importPath
2218
+ })), changedFiles, process.cwd());
2219
+ log.process("info", "general", `⚡ TurboSnap: ${turboResult.affected.length} affected, ${turboResult.skipped.length} skipped out of ${turboResult.total} stories`);
2220
+ turboSnapFilter = new Set(turboResult.affected);
2221
+ turboSnapDependencyMap = turboResult.dependencyMap;
2222
+ } else try {
2223
+ const serverResult = await getAffectedStories(config, apiToken, changedFiles);
2224
+ log.process("info", "general", `⚡ TurboSnap (server): ${serverResult.affectedCount} affected, ${serverResult.skippedCount} skipped`);
2225
+ if (serverResult.affected.length > 0) turboSnapFilter = new Set(serverResult.affected);
2226
+ } catch (error) {
2227
+ if (error instanceof Error) log.process("warn", "general", `TurboSnap server query failed, capturing all stories: ${error.message}`);
1962
2228
  }
1963
2229
  } catch (error) {
1964
2230
  if (error instanceof Error) log.process("warn", "general", `TurboSnap filtering failed, capturing all stories: ${error.message}`);
1965
2231
  }
1966
2232
  } else log.process("info", "general", "TurboSnap: no changed files detected, capturing all stories");
1967
2233
  }
2234
+ log.process("info", "general", "📸 Creating shots");
2235
+ let shotItems = await createShots(turboSnapFilter);
1968
2236
  const shotNames = shotItems.map((shotItem) => shotItem.shotName);
1969
2237
  const uniqueShotNames = new Set(shotNames);
1970
2238
  if (shotNames.length !== uniqueShotNames.size) {
@@ -2000,7 +2268,8 @@ const platformRunner = async (config, apiToken) => {
2000
2268
  apiToken,
2001
2269
  uploadToken,
2002
2270
  requiredFileHashes,
2003
- extendedShotItems
2271
+ extendedShotItems,
2272
+ dependencyMap: turboSnapDependencyMap
2004
2273
  });
2005
2274
  await processShots(config, apiToken, uploadToken, shotItems.map((shotItem) => ({
2006
2275
  name: `${shotItem.shotMode}/${shotItem.shotName}`,