@gfxlabs/third-eye-cli 3.24.3 → 3.25.1

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
@@ -27,6 +27,16 @@ import { RPCLink } from "@orpc/client/fetch";
27
27
  import { execa } from "execa";
28
28
  import { XMLParser } from "fast-xml-parser";
29
29
  //#region \0rolldown/runtime.js
30
+ var __defProp = Object.defineProperty;
31
+ var __exportAll = (all, no_symbols) => {
32
+ let target = {};
33
+ for (var name in all) __defProp(target, name, {
34
+ get: all[name],
35
+ enumerable: true
36
+ });
37
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
38
+ return target;
39
+ };
30
40
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
31
41
  //#endregion
32
42
  //#region src/log.ts
@@ -184,7 +194,8 @@ z$1.object({
184
194
  storyName: z$1.string().optional(),
185
195
  storyId: z$1.string().optional(),
186
196
  storyArgs: z$1.record(z$1.string(), z$1.unknown()).optional(),
187
- tags: z$1.array(z$1.string()).optional()
197
+ tags: z$1.array(z$1.string()).optional(),
198
+ designUrl: z$1.string().optional()
188
199
  });
189
200
  //#endregion
190
201
  //#region src/config.ts
@@ -1035,7 +1046,8 @@ const generateStorybookShotItems = (baseUrl, stories, mask, modeBreakpoints, bro
1035
1046
  componentPath: story.kind,
1036
1047
  storyName: story.story,
1037
1048
  storyId: story.id,
1038
- storyArgs: story.parameters?.thirdeye?.args
1049
+ storyArgs: story.parameters?.thirdeye?.args,
1050
+ designUrl: (story.parameters?.design)?.url
1039
1051
  };
1040
1052
  const storyLevelBreakpoints = story.parameters?.thirdeye?.breakpoints ?? [];
1041
1053
  const breakpoints = selectBreakpoints(config.breakpoints, modeBreakpoints, storyLevelBreakpoints);
@@ -1229,6 +1241,22 @@ const getPagesFromExternalLoader = async () => {
1229
1241
  };
1230
1242
  //#endregion
1231
1243
  //#region src/shots/shots.ts
1244
+ /**
1245
+ * Wait for Storybook play functions to complete before screenshotting.
1246
+ * Checks __STORYBOOK_PREVIEW__.currentRender.phase and data-play-fn-done attribute.
1247
+ */
1248
+ const waitForPlayFunction = async (page, shotItem, logger) => {
1249
+ if (shotItem.shotMode !== "storybook") return;
1250
+ try {
1251
+ await page.waitForFunction(() => {
1252
+ if (window.__STORYBOOK_PREVIEW__?.currentRender?.phase === "playing") return false;
1253
+ if (document.getElementById("storybook-root")?.dataset.playFnDone === "false") return false;
1254
+ return true;
1255
+ }, { timeout: config.timeouts?.loadState ?? 3e4 });
1256
+ } catch {
1257
+ logger.process("info", "general", `Play function wait timed out for ${shotItem.id} — proceeding with screenshot`);
1258
+ }
1259
+ };
1232
1260
  const takeScreenShot = async ({ browser, shotItem, logger }) => {
1233
1261
  const context = await browser.newContext(shotItem.browserConfig);
1234
1262
  const page = await context.newPage();
@@ -1273,6 +1301,7 @@ const takeScreenShot = async ({ browser, shotItem, logger }) => {
1273
1301
  } catch (error) {
1274
1302
  logger.process("error", "timeout", `Timeout while waiting for all network requests: ${shotItem.url}`, error);
1275
1303
  }
1304
+ await waitForPlayFunction(page, shotItem, logger);
1276
1305
  if (config.beforeScreenshot) await config.beforeScreenshot(page, {
1277
1306
  shotMode: shotItem.shotMode,
1278
1307
  id: shotItem.id,
@@ -1640,6 +1669,12 @@ const createShots = async (turboSnapFilter) => {
1640
1669
  //#region src/git.ts
1641
1670
  const INITIAL_BATCH_SIZE = 20;
1642
1671
  /**
1672
+ * Check if a commit exists in the local git index.
1673
+ */
1674
+ const commitExists = (commit) => {
1675
+ return execGit(`git cat-file -t ${commit}`) === "commit";
1676
+ };
1677
+ /**
1643
1678
  * Execute a git command and return the trimmed output.
1644
1679
  */
1645
1680
  const execGit = (command) => {
@@ -1652,34 +1687,28 @@ const execGit = (command) => {
1652
1687
  return "";
1653
1688
  }
1654
1689
  };
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) => {
1690
+ const getParentCommits = async (hasBuildsWithCommits, options) => {
1669
1691
  if (!execGit("git rev-parse HEAD")) {
1670
1692
  log.process("info", "general", "Not a git repository, skipping ancestor detection");
1671
- return [];
1693
+ return {
1694
+ parentCommits: [],
1695
+ visitedCommitsWithoutBuilds: []
1696
+ };
1672
1697
  }
1673
1698
  if (!execGit("git --no-pager log -n 1 --skip=1 --format=\"%H\"")) {
1674
1699
  log.process("info", "general", "Initial commit, no ancestors");
1675
- return [];
1700
+ return {
1701
+ parentCommits: [],
1702
+ visitedCommitsWithoutBuilds: []
1703
+ };
1676
1704
  }
1677
- let commitsWithBuilds = [];
1705
+ const sinceArg = options?.firstCommittedAtSeconds ? `--since ${options.firstCommittedAtSeconds}` : "";
1706
+ let commitsWithBuilds = [...options?.initialCommitsWithBuilds ?? []];
1678
1707
  let commitsWithoutBuilds = [];
1679
1708
  let limit = INITIAL_BATCH_SIZE;
1680
1709
  for (;;) {
1681
1710
  const notArgs = commitsWithBuilds.map((c) => c.trim()).join(" ");
1682
- const output = execGit(`git rev-list HEAD -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
1711
+ const output = execGit(`git rev-list HEAD ${sinceArg} -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
1683
1712
  const candidates = (output ? output.split("\n").filter(Boolean) : []).filter((c) => !commitsWithBuilds.includes(c)).filter((c) => !commitsWithoutBuilds.includes(c)).slice(0, limit);
1684
1713
  if (candidates.length === 0) break;
1685
1714
  log.process("info", "general", `🔍 Checking ${candidates.length} commits for builds (batch size ${limit})`);
@@ -1695,7 +1724,10 @@ const getParentCommits = async (hasBuildsWithCommits) => {
1695
1724
  }
1696
1725
  if (commitsWithBuilds.length === 0) {
1697
1726
  log.process("info", "general", "No ancestor builds found — this may be the first build");
1698
- return [];
1727
+ return {
1728
+ parentCommits: [],
1729
+ visitedCommitsWithoutBuilds: commitsWithoutBuilds
1730
+ };
1699
1731
  }
1700
1732
  if (commitsWithBuilds.length > 1) {
1701
1733
  const parentArgs = commitsWithBuilds.map((c) => `"${c}^@"`).join(" ");
@@ -1704,7 +1736,31 @@ const getParentCommits = async (hasBuildsWithCommits) => {
1704
1736
  if (maxCommits.length > 0) commitsWithBuilds = maxCommits;
1705
1737
  }
1706
1738
  log.process("info", "general", `📌 Found ${commitsWithBuilds.length} ancestor build(s): ${commitsWithBuilds.map((c) => c.slice(0, 7)).join(", ")}`);
1707
- return commitsWithBuilds;
1739
+ return {
1740
+ parentCommits: commitsWithBuilds,
1741
+ visitedCommitsWithoutBuilds: commitsWithoutBuilds
1742
+ };
1743
+ };
1744
+ /**
1745
+ * Compute the merge base commit between the current HEAD and a base branch,
1746
+ * plus a list of ancestor commits on the base branch from that point backwards.
1747
+ * This gives the server enough git history to walk back and find a build
1748
+ * even if the exact merge base commit doesn't have one.
1749
+ */
1750
+ const getMergeBaseInfo = (baseBranch) => {
1751
+ const mergeBaseCommit = execGit(`git merge-base HEAD ${execGit(`git rev-parse origin/${baseBranch}`) ? `origin/${baseBranch}` : baseBranch}`);
1752
+ if (!mergeBaseCommit) {
1753
+ log.process("info", "general", `Could not compute merge base with ${baseBranch}`);
1754
+ return null;
1755
+ }
1756
+ log.process("info", "general", `📍 Merge base with ${baseBranch}: ${mergeBaseCommit.slice(0, 7)}`);
1757
+ const ancestorsOutput = execGit(`git rev-list ${mergeBaseCommit} -n 50`);
1758
+ const mergeBaseAncestors = ancestorsOutput ? ancestorsOutput.split("\n").filter(Boolean) : [];
1759
+ log.process("info", "general", `📍 Got ${mergeBaseAncestors.length} merge base ancestors for build lookup`);
1760
+ return {
1761
+ mergeBaseCommit,
1762
+ mergeBaseAncestors
1763
+ };
1708
1764
  };
1709
1765
  //#endregion
1710
1766
  //#region ../shared/dist/client.js
@@ -1744,6 +1800,21 @@ const createTypedClient = (options) => {
1744
1800
  };
1745
1801
  //#endregion
1746
1802
  //#region src/api.ts
1803
+ var api_exports = /* @__PURE__ */ __exportAll({
1804
+ getAffectedStories: () => getAffectedStories,
1805
+ getApiToken: () => getApiToken,
1806
+ getGitInfoFromAPI: () => getGitInfoFromAPI,
1807
+ prepareUpload: () => prepareUpload,
1808
+ processShots: () => processShots,
1809
+ sendCheckCacheToAPI: () => sendCheckCacheToAPI,
1810
+ sendFinalizeToAPI: () => sendFinalizeToAPI,
1811
+ sendFindSquashMergeParentsToAPI: () => sendFindSquashMergeParentsToAPI,
1812
+ sendHasBuildsWithCommitsToAPI: () => sendHasBuildsWithCommitsToAPI,
1813
+ sendInitToAPI: () => sendInitToAPI,
1814
+ sendRecordLogsToAPI: () => sendRecordLogsToAPI,
1815
+ uploadShot: () => uploadShot,
1816
+ uploadStorybookArchive: () => uploadStorybookArchive
1817
+ });
1747
1818
  const createClient = (platformUrl, apiKey, apiToken) => createTypedClient({
1748
1819
  url: platformUrl,
1749
1820
  apiKey,
@@ -1775,7 +1846,7 @@ const getApiToken = async (config) => {
1775
1846
  process.exit(1);
1776
1847
  }
1777
1848
  };
1778
- const sendInitToAPI = async (config, apiToken, parentCommits) => {
1849
+ const sendInitToAPI = async (config, apiToken, parentCommits, mergeBaseInfo) => {
1779
1850
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1780
1851
  return withRetry("init", () => client.orgs.projects.builds.init({
1781
1852
  orgId: config.thirdEyeOrgId,
@@ -1785,7 +1856,9 @@ const sendInitToAPI = async (config, apiToken, parentCommits) => {
1785
1856
  buildNumber: config.ciBuildNumber,
1786
1857
  baseBranch: config.baseBranch || void 0,
1787
1858
  prNumber: config.prNumber,
1788
- parentCommits
1859
+ parentCommits,
1860
+ mergeBaseCommit: mergeBaseInfo?.mergeBaseCommit,
1861
+ mergeBaseAncestors: mergeBaseInfo?.mergeBaseAncestors
1789
1862
  }));
1790
1863
  };
1791
1864
  const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
@@ -1796,6 +1869,22 @@ const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
1796
1869
  commits
1797
1870
  }))).commits;
1798
1871
  };
1872
+ const getGitInfoFromAPI = async (config, apiToken, branch) => {
1873
+ const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1874
+ return withRetry("getGitInfo", () => client.orgs.projects.builds.getGitInfo({
1875
+ orgId: config.thirdEyeOrgId,
1876
+ projectId: config.thirdEyeProjectId,
1877
+ branch
1878
+ }));
1879
+ };
1880
+ const sendFindSquashMergeParentsToAPI = async (config, apiToken, commits) => {
1881
+ const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1882
+ return (await withRetry("findSquashMergeParents", () => client.orgs.projects.builds.findSquashMergeParents({
1883
+ orgId: config.thirdEyeOrgId,
1884
+ projectId: config.thirdEyeProjectId,
1885
+ commits
1886
+ }))).parents;
1887
+ };
1799
1888
  const sendFinalizeToAPI = async (config, apiToken) => {
1800
1889
  const client = createClient(config.thirdEyePlatform, void 0, apiToken);
1801
1890
  return withRetry("finalize", () => client.orgs.projects.builds.finalize({
@@ -1918,7 +2007,8 @@ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFile
1918
2007
  ...shotItem.viewport ? { viewport: shotItem.viewport } : {},
1919
2008
  ...shotItem.breakpoint !== void 0 ? { breakpoint: shotItem.breakpoint } : {},
1920
2009
  ...domHtml ? { dom_html: domHtml } : {},
1921
- ...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {}
2010
+ ...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {},
2011
+ ...shotItem.designUrl ? { designUrl: shotItem.designUrl } : {}
1922
2012
  },
1923
2013
  logger
1924
2014
  });
@@ -2195,8 +2285,44 @@ const platformRunner = async (config, apiToken) => {
2195
2285
  `commitRefName = ${config.commitRefName}`,
2196
2286
  `commitHash = ${config.commitHash}`
2197
2287
  ].join("\n - "));
2288
+ log.process("info", "general", "🔍 Querying server for git info...");
2289
+ const gitInfo = await getGitInfoFromAPI(config, apiToken, config.commitRefName);
2290
+ const initialCommitsWithBuilds = [];
2291
+ if (gitInfo.lastBuildOnBranch) {
2292
+ const lastCommit = gitInfo.lastBuildOnBranch.commit;
2293
+ if (commitExists(lastCommit)) {
2294
+ log.process("info", "general", `📌 Seeding with last build on branch: ${lastCommit.slice(0, 7)}`);
2295
+ initialCommitsWithBuilds.push(lastCommit);
2296
+ } else log.process("info", "general", `📌 Last build on branch ${lastCommit.slice(0, 7)} not in local git (rebase?), skipping`);
2297
+ }
2298
+ if (config.baseBranch) {
2299
+ const mergeBase = getMergeBaseInfo(config.baseBranch);
2300
+ if (mergeBase) {
2301
+ const mbCommitsWithBuilds = await sendHasBuildsWithCommitsToAPI(config, apiToken, mergeBase.mergeBaseAncestors);
2302
+ for (const c of mbCommitsWithBuilds) if (!initialCommitsWithBuilds.includes(c)) {
2303
+ log.process("info", "general", `📌 Seeding with base branch build: ${c.slice(0, 7)}`);
2304
+ initialCommitsWithBuilds.push(c);
2305
+ }
2306
+ }
2307
+ }
2198
2308
  log.process("info", "general", "🔍 Resolving ancestor builds from git history...");
2199
- await sendInitToAPI(config, apiToken, await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits)));
2309
+ const { parentCommits, visitedCommitsWithoutBuilds } = await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits), {
2310
+ firstCommittedAtSeconds: gitInfo.firstBuildCreatedAt ?? void 0,
2311
+ initialCommitsWithBuilds: initialCommitsWithBuilds.length > 0 ? initialCommitsWithBuilds : void 0
2312
+ });
2313
+ if (visitedCommitsWithoutBuilds.length > 0) {
2314
+ log.process("info", "general", `🔀 Checking ${Math.min(visitedCommitsWithoutBuilds.length, 100)} commits for squash merges...`);
2315
+ try {
2316
+ const squashResult = await sendFindSquashMergeParentsToAPI(config, apiToken, visitedCommitsWithoutBuilds.slice(0, 100));
2317
+ for (const { buildCommit } of squashResult) if (commitExists(buildCommit) && !parentCommits.includes(buildCommit)) {
2318
+ log.process("info", "general", `🔀 Found squash merge parent: ${buildCommit.slice(0, 7)}`);
2319
+ parentCommits.push(buildCommit);
2320
+ }
2321
+ } catch (err) {
2322
+ log.process("info", "general", `Squash merge detection failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2323
+ }
2324
+ }
2325
+ await sendInitToAPI(config, apiToken, parentCommits, config.baseBranch ? getMergeBaseInfo(config.baseBranch) : null);
2200
2326
  if (!await checkForCachedBuild(config, apiToken)) {
2201
2327
  log.process("info", "general", "📂 Creating shot folders");
2202
2328
  const createShotsStart = process.hrtime();
@@ -2298,6 +2424,90 @@ const platformRunner = async (config, apiToken) => {
2298
2424
  process.exit(1);
2299
2425
  }
2300
2426
  };
2427
+ const patchBuildRunner = async (patchBuildArg, config, apiToken) => {
2428
+ const { execSync } = await import("node:child_process");
2429
+ const parts = patchBuildArg.split("...");
2430
+ if (parts.length !== 2) {
2431
+ log.process("error", "general", `Invalid --patch-build format. Expected: head...base (e.g. feature...main)`);
2432
+ process.exit(1);
2433
+ }
2434
+ const [headRef, baseRef] = parts;
2435
+ log.process("info", "general", `🩹 Patch build: computing merge base between ${headRef} and ${baseRef}...`);
2436
+ let mergeBaseCommit;
2437
+ try {
2438
+ mergeBaseCommit = execSync(`git merge-base ${headRef} ${baseRef}`, { encoding: "utf-8" }).trim();
2439
+ } catch {
2440
+ log.process("error", "general", `Failed to compute merge base between ${headRef} and ${baseRef}`);
2441
+ process.exit(1);
2442
+ }
2443
+ log.process("info", "general", `📍 Merge base commit: ${mergeBaseCommit.slice(0, 7)}`);
2444
+ const { sendHasBuildsWithCommitsToAPI: checkCommits } = await Promise.resolve().then(() => api_exports);
2445
+ if ((await checkCommits(config, apiToken, [mergeBaseCommit])).length > 0) {
2446
+ log.process("info", "general", `✅ Build already exists for merge base ${mergeBaseCommit.slice(0, 7)}, no patch build needed.`);
2447
+ return;
2448
+ }
2449
+ const originalRef = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
2450
+ const originalBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
2451
+ log.process("info", "general", `📦 Saving current state (${originalBranch} @ ${originalRef.slice(0, 7)})...`);
2452
+ let hasStash = false;
2453
+ try {
2454
+ hasStash = !execSync("git stash", { encoding: "utf-8" }).trim().includes("No local changes");
2455
+ } catch {}
2456
+ try {
2457
+ log.process("info", "general", `🔄 Checking out merge base ${mergeBaseCommit.slice(0, 7)}...`);
2458
+ execSync(`git checkout ${mergeBaseCommit}`, { stdio: "pipe" });
2459
+ log.process("info", "general", "📦 Installing dependencies...");
2460
+ try {
2461
+ execSync("pnpm install --frozen-lockfile", {
2462
+ stdio: "pipe",
2463
+ timeout: 12e4
2464
+ });
2465
+ } catch {
2466
+ try {
2467
+ execSync("npm ci", {
2468
+ stdio: "pipe",
2469
+ timeout: 12e4
2470
+ });
2471
+ } catch {
2472
+ execSync("npm install", {
2473
+ stdio: "pipe",
2474
+ timeout: 12e4
2475
+ });
2476
+ }
2477
+ }
2478
+ log.process("info", "general", "📖 Building Storybook...");
2479
+ if (config.storybookShots?.storybookUrl) try {
2480
+ execSync("npx storybook build", {
2481
+ stdio: "pipe",
2482
+ timeout: 3e5
2483
+ });
2484
+ } catch {
2485
+ log.process("info", "general", "Storybook build command failed, trying build-storybook...");
2486
+ execSync("npx build-storybook", {
2487
+ stdio: "pipe",
2488
+ timeout: 3e5
2489
+ });
2490
+ }
2491
+ const originalCommitHash = config.commitHash;
2492
+ const originalRefName = config.commitRefName;
2493
+ config.commitHash = mergeBaseCommit;
2494
+ config.commitRefName = baseRef;
2495
+ log.process("info", "general", "🚀 Running Third Eye for merge base...");
2496
+ await platformRunner(config, apiToken);
2497
+ config.commitHash = originalCommitHash;
2498
+ config.commitRefName = originalRefName;
2499
+ log.process("info", "general", `✅ Patch build complete for merge base ${mergeBaseCommit.slice(0, 7)}`);
2500
+ } finally {
2501
+ log.process("info", "general", `🔄 Restoring original state...`);
2502
+ try {
2503
+ if (originalBranch === "HEAD") execSync(`git checkout ${originalRef}`, { stdio: "pipe" });
2504
+ else execSync(`git checkout ${originalBranch}`, { stdio: "pipe" });
2505
+ if (hasStash) execSync("git stash pop", { stdio: "pipe" });
2506
+ } catch (err) {
2507
+ log.process("error", "general", `Failed to restore git state: ${err instanceof Error ? err.message : String(err)}`);
2508
+ }
2509
+ }
2510
+ };
2301
2511
  //#endregion
2302
2512
  //#region src/docker-runner/utils.ts
2303
2513
  const executeDockerRun = async ({ version }) => {
@@ -2372,7 +2582,11 @@ const generatePagesFromSitemap = async () => {
2372
2582
  //#endregion
2373
2583
  //#region src/bin.ts
2374
2584
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
2375
- const commandArgs = yargs(hideBin(process.argv)).parseSync()._;
2585
+ const args = yargs(hideBin(process.argv)).option("patch-build", {
2586
+ type: "string",
2587
+ describe: "Create a patch build for the merge base. Format: head...base (e.g. feature...main)"
2588
+ }).parseSync();
2589
+ const commandArgs = args._;
2376
2590
  const version = getVersion();
2377
2591
  if (version) log.process("info", "general", `Version: ${version}`);
2378
2592
  (async () => {
@@ -2401,6 +2615,7 @@ if (version) log.process("info", "general", `Version: ${version}`);
2401
2615
  log.process("info", "general", `🚀 Starting Lost Pixel in 'platform' mode`);
2402
2616
  const apiToken = await getPlatformApiToken(config);
2403
2617
  if (commandArgs.includes("finalize")) await sendFinalizeToAPI(config, apiToken);
2618
+ else if (args["patch-build"]) await patchBuildRunner(args["patch-build"], config, apiToken);
2404
2619
  else await platformRunner(config, apiToken);
2405
2620
  } else {
2406
2621
  log.process("info", "general", `🚀 Starting Lost Pixel in 'generateOnly' mode`);