@gfxlabs/third-eye-cli 3.24.2 → 3.25.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
@@ -184,7 +184,8 @@ z$1.object({
184
184
  storyName: z$1.string().optional(),
185
185
  storyId: z$1.string().optional(),
186
186
  storyArgs: z$1.record(z$1.string(), z$1.unknown()).optional(),
187
- tags: z$1.array(z$1.string()).optional()
187
+ tags: z$1.array(z$1.string()).optional(),
188
+ designUrl: z$1.string().optional()
188
189
  });
189
190
  //#endregion
190
191
  //#region src/config.ts
@@ -1035,7 +1036,8 @@ const generateStorybookShotItems = (baseUrl, stories, mask, modeBreakpoints, bro
1035
1036
  componentPath: story.kind,
1036
1037
  storyName: story.story,
1037
1038
  storyId: story.id,
1038
- storyArgs: story.parameters?.thirdeye?.args
1039
+ storyArgs: story.parameters?.thirdeye?.args,
1040
+ designUrl: (story.parameters?.design)?.url
1039
1041
  };
1040
1042
  const storyLevelBreakpoints = story.parameters?.thirdeye?.breakpoints ?? [];
1041
1043
  const breakpoints = selectBreakpoints(config.breakpoints, modeBreakpoints, storyLevelBreakpoints);
@@ -1640,6 +1642,12 @@ const createShots = async (turboSnapFilter) => {
1640
1642
  //#region src/git.ts
1641
1643
  const INITIAL_BATCH_SIZE = 20;
1642
1644
  /**
1645
+ * Check if a commit exists in the local git index.
1646
+ */
1647
+ const commitExists = (commit) => {
1648
+ return execGit(`git cat-file -t ${commit}`) === "commit";
1649
+ };
1650
+ /**
1643
1651
  * Execute a git command and return the trimmed output.
1644
1652
  */
1645
1653
  const execGit = (command) => {
@@ -1652,34 +1660,28 @@ const execGit = (command) => {
1652
1660
  return "";
1653
1661
  }
1654
1662
  };
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) => {
1663
+ const getParentCommits = async (hasBuildsWithCommits, options) => {
1669
1664
  if (!execGit("git rev-parse HEAD")) {
1670
1665
  log.process("info", "general", "Not a git repository, skipping ancestor detection");
1671
- return [];
1666
+ return {
1667
+ parentCommits: [],
1668
+ visitedCommitsWithoutBuilds: []
1669
+ };
1672
1670
  }
1673
1671
  if (!execGit("git --no-pager log -n 1 --skip=1 --format=\"%H\"")) {
1674
1672
  log.process("info", "general", "Initial commit, no ancestors");
1675
- return [];
1673
+ return {
1674
+ parentCommits: [],
1675
+ visitedCommitsWithoutBuilds: []
1676
+ };
1676
1677
  }
1677
- let commitsWithBuilds = [];
1678
+ const sinceArg = options?.firstCommittedAtSeconds ? `--since ${options.firstCommittedAtSeconds}` : "";
1679
+ let commitsWithBuilds = [...options?.initialCommitsWithBuilds ?? []];
1678
1680
  let commitsWithoutBuilds = [];
1679
1681
  let limit = INITIAL_BATCH_SIZE;
1680
1682
  for (;;) {
1681
1683
  const notArgs = commitsWithBuilds.map((c) => c.trim()).join(" ");
1682
- const output = execGit(`git rev-list HEAD -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
1684
+ const output = execGit(`git rev-list HEAD ${sinceArg} -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
1683
1685
  const candidates = (output ? output.split("\n").filter(Boolean) : []).filter((c) => !commitsWithBuilds.includes(c)).filter((c) => !commitsWithoutBuilds.includes(c)).slice(0, limit);
1684
1686
  if (candidates.length === 0) break;
1685
1687
  log.process("info", "general", `🔍 Checking ${candidates.length} commits for builds (batch size ${limit})`);
@@ -1695,7 +1697,10 @@ const getParentCommits = async (hasBuildsWithCommits) => {
1695
1697
  }
1696
1698
  if (commitsWithBuilds.length === 0) {
1697
1699
  log.process("info", "general", "No ancestor builds found — this may be the first build");
1698
- return [];
1700
+ return {
1701
+ parentCommits: [],
1702
+ visitedCommitsWithoutBuilds: commitsWithoutBuilds
1703
+ };
1699
1704
  }
1700
1705
  if (commitsWithBuilds.length > 1) {
1701
1706
  const parentArgs = commitsWithBuilds.map((c) => `"${c}^@"`).join(" ");
@@ -1704,7 +1709,31 @@ const getParentCommits = async (hasBuildsWithCommits) => {
1704
1709
  if (maxCommits.length > 0) commitsWithBuilds = maxCommits;
1705
1710
  }
1706
1711
  log.process("info", "general", `📌 Found ${commitsWithBuilds.length} ancestor build(s): ${commitsWithBuilds.map((c) => c.slice(0, 7)).join(", ")}`);
1707
- return commitsWithBuilds;
1712
+ return {
1713
+ parentCommits: commitsWithBuilds,
1714
+ visitedCommitsWithoutBuilds: commitsWithoutBuilds
1715
+ };
1716
+ };
1717
+ /**
1718
+ * Compute the merge base commit between the current HEAD and a base branch,
1719
+ * plus a list of ancestor commits on the base branch from that point backwards.
1720
+ * This gives the server enough git history to walk back and find a build
1721
+ * even if the exact merge base commit doesn't have one.
1722
+ */
1723
+ const getMergeBaseInfo = (baseBranch) => {
1724
+ const mergeBaseCommit = execGit(`git merge-base HEAD ${execGit(`git rev-parse origin/${baseBranch}`) ? `origin/${baseBranch}` : baseBranch}`);
1725
+ if (!mergeBaseCommit) {
1726
+ log.process("info", "general", `Could not compute merge base with ${baseBranch}`);
1727
+ return null;
1728
+ }
1729
+ log.process("info", "general", `📍 Merge base with ${baseBranch}: ${mergeBaseCommit.slice(0, 7)}`);
1730
+ const ancestorsOutput = execGit(`git rev-list ${mergeBaseCommit} -n 50`);
1731
+ const mergeBaseAncestors = ancestorsOutput ? ancestorsOutput.split("\n").filter(Boolean) : [];
1732
+ log.process("info", "general", `📍 Got ${mergeBaseAncestors.length} merge base ancestors for build lookup`);
1733
+ return {
1734
+ mergeBaseCommit,
1735
+ mergeBaseAncestors
1736
+ };
1708
1737
  };
1709
1738
  //#endregion
1710
1739
  //#region ../shared/dist/client.js
@@ -1775,7 +1804,7 @@ const getApiToken = async (config) => {
1775
1804
  process.exit(1);
1776
1805
  }
1777
1806
  };
1778
- const sendInitToAPI = async (config, apiToken, parentCommits) => {
1807
+ const sendInitToAPI = async (config, apiToken, parentCommits, mergeBaseInfo) => {
1779
1808
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1780
1809
  return withRetry("init", () => client.orgs.projects.builds.init({
1781
1810
  orgId: config.thirdEyeOrgId,
@@ -1785,7 +1814,9 @@ const sendInitToAPI = async (config, apiToken, parentCommits) => {
1785
1814
  buildNumber: config.ciBuildNumber,
1786
1815
  baseBranch: config.baseBranch || void 0,
1787
1816
  prNumber: config.prNumber,
1788
- parentCommits
1817
+ parentCommits,
1818
+ mergeBaseCommit: mergeBaseInfo?.mergeBaseCommit,
1819
+ mergeBaseAncestors: mergeBaseInfo?.mergeBaseAncestors
1789
1820
  }));
1790
1821
  };
1791
1822
  const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
@@ -1796,6 +1827,22 @@ const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
1796
1827
  commits
1797
1828
  }))).commits;
1798
1829
  };
1830
+ const getGitInfoFromAPI = async (config, apiToken, branch) => {
1831
+ const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1832
+ return withRetry("getGitInfo", () => client.orgs.projects.builds.getGitInfo({
1833
+ orgId: config.thirdEyeOrgId,
1834
+ projectId: config.thirdEyeProjectId,
1835
+ branch
1836
+ }));
1837
+ };
1838
+ const sendFindSquashMergeParentsToAPI = async (config, apiToken, commits) => {
1839
+ const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1840
+ return (await withRetry("findSquashMergeParents", () => client.orgs.projects.builds.findSquashMergeParents({
1841
+ orgId: config.thirdEyeOrgId,
1842
+ projectId: config.thirdEyeProjectId,
1843
+ commits
1844
+ }))).parents;
1845
+ };
1799
1846
  const sendFinalizeToAPI = async (config, apiToken) => {
1800
1847
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1801
1848
  return withRetry("finalize", () => client.orgs.projects.builds.finalize({
@@ -1843,7 +1890,7 @@ const uploadShot = async ({ config, apiToken, uploadToken, name, file, shotMode,
1843
1890
  metadata
1844
1891
  }), logFn);
1845
1892
  };
1846
- const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey, turboSnap) => {
1893
+ const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey, turboSnap, turboSnapAllStoryNames) => {
1847
1894
  const client = createTypedClient({
1848
1895
  url: config.thirdEyePlatform,
1849
1896
  apiToken,
@@ -1859,7 +1906,8 @@ const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey
1859
1906
  },
1860
1907
  log: logMemory,
1861
1908
  cacheKey,
1862
- turboSnap
1909
+ turboSnap,
1910
+ turboSnapAllStoryNames
1863
1911
  });
1864
1912
  log.process("info", "api", "Successfully sent to API [processShots]");
1865
1913
  return result;
@@ -1917,7 +1965,8 @@ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFile
1917
1965
  ...shotItem.viewport ? { viewport: shotItem.viewport } : {},
1918
1966
  ...shotItem.breakpoint !== void 0 ? { breakpoint: shotItem.breakpoint } : {},
1919
1967
  ...domHtml ? { dom_html: domHtml } : {},
1920
- ...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {}
1968
+ ...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {},
1969
+ ...shotItem.designUrl ? { designUrl: shotItem.designUrl } : {}
1921
1970
  },
1922
1971
  logger
1923
1972
  });
@@ -2194,14 +2243,41 @@ const platformRunner = async (config, apiToken) => {
2194
2243
  `commitRefName = ${config.commitRefName}`,
2195
2244
  `commitHash = ${config.commitHash}`
2196
2245
  ].join("\n - "));
2246
+ log.process("info", "general", "🔍 Querying server for git info...");
2247
+ const gitInfo = await getGitInfoFromAPI(config, apiToken, config.commitRefName);
2248
+ const initialCommitsWithBuilds = [];
2249
+ if (gitInfo.lastBuildOnBranch) {
2250
+ const lastCommit = gitInfo.lastBuildOnBranch.commit;
2251
+ if (commitExists(lastCommit)) {
2252
+ log.process("info", "general", `📌 Seeding with last build on branch: ${lastCommit.slice(0, 7)}`);
2253
+ initialCommitsWithBuilds.push(lastCommit);
2254
+ } else log.process("info", "general", `📌 Last build on branch ${lastCommit.slice(0, 7)} not in local git (rebase?), skipping`);
2255
+ }
2197
2256
  log.process("info", "general", "🔍 Resolving ancestor builds from git history...");
2198
- await sendInitToAPI(config, apiToken, await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits)));
2257
+ const { parentCommits, visitedCommitsWithoutBuilds } = await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits), {
2258
+ firstCommittedAtSeconds: gitInfo.firstBuildCreatedAt ?? void 0,
2259
+ initialCommitsWithBuilds: initialCommitsWithBuilds.length > 0 ? initialCommitsWithBuilds : void 0
2260
+ });
2261
+ if (visitedCommitsWithoutBuilds.length > 0) {
2262
+ log.process("info", "general", `🔀 Checking ${Math.min(visitedCommitsWithoutBuilds.length, 100)} commits for squash merges...`);
2263
+ try {
2264
+ const squashResult = await sendFindSquashMergeParentsToAPI(config, apiToken, visitedCommitsWithoutBuilds.slice(0, 100));
2265
+ for (const { buildCommit } of squashResult) if (commitExists(buildCommit) && !parentCommits.includes(buildCommit)) {
2266
+ log.process("info", "general", `🔀 Found squash merge parent: ${buildCommit.slice(0, 7)}`);
2267
+ parentCommits.push(buildCommit);
2268
+ }
2269
+ } catch (err) {
2270
+ log.process("info", "general", `Squash merge detection failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2271
+ }
2272
+ }
2273
+ await sendInitToAPI(config, apiToken, parentCommits, config.baseBranch ? getMergeBaseInfo(config.baseBranch) : null);
2199
2274
  if (!await checkForCachedBuild(config, apiToken)) {
2200
2275
  log.process("info", "general", "📂 Creating shot folders");
2201
2276
  const createShotsStart = process.hrtime();
2202
2277
  createShotsFolders();
2203
2278
  let turboSnapFilter;
2204
2279
  let turboSnapDependencyMap;
2280
+ let turboSnapAllStoryNames;
2205
2281
  if (config.turboSnap && config.baseBranch) {
2206
2282
  log.process("info", "general", `⚡ TurboSnap enabled, checking changed files against ${config.baseBranch}`);
2207
2283
  const changedFiles = getChangedFiles(config.baseBranch);
@@ -2213,10 +2289,12 @@ const platformRunner = async (config, apiToken) => {
2213
2289
  if (existsSync(indexJsonPath)) {
2214
2290
  const indexJson = JSON.parse(fs.readFileSync(indexJsonPath, "utf-8"));
2215
2291
  const entries = indexJson.entries ?? indexJson.stories ?? {};
2216
- const turboResult = getAffectedStoriesLocal(Object.values(entries).filter((e) => e.type !== "docs").map((e) => ({
2292
+ const stories = Object.values(entries).filter((e) => e.type !== "docs").map((e) => ({
2217
2293
  shotName: generateShotName(e.title, e.name),
2218
2294
  importPath: e.importPath
2219
- })), changedFiles, process.cwd());
2295
+ }));
2296
+ const turboResult = getAffectedStoriesLocal(stories, changedFiles, process.cwd());
2297
+ turboSnapAllStoryNames = stories.map((s) => `storybook/${s.shotName}`);
2220
2298
  log.process("info", "general", `⚡ TurboSnap: ${turboResult.affected.length} affected, ${turboResult.skipped.length} skipped out of ${turboResult.total} stories`);
2221
2299
  turboSnapFilter = new Set(turboResult.affected);
2222
2300
  turboSnapDependencyMap = turboResult.dependencyMap;
@@ -2275,7 +2353,7 @@ const platformRunner = async (config, apiToken) => {
2275
2353
  await processShots(config, apiToken, uploadToken, shotItems.map((shotItem) => ({
2276
2354
  name: `${shotItem.shotMode}/${shotItem.shotName}`,
2277
2355
  threshold: shotItem.threshold
2278
- })), process.env.THIRD_EYE_CACHE_KEY, !!turboSnapFilter);
2356
+ })), process.env.THIRD_EYE_CACHE_KEY, !!turboSnapFilter, turboSnapAllStoryNames);
2279
2357
  if (config.storybookStaticDir && existsSync(config.storybookStaticDir)) {
2280
2358
  log.process("info", "general", "Uploading Storybook archive...");
2281
2359
  const archive = execSync(`tar -czf - -C ${JSON.stringify(config.storybookStaticDir)} .`, { maxBuffer: 50 * 1024 * 1024 }).toString("base64");