foss-design 0.9.7 → 0.9.9

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/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  import { spawnSync as spawnSync3 } from "child_process";
5
5
  import fs37 from "fs";
6
- import path35 from "path";
6
+ import path36 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/core/paths.ts
@@ -348,8 +348,8 @@ function routeError(pattern) {
348
348
  }
349
349
  return null;
350
350
  }
351
- function split(path36) {
352
- return path36.split("/").filter(Boolean);
351
+ function split(path37) {
352
+ return path37.split("/").filter(Boolean);
353
353
  }
354
354
  function compileRoute(pattern) {
355
355
  return {
@@ -368,8 +368,8 @@ function decode2(part) {
368
368
  return part;
369
369
  }
370
370
  }
371
- function scoreRoute(route2, path36) {
372
- const parts = split(path36.split(/[?#]/)[0] ?? "").map(decode2);
371
+ function scoreRoute(route2, path37) {
372
+ const parts = split(path37.split(/[?#]/)[0] ?? "").map(decode2);
373
373
  const walk2 = (s, p) => {
374
374
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
375
375
  const segment = route2.segments[s];
@@ -387,11 +387,11 @@ function scoreRoute(route2, path36) {
387
387
  };
388
388
  return walk2(0, 0);
389
389
  }
390
- function matchRoute(entries, path36) {
390
+ function matchRoute(entries, path37) {
391
391
  let best = null;
392
392
  for (const entry of entries) {
393
393
  for (const route2 of entry.routes) {
394
- const score = scoreRoute(route2, path36);
394
+ const score = scoreRoute(route2, path37);
395
395
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
396
396
  }
397
397
  }
@@ -518,9 +518,9 @@ var CanvasSchema = z.strictObject({
518
518
  pages: z.array(z.unknown()).min(1, "add at least one page")
519
519
  });
520
520
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
521
- function formatPath(base, path36) {
521
+ function formatPath(base, path37) {
522
522
  let out = base;
523
- for (const part of path36) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
523
+ for (const part of path37) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
524
524
  return out;
525
525
  }
526
526
  function describeZodError(error, base = "") {
@@ -2150,10 +2150,23 @@ async function buildSite(paths, out, options = {}) {
2150
2150
  inputs.push(file);
2151
2151
  }
2152
2152
  fs12.mkdirSync(out, { recursive: true });
2153
+ const moduleMap = {};
2153
2154
  if (inputs.length) {
2154
2155
  const base = baseConfig(project);
2155
2156
  await build({
2156
2157
  ...base,
2158
+ plugins: [
2159
+ ...base.plugins ?? [],
2160
+ options.moduleMap ? {
2161
+ name: "design-module-map",
2162
+ generateBundle(_, bundle) {
2163
+ for (const [file, chunk2] of Object.entries(bundle)) {
2164
+ if (chunk2.type !== "chunk") continue;
2165
+ moduleMap[file] = (chunk2.moduleIds ?? []).map((id2) => id2.split("?")[0]).filter((id2) => path12.isAbsolute(id2) && !id2.includes(`${path12.sep}node_modules${path12.sep}`)).map((id2) => toPosix(path12.relative(paths.root, id2))).filter((rel) => !rel.startsWith(".."));
2166
+ }
2167
+ }
2168
+ } : null
2169
+ ],
2157
2170
  root: staging,
2158
2171
  base: "./",
2159
2172
  logLevel: "warn",
@@ -2173,6 +2186,7 @@ async function buildSite(paths, out, options = {}) {
2173
2186
  fs12.rmSync(out, { recursive: true, force: true });
2174
2187
  fs12.mkdirSync(out, { recursive: true });
2175
2188
  }
2189
+ if (options.moduleMap) fs12.writeFileSync(path12.join(out, "modules.json"), JSON.stringify(moduleMap));
2176
2190
  for (const { source, dir } of htmlScreens) {
2177
2191
  copyTree(
2178
2192
  path12.dirname(source.file),
@@ -2634,7 +2648,7 @@ var CloudClient = class {
2634
2648
  }
2635
2649
  host;
2636
2650
  token;
2637
- async send(method, path36, init) {
2651
+ async send(method, path37, init) {
2638
2652
  const headers = {
2639
2653
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2640
2654
  Accept: "application/json"
@@ -2642,7 +2656,7 @@ var CloudClient = class {
2642
2656
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2643
2657
  if (init.type) headers["Content-Type"] = init.type;
2644
2658
  try {
2645
- return await fetch(`${this.host}/api${path36}`, {
2659
+ return await fetch(`${this.host}/api${path37}`, {
2646
2660
  method,
2647
2661
  headers,
2648
2662
  body: init.body,
@@ -2663,8 +2677,8 @@ var CloudClient = class {
2663
2677
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2664
2678
  }
2665
2679
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2666
- async request(method, path36, body) {
2667
- const response = await this.send(method, path36, {
2680
+ async request(method, path37, body) {
2681
+ const response = await this.send(method, path37, {
2668
2682
  body: body === void 0 ? void 0 : JSON.stringify(body),
2669
2683
  type: body === void 0 ? void 0 : "application/json",
2670
2684
  timeout: JSON_TIMEOUT
@@ -2676,7 +2690,7 @@ var CloudClient = class {
2676
2690
  try {
2677
2691
  return JSON.parse(text2);
2678
2692
  } catch {
2679
- throw new CloudError(`${this.host} answered ${method} ${path36} with something other than JSON`, 0, "bad_response");
2693
+ throw new CloudError(`${this.host} answered ${method} ${path37} with something other than JSON`, 0, "bad_response");
2680
2694
  }
2681
2695
  }
2682
2696
  /** POST /device/code. */
@@ -3031,10 +3045,32 @@ var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word
3031
3045
  // src/capture/cover.ts
3032
3046
  import fs20 from "fs";
3033
3047
  import os3 from "os";
3048
+ import path19 from "path";
3034
3049
 
3035
3050
  // src/server/snapshots.ts
3051
+ import { createHash as createHash5 } from "crypto";
3036
3052
  import fs17 from "fs";
3037
3053
  import path17 from "path";
3054
+ function depsHash(root, rels, cache = /* @__PURE__ */ new Map()) {
3055
+ const hash = createHash5("sha1");
3056
+ for (const rel of [...rels].sort()) {
3057
+ let one = cache.get(rel);
3058
+ if (one === void 0) {
3059
+ one = sourceHash(path17.join(root, rel)) ?? "missing";
3060
+ cache.set(rel, one);
3061
+ }
3062
+ hash.update(`${rel}\0${one}
3063
+ `);
3064
+ }
3065
+ return hash.digest("hex").slice(0, 16);
3066
+ }
3067
+ function sourceHash(file) {
3068
+ try {
3069
+ return createHash5("sha1").update(fs17.readFileSync(file)).digest("hex").slice(0, 16);
3070
+ } catch {
3071
+ return null;
3072
+ }
3073
+ }
3038
3074
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
3039
3075
  function isSafeSegment(value) {
3040
3076
  return SAFE.test(value);
@@ -3050,6 +3086,12 @@ var SnapshotStore = class {
3050
3086
  metaFile(canvas) {
3051
3087
  return path17.join(this.dir, canvas, "meta.json");
3052
3088
  }
3089
+ /** Written aside and renamed: a run stopped mid-write leaves the old file, not half of one. */
3090
+ writeMeta(canvas, data) {
3091
+ const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3092
+ fs17.writeFileSync(temp, JSON.stringify(data));
3093
+ fs17.renameSync(temp, this.metaFile(canvas));
3094
+ }
3053
3095
  load(canvas) {
3054
3096
  const cached = this.meta.get(canvas);
3055
3097
  if (cached) return cached;
@@ -3084,7 +3126,26 @@ var SnapshotStore = class {
3084
3126
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
3085
3127
  return this.load(canvas)[id2]?.[theme2] ?? null;
3086
3128
  }
3087
- save(canvas, id2, theme2, png, height) {
3129
+ /** The source hash recorded with the snapshot; undefined for snapshots taken before hashes were kept. */
3130
+ source(canvas, id2, theme2) {
3131
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3132
+ return this.load(canvas)[id2]?.sources?.[theme2];
3133
+ }
3134
+ /** The files the snapshot's frame loaded, when a build told (see `Meta.deps`). */
3135
+ deps(canvas, id2) {
3136
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3137
+ return this.load(canvas)[id2]?.deps;
3138
+ }
3139
+ /** Record `hash` for a snapshot taken before hashes were kept: from now on it counts as that content's. */
3140
+ adopt(canvas, id2, theme2, hash) {
3141
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return;
3142
+ const data = this.load(canvas);
3143
+ const entry = data[id2];
3144
+ if (!entry?.[theme2]) return;
3145
+ entry.sources = { ...entry.sources, [theme2]: hash };
3146
+ this.writeMeta(canvas, data);
3147
+ }
3148
+ save(canvas, id2, theme2, png, height, source, deps) {
3088
3149
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3089
3150
  const dir = path17.join(this.dir, canvas);
3090
3151
  fs17.mkdirSync(dir, { recursive: true });
@@ -3093,10 +3154,12 @@ var SnapshotStore = class {
3093
3154
  fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3094
3155
  const data = this.load(canvas);
3095
3156
  const version = Date.now();
3096
- data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3097
- const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3098
- fs17.writeFileSync(temp, JSON.stringify(data));
3099
- fs17.renameSync(temp, this.metaFile(canvas));
3157
+ const previous = data[id2];
3158
+ const sources = { ...previous?.sources };
3159
+ if (source) sources[theme2] = source;
3160
+ else delete sources[theme2];
3161
+ data[id2] = { ...previous, [theme2]: version, ...height ? { height } : {}, sources, ...deps ? { deps } : {} };
3162
+ this.writeMeta(canvas, data);
3100
3163
  return { url: this.url(canvas, id2, theme2, version) };
3101
3164
  }
3102
3165
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
@@ -3120,7 +3183,8 @@ async function serveBuild(paths, name, options) {
3120
3183
  await buildSite(paths, out, {
3121
3184
  canvases: options.canvases,
3122
3185
  includeSystem: options.includeSystem,
3123
- includeViewer: false
3186
+ includeViewer: false,
3187
+ moduleMap: options.moduleMap
3124
3188
  });
3125
3189
  const files = sirv(out, { dev: true });
3126
3190
  const server = http.createServer((req, res) => {
@@ -3341,6 +3405,7 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3341
3405
  const store = new SnapshotStore(paths.snapshots);
3342
3406
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3343
3407
  const stale = [];
3408
+ const hashes2 = /* @__PURE__ */ new Map();
3344
3409
  for (const id2 of canvasIds) {
3345
3410
  const canvas = project.canvas(id2);
3346
3411
  if (!canvas) continue;
@@ -3353,13 +3418,19 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3353
3418
  if (filter.screens && !filter.screens.has(job.id)) continue;
3354
3419
  const file = files.get(job.id);
3355
3420
  if (!file || !fs20.existsSync(file)) continue;
3356
- const changed = fs20.statSync(file).mtimeMs;
3421
+ const deps = store.deps(id2, job.id);
3422
+ const hash = deps ? depsHash(paths.root, deps, hashes2) : sourceHash(file);
3357
3423
  const pinned = inDark.get(job.id) === job.theme;
3358
3424
  const themes = pinned ? [job.theme] : THEMES2;
3359
- const screen = { id: job.id };
3360
3425
  for (const theme2 of themes) {
3361
- const taken = store.version(id2, screen.id, theme2);
3362
- if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
3426
+ if (!store.version(id2, job.id, theme2)) {
3427
+ stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3428
+ continue;
3429
+ }
3430
+ const recorded = store.source(id2, job.id, theme2);
3431
+ if (recorded === void 0) {
3432
+ if (hash) store.adopt(id2, job.id, theme2, hash);
3433
+ } else if (recorded !== hash) stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3363
3434
  }
3364
3435
  }
3365
3436
  }
@@ -3376,9 +3447,12 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3376
3447
  let docOf = (id2) => project.canvas(id2).doc;
3377
3448
  let stop = async () => {
3378
3449
  };
3450
+ let modules = null;
3451
+ const hashes2 = /* @__PURE__ */ new Map();
3379
3452
  try {
3380
- const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
3453
+ const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false, moduleMap: true });
3381
3454
  url = build2.base;
3455
+ modules = build2.read("modules.json");
3382
3456
  docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
3383
3457
  stop = build2.close;
3384
3458
  } catch {
@@ -3391,7 +3465,7 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3391
3465
  const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
3392
3466
  progress2.update(line());
3393
3467
  try {
3394
- await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
3468
+ await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2, source }) => {
3395
3469
  const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3396
3470
  if (job) {
3397
3471
  const page = await browser.newPage({
@@ -3399,12 +3473,25 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3399
3473
  deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width),
3400
3474
  colorScheme: theme2
3401
3475
  });
3476
+ const loaded = /* @__PURE__ */ new Set();
3477
+ page.on("request", (request) => {
3478
+ const at = request.url();
3479
+ if (at.startsWith(url)) loaded.add(decodeURIComponent(new URL(at).pathname.slice(1)));
3480
+ });
3402
3481
  try {
3403
3482
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
3404
3483
  if (report.ready) {
3405
3484
  const png = await page.screenshot({ fullPage: job.fullPage });
3406
3485
  const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3407
- store.save(canvas, screen, theme2, png, height);
3486
+ const file = project.source(canvas, screen)?.file;
3487
+ const deps = modules ? [
3488
+ .../* @__PURE__ */ new Set([
3489
+ ...[...loaded].flatMap((rel) => modules[rel] ?? []),
3490
+ ...file ? [toPosix(path19.relative(paths.root, file))] : []
3491
+ ])
3492
+ ].sort() : [];
3493
+ const hash = deps.length ? depsHash(paths.root, deps, hashes2) : source;
3494
+ store.save(canvas, screen, theme2, png, height, hash, deps.length ? deps : void 0);
3408
3495
  } else failed.push(`${canvas}/${screen} (${theme2})`);
3409
3496
  } catch {
3410
3497
  failed.push(`${canvas}/${screen} (${theme2})`);
@@ -3431,21 +3518,21 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3431
3518
 
3432
3519
  // src/cloud/pull.ts
3433
3520
  import fs24 from "fs";
3434
- import path22 from "path";
3521
+ import path23 from "path";
3435
3522
 
3436
3523
  // src/cloud/merge.ts
3437
3524
  import fs22 from "fs";
3438
- import path20 from "path";
3525
+ import path21 from "path";
3439
3526
  import { merge as diff3 } from "node-diff3";
3440
3527
 
3441
3528
  // src/cloud/write.ts
3442
3529
  import fs21 from "fs";
3443
- import path19 from "path";
3530
+ import path20 from "path";
3444
3531
  function writeInside(root, rel, data) {
3445
- const file = path19.join(root, rel);
3532
+ const file = path20.join(root, rel);
3446
3533
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3447
- fs21.mkdirSync(path19.dirname(file), { recursive: true });
3448
- if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
3534
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
3535
+ if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path20.dirname(file))))
3449
3536
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3450
3537
  if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3451
3538
  fs21.writeFileSync(file, data);
@@ -3459,16 +3546,16 @@ function pruneEmpty(dir, stop) {
3459
3546
  } catch {
3460
3547
  return;
3461
3548
  }
3462
- current = path19.dirname(current);
3549
+ current = path20.dirname(current);
3463
3550
  }
3464
3551
  }
3465
3552
 
3466
3553
  // src/cloud/merge.ts
3467
3554
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3468
3555
  var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
3469
- var mergeDir = (paths, key) => path20.join(cloudCache(paths), "merge", ...key.split("/"));
3470
- var stateFile = (paths, key) => path20.join(mergeDir(paths, key), "state.json");
3471
- var sideFile = (paths, key, rel, side) => path20.join(mergeDir(paths, key), side, rel);
3556
+ var mergeDir = (paths, key) => path21.join(cloudCache(paths), "merge", ...key.split("/"));
3557
+ var stateFile = (paths, key) => path21.join(mergeDir(paths, key), "state.json");
3558
+ var sideFile = (paths, key, rel, side) => path21.join(mergeDir(paths, key), side, rel);
3472
3559
  function readMergeState(paths, key) {
3473
3560
  try {
3474
3561
  return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
@@ -3482,10 +3569,10 @@ function saveState(paths, state) {
3482
3569
  `);
3483
3570
  }
3484
3571
  function mergingUnits(paths) {
3485
- const root = path20.join(cloudCache(paths), "merge");
3572
+ const root = path21.join(cloudCache(paths), "merge");
3486
3573
  const found = [];
3487
3574
  const visit = (dir, prefix) => {
3488
- if (fs22.existsSync(path20.join(dir, "state.json"))) {
3575
+ if (fs22.existsSync(path21.join(dir, "state.json"))) {
3489
3576
  const state = readMergeState(paths, prefix);
3490
3577
  if (state) found.push(state);
3491
3578
  return;
@@ -3496,7 +3583,7 @@ function mergingUnits(paths) {
3496
3583
  } catch {
3497
3584
  }
3498
3585
  for (const entry of entries)
3499
- if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3586
+ if (entry.isDirectory()) visit(path21.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3500
3587
  };
3501
3588
  visit(root, "");
3502
3589
  return found;
@@ -3506,7 +3593,7 @@ function openConflicts(paths, state) {
3506
3593
  return state.conflicts.filter((conflict) => {
3507
3594
  if (conflict.kind !== "text") return true;
3508
3595
  try {
3509
- return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
3596
+ return MARKER.test(fs22.readFileSync(path21.join(paths.design, conflict.path), "utf8"));
3510
3597
  } catch {
3511
3598
  return false;
3512
3599
  }
@@ -3515,19 +3602,19 @@ function openConflicts(paths, state) {
3515
3602
  function keep(paths, key, rel, side, data) {
3516
3603
  if (!data) return;
3517
3604
  const file = sideFile(paths, key, rel, side);
3518
- fs22.mkdirSync(path20.dirname(file), { recursive: true });
3605
+ fs22.mkdirSync(path21.dirname(file), { recursive: true });
3519
3606
  fs22.writeFileSync(file, data);
3520
3607
  }
3521
3608
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3522
3609
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3523
3610
  const conflicts = [];
3524
- const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
3611
+ const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3525
3612
  fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3526
- const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
3613
+ const read = (rel) => fs22.readFileSync(path21.join(paths.design, rel));
3527
3614
  const remove = (rel) => {
3528
- const file = path20.join(paths.design, rel);
3615
+ const file = path21.join(paths.design, rel);
3529
3616
  fs22.rmSync(file, { force: true });
3530
- pruneEmpty(path20.dirname(file), stop);
3617
+ pruneEmpty(path21.dirname(file), stop);
3531
3618
  };
3532
3619
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3533
3620
  const b = base[rel];
@@ -3583,11 +3670,11 @@ function resolveConflict(paths, state, rel, side) {
3583
3670
  const conflict = state.conflicts.find((item) => item.path === rel);
3584
3671
  if (!conflict) return;
3585
3672
  const kept = sideFile(paths, state.unit, rel, side);
3586
- const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
3673
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, state.unit));
3587
3674
  if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3588
3675
  else {
3589
- fs22.rmSync(path20.join(paths.design, rel), { force: true });
3590
- pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
3676
+ fs22.rmSync(path21.join(paths.design, rel), { force: true });
3677
+ pruneEmpty(path21.dirname(path21.join(paths.design, rel)), stop);
3591
3678
  }
3592
3679
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3593
3680
  saveState(paths, state);
@@ -3605,7 +3692,7 @@ function finishMerge(paths, link, key, accept = false) {
3605
3692
 
3606
3693
  // src/cloud/meta.ts
3607
3694
  import fs23 from "fs";
3608
- import path21 from "path";
3695
+ import path22 from "path";
3609
3696
  async function syncProjectMeta(paths, client, link) {
3610
3697
  const remote = await client.project(link.project);
3611
3698
  if (!remote) return [];
@@ -3624,7 +3711,7 @@ async function syncProjectMeta(paths, client, link) {
3624
3711
  if (localChanged && !cloudChanged) {
3625
3712
  try {
3626
3713
  if (file) {
3627
- const ext = path21.extname(file).slice(1);
3714
+ const ext = path22.extname(file).slice(1);
3628
3715
  await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3629
3716
  lines.push("The project icon went up to the cloud.");
3630
3717
  } else {
@@ -3660,12 +3747,12 @@ function checkManifest(key, manifest) {
3660
3747
  }
3661
3748
  function applyUnit(paths, key, local, target, blobs) {
3662
3749
  const counts = { added: 0, changed: 0, removed: 0 };
3663
- const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3750
+ const stop = key === SYSTEM_UNIT ? paths.design : path23.dirname(unitRoot(paths, key));
3664
3751
  for (const rel of Object.keys(local)) {
3665
3752
  if (rel in target) continue;
3666
- const file = path22.join(paths.design, rel);
3753
+ const file = path23.join(paths.design, rel);
3667
3754
  fs24.rmSync(file, { force: true });
3668
- pruneEmpty(path22.dirname(file), stop);
3755
+ pruneEmpty(path23.dirname(file), stop);
3669
3756
  counts.removed++;
3670
3757
  }
3671
3758
  for (const [rel, entry] of Object.entries(target)) {
@@ -3764,7 +3851,7 @@ async function pull(ctx, options) {
3764
3851
  }
3765
3852
  const byHash = /* @__PURE__ */ new Map();
3766
3853
  for (const unit of scan.units.values())
3767
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
3854
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path23.join(paths.design, rel));
3768
3855
  const needed = /* @__PURE__ */ new Set();
3769
3856
  const bases = /* @__PURE__ */ new Set();
3770
3857
  for (const plan of plans) {
@@ -3863,7 +3950,7 @@ async function pull(ctx, options) {
3863
3950
 
3864
3951
  // src/cloud/push.ts
3865
3952
  import fs25 from "fs";
3866
- import path23 from "path";
3953
+ import path24 from "path";
3867
3954
  function blockedReport(status, message) {
3868
3955
  return {
3869
3956
  unit: status.key,
@@ -3979,7 +4066,7 @@ async function push(ctx, options) {
3979
4066
  for (const key of toPush) {
3980
4067
  const s = status(key);
3981
4068
  if (s.conflictRev !== void 0) {
3982
- const dir = path23.relative(paths.root, incomingDir(paths, key));
4069
+ const dir = path24.relative(paths.root, incomingDir(paths, key));
3983
4070
  blocked.push(
3984
4071
  blockedReport(
3985
4072
  s,
@@ -4028,7 +4115,7 @@ async function push(ctx, options) {
4028
4115
  const files = /* @__PURE__ */ new Map();
4029
4116
  const addFiles = (manifest, dir) => {
4030
4117
  for (const [rel, entry] of Object.entries(manifest))
4031
- if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
4118
+ if (!files.has(entry.hash)) files.set(entry.hash, path24.join(dir, rel));
4032
4119
  };
4033
4120
  for (const key of pushing) {
4034
4121
  const id2 = unitCanvasId(key);
@@ -4074,7 +4161,7 @@ async function push(ctx, options) {
4074
4161
  await client.putBlob(link.project, hash, data);
4075
4162
  } catch (error) {
4076
4163
  if (error instanceof CloudError && error.status === 413)
4077
- throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4164
+ throw new CliError(`${path24.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4078
4165
  throw error;
4079
4166
  }
4080
4167
  sentFiles++;
@@ -4118,7 +4205,7 @@ function refuseLegacyGo(paths, keys) {
4118
4205
  for (const key of keys) {
4119
4206
  if (!key.startsWith("canvas/")) continue;
4120
4207
  const id2 = key.slice("canvas/".length);
4121
- for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4208
+ for (const file of canvasCodeFiles(path24.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4122
4209
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4123
4210
  }
4124
4211
  }
@@ -4139,13 +4226,13 @@ import fs29 from "fs";
4139
4226
  // src/server/index.ts
4140
4227
  import fs28 from "fs";
4141
4228
  import http2 from "http";
4142
- import path26 from "path";
4229
+ import path27 from "path";
4143
4230
  import sirv2 from "sirv";
4144
4231
  import { createServer, searchForWorkspaceRoot } from "vite";
4145
4232
 
4146
4233
  // src/server/api.ts
4147
4234
  import fs26 from "fs";
4148
- import path24 from "path";
4235
+ import path25 from "path";
4149
4236
  function sendJson(res, status, body) {
4150
4237
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4151
4238
  res.end(JSON.stringify(body));
@@ -4196,7 +4283,7 @@ async function handleApi(ctx, req, res, url) {
4196
4283
  }
4197
4284
  if (route2 === "/source") {
4198
4285
  const rel = url.searchParams.get("path") ?? "";
4199
- const file = path24.resolve(project.paths.root, rel);
4286
+ const file = path25.resolve(project.paths.root, rel);
4200
4287
  if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4201
4288
  sendJson(res, 404, { error: "not found" });
4202
4289
  return true;
@@ -4236,7 +4323,10 @@ async function handleApi(ctx, req, res, url) {
4236
4323
  try {
4237
4324
  const body = await readBody(req);
4238
4325
  if (body.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("not a PNG");
4239
- const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height);
4326
+ const deps = snapshots.deps(canvas, id2);
4327
+ const file = project.source(canvas, id2)?.file;
4328
+ const source = deps ? depsHash(project.paths.root, deps) : file ? sourceHash(file) : null;
4329
+ const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height, source);
4240
4330
  project.invalidateCanvas(canvas);
4241
4331
  events.send({ type: "snapshot", canvas, id: id2, theme: theme2, url: snapshotUrl, height });
4242
4332
  sendJson(res, 200, { url: snapshotUrl });
@@ -4295,9 +4385,9 @@ var EventHub = class {
4295
4385
  // src/server/state.ts
4296
4386
  import fs27 from "fs";
4297
4387
  import os4 from "os";
4298
- import path25 from "path";
4299
- var stateFile2 = (paths) => path25.join(paths.cache, "server.json");
4300
- var logFile = (paths) => path25.join(paths.cache, "server.log");
4388
+ import path26 from "path";
4389
+ var stateFile2 = (paths) => path26.join(paths.cache, "server.json");
4390
+ var logFile = (paths) => path26.join(paths.cache, "server.log");
4301
4391
  function readState(paths) {
4302
4392
  try {
4303
4393
  return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
@@ -4306,10 +4396,10 @@ function readState(paths) {
4306
4396
  }
4307
4397
  }
4308
4398
  function registryDir() {
4309
- const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path25.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path25.join(os4.homedir(), ".cache");
4310
- return path25.join(base, "foss-design", "previews");
4399
+ const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path26.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path26.join(os4.homedir(), ".cache");
4400
+ return path26.join(base, "foss-design", "previews");
4311
4401
  }
4312
- var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
4402
+ var entryFile = (pid) => path26.join(registryDir(), `${pid}.json`);
4313
4403
  function writeState(paths, state) {
4314
4404
  fs27.mkdirSync(paths.cache, { recursive: true });
4315
4405
  fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
@@ -4336,7 +4426,7 @@ function listPreviews() {
4336
4426
  }
4337
4427
  const found = [];
4338
4428
  for (const name of files) {
4339
- const file = path25.join(registryDir(), name);
4429
+ const file = path26.join(registryDir(), name);
4340
4430
  let state = null;
4341
4431
  try {
4342
4432
  state = JSON.parse(fs27.readFileSync(file, "utf8"));
@@ -4348,7 +4438,7 @@ function listPreviews() {
4348
4438
  }
4349
4439
  let recorded = null;
4350
4440
  try {
4351
- recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4441
+ recorded = JSON.parse(fs27.readFileSync(path26.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4352
4442
  } catch {
4353
4443
  }
4354
4444
  found.push({ ...state, tracked: recorded?.pid === state.pid });
@@ -4363,7 +4453,7 @@ async function stopPreview(entry) {
4363
4453
  const until = Date.now() + 5e3;
4364
4454
  while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
4365
4455
  fs27.rmSync(entryFile(entry.pid), { force: true });
4366
- const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
4456
+ const stateAt = path26.join(entry.root, ".design", ".cache", "server.json");
4367
4457
  try {
4368
4458
  if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4369
4459
  fs27.rmSync(stateAt, { force: true });
@@ -4452,8 +4542,8 @@ async function startDevServer(options) {
4452
4542
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4453
4543
  // generated entries are modules the frames load, so those stay watched.
4454
4544
  ignored: (file) => {
4455
- const rel = toPosix(path26.relative(paths.cache, file));
4456
- const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
4545
+ const rel = toPosix(path27.relative(paths.cache, file));
4546
+ const inCache = rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
4457
4547
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4458
4548
  }
4459
4549
  },
@@ -4462,7 +4552,7 @@ async function startDevServer(options) {
4462
4552
  forwardConsole: false
4463
4553
  },
4464
4554
  optimizeDeps: {
4465
- entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
4555
+ entries: entries.files().map((file) => toPosix(path27.relative(paths.design, file)))
4466
4556
  }
4467
4557
  });
4468
4558
  const publicDir = project.publicDir();
@@ -4475,7 +4565,7 @@ async function startDevServer(options) {
4475
4565
  }
4476
4566
  };
4477
4567
  const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4478
- const viewerIndex = path26.join(PKG.viewer, "index.html");
4568
+ const viewerIndex = path27.join(PKG.viewer, "index.html");
4479
4569
  const serveViewerIndex = (res) => {
4480
4570
  const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4481
4571
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4483,7 +4573,7 @@ async function startDevServer(options) {
4483
4573
  };
4484
4574
  const ctx = { project, events, snapshots };
4485
4575
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4486
- const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
4576
+ const bootUrl = `/${relToDesign(paths, path27.join(entries.dir, HTML_BOOT))}`;
4487
4577
  const frameHead = (source, extra = {}) => {
4488
4578
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4489
4579
  return {
@@ -4513,7 +4603,7 @@ async function startDevServer(options) {
4513
4603
  };
4514
4604
  const serveHtmlScreen = async (res, url2) => {
4515
4605
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4516
- const file = path26.resolve(paths.design, rel);
4606
+ const file = path27.resolve(paths.design, rel);
4517
4607
  if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4518
4608
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4519
4609
  return;
@@ -4524,7 +4614,7 @@ async function startDevServer(options) {
4524
4614
  key: `${canvas}/${id2}`,
4525
4615
  canvas,
4526
4616
  id: id2,
4527
- title: path26.basename(file),
4617
+ title: path27.basename(file),
4528
4618
  file,
4529
4619
  format: "html",
4530
4620
  props: {},
@@ -4563,7 +4653,7 @@ async function startDevServer(options) {
4563
4653
  }
4564
4654
  if (url2.pathname.startsWith("/_fs/")) {
4565
4655
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4566
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
4656
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path27.join(publicDir, rel))) {
4567
4657
  notFoundPage(res, "Not found", url2.pathname);
4568
4658
  return;
4569
4659
  }
@@ -4575,7 +4665,7 @@ async function startDevServer(options) {
4575
4665
  const rel = publicRel(url2.pathname.slice(1));
4576
4666
  if (servePublic && rel !== null && project.isPublicFile(rel))
4577
4667
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4578
- else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4668
+ else if (path27.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4579
4669
  else serveViewerIndex(res);
4580
4670
  };
4581
4671
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4603,7 +4693,7 @@ async function startDevServer(options) {
4603
4693
  };
4604
4694
  vite.watcher.on("all", (event, file) => {
4605
4695
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4606
- const rel = toPosix(path26.relative(paths.design, file));
4696
+ const rel = toPosix(path27.relative(paths.design, file));
4607
4697
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4608
4698
  if (rel === "design.json") {
4609
4699
  queue("config", () => {
@@ -5328,7 +5418,7 @@ function printLinkIssues(issues, project) {
5328
5418
  // src/cli/commands/cloud.ts
5329
5419
  import fs30 from "fs";
5330
5420
  import os5 from "os";
5331
- import path27 from "path";
5421
+ import path28 from "path";
5332
5422
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5333
5423
  async function runLogin() {
5334
5424
  const host = cloudHost();
@@ -5429,7 +5519,7 @@ async function runLink(paths, wanted, options) {
5429
5519
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
5430
5520
  } else {
5431
5521
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
5432
- fs30.rmSync(path27.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5522
+ fs30.rmSync(path28.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5433
5523
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
5434
5524
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
5435
5525
  }
@@ -5457,7 +5547,7 @@ async function runLink(paths, wanted, options) {
5457
5547
 
5458
5548
  // src/cli/commands/icon.ts
5459
5549
  import fs31 from "fs";
5460
- import path28 from "path";
5550
+ import path29 from "path";
5461
5551
  async function toCloud(paths, file) {
5462
5552
  const link = readLink(paths);
5463
5553
  if (!link) return;
@@ -5468,7 +5558,7 @@ async function toCloud(paths, file) {
5468
5558
  }
5469
5559
  const client = new CloudClient(link.host, credential.token);
5470
5560
  try {
5471
- const ext = file ? path28.extname(file).slice(1) : null;
5561
+ const ext = file ? path29.extname(file).slice(1) : null;
5472
5562
  await client.setProjectIcon(
5473
5563
  link.project,
5474
5564
  file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
@@ -5484,14 +5574,14 @@ async function runIcon(paths, file, options) {
5484
5574
  if (options.remove) {
5485
5575
  const had = iconFile(paths);
5486
5576
  writeIcon(paths, null);
5487
- print(had ? `${green("Removed")} ${path28.relative(process.cwd(), had)}` : "The project has no icon.");
5577
+ print(had ? `${green("Removed")} ${path29.relative(process.cwd(), had)}` : "The project has no icon.");
5488
5578
  await toCloud(paths, null);
5489
5579
  return;
5490
5580
  }
5491
5581
  if (!file) {
5492
5582
  const current = iconFile(paths);
5493
5583
  if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
5494
- return print(`${path28.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5584
+ return print(`${path29.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5495
5585
  }
5496
5586
  let data;
5497
5587
  try {
@@ -5499,20 +5589,20 @@ async function runIcon(paths, file, options) {
5499
5589
  } catch {
5500
5590
  throw new CliError(`No file ${file}`);
5501
5591
  }
5502
- const problem = iconProblem(data, path28.extname(file).slice(1).toLowerCase() || void 0);
5592
+ const problem = iconProblem(data, path29.extname(file).slice(1).toLowerCase() || void 0);
5503
5593
  if (problem) throw new CliError(`${file}: ${problem}`);
5504
5594
  const written = writeIcon(paths, data);
5505
- print(`${green("Set")} the project icon: ${path28.relative(process.cwd(), written)}`);
5595
+ print(`${green("Set")} the project icon: ${path29.relative(process.cwd(), written)}`);
5506
5596
  await toCloud(paths, written);
5507
5597
  }
5508
5598
 
5509
5599
  // src/cli/commands/import.ts
5510
5600
  import fs33 from "fs";
5511
- import path30 from "path";
5601
+ import path31 from "path";
5512
5602
 
5513
5603
  // src/import/claude-design.ts
5514
5604
  import fs32 from "fs";
5515
- import path29 from "path";
5605
+ import path30 from "path";
5516
5606
 
5517
5607
  // src/import/markup.ts
5518
5608
  var VOID = /* @__PURE__ */ new Set([
@@ -6120,7 +6210,7 @@ function boardId(file, used) {
6120
6210
  return id2;
6121
6211
  }
6122
6212
  function importClaudeDesign(dir, options = {}) {
6123
- const canvasFile = path29.join(dir, "canvas.json");
6213
+ const canvasFile = path30.join(dir, "canvas.json");
6124
6214
  if (!fs32.existsSync(canvasFile))
6125
6215
  throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
6126
6216
  const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
@@ -6151,7 +6241,7 @@ function importClaudeDesign(dir, options = {}) {
6151
6241
  try {
6152
6242
  files.set(
6153
6243
  `${name}.jsx`,
6154
- convertFile(name, fs32.readFileSync(path29.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6244
+ convertFile(name, fs32.readFileSync(path30.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6155
6245
  );
6156
6246
  } catch (error) {
6157
6247
  warn2(`could not be converted: ${error.message}`);
@@ -6199,7 +6289,7 @@ function importClaudeDesign(dir, options = {}) {
6199
6289
  screens++;
6200
6290
  }
6201
6291
  const canvas = {
6202
- title: options.title ?? source.title ?? path29.basename(path29.resolve(dir)),
6292
+ title: options.title ?? source.title ?? path30.basename(path30.resolve(dir)),
6203
6293
  description: "Imported from Claude Design.",
6204
6294
  // The templates bring their own styles; the design system's reset would restyle them.
6205
6295
  system: false,
@@ -6211,7 +6301,7 @@ function importClaudeDesign(dir, options = {}) {
6211
6301
  // src/cli/commands/import.ts
6212
6302
  var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
6213
6303
  function runImport(paths, from, options) {
6214
- const dir = path30.resolve(from);
6304
+ const dir = path31.resolve(from);
6215
6305
  if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
6216
6306
  let result;
6217
6307
  try {
@@ -6219,13 +6309,13 @@ function runImport(paths, from, options) {
6219
6309
  } catch (error) {
6220
6310
  throw new CliError(error.message);
6221
6311
  }
6222
- const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path30.basename(dir)) || "imported");
6312
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path31.basename(dir)) || "imported");
6223
6313
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
6224
- const out = path30.join(paths.canvases, id2);
6314
+ const out = path31.join(paths.canvases, id2);
6225
6315
  if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
6226
- fs33.mkdirSync(path30.join(out, "screens"), { recursive: true });
6227
- for (const [name, content] of result.files) fs33.writeFileSync(path30.join(out, "screens", name), content);
6228
- fs33.writeFileSync(path30.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6316
+ fs33.mkdirSync(path31.join(out, "screens"), { recursive: true });
6317
+ for (const [name, content] of result.files) fs33.writeFileSync(path31.join(out, "screens", name), content);
6318
+ fs33.writeFileSync(path31.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6229
6319
  `);
6230
6320
  print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
6231
6321
  print(
@@ -6245,7 +6335,7 @@ function runImport(paths, from, options) {
6245
6335
 
6246
6336
  // src/cli/commands/merge.ts
6247
6337
  import { spawnSync as spawnSync2 } from "child_process";
6248
- import path31 from "path";
6338
+ import path32 from "path";
6249
6339
  import { createInterface } from "readline/promises";
6250
6340
  var KIND = {
6251
6341
  text: "both sides changed lines, the markers show them",
@@ -6261,7 +6351,7 @@ function matchTargets(paths, states, targets) {
6261
6351
  for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
6262
6352
  continue;
6263
6353
  }
6264
- const rel = [path31.relative(paths.design, path31.resolve(target)), target.replace(/^\.design\//, "")];
6354
+ const rel = [path32.relative(paths.design, path32.resolve(target)), target.replace(/^\.design\//, "")];
6265
6355
  const found = states.flatMap(
6266
6356
  (state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
6267
6357
  );
@@ -6277,7 +6367,7 @@ function printOpen(paths, states) {
6277
6367
  print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
6278
6368
  for (const conflict of open2) {
6279
6369
  print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
6280
- const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path31.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6370
+ const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path32.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6281
6371
  print(` ${dim(sides)}`);
6282
6372
  }
6283
6373
  }
@@ -6358,7 +6448,7 @@ async function runMerge(paths, targets, options) {
6358
6448
  if (answer === "s") break;
6359
6449
  if (answer === "e" && edit) {
6360
6450
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6361
- spawnSync2(editor, [path31.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6451
+ spawnSync2(editor, [path32.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6362
6452
  if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
6363
6453
  print(yellow(" Conflict markers are still there."));
6364
6454
  }
@@ -6402,7 +6492,7 @@ async function runRename(paths, name) {
6402
6492
 
6403
6493
  // src/cli/commands/scaffold.ts
6404
6494
  import fs35 from "fs";
6405
- import path32 from "path";
6495
+ import path33 from "path";
6406
6496
 
6407
6497
  // src/cli/templates.ts
6408
6498
  var TOKENS_CSS = `/*
@@ -6614,16 +6704,16 @@ function mainScreen(canvasId) {
6614
6704
  // src/cli/commands/scaffold.ts
6615
6705
  function write(file, content, root) {
6616
6706
  if (fs35.existsSync(file)) {
6617
- print(` ${dim("exists ")} ${path32.relative(root, file)}`);
6707
+ print(` ${dim("exists ")} ${path33.relative(root, file)}`);
6618
6708
  return false;
6619
6709
  }
6620
- fs35.mkdirSync(path32.dirname(file), { recursive: true });
6710
+ fs35.mkdirSync(path33.dirname(file), { recursive: true });
6621
6711
  fs35.writeFileSync(file, content);
6622
- print(` ${green("created")} ${path32.relative(root, file)}`);
6712
+ print(` ${green("created")} ${path33.relative(root, file)}`);
6623
6713
  return true;
6624
6714
  }
6625
6715
  function ensureGitignored(root) {
6626
- const file = path32.join(root, ".gitignore");
6716
+ const file = path33.join(root, ".gitignore");
6627
6717
  const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
6628
6718
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
6629
6719
  if (covered) return "present";
@@ -6635,11 +6725,11 @@ function ensureGitignored(root) {
6635
6725
  }
6636
6726
  function projectName(root) {
6637
6727
  try {
6638
- const manifest = JSON.parse(fs35.readFileSync(path32.join(root, "package.json"), "utf8"));
6728
+ const manifest = JSON.parse(fs35.readFileSync(path33.join(root, "package.json"), "utf8"));
6639
6729
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
6640
6730
  } catch {
6641
6731
  }
6642
- return path32.basename(root);
6732
+ return path33.basename(root);
6643
6733
  }
6644
6734
  function runInit(root, options) {
6645
6735
  const paths = designPaths(root);
@@ -6664,40 +6754,40 @@ function runSystemInit(root, options) {
6664
6754
  } catch {
6665
6755
  }
6666
6756
  const name = options.name ?? `${base} design system`;
6667
- print(`Scaffolding the design system in ${path32.relative(root, paths.system)}`);
6668
- write(path32.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6757
+ print(`Scaffolding the design system in ${path33.relative(root, paths.system)}`);
6758
+ write(path33.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6669
6759
  `, root);
6670
- write(path32.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6760
+ write(path33.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6671
6761
  if (!options.empty) {
6672
- write(path32.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6673
- write(path32.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6674
- write(path32.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6762
+ write(path33.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6763
+ write(path33.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6764
+ write(path33.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6675
6765
  }
6676
6766
  }
6677
6767
  function runNew(root, id2, options) {
6678
6768
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
6679
6769
  const paths = designPaths(root);
6680
- const dir = path32.join(paths.canvases, id2);
6681
- if (fs35.existsSync(path32.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6770
+ const dir = path33.join(paths.canvases, id2);
6771
+ if (fs35.existsSync(path33.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6682
6772
  print(`Creating canvas "${id2}"`);
6683
6773
  const title = options.title ?? titleize(id2);
6684
6774
  if (options.empty) {
6685
6775
  write(
6686
- path32.join(dir, "canvas.json"),
6776
+ path33.join(dir, "canvas.json"),
6687
6777
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
6688
6778
  `,
6689
6779
  root
6690
6780
  );
6691
- fs35.mkdirSync(path32.join(dir, "screens"), { recursive: true });
6781
+ fs35.mkdirSync(path33.join(dir, "screens"), { recursive: true });
6692
6782
  } else {
6693
- write(path32.join(dir, "canvas.json"), canvasJson(title), root);
6694
- write(path32.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6783
+ write(path33.join(dir, "canvas.json"), canvasJson(title), root);
6784
+ write(path33.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6695
6785
  }
6696
6786
  }
6697
6787
 
6698
6788
  // src/cli/commands/shot.ts
6699
6789
  import fs36 from "fs";
6700
- import path33 from "path";
6790
+ import path34 from "path";
6701
6791
  var OVERVIEW_TIMEOUT = 9e4;
6702
6792
  async function runShot(paths, target, options) {
6703
6793
  const [canvasId = "", screenId] = target.split("/");
@@ -6713,8 +6803,8 @@ async function runShot(paths, target, options) {
6713
6803
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
6714
6804
  }
6715
6805
  const theme2 = options.theme ?? doc.theme ?? "light";
6716
- const outBase = path33.resolve(options.out ?? paths.shots);
6717
- const outDir = path33.join(outBase, canvasId);
6806
+ const outBase = path34.resolve(options.out ?? paths.shots);
6807
+ const outDir = path34.join(outBase, canvasId);
6718
6808
  fs36.mkdirSync(outDir, { recursive: true });
6719
6809
  const base = await serverBase(paths);
6720
6810
  const browser = await launchChrome();
@@ -6737,7 +6827,7 @@ async function runShot(paths, target, options) {
6737
6827
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
6738
6828
  }
6739
6829
  await page.waitForTimeout(400);
6740
- const file = path33.join(outDir, `${pageId}.overview.png`);
6830
+ const file = path34.join(outDir, `${pageId}.overview.png`);
6741
6831
  await page.screenshot({ path: file });
6742
6832
  await page.close();
6743
6833
  print(file);
@@ -6766,7 +6856,7 @@ async function shootFrames(browser, jobs, outDir) {
6766
6856
  let failed = false;
6767
6857
  const results = await mapLimit(jobs, 4, async (job) => {
6768
6858
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
6769
- const file = path33.join(outDir, `${job.id}.${job.theme}.png`);
6859
+ const file = path34.join(outDir, `${job.id}.${job.theme}.png`);
6770
6860
  try {
6771
6861
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
6772
6862
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -6798,7 +6888,7 @@ async function shootSystem(paths, project, id2, options) {
6798
6888
  if (id2 && !all.some((job) => job.id === id2)) {
6799
6889
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
6800
6890
  }
6801
- const outDir = path33.join(path33.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6891
+ const outDir = path34.join(path34.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6802
6892
  fs36.mkdirSync(outDir, { recursive: true });
6803
6893
  const base = await serverBase(paths);
6804
6894
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
@@ -6823,6 +6913,16 @@ async function runSnapshots(paths, targets, options) {
6823
6913
  }
6824
6914
  if (options.page && picked.size !== 1)
6825
6915
  throw new CliError("--page needs exactly one canvas: `design snapshots <canvas> --page <id>`");
6916
+ const project = new DesignProject(paths, DEV_URLS);
6917
+ for (const [canvas, screens] of picked) {
6918
+ if (!screens) continue;
6919
+ const ids = new Set(project.canvas(canvas).screens.map((screen) => screen.id));
6920
+ const missing = [...screens].filter((id2) => !ids.has(id2));
6921
+ if (missing.length)
6922
+ throw new CliError(
6923
+ `No screen ${missing.map((id2) => `"${id2}"`).join(", ")} in "${canvas}". Screens: ${[...ids].slice(0, 30).join(", ")}${ids.size > 30 ? ", \u2026" : ""}`
6924
+ );
6925
+ }
6826
6926
  const canvases = picked.size ? [...picked.keys()] : known;
6827
6927
  const stale = canvases.flatMap(
6828
6928
  (canvas) => staleSnapshots(paths, [canvas], { page: options.page, screens: picked.get(canvas) ?? void 0 })
@@ -6839,7 +6939,7 @@ async function runSnapshots(paths, targets, options) {
6839
6939
  }
6840
6940
 
6841
6941
  // src/cli/commands/cloud-status.ts
6842
- import path34 from "path";
6942
+ import path35 from "path";
6843
6943
  var STATE_TEXT = {
6844
6944
  in_sync: green,
6845
6945
  local_changes: yellow,
@@ -6862,7 +6962,7 @@ function detail(paths, status) {
6862
6962
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
6863
6963
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
6864
6964
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
6865
- if (status.conflictRev !== void 0) parts.push(`merge ${path34.relative(paths.root, incomingDir(paths, status.key))}`);
6965
+ if (status.conflictRev !== void 0) parts.push(`merge ${path35.relative(paths.root, incomingDir(paths, status.key))}`);
6866
6966
  return parts.join(" \xB7 ");
6867
6967
  }
6868
6968
  async function printCloudStatus(paths, label) {
@@ -7057,7 +7157,7 @@ ${bold("Environment")}
7057
7157
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
7058
7158
  `;
7059
7159
  function projectPaths(rootOption, requireDesign = true) {
7060
- const found = rootOption ? path35.resolve(rootOption) : findProjectRoot();
7160
+ const found = rootOption ? path36.resolve(rootOption) : findProjectRoot();
7061
7161
  if (!found) {
7062
7162
  if (!requireDesign) return designPaths(process.cwd());
7063
7163
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -7110,7 +7210,7 @@ async function main(argv) {
7110
7210
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
7111
7211
  switch (command) {
7112
7212
  case "init":
7113
- return runInit(values.root ? path35.resolve(values.root) : process.cwd(), {
7213
+ return runInit(values.root ? path36.resolve(values.root) : process.cwd(), {
7114
7214
  name: values.name,
7115
7215
  gitignore: !values["no-gitignore"]
7116
7216
  });
@@ -7222,7 +7322,7 @@ function relaunchFromRealPath() {
7222
7322
  } catch {
7223
7323
  return false;
7224
7324
  }
7225
- const same = (a, b) => path35.resolve(a).toLowerCase() === path35.resolve(b).toLowerCase();
7325
+ const same = (a, b) => path36.resolve(a).toLowerCase() === path36.resolve(b).toLowerCase();
7226
7326
  if (same(realScript, script) && same(realCwd, process.cwd())) return false;
7227
7327
  const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
7228
7328
  stdio: "inherit",