foss-design 0.9.6 → 0.9.7

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.
Files changed (2) hide show
  1. package/dist/cli.js +232 -171
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3029,7 +3029,8 @@ 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
3035
3036
  import fs17 from "fs";
@@ -3087,11 +3088,15 @@ var SnapshotStore = class {
3087
3088
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3088
3089
  const dir = path17.join(this.dir, canvas);
3089
3090
  fs17.mkdirSync(dir, { recursive: true });
3090
- fs17.writeFileSync(path17.join(dir, `${id2}.${theme2}.png`), png);
3091
+ const file = path17.join(dir, `${id2}.${theme2}.png`);
3092
+ fs17.writeFileSync(`${file}.${process.pid}.tmp`, png);
3093
+ fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3091
3094
  const data = this.load(canvas);
3092
3095
  const version = Date.now();
3093
3096
  data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3094
- fs17.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3097
+ const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3098
+ fs17.writeFileSync(temp, JSON.stringify(data));
3099
+ fs17.renameSync(temp, this.metaFile(canvas));
3095
3100
  return { url: this.url(canvas, id2, theme2, version) };
3096
3101
  }
3097
3102
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
@@ -3104,8 +3109,54 @@ var SnapshotStore = class {
3104
3109
  }
3105
3110
  };
3106
3111
 
3107
- // src/capture/chrome.ts
3112
+ // src/capture/build-server.ts
3108
3113
  import fs18 from "fs";
3114
+ import http from "http";
3115
+ import path18 from "path";
3116
+ import sirv from "sirv";
3117
+ async function serveBuild(paths, name, options) {
3118
+ const out = path18.join(paths.cache, name);
3119
+ fs18.rmSync(out, { recursive: true, force: true });
3120
+ await buildSite(paths, out, {
3121
+ canvases: options.canvases,
3122
+ includeSystem: options.includeSystem,
3123
+ includeViewer: false
3124
+ });
3125
+ const files = sirv(out, { dev: true });
3126
+ const server = http.createServer((req, res) => {
3127
+ const url = new URL(req.url ?? "/", "http://localhost");
3128
+ const segments = url.pathname.split("/").map((segment) => {
3129
+ try {
3130
+ return decodeURIComponent(segment);
3131
+ } catch {
3132
+ return segment;
3133
+ }
3134
+ });
3135
+ req.url = segments.join("/") + url.search;
3136
+ files(req, res, () => {
3137
+ res.statusCode = 404;
3138
+ res.end("Not found");
3139
+ });
3140
+ });
3141
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
3142
+ return {
3143
+ base: `http://127.0.0.1:${server.address().port}`,
3144
+ read(rel) {
3145
+ try {
3146
+ return JSON.parse(fs18.readFileSync(path18.join(out, rel), "utf8"));
3147
+ } catch {
3148
+ return null;
3149
+ }
3150
+ },
3151
+ close: () => new Promise((resolve) => {
3152
+ server.closeAllConnections();
3153
+ server.close(() => resolve());
3154
+ })
3155
+ };
3156
+ }
3157
+
3158
+ // src/capture/chrome.ts
3159
+ import fs19 from "fs";
3109
3160
  var CANDIDATES = {
3110
3161
  darwin: [
3111
3162
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3129,8 +3180,8 @@ var CANDIDATES = {
3129
3180
  };
3130
3181
  function findChrome() {
3131
3182
  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;
3183
+ if (configured && fs19.existsSync(configured)) return configured;
3184
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
3134
3185
  return null;
3135
3186
  }
3136
3187
  async function launchChrome() {
@@ -3161,6 +3212,8 @@ async function openFrame(page, url, options = {}) {
3161
3212
  record2(text2);
3162
3213
  });
3163
3214
  let ready = false;
3215
+ let loadedAt = 0;
3216
+ let readyMs;
3164
3217
  let reloads = 0;
3165
3218
  const onNavigate = () => {
3166
3219
  reloads++;
@@ -3173,6 +3226,7 @@ async function openFrame(page, url, options = {}) {
3173
3226
  outdated = () => resolve("outdated");
3174
3227
  });
3175
3228
  await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3229
+ loadedAt = Date.now();
3176
3230
  page.on("load", onNavigate);
3177
3231
  if (!ready) {
3178
3232
  const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
@@ -3181,6 +3235,7 @@ async function openFrame(page, url, options = {}) {
3181
3235
  signal.catch(() => {
3182
3236
  });
3183
3237
  ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
3238
+ if (ready) readyMs = Date.now() - loadedAt;
3184
3239
  }
3185
3240
  if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
3186
3241
  page.off("load", onNavigate);
@@ -3198,7 +3253,12 @@ async function openFrame(page, url, options = {}) {
3198
3253
  const unique2 = errors.filter(
3199
3254
  (error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
3200
3255
  );
3201
- return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
3256
+ return {
3257
+ errors: [...new Set(unique2)],
3258
+ ready,
3259
+ ...held ? { held } : {},
3260
+ ...readyMs !== void 0 ? { readyMs } : {}
3261
+ };
3202
3262
  }
3203
3263
 
3204
3264
  // src/capture/frames.ts
@@ -3275,7 +3335,7 @@ async function mapLimit(items, limit, fn) {
3275
3335
 
3276
3336
  // src/capture/cover.ts
3277
3337
  var SNAPSHOT_WIDTH = 900;
3278
- var TABS = 4;
3338
+ var TABS = Math.min(8, Math.max(2, os3.availableParallelism() - 1));
3279
3339
  var THEMES2 = ["light", "dark"];
3280
3340
  function staleSnapshots(paths, canvasIds, filter = {}) {
3281
3341
  const store = new SnapshotStore(paths.snapshots);
@@ -3292,8 +3352,8 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3292
3352
  for (const job of inLight) {
3293
3353
  if (filter.screens && !filter.screens.has(job.id)) continue;
3294
3354
  const file = files.get(job.id);
3295
- if (!file || !fs19.existsSync(file)) continue;
3296
- const changed = fs19.statSync(file).mtimeMs;
3355
+ if (!file || !fs20.existsSync(file)) continue;
3356
+ const changed = fs20.statSync(file).mtimeMs;
3297
3357
  const pinned = inDark.get(job.id) === job.theme;
3298
3358
  const themes = pinned ? [job.theme] : THEMES2;
3299
3359
  const screen = { id: job.id };
@@ -3305,12 +3365,25 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3305
3365
  }
3306
3366
  return stale;
3307
3367
  }
3308
- async function takeSnapshots(paths, targets, base, progress2) {
3368
+ async function takeSnapshots(paths, targets, preview, progress2) {
3309
3369
  if (!targets.length) return [];
3310
3370
  if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
3311
3371
  const store = new SnapshotStore(paths.snapshots);
3312
3372
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3313
- const url = await base();
3373
+ const canvases = [...new Set(targets.map((target) => target.canvas))];
3374
+ progress2.update(`Building ${canvases.join(", ")} to take snapshots from\u2026`);
3375
+ let url;
3376
+ let docOf = (id2) => project.canvas(id2).doc;
3377
+ let stop = async () => {
3378
+ };
3379
+ try {
3380
+ const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
3381
+ url = build2.base;
3382
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
3383
+ stop = build2.close;
3384
+ } catch {
3385
+ url = await preview();
3386
+ }
3314
3387
  const browser = await launchChrome();
3315
3388
  const failed = [];
3316
3389
  const started = Date.now();
@@ -3319,7 +3392,7 @@ async function takeSnapshots(paths, targets, base, progress2) {
3319
3392
  progress2.update(line());
3320
3393
  try {
3321
3394
  await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
3322
- const job = canvasFrames(project.canvas(canvas).doc, url, theme2, { id: screen })[0];
3395
+ const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3323
3396
  if (job) {
3324
3397
  const page = await browser.newPage({
3325
3398
  viewport: { width: job.width, height: job.height },
@@ -3345,6 +3418,7 @@ async function takeSnapshots(paths, targets, base, progress2) {
3345
3418
  });
3346
3419
  } finally {
3347
3420
  await browser.close();
3421
+ await stop();
3348
3422
  }
3349
3423
  const seconds2 = Math.round((Date.now() - started) / 1e3);
3350
3424
  progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
@@ -3356,73 +3430,73 @@ async function takeSnapshots(paths, targets, base, progress2) {
3356
3430
  }
3357
3431
 
3358
3432
  // src/cloud/pull.ts
3359
- import fs23 from "fs";
3360
- import path21 from "path";
3433
+ import fs24 from "fs";
3434
+ import path22 from "path";
3361
3435
 
3362
3436
  // src/cloud/merge.ts
3363
- import fs21 from "fs";
3364
- import path19 from "path";
3437
+ import fs22 from "fs";
3438
+ import path20 from "path";
3365
3439
  import { merge as diff3 } from "node-diff3";
3366
3440
 
3367
3441
  // src/cloud/write.ts
3368
- import fs20 from "fs";
3369
- import path18 from "path";
3442
+ import fs21 from "fs";
3443
+ import path19 from "path";
3370
3444
  function writeInside(root, rel, data) {
3371
- const file = path18.join(root, rel);
3445
+ const file = path19.join(root, rel);
3372
3446
  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))))
3447
+ fs21.mkdirSync(path19.dirname(file), { recursive: true });
3448
+ if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
3375
3449
  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);
3450
+ if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3451
+ fs21.writeFileSync(file, data);
3378
3452
  }
3379
3453
  function pruneEmpty(dir, stop) {
3380
3454
  let current = dir;
3381
3455
  while (isInside(stop, current) && current !== stop) {
3382
3456
  try {
3383
- if (fs20.readdirSync(current).length) return;
3384
- fs20.rmdirSync(current);
3457
+ if (fs21.readdirSync(current).length) return;
3458
+ fs21.rmdirSync(current);
3385
3459
  } catch {
3386
3460
  return;
3387
3461
  }
3388
- current = path18.dirname(current);
3462
+ current = path19.dirname(current);
3389
3463
  }
3390
3464
  }
3391
3465
 
3392
3466
  // src/cloud/merge.ts
3393
3467
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3394
3468
  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);
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);
3398
3472
  function readMergeState(paths, key) {
3399
3473
  try {
3400
- return JSON.parse(fs21.readFileSync(stateFile(paths, key), "utf8"));
3474
+ return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
3401
3475
  } catch {
3402
3476
  return null;
3403
3477
  }
3404
3478
  }
3405
3479
  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)}
3480
+ fs22.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3481
+ fs22.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3408
3482
  `);
3409
3483
  }
3410
3484
  function mergingUnits(paths) {
3411
- const root = path19.join(cloudCache(paths), "merge");
3485
+ const root = path20.join(cloudCache(paths), "merge");
3412
3486
  const found = [];
3413
3487
  const visit = (dir, prefix) => {
3414
- if (fs21.existsSync(path19.join(dir, "state.json"))) {
3488
+ if (fs22.existsSync(path20.join(dir, "state.json"))) {
3415
3489
  const state = readMergeState(paths, prefix);
3416
3490
  if (state) found.push(state);
3417
3491
  return;
3418
3492
  }
3419
3493
  let entries = [];
3420
3494
  try {
3421
- entries = fs21.readdirSync(dir, { withFileTypes: true });
3495
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
3422
3496
  } catch {
3423
3497
  }
3424
3498
  for (const entry of entries)
3425
- if (entry.isDirectory()) visit(path19.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3499
+ if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3426
3500
  };
3427
3501
  visit(root, "");
3428
3502
  return found;
@@ -3432,7 +3506,7 @@ function openConflicts(paths, state) {
3432
3506
  return state.conflicts.filter((conflict) => {
3433
3507
  if (conflict.kind !== "text") return true;
3434
3508
  try {
3435
- return MARKER.test(fs21.readFileSync(path19.join(paths.design, conflict.path), "utf8"));
3509
+ return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
3436
3510
  } catch {
3437
3511
  return false;
3438
3512
  }
@@ -3441,19 +3515,19 @@ function openConflicts(paths, state) {
3441
3515
  function keep(paths, key, rel, side, data) {
3442
3516
  if (!data) return;
3443
3517
  const file = sideFile(paths, key, rel, side);
3444
- fs21.mkdirSync(path19.dirname(file), { recursive: true });
3445
- fs21.writeFileSync(file, data);
3518
+ fs22.mkdirSync(path20.dirname(file), { recursive: true });
3519
+ fs22.writeFileSync(file, data);
3446
3520
  }
3447
3521
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3448
3522
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3449
3523
  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));
3524
+ const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
3525
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3526
+ const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
3453
3527
  const remove = (rel) => {
3454
- const file = path19.join(paths.design, rel);
3455
- fs21.rmSync(file, { force: true });
3456
- pruneEmpty(path19.dirname(file), stop);
3528
+ const file = path20.join(paths.design, rel);
3529
+ fs22.rmSync(file, { force: true });
3530
+ pruneEmpty(path20.dirname(file), stop);
3457
3531
  };
3458
3532
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3459
3533
  const b = base[rel];
@@ -3509,11 +3583,11 @@ function resolveConflict(paths, state, rel, side) {
3509
3583
  const conflict = state.conflicts.find((item) => item.path === rel);
3510
3584
  if (!conflict) return;
3511
3585
  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));
3586
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
3587
+ if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3514
3588
  else {
3515
- fs21.rmSync(path19.join(paths.design, rel), { force: true });
3516
- pruneEmpty(path19.dirname(path19.join(paths.design, rel)), stop);
3589
+ fs22.rmSync(path20.join(paths.design, rel), { force: true });
3590
+ pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
3517
3591
  }
3518
3592
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3519
3593
  saveState(paths, state);
@@ -3523,15 +3597,15 @@ function finishMerge(paths, link, key, accept = false) {
3523
3597
  if (state) {
3524
3598
  const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
3525
3599
  if (open2.length) return open2;
3526
- fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3600
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3527
3601
  }
3528
3602
  delete link.conflicts[key];
3529
3603
  return [];
3530
3604
  }
3531
3605
 
3532
3606
  // src/cloud/meta.ts
3533
- import fs22 from "fs";
3534
- import path20 from "path";
3607
+ import fs23 from "fs";
3608
+ import path21 from "path";
3535
3609
  async function syncProjectMeta(paths, client, link) {
3536
3610
  const remote = await client.project(link.project);
3537
3611
  if (!remote) return [];
@@ -3550,8 +3624,8 @@ async function syncProjectMeta(paths, client, link) {
3550
3624
  if (localChanged && !cloudChanged) {
3551
3625
  try {
3552
3626
  if (file) {
3553
- const ext = path20.extname(file).slice(1);
3554
- await client.setProjectIcon(link.project, { data: fs22.readFileSync(file), type: ICON_TYPES[ext] });
3627
+ const ext = path21.extname(file).slice(1);
3628
+ await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3555
3629
  lines.push("The project icon went up to the cloud.");
3556
3630
  } else {
3557
3631
  await client.setProjectIcon(link.project, null);
@@ -3586,12 +3660,12 @@ function checkManifest(key, manifest) {
3586
3660
  }
3587
3661
  function applyUnit(paths, key, local, target, blobs) {
3588
3662
  const counts = { added: 0, changed: 0, removed: 0 };
3589
- const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3663
+ const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3590
3664
  for (const rel of Object.keys(local)) {
3591
3665
  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);
3666
+ const file = path22.join(paths.design, rel);
3667
+ fs24.rmSync(file, { force: true });
3668
+ pruneEmpty(path22.dirname(file), stop);
3595
3669
  counts.removed++;
3596
3670
  }
3597
3671
  for (const [rel, entry] of Object.entries(target)) {
@@ -3661,7 +3735,7 @@ async function pull(ctx, options) {
3661
3735
  }
3662
3736
  if (s.conflictRev !== void 0) {
3663
3737
  finishMerge(paths, link, key, true);
3664
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3738
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3665
3739
  }
3666
3740
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
3667
3741
  } else if (!s.changed.length) {
@@ -3690,7 +3764,7 @@ async function pull(ctx, options) {
3690
3764
  }
3691
3765
  const byHash = /* @__PURE__ */ new Map();
3692
3766
  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));
3767
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
3694
3768
  const needed = /* @__PURE__ */ new Set();
3695
3769
  const bases = /* @__PURE__ */ new Set();
3696
3770
  for (const plan of plans) {
@@ -3711,7 +3785,7 @@ async function pull(ctx, options) {
3711
3785
  const download = [];
3712
3786
  for (const hash of needed) {
3713
3787
  const file = byHash.get(hash);
3714
- if (file) blobs.set(hash, fs23.readFileSync(file));
3788
+ if (file) blobs.set(hash, fs24.readFileSync(file));
3715
3789
  else download.push(hash);
3716
3790
  }
3717
3791
  if (download.length) {
@@ -3747,7 +3821,7 @@ async function pull(ctx, options) {
3747
3821
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3748
3822
  else delete link.units[key];
3749
3823
  delete link.conflicts[key];
3750
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3824
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3751
3825
  } else {
3752
3826
  const result = mergeUnit(
3753
3827
  paths,
@@ -3758,7 +3832,7 @@ async function pull(ctx, options) {
3758
3832
  plan.remote.manifest,
3759
3833
  (hash) => blobs.get(hash)
3760
3834
  );
3761
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3835
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3762
3836
  link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3763
3837
  entry.rev = plan.remote.headRev;
3764
3838
  entry.files = result.files;
@@ -3788,8 +3862,8 @@ async function pull(ctx, options) {
3788
3862
  }
3789
3863
 
3790
3864
  // src/cloud/push.ts
3791
- import fs24 from "fs";
3792
- import path22 from "path";
3865
+ import fs25 from "fs";
3866
+ import path23 from "path";
3793
3867
  function blockedReport(status, message) {
3794
3868
  return {
3795
3869
  unit: status.key,
@@ -3905,7 +3979,7 @@ async function push(ctx, options) {
3905
3979
  for (const key of toPush) {
3906
3980
  const s = status(key);
3907
3981
  if (s.conflictRev !== void 0) {
3908
- const dir = path22.relative(paths.root, incomingDir(paths, key));
3982
+ const dir = path23.relative(paths.root, incomingDir(paths, key));
3909
3983
  blocked.push(
3910
3984
  blockedReport(
3911
3985
  s,
@@ -3954,7 +4028,7 @@ async function push(ctx, options) {
3954
4028
  const files = /* @__PURE__ */ new Map();
3955
4029
  const addFiles = (manifest, dir) => {
3956
4030
  for (const [rel, entry] of Object.entries(manifest))
3957
- if (!files.has(entry.hash)) files.set(entry.hash, path22.join(dir, rel));
4031
+ if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
3958
4032
  };
3959
4033
  for (const key of pushing) {
3960
4034
  const id2 = unitCanvasId(key);
@@ -3986,7 +4060,7 @@ async function push(ctx, options) {
3986
4060
  if (missing.length) {
3987
4061
  const unknown = missing.find((hash) => !files.has(hash));
3988
4062
  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);
4063
+ const bytes = missing.reduce((sum, hash) => sum + fs25.statSync(files.get(hash)).size, 0);
3990
4064
  const bar = ctx.progress();
3991
4065
  const started = Date.now();
3992
4066
  let sentFiles = 0;
@@ -3995,12 +4069,12 @@ async function push(ctx, options) {
3995
4069
  bar.update(line());
3996
4070
  await mapLimit(missing, TRANSFERS, async (hash) => {
3997
4071
  const file = files.get(hash);
3998
- const data = fs24.readFileSync(file);
4072
+ const data = fs25.readFileSync(file);
3999
4073
  try {
4000
4074
  await client.putBlob(link.project, hash, data);
4001
4075
  } catch (error) {
4002
4076
  if (error instanceof CloudError && error.status === 413)
4003
- throw new CliError(`${path22.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4077
+ throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4004
4078
  throw error;
4005
4079
  }
4006
4080
  sentFiles++;
@@ -4033,7 +4107,7 @@ async function push(ctx, options) {
4033
4107
  url: webUrl(link, unit.key)
4034
4108
  });
4035
4109
  }
4036
- for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4110
+ for (const key of resolved) fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4037
4111
  writeLink(paths, link);
4038
4112
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
4039
4113
  return report;
@@ -4044,7 +4118,7 @@ function refuseLegacyGo(paths, keys) {
4044
4118
  for (const key of keys) {
4045
4119
  if (!key.startsWith("canvas/")) continue;
4046
4120
  const id2 = key.slice("canvas/".length);
4047
- for (const file of canvasCodeFiles(path22.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4121
+ for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4048
4122
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4049
4123
  }
4050
4124
  }
@@ -4060,18 +4134,18 @@ Still using go():
4060
4134
 
4061
4135
  // src/cli/commands/preview.ts
4062
4136
  import { spawn } from "child_process";
4063
- import fs28 from "fs";
4137
+ import fs29 from "fs";
4064
4138
 
4065
4139
  // src/server/index.ts
4066
- import fs27 from "fs";
4067
- import http from "http";
4068
- import path25 from "path";
4069
- import sirv from "sirv";
4140
+ import fs28 from "fs";
4141
+ import http2 from "http";
4142
+ import path26 from "path";
4143
+ import sirv2 from "sirv";
4070
4144
  import { createServer, searchForWorkspaceRoot } from "vite";
4071
4145
 
4072
4146
  // src/server/api.ts
4073
- import fs25 from "fs";
4074
- import path23 from "path";
4147
+ import fs26 from "fs";
4148
+ import path24 from "path";
4075
4149
  function sendJson(res, status, body) {
4076
4150
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4077
4151
  res.end(JSON.stringify(body));
@@ -4122,17 +4196,17 @@ async function handleApi(ctx, req, res, url) {
4122
4196
  }
4123
4197
  if (route2 === "/source") {
4124
4198
  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()) {
4199
+ const file = path24.resolve(project.paths.root, rel);
4200
+ if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4127
4201
  sendJson(res, 404, { error: "not found" });
4128
4202
  return true;
4129
4203
  }
4130
- if (fs25.statSync(file).size > 2 * 1024 * 1024) {
4204
+ if (fs26.statSync(file).size > 2 * 1024 * 1024) {
4131
4205
  sendJson(res, 413, { error: "file too large to show" });
4132
4206
  return true;
4133
4207
  }
4134
4208
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
4135
- res.end(fs25.readFileSync(file, "utf8"));
4209
+ res.end(fs26.readFileSync(file, "utf8"));
4136
4210
  return true;
4137
4211
  }
4138
4212
  if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
@@ -4179,7 +4253,7 @@ async function handleApi(ctx, req, res, url) {
4179
4253
  return true;
4180
4254
  }
4181
4255
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
4182
- fs25.createReadStream(file).pipe(res);
4256
+ fs26.createReadStream(file).pipe(res);
4183
4257
  return true;
4184
4258
  }
4185
4259
  return false;
@@ -4219,62 +4293,62 @@ var EventHub = class {
4219
4293
  };
4220
4294
 
4221
4295
  // 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");
4296
+ import fs27 from "fs";
4297
+ 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");
4227
4301
  function readState(paths) {
4228
4302
  try {
4229
- return JSON.parse(fs26.readFileSync(stateFile2(paths), "utf8"));
4303
+ return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
4230
4304
  } catch {
4231
4305
  return null;
4232
4306
  }
4233
4307
  }
4234
4308
  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");
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");
4237
4311
  }
4238
- var entryFile = (pid) => path24.join(registryDir(), `${pid}.json`);
4312
+ var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
4239
4313
  function writeState(paths, state) {
4240
- fs26.mkdirSync(paths.cache, { recursive: true });
4241
- fs26.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4314
+ fs27.mkdirSync(paths.cache, { recursive: true });
4315
+ fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4242
4316
  `);
4243
4317
  try {
4244
- fs26.mkdirSync(registryDir(), { recursive: true });
4245
- fs26.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4318
+ fs27.mkdirSync(registryDir(), { recursive: true });
4319
+ fs27.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4246
4320
  `);
4247
4321
  } catch {
4248
4322
  }
4249
4323
  }
4250
4324
  function clearState(paths, pid) {
4251
4325
  const state = readState(paths);
4252
- fs26.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4326
+ fs27.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4253
4327
  if (pid !== void 0 && state && state.pid !== pid) return;
4254
- fs26.rmSync(stateFile2(paths), { force: true });
4328
+ fs27.rmSync(stateFile2(paths), { force: true });
4255
4329
  }
4256
4330
  function listPreviews() {
4257
4331
  let files = [];
4258
4332
  try {
4259
- files = fs26.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4333
+ files = fs27.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4260
4334
  } catch {
4261
4335
  return [];
4262
4336
  }
4263
4337
  const found = [];
4264
4338
  for (const name of files) {
4265
- const file = path24.join(registryDir(), name);
4339
+ const file = path25.join(registryDir(), name);
4266
4340
  let state = null;
4267
4341
  try {
4268
- state = JSON.parse(fs26.readFileSync(file, "utf8"));
4342
+ state = JSON.parse(fs27.readFileSync(file, "utf8"));
4269
4343
  } catch {
4270
4344
  }
4271
4345
  if (!state?.pid || !pidAlive(state.pid)) {
4272
- fs26.rmSync(file, { force: true });
4346
+ fs27.rmSync(file, { force: true });
4273
4347
  continue;
4274
4348
  }
4275
4349
  let recorded = null;
4276
4350
  try {
4277
- recorded = JSON.parse(fs26.readFileSync(path24.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4351
+ recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4278
4352
  } catch {
4279
4353
  }
4280
4354
  found.push({ ...state, tracked: recorded?.pid === state.pid });
@@ -4288,11 +4362,11 @@ async function stopPreview(entry) {
4288
4362
  }
4289
4363
  const until = Date.now() + 5e3;
4290
4364
  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");
4365
+ fs27.rmSync(entryFile(entry.pid), { force: true });
4366
+ const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
4293
4367
  try {
4294
- if (JSON.parse(fs26.readFileSync(stateAt, "utf8")).pid === entry.pid)
4295
- fs26.rmSync(stateAt, { force: true });
4368
+ if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4369
+ fs27.rmSync(stateAt, { force: true });
4296
4370
  } catch {
4297
4371
  }
4298
4372
  }
@@ -4351,15 +4425,15 @@ function notFoundPage(res, title, text2) {
4351
4425
  }
4352
4426
  async function startDevServer(options) {
4353
4427
  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 });
4428
+ if (!fs28.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4429
+ fs28.mkdirSync(paths.cache, { recursive: true });
4356
4430
  const snapshots = new SnapshotStore(paths.snapshots);
4357
4431
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
4358
4432
  const entries = new Entries(project);
4359
4433
  entries.sync();
4360
4434
  const links = linkShippedPackages(paths, project.appDir());
4361
4435
  const events = new EventHub();
4362
- const httpServer = http.createServer();
4436
+ const httpServer = http2.createServer();
4363
4437
  dropStaleDepCache(paths, project.appDir());
4364
4438
  const base = baseConfig(project);
4365
4439
  const vite = await createServer({
@@ -4378,8 +4452,8 @@ async function startDevServer(options) {
4378
4452
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4379
4453
  // generated entries are modules the frames load, so those stay watched.
4380
4454
  ignored: (file) => {
4381
- const rel = toPosix(path25.relative(paths.cache, file));
4382
- const inCache = rel !== "" && !rel.startsWith("..") && !path25.isAbsolute(rel);
4455
+ const rel = toPosix(path26.relative(paths.cache, file));
4456
+ const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
4383
4457
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4384
4458
  }
4385
4459
  },
@@ -4388,11 +4462,11 @@ async function startDevServer(options) {
4388
4462
  forwardConsole: false
4389
4463
  },
4390
4464
  optimizeDeps: {
4391
- entries: entries.files().map((file) => toPosix(path25.relative(paths.design, file)))
4465
+ entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
4392
4466
  }
4393
4467
  });
4394
4468
  const publicDir = project.publicDir();
4395
- const servePublic = publicDir ? sirv(publicDir, { dev: true }) : null;
4469
+ const servePublic = publicDir ? sirv2(publicDir, { dev: true }) : null;
4396
4470
  const publicRel = (encoded) => {
4397
4471
  try {
4398
4472
  return decodeURIComponent(encoded);
@@ -4400,16 +4474,16 @@ async function startDevServer(options) {
4400
4474
  return null;
4401
4475
  }
4402
4476
  };
4403
- const viewer = fs27.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4404
- const viewerIndex = path25.join(PKG.viewer, "index.html");
4477
+ const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4478
+ const viewerIndex = path26.join(PKG.viewer, "index.html");
4405
4479
  const serveViewerIndex = (res) => {
4406
- const html = fs27.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4480
+ const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4407
4481
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
4408
4482
  res.end(html);
4409
4483
  };
4410
4484
  const ctx = { project, events, snapshots };
4411
4485
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4412
- const bootUrl = `/${relToDesign(paths, path25.join(entries.dir, HTML_BOOT))}`;
4486
+ const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
4413
4487
  const frameHead = (source, extra = {}) => {
4414
4488
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4415
4489
  return {
@@ -4430,7 +4504,7 @@ async function startDevServer(options) {
4430
4504
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
4431
4505
  return;
4432
4506
  }
4433
- if (!fs27.existsSync(entries.entryFile(source))) entries.sync();
4507
+ if (!fs28.existsSync(entries.entryFile(source))) entries.sync();
4434
4508
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
4435
4509
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
4436
4510
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4439,8 +4513,8 @@ async function startDevServer(options) {
4439
4513
  };
4440
4514
  const serveHtmlScreen = async (res, url2) => {
4441
4515
  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)) {
4516
+ const file = path26.resolve(paths.design, rel);
4517
+ if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4444
4518
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4445
4519
  return;
4446
4520
  }
@@ -4450,13 +4524,13 @@ async function startDevServer(options) {
4450
4524
  key: `${canvas}/${id2}`,
4451
4525
  canvas,
4452
4526
  id: id2,
4453
- title: path25.basename(file),
4527
+ title: path26.basename(file),
4454
4528
  file,
4455
4529
  format: "html",
4456
4530
  props: {},
4457
4531
  system: true
4458
4532
  };
4459
- const raw = fs27.readFileSync(file, "utf8");
4533
+ const raw = fs28.readFileSync(file, "utf8");
4460
4534
  const withSystem = source.system && !optsOutOfSystem(raw);
4461
4535
  const html = injectIntoHtml(
4462
4536
  raw,
@@ -4489,7 +4563,7 @@ async function startDevServer(options) {
4489
4563
  }
4490
4564
  if (url2.pathname.startsWith("/_fs/")) {
4491
4565
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4492
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs27.existsSync(path25.join(publicDir, rel))) {
4566
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
4493
4567
  notFoundPage(res, "Not found", url2.pathname);
4494
4568
  return;
4495
4569
  }
@@ -4501,7 +4575,7 @@ async function startDevServer(options) {
4501
4575
  const rel = publicRel(url2.pathname.slice(1));
4502
4576
  if (servePublic && rel !== null && project.isPublicFile(rel))
4503
4577
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4504
- else if (path25.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4578
+ else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4505
4579
  else serveViewerIndex(res);
4506
4580
  };
4507
4581
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4529,7 +4603,7 @@ async function startDevServer(options) {
4529
4603
  };
4530
4604
  vite.watcher.on("all", (event, file) => {
4531
4605
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4532
- const rel = toPosix(path25.relative(paths.design, file));
4606
+ const rel = toPosix(path26.relative(paths.design, file));
4533
4607
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4534
4608
  if (rel === "design.json") {
4535
4609
  queue("config", () => {
@@ -4640,8 +4714,8 @@ async function ensureServer(paths, options) {
4640
4714
  const recorded = readState(paths);
4641
4715
  if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
4642
4716
  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");
4717
+ fs29.mkdirSync(paths.cache, { recursive: true });
4718
+ const log = fs29.openSync(logFile(paths), "w");
4645
4719
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4646
4720
  if (options.port) args.push("--port", String(options.port));
4647
4721
  const child = spawn(process.execPath, args, {
@@ -4653,7 +4727,7 @@ async function ensureServer(paths, options) {
4653
4727
  child.unref();
4654
4728
  const state = await waitForServer(paths, 4e4);
4655
4729
  if (!state) {
4656
- const tail = fs28.existsSync(logFile(paths)) ? fs28.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4730
+ const tail = fs29.existsSync(logFile(paths)) ? fs29.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4657
4731
  throw new CliError(`The preview server did not start.
4658
4732
  ${tail.join("\n")}`);
4659
4733
  }
@@ -5035,11 +5109,8 @@ async function runMe(paths, options) {
5035
5109
  }
5036
5110
 
5037
5111
  // 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
5112
  var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
5113
+ var SLOW_MS = 3e3;
5043
5114
  function printIssues(issues) {
5044
5115
  const byFile = /* @__PURE__ */ new Map();
5045
5116
  for (const issue of issues) byFile.set(issue.file, [...byFile.get(issue.file) ?? [], issue]);
@@ -5072,7 +5143,15 @@ async function renderFrames(jobs) {
5072
5143
  await page.close().catch(() => {
5073
5144
  });
5074
5145
  }
5075
- return { key: job.key, url: job.url, ready: report.ready, held: report.held, errors: report.errors, links };
5146
+ return {
5147
+ key: job.key,
5148
+ url: job.url,
5149
+ ready: report.ready,
5150
+ held: report.held,
5151
+ readyMs: report.readyMs,
5152
+ errors: report.errors,
5153
+ links
5154
+ };
5076
5155
  });
5077
5156
  } finally {
5078
5157
  await browser.close();
@@ -5117,42 +5196,15 @@ async function runCheck(paths, targets, options) {
5117
5196
  let close = async () => {
5118
5197
  };
5119
5198
  if (options.built) {
5120
- const out = path26.join(paths.cache, "check-build");
5121
- fs29.rmSync(out, { recursive: true, force: true });
5122
5199
  if (!options.json) print(dim("Building the screens as design push does\u2026"));
5123
- await buildSite(paths, out, {
5200
+ const build2 = await serveBuild(paths, "check-build", {
5124
5201
  canvases: canvases.length ? canvases : void 0,
5125
- includeSystem: !canvases.length,
5126
- includeViewer: false
5127
- });
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
- });
5202
+ includeSystem: !canvases.length
5152
5203
  });
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()));
5204
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
5205
+ systemDoc = canvases.length ? null : build2.read("api/system.json");
5206
+ base = build2.base;
5207
+ close = build2.close;
5156
5208
  if (!options.json) print(dim(`Serving the build at ${base}`));
5157
5209
  } else {
5158
5210
  const wasRunning = await liveServer(paths);
@@ -5192,7 +5244,16 @@ async function runCheck(paths, targets, options) {
5192
5244
  if (frame.held) print(` ${red(`holdReady() was not released within 20s (${frame.held} still held)`)}`);
5193
5245
  else if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
5194
5246
  for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
5247
+ if (frame.readyMs !== void 0 && frame.readyMs > SLOW_MS)
5248
+ print(` ${yellow(`slow: ${(frame.readyMs / 1e3).toFixed(1)}s to become ready`)}`);
5195
5249
  }
5250
+ const slow = frames.filter((frame) => (frame.readyMs ?? 0) > SLOW_MS);
5251
+ if (slow.length)
5252
+ print(
5253
+ dim(
5254
+ `${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).`
5255
+ )
5256
+ );
5196
5257
  }
5197
5258
  if (linkIssues.length) printLinkIssues(linkIssues, project);
5198
5259
  const screens = [...picked.values()].reduce((sum, set) => sum + (set?.size ?? 0), 0);
@@ -5266,13 +5327,13 @@ function printLinkIssues(issues, project) {
5266
5327
 
5267
5328
  // src/cli/commands/cloud.ts
5268
5329
  import fs30 from "fs";
5269
- import os4 from "os";
5330
+ import os5 from "os";
5270
5331
  import path27 from "path";
5271
5332
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5272
5333
  async function runLogin() {
5273
5334
  const host = cloudHost();
5274
5335
  const client = new CloudClient(host, null);
5275
- const code = await client.deviceCode(os4.hostname());
5336
+ const code = await client.deviceCode(os5.hostname());
5276
5337
  const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
5277
5338
  const expiresIn = code.expires_in ?? 600;
5278
5339
  print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foss-design",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "Design canvases and design systems for coding agents: live, animated, clickable screens on an infinite canvas, served locally from a .design folder.",
5
5
  "type": "module",
6
6
  "license": "MIT",