foss-design 0.9.6 → 0.9.8

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
@@ -3029,11 +3029,20 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
3029
3029
  var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3030
3030
 
3031
3031
  // src/capture/cover.ts
3032
- import fs19 from "fs";
3032
+ import fs20 from "fs";
3033
+ import os3 from "os";
3033
3034
 
3034
3035
  // src/server/snapshots.ts
3036
+ import { createHash as createHash5 } from "crypto";
3035
3037
  import fs17 from "fs";
3036
3038
  import path17 from "path";
3039
+ function sourceHash(file) {
3040
+ try {
3041
+ return createHash5("sha1").update(fs17.readFileSync(file)).digest("hex").slice(0, 16);
3042
+ } catch {
3043
+ return null;
3044
+ }
3045
+ }
3037
3046
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
3038
3047
  function isSafeSegment(value) {
3039
3048
  return SAFE.test(value);
@@ -3049,6 +3058,12 @@ var SnapshotStore = class {
3049
3058
  metaFile(canvas) {
3050
3059
  return path17.join(this.dir, canvas, "meta.json");
3051
3060
  }
3061
+ /** Written aside and renamed: a run stopped mid-write leaves the old file, not half of one. */
3062
+ writeMeta(canvas, data) {
3063
+ const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3064
+ fs17.writeFileSync(temp, JSON.stringify(data));
3065
+ fs17.renameSync(temp, this.metaFile(canvas));
3066
+ }
3052
3067
  load(canvas) {
3053
3068
  const cached = this.meta.get(canvas);
3054
3069
  if (cached) return cached;
@@ -3083,15 +3098,35 @@ var SnapshotStore = class {
3083
3098
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
3084
3099
  return this.load(canvas)[id2]?.[theme2] ?? null;
3085
3100
  }
3086
- save(canvas, id2, theme2, png, height) {
3101
+ /** The source hash recorded with the snapshot; undefined for snapshots taken before hashes were kept. */
3102
+ source(canvas, id2, theme2) {
3103
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3104
+ return this.load(canvas)[id2]?.sources?.[theme2];
3105
+ }
3106
+ /** Record `hash` for a snapshot taken before hashes were kept: from now on it counts as that content's. */
3107
+ adopt(canvas, id2, theme2, hash) {
3108
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return;
3109
+ const data = this.load(canvas);
3110
+ const entry = data[id2];
3111
+ if (!entry?.[theme2]) return;
3112
+ entry.sources = { ...entry.sources, [theme2]: hash };
3113
+ this.writeMeta(canvas, data);
3114
+ }
3115
+ save(canvas, id2, theme2, png, height, source) {
3087
3116
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3088
3117
  const dir = path17.join(this.dir, canvas);
3089
3118
  fs17.mkdirSync(dir, { recursive: true });
3090
- fs17.writeFileSync(path17.join(dir, `${id2}.${theme2}.png`), png);
3119
+ const file = path17.join(dir, `${id2}.${theme2}.png`);
3120
+ fs17.writeFileSync(`${file}.${process.pid}.tmp`, png);
3121
+ fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3091
3122
  const data = this.load(canvas);
3092
3123
  const version = Date.now();
3093
- data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3094
- fs17.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3124
+ const previous = data[id2];
3125
+ const sources = { ...previous?.sources };
3126
+ if (source) sources[theme2] = source;
3127
+ else delete sources[theme2];
3128
+ data[id2] = { ...previous, [theme2]: version, ...height ? { height } : {}, sources };
3129
+ this.writeMeta(canvas, data);
3095
3130
  return { url: this.url(canvas, id2, theme2, version) };
3096
3131
  }
3097
3132
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
@@ -3104,8 +3139,54 @@ var SnapshotStore = class {
3104
3139
  }
3105
3140
  };
3106
3141
 
3107
- // src/capture/chrome.ts
3142
+ // src/capture/build-server.ts
3108
3143
  import fs18 from "fs";
3144
+ import http from "http";
3145
+ import path18 from "path";
3146
+ import sirv from "sirv";
3147
+ async function serveBuild(paths, name, options) {
3148
+ const out = path18.join(paths.cache, name);
3149
+ fs18.rmSync(out, { recursive: true, force: true });
3150
+ await buildSite(paths, out, {
3151
+ canvases: options.canvases,
3152
+ includeSystem: options.includeSystem,
3153
+ includeViewer: false
3154
+ });
3155
+ const files = sirv(out, { dev: true });
3156
+ const server = http.createServer((req, res) => {
3157
+ const url = new URL(req.url ?? "/", "http://localhost");
3158
+ const segments = url.pathname.split("/").map((segment) => {
3159
+ try {
3160
+ return decodeURIComponent(segment);
3161
+ } catch {
3162
+ return segment;
3163
+ }
3164
+ });
3165
+ req.url = segments.join("/") + url.search;
3166
+ files(req, res, () => {
3167
+ res.statusCode = 404;
3168
+ res.end("Not found");
3169
+ });
3170
+ });
3171
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
3172
+ return {
3173
+ base: `http://127.0.0.1:${server.address().port}`,
3174
+ read(rel) {
3175
+ try {
3176
+ return JSON.parse(fs18.readFileSync(path18.join(out, rel), "utf8"));
3177
+ } catch {
3178
+ return null;
3179
+ }
3180
+ },
3181
+ close: () => new Promise((resolve) => {
3182
+ server.closeAllConnections();
3183
+ server.close(() => resolve());
3184
+ })
3185
+ };
3186
+ }
3187
+
3188
+ // src/capture/chrome.ts
3189
+ import fs19 from "fs";
3109
3190
  var CANDIDATES = {
3110
3191
  darwin: [
3111
3192
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3129,8 +3210,8 @@ var CANDIDATES = {
3129
3210
  };
3130
3211
  function findChrome() {
3131
3212
  const configured = process.env.DESIGN_CHROME;
3132
- if (configured && fs18.existsSync(configured)) return configured;
3133
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs18.existsSync(candidate)) return candidate;
3213
+ if (configured && fs19.existsSync(configured)) return configured;
3214
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
3134
3215
  return null;
3135
3216
  }
3136
3217
  async function launchChrome() {
@@ -3161,6 +3242,8 @@ async function openFrame(page, url, options = {}) {
3161
3242
  record2(text2);
3162
3243
  });
3163
3244
  let ready = false;
3245
+ let loadedAt = 0;
3246
+ let readyMs;
3164
3247
  let reloads = 0;
3165
3248
  const onNavigate = () => {
3166
3249
  reloads++;
@@ -3173,6 +3256,7 @@ async function openFrame(page, url, options = {}) {
3173
3256
  outdated = () => resolve("outdated");
3174
3257
  });
3175
3258
  await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3259
+ loadedAt = Date.now();
3176
3260
  page.on("load", onNavigate);
3177
3261
  if (!ready) {
3178
3262
  const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
@@ -3181,6 +3265,7 @@ async function openFrame(page, url, options = {}) {
3181
3265
  signal.catch(() => {
3182
3266
  });
3183
3267
  ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
3268
+ if (ready) readyMs = Date.now() - loadedAt;
3184
3269
  }
3185
3270
  if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
3186
3271
  page.off("load", onNavigate);
@@ -3198,7 +3283,12 @@ async function openFrame(page, url, options = {}) {
3198
3283
  const unique2 = errors.filter(
3199
3284
  (error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
3200
3285
  );
3201
- return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
3286
+ return {
3287
+ errors: [...new Set(unique2)],
3288
+ ready,
3289
+ ...held ? { held } : {},
3290
+ ...readyMs !== void 0 ? { readyMs } : {}
3291
+ };
3202
3292
  }
3203
3293
 
3204
3294
  // src/capture/frames.ts
@@ -3275,7 +3365,7 @@ async function mapLimit(items, limit, fn) {
3275
3365
 
3276
3366
  // src/capture/cover.ts
3277
3367
  var SNAPSHOT_WIDTH = 900;
3278
- var TABS = 4;
3368
+ var TABS = Math.min(8, Math.max(2, os3.availableParallelism() - 1));
3279
3369
  var THEMES2 = ["light", "dark"];
3280
3370
  function staleSnapshots(paths, canvasIds, filter = {}) {
3281
3371
  const store = new SnapshotStore(paths.snapshots);
@@ -3292,25 +3382,43 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3292
3382
  for (const job of inLight) {
3293
3383
  if (filter.screens && !filter.screens.has(job.id)) continue;
3294
3384
  const file = files.get(job.id);
3295
- if (!file || !fs19.existsSync(file)) continue;
3296
- const changed = fs19.statSync(file).mtimeMs;
3385
+ if (!file || !fs20.existsSync(file)) continue;
3386
+ const hash = sourceHash(file);
3297
3387
  const pinned = inDark.get(job.id) === job.theme;
3298
3388
  const themes = pinned ? [job.theme] : THEMES2;
3299
- const screen = { id: job.id };
3300
3389
  for (const theme2 of themes) {
3301
- const taken = store.version(id2, screen.id, theme2);
3302
- if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
3390
+ if (!store.version(id2, job.id, theme2)) {
3391
+ stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3392
+ continue;
3393
+ }
3394
+ const recorded = store.source(id2, job.id, theme2);
3395
+ if (recorded === void 0) {
3396
+ if (hash) store.adopt(id2, job.id, theme2, hash);
3397
+ } else if (recorded !== hash) stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3303
3398
  }
3304
3399
  }
3305
3400
  }
3306
3401
  return stale;
3307
3402
  }
3308
- async function takeSnapshots(paths, targets, base, progress2) {
3403
+ async function takeSnapshots(paths, targets, preview, progress2) {
3309
3404
  if (!targets.length) return [];
3310
3405
  if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
3311
3406
  const store = new SnapshotStore(paths.snapshots);
3312
3407
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3313
- const url = await base();
3408
+ const canvases = [...new Set(targets.map((target) => target.canvas))];
3409
+ progress2.update(`Building ${canvases.join(", ")} to take snapshots from\u2026`);
3410
+ let url;
3411
+ let docOf = (id2) => project.canvas(id2).doc;
3412
+ let stop = async () => {
3413
+ };
3414
+ try {
3415
+ const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
3416
+ url = build2.base;
3417
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
3418
+ stop = build2.close;
3419
+ } catch {
3420
+ url = await preview();
3421
+ }
3314
3422
  const browser = await launchChrome();
3315
3423
  const failed = [];
3316
3424
  const started = Date.now();
@@ -3318,8 +3426,8 @@ async function takeSnapshots(paths, targets, base, progress2) {
3318
3426
  const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
3319
3427
  progress2.update(line());
3320
3428
  try {
3321
- await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
3322
- const job = canvasFrames(project.canvas(canvas).doc, url, theme2, { id: screen })[0];
3429
+ await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2, source }) => {
3430
+ const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3323
3431
  if (job) {
3324
3432
  const page = await browser.newPage({
3325
3433
  viewport: { width: job.width, height: job.height },
@@ -3331,7 +3439,7 @@ async function takeSnapshots(paths, targets, base, progress2) {
3331
3439
  if (report.ready) {
3332
3440
  const png = await page.screenshot({ fullPage: job.fullPage });
3333
3441
  const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3334
- store.save(canvas, screen, theme2, png, height);
3442
+ store.save(canvas, screen, theme2, png, height, source);
3335
3443
  } else failed.push(`${canvas}/${screen} (${theme2})`);
3336
3444
  } catch {
3337
3445
  failed.push(`${canvas}/${screen} (${theme2})`);
@@ -3345,6 +3453,7 @@ async function takeSnapshots(paths, targets, base, progress2) {
3345
3453
  });
3346
3454
  } finally {
3347
3455
  await browser.close();
3456
+ await stop();
3348
3457
  }
3349
3458
  const seconds2 = Math.round((Date.now() - started) / 1e3);
3350
3459
  progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
@@ -3356,73 +3465,73 @@ async function takeSnapshots(paths, targets, base, progress2) {
3356
3465
  }
3357
3466
 
3358
3467
  // src/cloud/pull.ts
3359
- import fs23 from "fs";
3360
- import path21 from "path";
3468
+ import fs24 from "fs";
3469
+ import path22 from "path";
3361
3470
 
3362
3471
  // src/cloud/merge.ts
3363
- import fs21 from "fs";
3364
- import path19 from "path";
3472
+ import fs22 from "fs";
3473
+ import path20 from "path";
3365
3474
  import { merge as diff3 } from "node-diff3";
3366
3475
 
3367
3476
  // src/cloud/write.ts
3368
- import fs20 from "fs";
3369
- import path18 from "path";
3477
+ import fs21 from "fs";
3478
+ import path19 from "path";
3370
3479
  function writeInside(root, rel, data) {
3371
- const file = path18.join(root, rel);
3480
+ const file = path19.join(root, rel);
3372
3481
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3373
- fs20.mkdirSync(path18.dirname(file), { recursive: true });
3374
- if (!isInside(fs20.realpathSync(root), fs20.realpathSync(path18.dirname(file))))
3482
+ fs21.mkdirSync(path19.dirname(file), { recursive: true });
3483
+ if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
3375
3484
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3376
- if (fs20.existsSync(file) && fs20.lstatSync(file).isSymbolicLink()) fs20.unlinkSync(file);
3377
- fs20.writeFileSync(file, data);
3485
+ if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3486
+ fs21.writeFileSync(file, data);
3378
3487
  }
3379
3488
  function pruneEmpty(dir, stop) {
3380
3489
  let current = dir;
3381
3490
  while (isInside(stop, current) && current !== stop) {
3382
3491
  try {
3383
- if (fs20.readdirSync(current).length) return;
3384
- fs20.rmdirSync(current);
3492
+ if (fs21.readdirSync(current).length) return;
3493
+ fs21.rmdirSync(current);
3385
3494
  } catch {
3386
3495
  return;
3387
3496
  }
3388
- current = path18.dirname(current);
3497
+ current = path19.dirname(current);
3389
3498
  }
3390
3499
  }
3391
3500
 
3392
3501
  // src/cloud/merge.ts
3393
3502
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3394
3503
  var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
3395
- var mergeDir = (paths, key) => path19.join(cloudCache(paths), "merge", ...key.split("/"));
3396
- var stateFile = (paths, key) => path19.join(mergeDir(paths, key), "state.json");
3397
- var sideFile = (paths, key, rel, side) => path19.join(mergeDir(paths, key), side, rel);
3504
+ var mergeDir = (paths, key) => path20.join(cloudCache(paths), "merge", ...key.split("/"));
3505
+ var stateFile = (paths, key) => path20.join(mergeDir(paths, key), "state.json");
3506
+ var sideFile = (paths, key, rel, side) => path20.join(mergeDir(paths, key), side, rel);
3398
3507
  function readMergeState(paths, key) {
3399
3508
  try {
3400
- return JSON.parse(fs21.readFileSync(stateFile(paths, key), "utf8"));
3509
+ return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
3401
3510
  } catch {
3402
3511
  return null;
3403
3512
  }
3404
3513
  }
3405
3514
  function saveState(paths, state) {
3406
- fs21.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3407
- fs21.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3515
+ fs22.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3516
+ fs22.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3408
3517
  `);
3409
3518
  }
3410
3519
  function mergingUnits(paths) {
3411
- const root = path19.join(cloudCache(paths), "merge");
3520
+ const root = path20.join(cloudCache(paths), "merge");
3412
3521
  const found = [];
3413
3522
  const visit = (dir, prefix) => {
3414
- if (fs21.existsSync(path19.join(dir, "state.json"))) {
3523
+ if (fs22.existsSync(path20.join(dir, "state.json"))) {
3415
3524
  const state = readMergeState(paths, prefix);
3416
3525
  if (state) found.push(state);
3417
3526
  return;
3418
3527
  }
3419
3528
  let entries = [];
3420
3529
  try {
3421
- entries = fs21.readdirSync(dir, { withFileTypes: true });
3530
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
3422
3531
  } catch {
3423
3532
  }
3424
3533
  for (const entry of entries)
3425
- if (entry.isDirectory()) visit(path19.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3534
+ if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3426
3535
  };
3427
3536
  visit(root, "");
3428
3537
  return found;
@@ -3432,7 +3541,7 @@ function openConflicts(paths, state) {
3432
3541
  return state.conflicts.filter((conflict) => {
3433
3542
  if (conflict.kind !== "text") return true;
3434
3543
  try {
3435
- return MARKER.test(fs21.readFileSync(path19.join(paths.design, conflict.path), "utf8"));
3544
+ return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
3436
3545
  } catch {
3437
3546
  return false;
3438
3547
  }
@@ -3441,19 +3550,19 @@ function openConflicts(paths, state) {
3441
3550
  function keep(paths, key, rel, side, data) {
3442
3551
  if (!data) return;
3443
3552
  const file = sideFile(paths, key, rel, side);
3444
- fs21.mkdirSync(path19.dirname(file), { recursive: true });
3445
- fs21.writeFileSync(file, data);
3553
+ fs22.mkdirSync(path20.dirname(file), { recursive: true });
3554
+ fs22.writeFileSync(file, data);
3446
3555
  }
3447
3556
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3448
3557
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3449
3558
  const conflicts = [];
3450
- const stop = key === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, key));
3451
- fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3452
- const read = (rel) => fs21.readFileSync(path19.join(paths.design, rel));
3559
+ const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
3560
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3561
+ const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
3453
3562
  const remove = (rel) => {
3454
- const file = path19.join(paths.design, rel);
3455
- fs21.rmSync(file, { force: true });
3456
- pruneEmpty(path19.dirname(file), stop);
3563
+ const file = path20.join(paths.design, rel);
3564
+ fs22.rmSync(file, { force: true });
3565
+ pruneEmpty(path20.dirname(file), stop);
3457
3566
  };
3458
3567
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3459
3568
  const b = base[rel];
@@ -3509,11 +3618,11 @@ function resolveConflict(paths, state, rel, side) {
3509
3618
  const conflict = state.conflicts.find((item) => item.path === rel);
3510
3619
  if (!conflict) return;
3511
3620
  const kept = sideFile(paths, state.unit, rel, side);
3512
- const stop = state.unit === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, state.unit));
3513
- if (fs21.existsSync(kept)) writeInside(paths.design, rel, fs21.readFileSync(kept));
3621
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
3622
+ if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3514
3623
  else {
3515
- fs21.rmSync(path19.join(paths.design, rel), { force: true });
3516
- pruneEmpty(path19.dirname(path19.join(paths.design, rel)), stop);
3624
+ fs22.rmSync(path20.join(paths.design, rel), { force: true });
3625
+ pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
3517
3626
  }
3518
3627
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3519
3628
  saveState(paths, state);
@@ -3523,15 +3632,15 @@ function finishMerge(paths, link, key, accept = false) {
3523
3632
  if (state) {
3524
3633
  const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
3525
3634
  if (open2.length) return open2;
3526
- fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3635
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3527
3636
  }
3528
3637
  delete link.conflicts[key];
3529
3638
  return [];
3530
3639
  }
3531
3640
 
3532
3641
  // src/cloud/meta.ts
3533
- import fs22 from "fs";
3534
- import path20 from "path";
3642
+ import fs23 from "fs";
3643
+ import path21 from "path";
3535
3644
  async function syncProjectMeta(paths, client, link) {
3536
3645
  const remote = await client.project(link.project);
3537
3646
  if (!remote) return [];
@@ -3550,8 +3659,8 @@ async function syncProjectMeta(paths, client, link) {
3550
3659
  if (localChanged && !cloudChanged) {
3551
3660
  try {
3552
3661
  if (file) {
3553
- const ext = path20.extname(file).slice(1);
3554
- await client.setProjectIcon(link.project, { data: fs22.readFileSync(file), type: ICON_TYPES[ext] });
3662
+ const ext = path21.extname(file).slice(1);
3663
+ await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3555
3664
  lines.push("The project icon went up to the cloud.");
3556
3665
  } else {
3557
3666
  await client.setProjectIcon(link.project, null);
@@ -3586,12 +3695,12 @@ function checkManifest(key, manifest) {
3586
3695
  }
3587
3696
  function applyUnit(paths, key, local, target, blobs) {
3588
3697
  const counts = { added: 0, changed: 0, removed: 0 };
3589
- const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3698
+ const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3590
3699
  for (const rel of Object.keys(local)) {
3591
3700
  if (rel in target) continue;
3592
- const file = path21.join(paths.design, rel);
3593
- fs23.rmSync(file, { force: true });
3594
- pruneEmpty(path21.dirname(file), stop);
3701
+ const file = path22.join(paths.design, rel);
3702
+ fs24.rmSync(file, { force: true });
3703
+ pruneEmpty(path22.dirname(file), stop);
3595
3704
  counts.removed++;
3596
3705
  }
3597
3706
  for (const [rel, entry] of Object.entries(target)) {
@@ -3661,7 +3770,7 @@ async function pull(ctx, options) {
3661
3770
  }
3662
3771
  if (s.conflictRev !== void 0) {
3663
3772
  finishMerge(paths, link, key, true);
3664
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3773
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3665
3774
  }
3666
3775
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
3667
3776
  } else if (!s.changed.length) {
@@ -3690,7 +3799,7 @@ async function pull(ctx, options) {
3690
3799
  }
3691
3800
  const byHash = /* @__PURE__ */ new Map();
3692
3801
  for (const unit of scan.units.values())
3693
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path21.join(paths.design, rel));
3802
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
3694
3803
  const needed = /* @__PURE__ */ new Set();
3695
3804
  const bases = /* @__PURE__ */ new Set();
3696
3805
  for (const plan of plans) {
@@ -3711,7 +3820,7 @@ async function pull(ctx, options) {
3711
3820
  const download = [];
3712
3821
  for (const hash of needed) {
3713
3822
  const file = byHash.get(hash);
3714
- if (file) blobs.set(hash, fs23.readFileSync(file));
3823
+ if (file) blobs.set(hash, fs24.readFileSync(file));
3715
3824
  else download.push(hash);
3716
3825
  }
3717
3826
  if (download.length) {
@@ -3747,7 +3856,7 @@ async function pull(ctx, options) {
3747
3856
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3748
3857
  else delete link.units[key];
3749
3858
  delete link.conflicts[key];
3750
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3859
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3751
3860
  } else {
3752
3861
  const result = mergeUnit(
3753
3862
  paths,
@@ -3758,7 +3867,7 @@ async function pull(ctx, options) {
3758
3867
  plan.remote.manifest,
3759
3868
  (hash) => blobs.get(hash)
3760
3869
  );
3761
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3870
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3762
3871
  link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3763
3872
  entry.rev = plan.remote.headRev;
3764
3873
  entry.files = result.files;
@@ -3788,8 +3897,8 @@ async function pull(ctx, options) {
3788
3897
  }
3789
3898
 
3790
3899
  // src/cloud/push.ts
3791
- import fs24 from "fs";
3792
- import path22 from "path";
3900
+ import fs25 from "fs";
3901
+ import path23 from "path";
3793
3902
  function blockedReport(status, message) {
3794
3903
  return {
3795
3904
  unit: status.key,
@@ -3905,7 +4014,7 @@ async function push(ctx, options) {
3905
4014
  for (const key of toPush) {
3906
4015
  const s = status(key);
3907
4016
  if (s.conflictRev !== void 0) {
3908
- const dir = path22.relative(paths.root, incomingDir(paths, key));
4017
+ const dir = path23.relative(paths.root, incomingDir(paths, key));
3909
4018
  blocked.push(
3910
4019
  blockedReport(
3911
4020
  s,
@@ -3954,7 +4063,7 @@ async function push(ctx, options) {
3954
4063
  const files = /* @__PURE__ */ new Map();
3955
4064
  const addFiles = (manifest, dir) => {
3956
4065
  for (const [rel, entry] of Object.entries(manifest))
3957
- if (!files.has(entry.hash)) files.set(entry.hash, path22.join(dir, rel));
4066
+ if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
3958
4067
  };
3959
4068
  for (const key of pushing) {
3960
4069
  const id2 = unitCanvasId(key);
@@ -3986,7 +4095,7 @@ async function push(ctx, options) {
3986
4095
  if (missing.length) {
3987
4096
  const unknown = missing.find((hash) => !files.has(hash));
3988
4097
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
3989
- const bytes = missing.reduce((sum, hash) => sum + fs24.statSync(files.get(hash)).size, 0);
4098
+ const bytes = missing.reduce((sum, hash) => sum + fs25.statSync(files.get(hash)).size, 0);
3990
4099
  const bar = ctx.progress();
3991
4100
  const started = Date.now();
3992
4101
  let sentFiles = 0;
@@ -3995,12 +4104,12 @@ async function push(ctx, options) {
3995
4104
  bar.update(line());
3996
4105
  await mapLimit(missing, TRANSFERS, async (hash) => {
3997
4106
  const file = files.get(hash);
3998
- const data = fs24.readFileSync(file);
4107
+ const data = fs25.readFileSync(file);
3999
4108
  try {
4000
4109
  await client.putBlob(link.project, hash, data);
4001
4110
  } catch (error) {
4002
4111
  if (error instanceof CloudError && error.status === 413)
4003
- throw new CliError(`${path22.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4112
+ throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4004
4113
  throw error;
4005
4114
  }
4006
4115
  sentFiles++;
@@ -4033,7 +4142,7 @@ async function push(ctx, options) {
4033
4142
  url: webUrl(link, unit.key)
4034
4143
  });
4035
4144
  }
4036
- for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4145
+ for (const key of resolved) fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4037
4146
  writeLink(paths, link);
4038
4147
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
4039
4148
  return report;
@@ -4044,7 +4153,7 @@ function refuseLegacyGo(paths, keys) {
4044
4153
  for (const key of keys) {
4045
4154
  if (!key.startsWith("canvas/")) continue;
4046
4155
  const id2 = key.slice("canvas/".length);
4047
- for (const file of canvasCodeFiles(path22.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4156
+ for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4048
4157
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4049
4158
  }
4050
4159
  }
@@ -4060,18 +4169,18 @@ Still using go():
4060
4169
 
4061
4170
  // src/cli/commands/preview.ts
4062
4171
  import { spawn } from "child_process";
4063
- import fs28 from "fs";
4172
+ import fs29 from "fs";
4064
4173
 
4065
4174
  // src/server/index.ts
4066
- import fs27 from "fs";
4067
- import http from "http";
4068
- import path25 from "path";
4069
- import sirv from "sirv";
4175
+ import fs28 from "fs";
4176
+ import http2 from "http";
4177
+ import path26 from "path";
4178
+ import sirv2 from "sirv";
4070
4179
  import { createServer, searchForWorkspaceRoot } from "vite";
4071
4180
 
4072
4181
  // src/server/api.ts
4073
- import fs25 from "fs";
4074
- import path23 from "path";
4182
+ import fs26 from "fs";
4183
+ import path24 from "path";
4075
4184
  function sendJson(res, status, body) {
4076
4185
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4077
4186
  res.end(JSON.stringify(body));
@@ -4122,17 +4231,17 @@ async function handleApi(ctx, req, res, url) {
4122
4231
  }
4123
4232
  if (route2 === "/source") {
4124
4233
  const rel = url.searchParams.get("path") ?? "";
4125
- const file = path23.resolve(project.paths.root, rel);
4126
- if (!rel || !isInside(project.paths.root, file) || !fs25.existsSync(file) || fs25.statSync(file).isDirectory()) {
4234
+ const file = path24.resolve(project.paths.root, rel);
4235
+ if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4127
4236
  sendJson(res, 404, { error: "not found" });
4128
4237
  return true;
4129
4238
  }
4130
- if (fs25.statSync(file).size > 2 * 1024 * 1024) {
4239
+ if (fs26.statSync(file).size > 2 * 1024 * 1024) {
4131
4240
  sendJson(res, 413, { error: "file too large to show" });
4132
4241
  return true;
4133
4242
  }
4134
4243
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
4135
- res.end(fs25.readFileSync(file, "utf8"));
4244
+ res.end(fs26.readFileSync(file, "utf8"));
4136
4245
  return true;
4137
4246
  }
4138
4247
  if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
@@ -4162,7 +4271,8 @@ async function handleApi(ctx, req, res, url) {
4162
4271
  try {
4163
4272
  const body = await readBody(req);
4164
4273
  if (body.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("not a PNG");
4165
- const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height);
4274
+ const file = project.source(canvas, id2)?.file;
4275
+ const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height, file ? sourceHash(file) : null);
4166
4276
  project.invalidateCanvas(canvas);
4167
4277
  events.send({ type: "snapshot", canvas, id: id2, theme: theme2, url: snapshotUrl, height });
4168
4278
  sendJson(res, 200, { url: snapshotUrl });
@@ -4179,7 +4289,7 @@ async function handleApi(ctx, req, res, url) {
4179
4289
  return true;
4180
4290
  }
4181
4291
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
4182
- fs25.createReadStream(file).pipe(res);
4292
+ fs26.createReadStream(file).pipe(res);
4183
4293
  return true;
4184
4294
  }
4185
4295
  return false;
@@ -4219,62 +4329,62 @@ var EventHub = class {
4219
4329
  };
4220
4330
 
4221
4331
  // src/server/state.ts
4222
- import fs26 from "fs";
4223
- import os3 from "os";
4224
- import path24 from "path";
4225
- var stateFile2 = (paths) => path24.join(paths.cache, "server.json");
4226
- var logFile = (paths) => path24.join(paths.cache, "server.log");
4332
+ import fs27 from "fs";
4333
+ import os4 from "os";
4334
+ import path25 from "path";
4335
+ var stateFile2 = (paths) => path25.join(paths.cache, "server.json");
4336
+ var logFile = (paths) => path25.join(paths.cache, "server.log");
4227
4337
  function readState(paths) {
4228
4338
  try {
4229
- return JSON.parse(fs26.readFileSync(stateFile2(paths), "utf8"));
4339
+ return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
4230
4340
  } catch {
4231
4341
  return null;
4232
4342
  }
4233
4343
  }
4234
4344
  function registryDir() {
4235
- const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path24.join(os3.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path24.join(os3.homedir(), ".cache");
4236
- return path24.join(base, "foss-design", "previews");
4345
+ 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");
4346
+ return path25.join(base, "foss-design", "previews");
4237
4347
  }
4238
- var entryFile = (pid) => path24.join(registryDir(), `${pid}.json`);
4348
+ var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
4239
4349
  function writeState(paths, state) {
4240
- fs26.mkdirSync(paths.cache, { recursive: true });
4241
- fs26.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4350
+ fs27.mkdirSync(paths.cache, { recursive: true });
4351
+ fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4242
4352
  `);
4243
4353
  try {
4244
- fs26.mkdirSync(registryDir(), { recursive: true });
4245
- fs26.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4354
+ fs27.mkdirSync(registryDir(), { recursive: true });
4355
+ fs27.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4246
4356
  `);
4247
4357
  } catch {
4248
4358
  }
4249
4359
  }
4250
4360
  function clearState(paths, pid) {
4251
4361
  const state = readState(paths);
4252
- fs26.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4362
+ fs27.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4253
4363
  if (pid !== void 0 && state && state.pid !== pid) return;
4254
- fs26.rmSync(stateFile2(paths), { force: true });
4364
+ fs27.rmSync(stateFile2(paths), { force: true });
4255
4365
  }
4256
4366
  function listPreviews() {
4257
4367
  let files = [];
4258
4368
  try {
4259
- files = fs26.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4369
+ files = fs27.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4260
4370
  } catch {
4261
4371
  return [];
4262
4372
  }
4263
4373
  const found = [];
4264
4374
  for (const name of files) {
4265
- const file = path24.join(registryDir(), name);
4375
+ const file = path25.join(registryDir(), name);
4266
4376
  let state = null;
4267
4377
  try {
4268
- state = JSON.parse(fs26.readFileSync(file, "utf8"));
4378
+ state = JSON.parse(fs27.readFileSync(file, "utf8"));
4269
4379
  } catch {
4270
4380
  }
4271
4381
  if (!state?.pid || !pidAlive(state.pid)) {
4272
- fs26.rmSync(file, { force: true });
4382
+ fs27.rmSync(file, { force: true });
4273
4383
  continue;
4274
4384
  }
4275
4385
  let recorded = null;
4276
4386
  try {
4277
- recorded = JSON.parse(fs26.readFileSync(path24.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4387
+ recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4278
4388
  } catch {
4279
4389
  }
4280
4390
  found.push({ ...state, tracked: recorded?.pid === state.pid });
@@ -4288,11 +4398,11 @@ async function stopPreview(entry) {
4288
4398
  }
4289
4399
  const until = Date.now() + 5e3;
4290
4400
  while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
4291
- fs26.rmSync(entryFile(entry.pid), { force: true });
4292
- const stateAt = path24.join(entry.root, ".design", ".cache", "server.json");
4401
+ fs27.rmSync(entryFile(entry.pid), { force: true });
4402
+ const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
4293
4403
  try {
4294
- if (JSON.parse(fs26.readFileSync(stateAt, "utf8")).pid === entry.pid)
4295
- fs26.rmSync(stateAt, { force: true });
4404
+ if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4405
+ fs27.rmSync(stateAt, { force: true });
4296
4406
  } catch {
4297
4407
  }
4298
4408
  }
@@ -4351,15 +4461,15 @@ function notFoundPage(res, title, text2) {
4351
4461
  }
4352
4462
  async function startDevServer(options) {
4353
4463
  const paths = designPaths(options.root);
4354
- if (!fs27.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4355
- fs27.mkdirSync(paths.cache, { recursive: true });
4464
+ if (!fs28.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4465
+ fs28.mkdirSync(paths.cache, { recursive: true });
4356
4466
  const snapshots = new SnapshotStore(paths.snapshots);
4357
4467
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
4358
4468
  const entries = new Entries(project);
4359
4469
  entries.sync();
4360
4470
  const links = linkShippedPackages(paths, project.appDir());
4361
4471
  const events = new EventHub();
4362
- const httpServer = http.createServer();
4472
+ const httpServer = http2.createServer();
4363
4473
  dropStaleDepCache(paths, project.appDir());
4364
4474
  const base = baseConfig(project);
4365
4475
  const vite = await createServer({
@@ -4378,8 +4488,8 @@ async function startDevServer(options) {
4378
4488
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4379
4489
  // generated entries are modules the frames load, so those stay watched.
4380
4490
  ignored: (file) => {
4381
- const rel = toPosix(path25.relative(paths.cache, file));
4382
- const inCache = rel !== "" && !rel.startsWith("..") && !path25.isAbsolute(rel);
4491
+ const rel = toPosix(path26.relative(paths.cache, file));
4492
+ const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
4383
4493
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4384
4494
  }
4385
4495
  },
@@ -4388,11 +4498,11 @@ async function startDevServer(options) {
4388
4498
  forwardConsole: false
4389
4499
  },
4390
4500
  optimizeDeps: {
4391
- entries: entries.files().map((file) => toPosix(path25.relative(paths.design, file)))
4501
+ entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
4392
4502
  }
4393
4503
  });
4394
4504
  const publicDir = project.publicDir();
4395
- const servePublic = publicDir ? sirv(publicDir, { dev: true }) : null;
4505
+ const servePublic = publicDir ? sirv2(publicDir, { dev: true }) : null;
4396
4506
  const publicRel = (encoded) => {
4397
4507
  try {
4398
4508
  return decodeURIComponent(encoded);
@@ -4400,16 +4510,16 @@ async function startDevServer(options) {
4400
4510
  return null;
4401
4511
  }
4402
4512
  };
4403
- const viewer = fs27.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4404
- const viewerIndex = path25.join(PKG.viewer, "index.html");
4513
+ const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4514
+ const viewerIndex = path26.join(PKG.viewer, "index.html");
4405
4515
  const serveViewerIndex = (res) => {
4406
- const html = fs27.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4516
+ const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4407
4517
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
4408
4518
  res.end(html);
4409
4519
  };
4410
4520
  const ctx = { project, events, snapshots };
4411
4521
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4412
- const bootUrl = `/${relToDesign(paths, path25.join(entries.dir, HTML_BOOT))}`;
4522
+ const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
4413
4523
  const frameHead = (source, extra = {}) => {
4414
4524
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4415
4525
  return {
@@ -4430,7 +4540,7 @@ async function startDevServer(options) {
4430
4540
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
4431
4541
  return;
4432
4542
  }
4433
- if (!fs27.existsSync(entries.entryFile(source))) entries.sync();
4543
+ if (!fs28.existsSync(entries.entryFile(source))) entries.sync();
4434
4544
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
4435
4545
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
4436
4546
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4439,8 +4549,8 @@ async function startDevServer(options) {
4439
4549
  };
4440
4550
  const serveHtmlScreen = async (res, url2) => {
4441
4551
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4442
- const file = path25.resolve(paths.design, rel);
4443
- if (!isInside(paths.design, file) || !fs27.existsSync(file)) {
4552
+ const file = path26.resolve(paths.design, rel);
4553
+ if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4444
4554
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4445
4555
  return;
4446
4556
  }
@@ -4450,13 +4560,13 @@ async function startDevServer(options) {
4450
4560
  key: `${canvas}/${id2}`,
4451
4561
  canvas,
4452
4562
  id: id2,
4453
- title: path25.basename(file),
4563
+ title: path26.basename(file),
4454
4564
  file,
4455
4565
  format: "html",
4456
4566
  props: {},
4457
4567
  system: true
4458
4568
  };
4459
- const raw = fs27.readFileSync(file, "utf8");
4569
+ const raw = fs28.readFileSync(file, "utf8");
4460
4570
  const withSystem = source.system && !optsOutOfSystem(raw);
4461
4571
  const html = injectIntoHtml(
4462
4572
  raw,
@@ -4489,7 +4599,7 @@ async function startDevServer(options) {
4489
4599
  }
4490
4600
  if (url2.pathname.startsWith("/_fs/")) {
4491
4601
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4492
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs27.existsSync(path25.join(publicDir, rel))) {
4602
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
4493
4603
  notFoundPage(res, "Not found", url2.pathname);
4494
4604
  return;
4495
4605
  }
@@ -4501,7 +4611,7 @@ async function startDevServer(options) {
4501
4611
  const rel = publicRel(url2.pathname.slice(1));
4502
4612
  if (servePublic && rel !== null && project.isPublicFile(rel))
4503
4613
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4504
- else if (path25.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4614
+ else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4505
4615
  else serveViewerIndex(res);
4506
4616
  };
4507
4617
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4529,7 +4639,7 @@ async function startDevServer(options) {
4529
4639
  };
4530
4640
  vite.watcher.on("all", (event, file) => {
4531
4641
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4532
- const rel = toPosix(path25.relative(paths.design, file));
4642
+ const rel = toPosix(path26.relative(paths.design, file));
4533
4643
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4534
4644
  if (rel === "design.json") {
4535
4645
  queue("config", () => {
@@ -4640,8 +4750,8 @@ async function ensureServer(paths, options) {
4640
4750
  const recorded = readState(paths);
4641
4751
  if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
4642
4752
  for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
4643
- fs28.mkdirSync(paths.cache, { recursive: true });
4644
- const log = fs28.openSync(logFile(paths), "w");
4753
+ fs29.mkdirSync(paths.cache, { recursive: true });
4754
+ const log = fs29.openSync(logFile(paths), "w");
4645
4755
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4646
4756
  if (options.port) args.push("--port", String(options.port));
4647
4757
  const child = spawn(process.execPath, args, {
@@ -4653,7 +4763,7 @@ async function ensureServer(paths, options) {
4653
4763
  child.unref();
4654
4764
  const state = await waitForServer(paths, 4e4);
4655
4765
  if (!state) {
4656
- const tail = fs28.existsSync(logFile(paths)) ? fs28.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4766
+ const tail = fs29.existsSync(logFile(paths)) ? fs29.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4657
4767
  throw new CliError(`The preview server did not start.
4658
4768
  ${tail.join("\n")}`);
4659
4769
  }
@@ -5035,11 +5145,8 @@ async function runMe(paths, options) {
5035
5145
  }
5036
5146
 
5037
5147
  // src/cli/commands/check.ts
5038
- import fs29 from "fs";
5039
- import http2 from "http";
5040
- import path26 from "path";
5041
- import sirv2 from "sirv";
5042
5148
  var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
5149
+ var SLOW_MS = 3e3;
5043
5150
  function printIssues(issues) {
5044
5151
  const byFile = /* @__PURE__ */ new Map();
5045
5152
  for (const issue of issues) byFile.set(issue.file, [...byFile.get(issue.file) ?? [], issue]);
@@ -5072,7 +5179,15 @@ async function renderFrames(jobs) {
5072
5179
  await page.close().catch(() => {
5073
5180
  });
5074
5181
  }
5075
- return { key: job.key, url: job.url, ready: report.ready, held: report.held, errors: report.errors, links };
5182
+ return {
5183
+ key: job.key,
5184
+ url: job.url,
5185
+ ready: report.ready,
5186
+ held: report.held,
5187
+ readyMs: report.readyMs,
5188
+ errors: report.errors,
5189
+ links
5190
+ };
5076
5191
  });
5077
5192
  } finally {
5078
5193
  await browser.close();
@@ -5117,42 +5232,15 @@ async function runCheck(paths, targets, options) {
5117
5232
  let close = async () => {
5118
5233
  };
5119
5234
  if (options.built) {
5120
- const out = path26.join(paths.cache, "check-build");
5121
- fs29.rmSync(out, { recursive: true, force: true });
5122
5235
  if (!options.json) print(dim("Building the screens as design push does\u2026"));
5123
- await buildSite(paths, out, {
5236
+ const build2 = await serveBuild(paths, "check-build", {
5124
5237
  canvases: canvases.length ? canvases : void 0,
5125
- includeSystem: !canvases.length,
5126
- includeViewer: false
5238
+ includeSystem: !canvases.length
5127
5239
  });
5128
- const read = (rel) => {
5129
- try {
5130
- return JSON.parse(fs29.readFileSync(path26.join(out, rel), "utf8"));
5131
- } catch {
5132
- return null;
5133
- }
5134
- };
5135
- docOf = (id2) => read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
5136
- systemDoc = canvases.length ? null : read("api/system.json");
5137
- const files = sirv2(out, { dev: true });
5138
- const server = http2.createServer((req, res) => {
5139
- const url = new URL(req.url ?? "/", "http://localhost");
5140
- const segments = url.pathname.split("/").map((segment) => {
5141
- try {
5142
- return decodeURIComponent(segment);
5143
- } catch {
5144
- return segment;
5145
- }
5146
- });
5147
- req.url = segments.join("/") + url.search;
5148
- files(req, res, () => {
5149
- res.statusCode = 404;
5150
- res.end("Not found");
5151
- });
5152
- });
5153
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
5154
- base = `http://127.0.0.1:${server.address().port}`;
5155
- close = () => new Promise((resolve) => server.close(() => resolve()));
5240
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
5241
+ systemDoc = canvases.length ? null : build2.read("api/system.json");
5242
+ base = build2.base;
5243
+ close = build2.close;
5156
5244
  if (!options.json) print(dim(`Serving the build at ${base}`));
5157
5245
  } else {
5158
5246
  const wasRunning = await liveServer(paths);
@@ -5192,7 +5280,16 @@ async function runCheck(paths, targets, options) {
5192
5280
  if (frame.held) print(` ${red(`holdReady() was not released within 20s (${frame.held} still held)`)}`);
5193
5281
  else if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
5194
5282
  for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
5283
+ if (frame.readyMs !== void 0 && frame.readyMs > SLOW_MS)
5284
+ print(` ${yellow(`slow: ${(frame.readyMs / 1e3).toFixed(1)}s to become ready`)}`);
5195
5285
  }
5286
+ const slow = frames.filter((frame) => (frame.readyMs ?? 0) > SLOW_MS);
5287
+ if (slow.length)
5288
+ print(
5289
+ dim(
5290
+ `${plural2(slow.length, "screen")} took over ${SLOW_MS / 1e3}s to become ready; every frame of them costs that in the viewer, check and push snapshots. See what they wait for (retries, timers, polling, entrance gates).`
5291
+ )
5292
+ );
5196
5293
  }
5197
5294
  if (linkIssues.length) printLinkIssues(linkIssues, project);
5198
5295
  const screens = [...picked.values()].reduce((sum, set) => sum + (set?.size ?? 0), 0);
@@ -5266,13 +5363,13 @@ function printLinkIssues(issues, project) {
5266
5363
 
5267
5364
  // src/cli/commands/cloud.ts
5268
5365
  import fs30 from "fs";
5269
- import os4 from "os";
5366
+ import os5 from "os";
5270
5367
  import path27 from "path";
5271
5368
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5272
5369
  async function runLogin() {
5273
5370
  const host = cloudHost();
5274
5371
  const client = new CloudClient(host, null);
5275
- const code = await client.deviceCode(os4.hostname());
5372
+ const code = await client.deviceCode(os5.hostname());
5276
5373
  const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
5277
5374
  const expiresIn = code.expires_in ?? 600;
5278
5375
  print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);