foss-design 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import path25 from "path";
4
+ import path26 from "path";
5
5
  import { parseArgs } from "util";
6
6
 
7
7
  // src/core/paths.ts
@@ -134,12 +134,12 @@ function resolveFrom(anchor, request) {
134
134
 
135
135
  // src/cli/commands/build.ts
136
136
  import { spawnSync } from "child_process";
137
- import fs10 from "fs";
138
- import path11 from "path";
137
+ import fs12 from "fs";
138
+ import path12 from "path";
139
139
 
140
140
  // src/build/static.ts
141
- import fs9 from "fs";
142
- import path10 from "path";
141
+ import fs11 from "fs";
142
+ import path11 from "path";
143
143
  import { build } from "vite";
144
144
 
145
145
  // src/core/png.ts
@@ -307,8 +307,8 @@ function ensureThumb(file) {
307
307
  }
308
308
 
309
309
  // src/core/project.ts
310
- import fs6 from "fs";
311
- import path6 from "path";
310
+ import fs7 from "fs";
311
+ import path7 from "path";
312
312
 
313
313
  // src/core/canvas.ts
314
314
  import { createHash } from "crypto";
@@ -346,8 +346,8 @@ function routeError(pattern) {
346
346
  }
347
347
  return null;
348
348
  }
349
- function split(path26) {
350
- return path26.split("/").filter(Boolean);
349
+ function split(path27) {
350
+ return path27.split("/").filter(Boolean);
351
351
  }
352
352
  function compileRoute(pattern) {
353
353
  return {
@@ -366,8 +366,8 @@ function decode2(part) {
366
366
  return part;
367
367
  }
368
368
  }
369
- function scoreRoute(route2, path26) {
370
- const parts = split(path26.split(/[?#]/)[0] ?? "").map(decode2);
369
+ function scoreRoute(route2, path27) {
370
+ const parts = split(path27.split(/[?#]/)[0] ?? "").map(decode2);
371
371
  const walk2 = (s, p) => {
372
372
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
373
373
  const segment = route2.segments[s];
@@ -385,11 +385,11 @@ function scoreRoute(route2, path26) {
385
385
  };
386
386
  return walk2(0, 0);
387
387
  }
388
- function matchRoute(entries, path26) {
388
+ function matchRoute(entries, path27) {
389
389
  let best = null;
390
390
  for (const entry of entries) {
391
391
  for (const route2 of entry.routes) {
392
- const score = scoreRoute(route2, path26);
392
+ const score = scoreRoute(route2, path27);
393
393
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
394
394
  }
395
395
  }
@@ -426,6 +426,8 @@ var DesignConfigSchema = z.strictObject({
426
426
  alias: z.record(z.string(), z.string()).optional(),
427
427
  /** Extra folders Tailwind scans for class names, relative to the project root. */
428
428
  sources: z.array(z.string()).optional(),
429
+ /** A folder served at the root of every screen, like Vite's `public`, relative to the project root. */
430
+ public: z.string().min(1).optional(),
429
431
  port: z.number().int().min(1).max(65535).optional()
430
432
  });
431
433
  var SystemConfigSchema = z.strictObject({
@@ -510,9 +512,9 @@ var CanvasSchema = z.strictObject({
510
512
  pages: z.array(z.unknown()).min(1, "add at least one page")
511
513
  });
512
514
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
513
- function formatPath(base, path26) {
515
+ function formatPath(base, path27) {
514
516
  let out = base;
515
- for (const part of path26) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
517
+ for (const part of path27) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
516
518
  return out;
517
519
  }
518
520
  function describeZodError(error, base = "") {
@@ -749,7 +751,7 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
749
751
  };
750
752
  doc.pages.push(out);
751
753
  });
752
- checkNavigation(doc, screens, issues, fileRel, (file2) => relToRoot(paths, file2));
754
+ checkNavigation(doc, canvasCodeFiles(dir, screens), issues, fileRel, (file2) => relToRoot(paths, file2));
753
755
  return { doc, screens };
754
756
  }
755
757
  var GO_IMPORT = /import\s*\{[^}]*\bgo\b[^}]*\}\s*from\s*['"]@design\/runtime['"]/;
@@ -768,7 +770,27 @@ function legacyGoLines(file) {
768
770
  const calls = [...source.matchAll(GO_USE)].map((match) => lineOf(match.index)).filter((line) => line !== lineOf(imported.index));
769
771
  return calls.length ? [...new Set(calls)] : [lineOf(imported.index)];
770
772
  }
771
- function checkNavigation(doc, screens, issues, canvasFile, rel) {
773
+ var CODE_FILE = /\.(?:[cm]?[jt]sx?|html?)$/i;
774
+ function canvasCodeFiles(dir, screens) {
775
+ const files = new Set(screens.map((screen) => screen.file));
776
+ const visit = (current) => {
777
+ let entries;
778
+ try {
779
+ entries = fs3.readdirSync(current, { withFileTypes: true });
780
+ } catch {
781
+ return;
782
+ }
783
+ for (const entry of entries) {
784
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
785
+ const full = path3.join(current, entry.name);
786
+ if (entry.isDirectory()) visit(full);
787
+ else if (entry.isFile() && CODE_FILE.test(entry.name)) files.add(full);
788
+ }
789
+ };
790
+ visit(dir);
791
+ return [...files];
792
+ }
793
+ function checkNavigation(doc, codeFiles, issues, canvasFile, rel) {
772
794
  const frames = doc.pages.flatMap(
773
795
  (page) => page.sections.flatMap(
774
796
  (section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url").map((item) => ({ page, item }))
@@ -787,7 +809,7 @@ function checkNavigation(doc, screens, issues, canvasFile, rel) {
787
809
  });
788
810
  }
789
811
  }
790
- for (const file of new Set(screens.map((screen) => screen.file))) {
812
+ for (const file of codeFiles) {
791
813
  for (const line of legacyGoLines(file)) {
792
814
  issues.push({ severity: "error", file: rel(file), at: `line ${line}`, message: GO_REMOVED });
793
815
  }
@@ -809,13 +831,49 @@ function summarize(canvas) {
809
831
  };
810
832
  }
811
833
 
834
+ // src/core/public.ts
835
+ import fs4 from "fs";
836
+ import path4 from "path";
837
+ function publicFiles(dir) {
838
+ const files = [];
839
+ let root;
840
+ try {
841
+ root = fs4.realpathSync(dir);
842
+ } catch {
843
+ return files;
844
+ }
845
+ const visit = (current) => {
846
+ let entries;
847
+ try {
848
+ entries = fs4.readdirSync(current, { withFileTypes: true });
849
+ } catch {
850
+ return;
851
+ }
852
+ for (const entry of entries) {
853
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
854
+ const full = path4.join(current, entry.name);
855
+ if (entry.isDirectory()) visit(full);
856
+ else if (entry.isFile()) files.push(toPosix(path4.relative(dir, full)));
857
+ else if (entry.isSymbolicLink()) {
858
+ try {
859
+ const target = fs4.realpathSync(full);
860
+ if (isInside(root, target) && fs4.statSync(target).isFile()) files.push(toPosix(path4.relative(dir, full)));
861
+ } catch {
862
+ }
863
+ }
864
+ }
865
+ };
866
+ visit(dir);
867
+ return files.sort();
868
+ }
869
+
812
870
  // src/core/system.ts
813
- import fs5 from "fs";
814
- import path5 from "path";
871
+ import fs6 from "fs";
872
+ import path6 from "path";
815
873
 
816
874
  // src/core/tokens.ts
817
- import fs4 from "fs";
818
- import path4 from "path";
875
+ import fs5 from "fs";
876
+ import path5 from "path";
819
877
  import postcss from "postcss";
820
878
  var NAMESPACES = [
821
879
  ["--color-", "color"],
@@ -974,12 +1032,12 @@ function readStylesheets(entry) {
974
1032
  const seen = /* @__PURE__ */ new Set();
975
1033
  const files = [];
976
1034
  const walk2 = (file) => {
977
- if (seen.has(file) || !fs4.existsSync(file)) return;
1035
+ if (seen.has(file) || !fs5.existsSync(file)) return;
978
1036
  seen.add(file);
979
- const css2 = fs4.readFileSync(file, "utf8");
1037
+ const css2 = fs5.readFileSync(file, "utf8");
980
1038
  const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
981
1039
  for (const spec of imports) {
982
- if (spec.startsWith("./") || spec.startsWith("../")) walk2(path4.resolve(path4.dirname(file), spec));
1040
+ if (spec.startsWith("./") || spec.startsWith("../")) walk2(path5.resolve(path5.dirname(file), spec));
983
1041
  }
984
1042
  files.push({ file, css: css2 });
985
1043
  };
@@ -1115,11 +1173,11 @@ var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
1115
1173
  var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
1116
1174
  var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
1117
1175
  function listFiles(dir, deep = false) {
1118
- if (!fs5.existsSync(dir)) return [];
1176
+ if (!fs6.existsSync(dir)) return [];
1119
1177
  const out = [];
1120
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1178
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1121
1179
  if (entry.name.startsWith(".")) continue;
1122
- const full = path5.join(dir, entry.name);
1180
+ const full = path6.join(dir, entry.name);
1123
1181
  if (entry.isDirectory()) {
1124
1182
  if (deep) out.push(...listFiles(full, true));
1125
1183
  } else out.push(full);
@@ -1127,16 +1185,16 @@ function listFiles(dir, deep = false) {
1127
1185
  return out.sort();
1128
1186
  }
1129
1187
  function guessSources(componentsDir, stem) {
1130
- if (!fs5.existsSync(componentsDir)) return [];
1188
+ if (!fs6.existsSync(componentsDir)) return [];
1131
1189
  const wanted = slugify(stem);
1132
1190
  for (const file of listFiles(componentsDir)) {
1133
- const ext = path5.extname(file);
1134
- if (COMPONENT_EXT.includes(ext) && slugify(path5.basename(file, ext)) === wanted) return [file];
1191
+ const ext = path6.extname(file);
1192
+ if (COMPONENT_EXT.includes(ext) && slugify(path6.basename(file, ext)) === wanted) return [file];
1135
1193
  }
1136
- for (const entry of fs5.readdirSync(componentsDir, { withFileTypes: true })) {
1194
+ for (const entry of fs6.readdirSync(componentsDir, { withFileTypes: true })) {
1137
1195
  if (entry.isDirectory() && slugify(entry.name) === wanted) {
1138
- const files = listFiles(path5.join(componentsDir, entry.name)).filter(
1139
- (f) => COMPONENT_EXT.includes(path5.extname(f))
1196
+ const files = listFiles(path6.join(componentsDir, entry.name)).filter(
1197
+ (f) => COMPONENT_EXT.includes(path6.extname(f))
1140
1198
  );
1141
1199
  if (files.length) return files;
1142
1200
  }
@@ -1144,14 +1202,14 @@ function guessSources(componentsDir, stem) {
1144
1202
  return [];
1145
1203
  }
1146
1204
  function loadSystem(paths, urls) {
1147
- if (!fs5.existsSync(paths.system)) return null;
1205
+ if (!fs6.existsSync(paths.system)) return null;
1148
1206
  const issues = [];
1149
- const configFile = path5.join(paths.system, "system.json");
1207
+ const configFile = path6.join(paths.system, "system.json");
1150
1208
  const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
1151
1209
  let config = null;
1152
- if (fs5.existsSync(configFile)) {
1210
+ if (fs6.existsSync(configFile)) {
1153
1211
  try {
1154
- const parsed = SystemConfigSchema.safeParse(JSON.parse(fs5.readFileSync(configFile, "utf8")));
1212
+ const parsed = SystemConfigSchema.safeParse(JSON.parse(fs6.readFileSync(configFile, "utf8")));
1155
1213
  if (parsed.success) config = parsed.data;
1156
1214
  else for (const issue of describeZodError(parsed.error)) report("error", configFile, issue.message, issue.at);
1157
1215
  } catch (error) {
@@ -1160,36 +1218,36 @@ function loadSystem(paths, urls) {
1160
1218
  } else {
1161
1219
  report("warning", configFile, 'system.json is missing; run `design system init` or add { "name": "\u2026" }');
1162
1220
  }
1163
- const tokensFile = path5.join(paths.system, "tokens.css");
1221
+ const tokensFile = path6.join(paths.system, "tokens.css");
1164
1222
  let stylesheet = null;
1165
1223
  if (config?.stylesheet) {
1166
- stylesheet = path5.resolve(paths.system, config.stylesheet);
1167
- if (!fs5.existsSync(stylesheet)) {
1224
+ stylesheet = path6.resolve(paths.system, config.stylesheet);
1225
+ if (!fs6.existsSync(stylesheet)) {
1168
1226
  report("error", configFile, `stylesheet not found: ${config.stylesheet}`, "stylesheet");
1169
1227
  stylesheet = null;
1170
1228
  }
1171
1229
  }
1172
- const tokenEntry = stylesheet ?? (fs5.existsSync(tokensFile) ? tokensFile : null);
1230
+ const tokenEntry = stylesheet ?? (fs6.existsSync(tokensFile) ? tokensFile : null);
1173
1231
  const tokens = tokenEntry ? parseTokens(readStylesheets(tokenEntry)) : [];
1174
1232
  if (!tokenEntry) report("warning", tokensFile, "no tokens.css: screens get the default Tailwind theme");
1175
- const specimensDir = path5.join(paths.system, "specimens");
1176
- const componentsDir = path5.join(paths.system, "components");
1233
+ const specimensDir = path6.join(paths.system, "specimens");
1234
+ const componentsDir = path6.join(paths.system, "components");
1177
1235
  const components = [];
1178
1236
  const specimens = [];
1179
1237
  const usedIds = /* @__PURE__ */ new Set();
1180
1238
  for (const file of listFiles(specimensDir)) {
1181
- const ext = path5.extname(file).toLowerCase();
1239
+ const ext = path6.extname(file).toLowerCase();
1182
1240
  if (!SPECIMEN_EXT.has(ext)) continue;
1183
- const stem = path5.basename(file, ext);
1241
+ const stem = path6.basename(file, ext);
1184
1242
  let id2 = slugify(stem);
1185
1243
  if (usedIds.has(id2)) id2 = `${id2}-${usedIds.size + 1}`;
1186
1244
  usedIds.add(id2);
1187
- const tags = parseDocTags(fs5.readFileSync(file, "utf8"));
1188
- const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path5.resolve(path5.dirname(file), value));
1245
+ const tags = parseDocTags(fs6.readFileSync(file, "utf8"));
1246
+ const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path6.resolve(path6.dirname(file), value));
1189
1247
  for (const source of sources) {
1190
- if (!fs5.existsSync(source)) report("warning", file, `@source not found: ${path5.relative(paths.system, source)}`);
1248
+ if (!fs6.existsSync(source)) report("warning", file, `@source not found: ${path6.relative(paths.system, source)}`);
1191
1249
  }
1192
- const found = sources.filter((source) => fs5.existsSync(source));
1250
+ const found = sources.filter((source) => fs6.existsSync(source));
1193
1251
  const title = tags.title?.[0] || titleize(stem);
1194
1252
  const format = ext === ".html" ? "html" : "module";
1195
1253
  components.push({
@@ -1215,10 +1273,10 @@ function loadSystem(paths, urls) {
1215
1273
  });
1216
1274
  }
1217
1275
  const guidelines = [];
1218
- for (const file of listFiles(path5.join(paths.system, "guidelines"))) {
1219
- if (path5.extname(file).toLowerCase() !== ".md") continue;
1220
- const stem = path5.basename(file, ".md");
1221
- const { data, body } = parseFrontmatter(fs5.readFileSync(file, "utf8"));
1276
+ for (const file of listFiles(path6.join(paths.system, "guidelines"))) {
1277
+ if (path6.extname(file).toLowerCase() !== ".md") continue;
1278
+ const stem = path6.basename(file, ".md");
1279
+ const { data, body } = parseFrontmatter(fs6.readFileSync(file, "utf8"));
1222
1280
  const prefix = /^(\d+)[-_. ]+(.*)$/.exec(stem);
1223
1281
  const order = data.order !== void 0 ? Number(data.order) : prefix ? Number(prefix[1]) : 1e3;
1224
1282
  guidelines.push({
@@ -1230,14 +1288,14 @@ function loadSystem(paths, urls) {
1230
1288
  }
1231
1289
  guidelines.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
1232
1290
  const assets = [];
1233
- const assetsDir = path5.join(paths.system, "assets");
1291
+ const assetsDir = path6.join(paths.system, "assets");
1234
1292
  for (const file of listFiles(assetsDir, true)) {
1235
- const ext = path5.extname(file).toLowerCase();
1293
+ const ext = path6.extname(file).toLowerCase();
1236
1294
  assets.push({
1237
- name: path5.basename(file),
1238
- path: path5.relative(paths.system, file).split(path5.sep).join("/"),
1295
+ name: path6.basename(file),
1296
+ path: path6.relative(paths.system, file).split(path6.sep).join("/"),
1239
1297
  url: urls.file(relToDesign(paths, file)),
1240
- size: fs5.statSync(file).size,
1298
+ size: fs6.statSync(file).size,
1241
1299
  kind: IMAGE_EXT.has(ext) ? "image" : FONT_EXT.has(ext) ? "font" : "other"
1242
1300
  });
1243
1301
  }
@@ -1273,7 +1331,7 @@ function loadSystem(paths, urls) {
1273
1331
  },
1274
1332
  config,
1275
1333
  stylesheet,
1276
- tokensFile: fs5.existsSync(tokensFile) ? tokensFile : null,
1334
+ tokensFile: fs6.existsSync(tokensFile) ? tokensFile : null,
1277
1335
  specimens
1278
1336
  };
1279
1337
  }
@@ -1289,6 +1347,14 @@ function summarizeSystem(system) {
1289
1347
  }
1290
1348
 
1291
1349
  // src/core/project.ts
1350
+ function publicProblem(root, value) {
1351
+ const dir = path7.resolve(root, value);
1352
+ if (!fs7.existsSync(dir) || !fs7.statSync(dir).isDirectory()) return `no folder "${value}"`;
1353
+ const real = fs7.realpathSync(dir);
1354
+ const realRoot = fs7.realpathSync(root);
1355
+ if (real === realRoot || !isInside(realRoot, real)) return `"${value}" is not a folder inside the project`;
1356
+ return null;
1357
+ }
1292
1358
  var DesignProject = class {
1293
1359
  constructor(paths, urls, snapshots = null) {
1294
1360
  this.paths = paths;
@@ -1301,14 +1367,15 @@ var DesignProject = class {
1301
1367
  configCache = null;
1302
1368
  systemCache;
1303
1369
  canvasCache = /* @__PURE__ */ new Map();
1370
+ publicCache = null;
1304
1371
  config() {
1305
1372
  if (this.configCache) return this.configCache;
1306
1373
  const issues = [];
1307
1374
  let config = {};
1308
1375
  const file = this.paths.config;
1309
- if (fs6.existsSync(file)) {
1376
+ if (fs7.existsSync(file)) {
1310
1377
  try {
1311
- const parsed = DesignConfigSchema.safeParse(JSON.parse(fs6.readFileSync(file, "utf8")));
1378
+ const parsed = DesignConfigSchema.safeParse(JSON.parse(fs7.readFileSync(file, "utf8")));
1312
1379
  if (parsed.success) config = parsed.data;
1313
1380
  else
1314
1381
  for (const issue of describeZodError(parsed.error))
@@ -1321,18 +1388,44 @@ var DesignProject = class {
1321
1388
  });
1322
1389
  }
1323
1390
  }
1391
+ if (config.public !== void 0) {
1392
+ const problem = publicProblem(this.paths.root, config.public);
1393
+ if (problem) {
1394
+ issues.push({ severity: "error", file: relToRoot(this.paths, file), at: "public", message: problem });
1395
+ config = { ...config, public: void 0 };
1396
+ }
1397
+ }
1324
1398
  this.configCache = { config, issues };
1325
1399
  return this.configCache;
1326
1400
  }
1401
+ /** The public folder from design.json, when it is a folder inside the project. */
1402
+ publicDir() {
1403
+ const configured = this.config().config.public;
1404
+ return configured === void 0 ? null : path7.resolve(this.paths.root, configured);
1405
+ }
1406
+ /** The public folder's files, relative to it; `invalidatePublic` when they change. */
1407
+ publicFiles() {
1408
+ if (!this.publicCache) {
1409
+ const dir = this.publicDir();
1410
+ const list = dir ? publicFiles(dir) : [];
1411
+ this.publicCache = { list, set: new Set(list) };
1412
+ }
1413
+ return this.publicCache.list;
1414
+ }
1415
+ /** Whether `rel` is a file the public folder serves (dot-files and links out of it are not). */
1416
+ isPublicFile(rel) {
1417
+ this.publicFiles();
1418
+ return this.publicCache?.set.has(rel) ?? false;
1419
+ }
1327
1420
  name() {
1328
1421
  const configured = this.config().config.name;
1329
1422
  if (configured) return configured;
1330
1423
  try {
1331
- const manifest = JSON.parse(fs6.readFileSync(path6.join(this.paths.root, "package.json"), "utf8"));
1424
+ const manifest = JSON.parse(fs7.readFileSync(path7.join(this.paths.root, "package.json"), "utf8"));
1332
1425
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
1333
1426
  } catch {
1334
1427
  }
1335
- return path6.basename(this.paths.root);
1428
+ return path7.basename(this.paths.root);
1336
1429
  }
1337
1430
  system() {
1338
1431
  if (this.systemCache === void 0) this.systemCache = loadSystem(this.paths, this.urls);
@@ -1344,7 +1437,7 @@ var DesignProject = class {
1344
1437
  canvas(id2) {
1345
1438
  const cached = this.canvasCache.get(id2);
1346
1439
  if (cached) return cached;
1347
- if (!fs6.existsSync(path6.join(this.paths.canvases, id2))) return null;
1440
+ if (!fs7.existsSync(path7.join(this.paths.canvases, id2))) return null;
1348
1441
  const canvas = loadCanvas(this.paths, id2, this.urls, this.snapshots);
1349
1442
  this.canvasCache.set(id2, canvas);
1350
1443
  return canvas;
@@ -1382,6 +1475,10 @@ var DesignProject = class {
1382
1475
  }
1383
1476
  invalidateConfig() {
1384
1477
  this.configCache = null;
1478
+ this.publicCache = null;
1479
+ }
1480
+ invalidatePublic() {
1481
+ this.publicCache = null;
1385
1482
  }
1386
1483
  invalidateSystem() {
1387
1484
  this.systemCache = void 0;
@@ -1411,21 +1508,21 @@ var STATIC_URLS = {
1411
1508
  };
1412
1509
 
1413
1510
  // src/server/entries.ts
1414
- import fs7 from "fs";
1415
- import path7 from "path";
1511
+ import fs8 from "fs";
1512
+ import path8 from "path";
1416
1513
  var SYSTEM_CSS = "system.css";
1417
1514
  var HTML_BOOT = "html-boot.js";
1418
1515
  function writeIfChanged(file, content) {
1419
1516
  try {
1420
- if (fs7.readFileSync(file, "utf8") === content) return false;
1517
+ if (fs8.readFileSync(file, "utf8") === content) return false;
1421
1518
  } catch {
1422
1519
  }
1423
- fs7.mkdirSync(path7.dirname(file), { recursive: true });
1424
- fs7.writeFileSync(file, content);
1520
+ fs8.mkdirSync(path8.dirname(file), { recursive: true });
1521
+ fs8.writeFileSync(file, content);
1425
1522
  return true;
1426
1523
  }
1427
1524
  function importPath(fromDir, file) {
1428
- const rel = toPosix(path7.relative(fromDir, file));
1525
+ const rel = toPosix(path8.relative(fromDir, file));
1429
1526
  return rel.startsWith(".") ? rel : `./${rel}`;
1430
1527
  }
1431
1528
  var css = (file) => JSON.stringify(toPosix(file));
@@ -1438,10 +1535,10 @@ var Entries = class {
1438
1535
  return this.project.paths.entries;
1439
1536
  }
1440
1537
  systemCssFile() {
1441
- return path7.join(this.dir, SYSTEM_CSS);
1538
+ return path8.join(this.dir, SYSTEM_CSS);
1442
1539
  }
1443
1540
  entryFile(source) {
1444
- return path7.join(this.dir, source.canvas, `${source.id}.js`);
1541
+ return path8.join(this.dir, source.canvas, `${source.id}.js`);
1445
1542
  }
1446
1543
  /** Tailwind with the design tokens, or the stylesheet system.json names; scans every file in `.design`. */
1447
1544
  systemCss() {
@@ -1458,14 +1555,14 @@ var Entries = class {
1458
1555
  if (system?.tokensFile) lines.push(`@import ${css(system.tokensFile)};`);
1459
1556
  }
1460
1557
  lines.push(`@source ${css(paths.design)};`, `@source not ${css(paths.cache)};`);
1461
- lines.push(`@source not ${css(path7.join(paths.design, "node_modules"))};`);
1462
- for (const extra of config.sources ?? []) lines.push(`@source ${css(path7.resolve(paths.root, extra))};`);
1558
+ lines.push(`@source not ${css(path8.join(paths.design, "node_modules"))};`);
1559
+ for (const extra of config.sources ?? []) lines.push(`@source ${css(path8.resolve(paths.root, extra))};`);
1463
1560
  return `${lines.join("\n")}
1464
1561
  `;
1465
1562
  }
1466
1563
  entrySource(source) {
1467
1564
  const file = this.entryFile(source);
1468
- const dir = path7.dirname(file);
1565
+ const dir = path8.dirname(file);
1469
1566
  const lines = [];
1470
1567
  if (source.system) lines.push(`import ${JSON.stringify(importPath(dir, this.systemCssFile()))}`);
1471
1568
  if (source.builtin === "typography") {
@@ -1480,7 +1577,7 @@ var Entries = class {
1480
1577
  `import * as screen from ${JSON.stringify(importPath(dir, source.file))}`,
1481
1578
  "",
1482
1579
  `const Screen = screen.default ?? Object.values(screen).find((value) => typeof value === 'function')`,
1483
- "if (!Screen) throw new Error(" + JSON.stringify(`${path7.basename(source.file)} has no component to render: export one as default`) + ")",
1580
+ "if (!Screen) throw new Error(" + JSON.stringify(`${path8.basename(source.file)} has no component to render: export one as default`) + ")",
1484
1581
  "hmr(import.meta.hot)",
1485
1582
  "mount(Screen)"
1486
1583
  );
@@ -1493,14 +1590,14 @@ ${lines.join("\n")}
1493
1590
  sync() {
1494
1591
  writeIfChanged(this.systemCssFile(), this.systemCss());
1495
1592
  writeIfChanged(
1496
- path7.join(this.dir, HTML_BOOT),
1593
+ path8.join(this.dir, HTML_BOOT),
1497
1594
  `// Generated by foss-design.
1498
1595
  import { boot, hmr } from '@design/runtime'
1499
1596
  boot()
1500
1597
  hmr(import.meta.hot)
1501
1598
  `
1502
1599
  );
1503
- const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path7.join(this.dir, HTML_BOOT)]);
1600
+ const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path8.join(this.dir, HTML_BOOT)]);
1504
1601
  for (const source of this.project.sources()) {
1505
1602
  if (source.format !== "module") continue;
1506
1603
  const file = this.entryFile(source);
@@ -1509,18 +1606,27 @@ hmr(import.meta.hot)
1509
1606
  }
1510
1607
  this.prune(this.dir, wanted);
1511
1608
  }
1512
- /** Entry files, for dependency pre-bundling. */
1609
+ /**
1610
+ * What the dev server scans for dependencies before any frame loads: the generated entries,
1611
+ * the HTML screens and the boot they load. A dependency found only once frames load makes
1612
+ * Vite bundle again and fail the requests in flight (504 Outdated Optimize Dep).
1613
+ */
1513
1614
  files() {
1514
- return this.project.sources().filter((source) => source.format === "module").map((source) => this.entryFile(source));
1615
+ const files = [path8.join(this.dir, HTML_BOOT)];
1616
+ for (const source of this.project.sources()) {
1617
+ if (source.format === "module") files.push(this.entryFile(source));
1618
+ else if (!source.builtin && fs8.existsSync(source.file)) files.push(source.file);
1619
+ }
1620
+ return files;
1515
1621
  }
1516
1622
  prune(dir, wanted) {
1517
- if (!fs7.existsSync(dir)) return;
1518
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1519
- const full = path7.join(dir, entry.name);
1623
+ if (!fs8.existsSync(dir)) return;
1624
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
1625
+ const full = path8.join(dir, entry.name);
1520
1626
  if (entry.isDirectory()) {
1521
1627
  this.prune(full, wanted);
1522
- if (fs7.readdirSync(full).length === 0) fs7.rmdirSync(full);
1523
- } else if (!wanted.has(full)) fs7.rmSync(full);
1628
+ if (fs8.readdirSync(full).length === 0) fs8.rmdirSync(full);
1629
+ } else if (!wanted.has(full)) fs8.rmSync(full);
1524
1630
  }
1525
1631
  }
1526
1632
  };
@@ -1536,7 +1642,8 @@ function headTags(head) {
1536
1642
  props: source.props,
1537
1643
  theme: source.theme,
1538
1644
  snapshots: head.snapshots,
1539
- autoHeight: head.autoHeight
1645
+ autoHeight: head.autoHeight,
1646
+ public: head.public
1540
1647
  };
1541
1648
  const fallback = source.theme ?? head.theme;
1542
1649
  const theme2 = `(function(){var q=new URLSearchParams(location.search).get('theme');var t=${JSON.stringify(source.theme ?? null)}||(q==='dark'||q==='light'?q:${JSON.stringify(fallback)});var r=document.documentElement;r.dataset.theme=t;r.classList.toggle('dark',t==='dark');r.style.colorScheme=t})()`;
@@ -1587,68 +1694,68 @@ ${html}
1587
1694
  }
1588
1695
 
1589
1696
  // src/server/links.ts
1590
- import fs8 from "fs";
1591
- import path8 from "path";
1697
+ import fs9 from "fs";
1698
+ import path9 from "path";
1592
1699
  var SHIPPED_PACKAGES = ["react", "react-dom", "motion", "lucide-react", "clsx", "tailwind-merge"];
1593
1700
  function findPackageDir(fromDir, name, skip) {
1594
- let dir = path8.resolve(fromDir);
1701
+ let dir = path9.resolve(fromDir);
1595
1702
  for (; ; ) {
1596
- const modules = path8.join(dir, "node_modules");
1597
- const candidate = path8.join(modules, name);
1598
- if (modules !== skip && fs8.existsSync(path8.join(candidate, "package.json"))) return candidate;
1599
- const parent = path8.dirname(dir);
1703
+ const modules = path9.join(dir, "node_modules");
1704
+ const candidate = path9.join(modules, name);
1705
+ if (modules !== skip && fs9.existsSync(path9.join(candidate, "package.json"))) return candidate;
1706
+ const parent = path9.dirname(dir);
1600
1707
  if (parent === dir) return null;
1601
1708
  dir = parent;
1602
1709
  }
1603
1710
  }
1604
1711
  function isLink(file) {
1605
1712
  try {
1606
- return fs8.lstatSync(file).isSymbolicLink();
1713
+ return fs9.lstatSync(file).isSymbolicLink();
1607
1714
  } catch {
1608
1715
  return false;
1609
1716
  }
1610
1717
  }
1611
1718
  function linkShippedPackages(paths) {
1612
- const modules = path8.join(paths.design, "node_modules");
1719
+ const modules = path9.join(paths.design, "node_modules");
1613
1720
  const linked = [];
1614
1721
  const allow = /* @__PURE__ */ new Set();
1615
1722
  for (const name of SHIPPED_PACKAGES) {
1616
- const link = path8.join(modules, name);
1617
- const placedByUser = fs8.existsSync(link) && !isLink(link);
1723
+ const link = path9.join(modules, name);
1724
+ const placedByUser = fs9.existsSync(link) && !isLink(link);
1618
1725
  const own = placedByUser ? link : findPackageDir(paths.design, name, modules);
1619
1726
  if (own) {
1620
- allow.add(path8.dirname(fs8.realpathSync(own)));
1621
- if (isLink(link)) fs8.unlinkSync(link);
1727
+ allow.add(path9.dirname(fs9.realpathSync(own)));
1728
+ if (isLink(link)) fs9.unlinkSync(link);
1622
1729
  continue;
1623
1730
  }
1624
1731
  const shipped = findPackageDir(PKG_ROOT, name);
1625
1732
  if (!shipped) continue;
1626
- const target2 = fs8.realpathSync(shipped);
1627
- allow.add(path8.dirname(target2));
1733
+ const target2 = fs9.realpathSync(shipped);
1734
+ allow.add(path9.dirname(target2));
1628
1735
  let current2 = null;
1629
1736
  try {
1630
- current2 = fs8.realpathSync(link);
1737
+ current2 = fs9.realpathSync(link);
1631
1738
  } catch {
1632
1739
  }
1633
1740
  if (current2 !== target2) {
1634
- if (isLink(link)) fs8.unlinkSync(link);
1635
- fs8.mkdirSync(path8.dirname(link), { recursive: true });
1636
- fs8.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
1741
+ if (isLink(link)) fs9.unlinkSync(link);
1742
+ fs9.mkdirSync(path9.dirname(link), { recursive: true });
1743
+ fs9.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
1637
1744
  }
1638
1745
  linked.push(name);
1639
1746
  }
1640
- const self = path8.join(modules, "foss-design");
1641
- const target = fs8.realpathSync(PKG_ROOT);
1747
+ const self = path9.join(modules, "foss-design");
1748
+ const target = fs9.realpathSync(PKG_ROOT);
1642
1749
  let current = null;
1643
1750
  try {
1644
- current = fs8.realpathSync(self);
1751
+ current = fs9.realpathSync(self);
1645
1752
  } catch {
1646
1753
  }
1647
1754
  if (current !== target) {
1648
- if (isLink(self)) fs8.unlinkSync(self);
1649
- if (!fs8.existsSync(self)) {
1650
- fs8.mkdirSync(modules, { recursive: true });
1651
- fs8.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1755
+ if (isLink(self)) fs9.unlinkSync(self);
1756
+ if (!fs9.existsSync(self)) {
1757
+ fs9.mkdirSync(modules, { recursive: true });
1758
+ fs9.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1652
1759
  }
1653
1760
  }
1654
1761
  allow.add(target);
@@ -1656,7 +1763,9 @@ function linkShippedPackages(paths) {
1656
1763
  }
1657
1764
 
1658
1765
  // src/server/vite.ts
1659
- import path9 from "path";
1766
+ import { createHash as createHash2 } from "crypto";
1767
+ import fs10 from "fs";
1768
+ import path10 from "path";
1660
1769
  import tailwindcss from "@tailwindcss/vite";
1661
1770
  import react from "@vitejs/plugin-react";
1662
1771
  var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1668,21 +1777,40 @@ function aliases(project) {
1668
1777
  { find: /^@system\//, replacement: `${toPosix(paths.system)}/` }
1669
1778
  ];
1670
1779
  for (const [key, target] of Object.entries(project.config().config.alias ?? {})) {
1671
- const replacement = toPosix(path9.resolve(paths.root, target));
1780
+ const replacement = toPosix(path10.resolve(paths.root, target));
1672
1781
  list.push(
1673
1782
  key.endsWith("/") ? { find: new RegExp(`^${escapeRegExp(key)}`), replacement: `${replacement.replace(/\/$/, "")}/` } : { find: new RegExp(`^${escapeRegExp(key)}(?=/|$)`), replacement }
1674
1783
  );
1675
1784
  }
1676
1785
  return list;
1677
1786
  }
1787
+ function dropStaleDepCache(paths) {
1788
+ const dir = path10.join(paths.cache, "vite");
1789
+ const stamp = path10.join(paths.cache, "vite-runtime");
1790
+ let runtime = "";
1791
+ try {
1792
+ runtime = createHash2("sha256").update(fs10.readFileSync(PKG.runtime)).digest("hex");
1793
+ } catch {
1794
+ }
1795
+ let seen = "";
1796
+ try {
1797
+ seen = fs10.readFileSync(stamp, "utf8");
1798
+ } catch {
1799
+ }
1800
+ if (seen === runtime) return;
1801
+ fs10.rmSync(dir, { recursive: true, force: true });
1802
+ fs10.mkdirSync(paths.cache, { recursive: true });
1803
+ fs10.writeFileSync(stamp, runtime);
1804
+ }
1678
1805
  function baseConfig(project) {
1679
1806
  const { paths } = project;
1680
1807
  return {
1681
1808
  configFile: false,
1682
1809
  envDir: false,
1683
1810
  root: paths.design,
1684
- cacheDir: path9.join(paths.cache, "vite"),
1685
- publicDir: false,
1811
+ cacheDir: path10.join(paths.cache, "vite"),
1812
+ // design.json `public`: served under the frames' base, so HTML and CSS paths such as `/logo.png` resolve.
1813
+ publicDir: project.publicDir() ?? false,
1686
1814
  logLevel: "warn",
1687
1815
  clearScreen: false,
1688
1816
  resolve: {
@@ -1700,7 +1828,7 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts",
1700
1828
  var BUNDLED_REF = /<(script|link|img|source|video|audio|image|use|input)\b[^>]*>/gi;
1701
1829
  var encodePath2 = (rel) => rel.split("/").map(encodeURIComponent).join("/");
1702
1830
  function relUrl(fromDir, file) {
1703
- const rel = toPosix(path10.relative(fromDir, file));
1831
+ const rel = toPosix(path11.relative(fromDir, file));
1704
1832
  return rel.startsWith(".") ? rel : `./${rel}`;
1705
1833
  }
1706
1834
  function staticSnapshots(paths) {
@@ -1710,7 +1838,7 @@ function staticSnapshots(paths) {
1710
1838
  let data = metaCache.get(canvas);
1711
1839
  if (!data) {
1712
1840
  try {
1713
- data = JSON.parse(fs9.readFileSync(path10.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1841
+ data = JSON.parse(fs11.readFileSync(path11.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1714
1842
  } catch {
1715
1843
  }
1716
1844
  data ??= {};
@@ -1724,8 +1852,8 @@ function staticSnapshots(paths) {
1724
1852
  const urls = {};
1725
1853
  const thumbs = {};
1726
1854
  for (const theme2 of THEMES) {
1727
- const file = path10.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
1728
- if (!entry[theme2] || !fs9.existsSync(file)) continue;
1855
+ const file = path11.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
1856
+ if (!entry[theme2] || !fs11.existsSync(file)) continue;
1729
1857
  const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
1730
1858
  copies.set(file, rel);
1731
1859
  urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
@@ -1749,29 +1877,29 @@ function rewriteRefs(html, originalDir, stagedDir) {
1749
1877
  (whole, attr, quote, value) => {
1750
1878
  if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
1751
1879
  const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
1752
- const target = path10.resolve(originalDir, decodeURIComponent(pathname));
1753
- if (!fs9.existsSync(target)) return whole;
1880
+ const target = path11.resolve(originalDir, decodeURIComponent(pathname));
1881
+ if (!fs11.existsSync(target)) return whole;
1754
1882
  return `${attr}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
1755
1883
  }
1756
1884
  );
1757
1885
  });
1758
1886
  }
1759
1887
  function copyTree(from, to, filter) {
1760
- if (!fs9.existsSync(from)) return;
1761
- for (const entry of fs9.readdirSync(from, { withFileTypes: true })) {
1888
+ if (!fs11.existsSync(from)) return;
1889
+ for (const entry of fs11.readdirSync(from, { withFileTypes: true })) {
1762
1890
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1763
- const source = path10.join(from, entry.name);
1764
- const target = path10.join(to, entry.name);
1891
+ const source = path11.join(from, entry.name);
1892
+ const target = path11.join(to, entry.name);
1765
1893
  if (entry.isDirectory()) copyTree(source, target, filter);
1766
- else if (filter(source) && !fs9.existsSync(target)) {
1767
- fs9.mkdirSync(path10.dirname(target), { recursive: true });
1768
- fs9.copyFileSync(source, target);
1894
+ else if (filter(source) && !fs11.existsSync(target)) {
1895
+ fs11.mkdirSync(path11.dirname(target), { recursive: true });
1896
+ fs11.copyFileSync(source, target);
1769
1897
  }
1770
1898
  }
1771
1899
  }
1772
1900
  function writeJson(file, value) {
1773
- fs9.mkdirSync(path10.dirname(file), { recursive: true });
1774
- fs9.writeFileSync(file, JSON.stringify(value));
1901
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
1902
+ fs11.writeFileSync(file, JSON.stringify(value));
1775
1903
  }
1776
1904
  async function buildSite(paths, out, options = {}) {
1777
1905
  const snapshots = staticSnapshots(paths);
@@ -1787,42 +1915,46 @@ async function buildSite(paths, out, options = {}) {
1787
1915
  const entries = new Entries(project);
1788
1916
  entries.sync();
1789
1917
  linkShippedPackages(paths);
1790
- const staging = path10.join(paths.cache, "build", "src");
1791
- fs9.rmSync(staging, { recursive: true, force: true });
1918
+ const publicDir = project.publicDir();
1919
+ const publicFiles2 = project.publicFiles();
1920
+ const staging = path11.join(paths.cache, "build", "src");
1921
+ fs11.rmSync(staging, { recursive: true, force: true });
1792
1922
  const fonts = loadedSystem?.doc.fonts ?? [];
1793
1923
  const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
1794
1924
  const inputs = [];
1795
1925
  const htmlScreens = [];
1796
1926
  for (const source of sources) {
1797
- if (!source.builtin && !fs9.existsSync(source.file)) continue;
1798
- const siteDir = path10.join("_s", source.canvas, source.id);
1799
- const dir = path10.join(staging, siteDir);
1800
- const file = path10.join(dir, "index.html");
1801
- fs9.mkdirSync(dir, { recursive: true });
1927
+ if (!source.builtin && !fs11.existsSync(source.file)) continue;
1928
+ const siteDir = path11.join("_s", source.canvas, source.id);
1929
+ const dir = path11.join(staging, siteDir);
1930
+ const file = path11.join(dir, "index.html");
1931
+ fs11.mkdirSync(dir, { recursive: true });
1802
1932
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
1803
1933
  const head = {
1804
1934
  source,
1805
1935
  theme: canvasTheme ?? "light",
1806
1936
  fonts,
1807
1937
  snapshots: false,
1808
- autoHeight: source.autoHeight ?? false
1938
+ autoHeight: source.autoHeight ?? false,
1939
+ // The public folder lands at the site root, which frames reach through `../`.
1940
+ public: publicFiles2.length ? { base: `${toPosix(path11.relative(dir, staging))}/`, files: publicFiles2 } : void 0
1809
1941
  };
1810
1942
  if (source.format === "module") {
1811
- fs9.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
1943
+ fs11.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
1812
1944
  } else {
1813
- const raw = rewriteRefs(fs9.readFileSync(source.file, "utf8"), path10.dirname(source.file), dir);
1945
+ const raw = rewriteRefs(fs11.readFileSync(source.file, "utf8"), path11.dirname(source.file), dir);
1814
1946
  const withSystem = source.system && !optsOutOfSystem(raw);
1815
1947
  const html = injectIntoHtml(raw, {
1816
1948
  ...head,
1817
1949
  systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
1818
- boot: relUrl(dir, path10.join(entries.dir, HTML_BOOT))
1950
+ boot: relUrl(dir, path11.join(entries.dir, HTML_BOOT))
1819
1951
  });
1820
- fs9.writeFileSync(file, html);
1952
+ fs11.writeFileSync(file, html);
1821
1953
  htmlScreens.push({ source, dir: siteDir });
1822
1954
  }
1823
1955
  inputs.push(file);
1824
1956
  }
1825
- fs9.mkdirSync(out, { recursive: true });
1957
+ fs11.mkdirSync(out, { recursive: true });
1826
1958
  if (inputs.length) {
1827
1959
  const base = baseConfig(project);
1828
1960
  await build({
@@ -1837,18 +1969,20 @@ async function buildSite(paths, out, options = {}) {
1837
1969
  target: "es2022",
1838
1970
  reportCompressedSize: false,
1839
1971
  chunkSizeWarningLimit: 8e3,
1972
+ // Copied below, without dot-files and links out of the folder, and after the site's own files.
1973
+ copyPublicDir: false,
1840
1974
  rolldownOptions: { input: inputs }
1841
1975
  }
1842
1976
  });
1843
1977
  } else {
1844
- fs9.rmSync(out, { recursive: true, force: true });
1845
- fs9.mkdirSync(out, { recursive: true });
1978
+ fs11.rmSync(out, { recursive: true, force: true });
1979
+ fs11.mkdirSync(out, { recursive: true });
1846
1980
  }
1847
1981
  for (const { source, dir } of htmlScreens) {
1848
1982
  copyTree(
1849
- path10.dirname(source.file),
1850
- path10.join(out, dir),
1851
- (file) => !SOURCE_EXT.has(path10.extname(file).toLowerCase())
1983
+ path11.dirname(source.file),
1984
+ path11.join(out, dir),
1985
+ (file) => !SOURCE_EXT.has(path11.extname(file).toLowerCase())
1852
1986
  );
1853
1987
  }
1854
1988
  const files = /* @__PURE__ */ new Set();
@@ -1860,51 +1994,59 @@ async function buildSite(paths, out, options = {}) {
1860
1994
  for (const asset of system?.doc.assets ?? []) files.add(asset.url);
1861
1995
  for (const url of files) {
1862
1996
  const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
1863
- const from = path10.join(paths.design, rel);
1864
- if (!fs9.existsSync(from)) continue;
1865
- const to = path10.join(out, "_f", rel);
1866
- fs9.mkdirSync(path10.dirname(to), { recursive: true });
1867
- fs9.copyFileSync(from, to);
1997
+ const from = path11.join(paths.design, rel);
1998
+ if (!fs11.existsSync(from)) continue;
1999
+ const to = path11.join(out, "_f", rel);
2000
+ fs11.mkdirSync(path11.dirname(to), { recursive: true });
2001
+ fs11.copyFileSync(from, to);
1868
2002
  }
1869
2003
  const info = project.info(true);
1870
2004
  const snapshotDirs = canvasIds.map((id2) => `_snap/${id2}/`);
1871
2005
  for (const [from, rel] of snapshots.copies) {
1872
2006
  if (!snapshotDirs.some((dir) => rel.startsWith(dir))) continue;
1873
- const to = path10.join(out, rel);
1874
- fs9.mkdirSync(path10.dirname(to), { recursive: true });
1875
- fs9.copyFileSync(from, to);
2007
+ const to = path11.join(out, rel);
2008
+ fs11.mkdirSync(path11.dirname(to), { recursive: true });
2009
+ fs11.copyFileSync(from, to);
1876
2010
  }
1877
2011
  if (!system) info.system = null;
1878
2012
  info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
1879
- writeJson(path10.join(out, "api", "project.json"), info);
1880
- for (const canvas of canvases) writeJson(path10.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
2013
+ writeJson(path11.join(out, "api", "project.json"), info);
2014
+ for (const canvas of canvases) writeJson(path11.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
1881
2015
  if (system) {
1882
- writeJson(path10.join(out, "api", "system.json"), system.doc);
2016
+ writeJson(path11.join(out, "api", "system.json"), system.doc);
1883
2017
  const texts = {};
1884
2018
  for (const component of system.doc.components) {
1885
2019
  for (const rel of [component.specimen, ...component.sources]) {
1886
- const file = path10.join(paths.root, rel);
1887
- if (!(rel in texts) && fs9.existsSync(file)) texts[rel] = fs9.readFileSync(file, "utf8");
2020
+ const file = path11.join(paths.root, rel);
2021
+ if (!(rel in texts) && fs11.existsSync(file)) texts[rel] = fs11.readFileSync(file, "utf8");
1888
2022
  }
1889
2023
  }
1890
- writeJson(path10.join(out, "api", "sources.json"), texts);
2024
+ writeJson(path11.join(out, "api", "sources.json"), texts);
1891
2025
  }
1892
- fs9.rmSync(staging, { recursive: true, force: true });
2026
+ fs11.rmSync(staging, { recursive: true, force: true });
1893
2027
  const errors = [
1894
2028
  ...only ? [] : project.config().issues,
1895
2029
  ...system?.doc.issues ?? [],
1896
2030
  ...canvases.flatMap((canvas) => canvas.doc.issues)
1897
2031
  ].filter((issue) => issue.severity === "error").length;
1898
2032
  const { absoluteAssets } = options.includeViewer === false ? { absoluteAssets: false } : copyViewer(PKG.viewer, out);
2033
+ if (publicDir) {
2034
+ for (const rel of publicFiles2) {
2035
+ const to = path11.join(out, rel);
2036
+ if (fs11.existsSync(to)) continue;
2037
+ fs11.mkdirSync(path11.dirname(to), { recursive: true });
2038
+ fs11.copyFileSync(path11.join(publicDir, rel), to);
2039
+ }
2040
+ }
1899
2041
  return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets };
1900
2042
  }
1901
2043
  function copyViewer(viewerDir, out) {
1902
2044
  copyTree(viewerDir, out, () => true);
1903
- const index = path10.join(out, "index.html");
1904
- const html = fs9.readFileSync(index, "utf8");
2045
+ const index = path11.join(out, "index.html");
2046
+ const html = fs11.readFileSync(index, "utf8");
1905
2047
  const flag2 = "<script>window.__DESIGN_STATIC__=true</script>";
1906
2048
  const head = /<head(\s[^>]*)?>/i.exec(html);
1907
- fs9.writeFileSync(
2049
+ fs11.writeFileSync(
1908
2050
  index,
1909
2051
  head ? html.slice(0, head.index + head[0].length) + flag2 + html.slice(head.index + head[0].length) : flag2 + html
1910
2052
  );
@@ -1986,8 +2128,8 @@ function progress() {
1986
2128
  // src/cli/commands/build.ts
1987
2129
  function pack(out) {
1988
2130
  const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
1989
- fs10.rmSync(archive, { force: true });
1990
- const entries = fs10.readdirSync(out);
2131
+ fs12.rmSync(archive, { force: true });
2132
+ const entries = fs12.readdirSync(out);
1991
2133
  const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
1992
2134
  stdio: ["ignore", "ignore", "pipe"],
1993
2135
  env: { ...process.env, COPYFILE_DISABLE: "1" }
@@ -1997,10 +2139,10 @@ function pack(out) {
1997
2139
  return archive;
1998
2140
  }
1999
2141
  async function runBuild(paths, canvases, options) {
2000
- if (!fs10.existsSync(path11.join(PKG.viewer, "index.html"))) {
2142
+ if (!fs12.existsSync(path12.join(PKG.viewer, "index.html"))) {
2001
2143
  throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
2002
2144
  }
2003
- const out = path11.resolve(options.out ?? path11.join(paths.cache, "site"));
2145
+ const out = path12.resolve(options.out ?? path12.join(paths.cache, "site"));
2004
2146
  if (out === paths.root || out === paths.design) throw new CliError(`Refusing to build into ${out}`);
2005
2147
  print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
2006
2148
  let result;
@@ -2024,9 +2166,9 @@ async function runBuild(paths, canvases, options) {
2024
2166
  import os from "os";
2025
2167
 
2026
2168
  // src/cloud/units.ts
2027
- import { createHash as createHash2 } from "crypto";
2028
- import fs11 from "fs";
2029
- import path12 from "path";
2169
+ import { createHash as createHash3 } from "crypto";
2170
+ import fs13 from "fs";
2171
+ import path13 from "path";
2030
2172
  var SYSTEM_UNIT = "system";
2031
2173
  var CANVAS_PREFIX = "canvas/";
2032
2174
  var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules"]);
@@ -2054,7 +2196,7 @@ function caseClashes(keys) {
2054
2196
  }
2055
2197
  function unitRoot(paths, key) {
2056
2198
  const id2 = unitCanvasId(key);
2057
- return id2 === null ? paths.design : path12.join(paths.canvases, id2);
2199
+ return id2 === null ? paths.design : path13.join(paths.canvases, id2);
2058
2200
  }
2059
2201
  function unitRelative(key, rel) {
2060
2202
  const id2 = unitCanvasId(key);
@@ -2069,10 +2211,10 @@ function isUnitPath(key, rel) {
2069
2211
  return id2 !== null && segments[0] === "canvas" && segments[1] === id2 && segments.length > 2;
2070
2212
  }
2071
2213
  function hashBuffer(data) {
2072
- return createHash2("sha256").update(data).digest("hex");
2214
+ return createHash3("sha256").update(data).digest("hex");
2073
2215
  }
2074
2216
  function hashFile(file) {
2075
- const data = fs11.readFileSync(file);
2217
+ const data = fs13.readFileSync(file);
2076
2218
  return { hash: hashBuffer(data), size: data.length };
2077
2219
  }
2078
2220
  var warnedLinks = /* @__PURE__ */ new Set();
@@ -2085,34 +2227,34 @@ function warnLink(rel) {
2085
2227
  function walk(dir, base, out) {
2086
2228
  let entries;
2087
2229
  try {
2088
- entries = fs11.readdirSync(dir, { withFileTypes: true });
2230
+ entries = fs13.readdirSync(dir, { withFileTypes: true });
2089
2231
  } catch {
2090
2232
  return;
2091
2233
  }
2092
2234
  for (const entry of entries) {
2093
2235
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2094
- const full = path12.join(dir, entry.name);
2236
+ const full = path13.join(dir, entry.name);
2095
2237
  let isFile = entry.isFile();
2096
2238
  let isDir = entry.isDirectory();
2097
2239
  if (entry.isSymbolicLink()) {
2098
2240
  isDir = false;
2099
2241
  isFile = false;
2100
2242
  try {
2101
- const target = fs11.realpathSync(full);
2102
- const root = fs11.realpathSync(base);
2103
- if (target.startsWith(root + path12.sep)) isFile = fs11.statSync(target).isFile();
2104
- else warnLink(toPosix(path12.relative(base, full)));
2243
+ const target = fs13.realpathSync(full);
2244
+ const root = fs13.realpathSync(base);
2245
+ if (target.startsWith(root + path13.sep)) isFile = fs13.statSync(target).isFile();
2246
+ else warnLink(toPosix(path13.relative(base, full)));
2105
2247
  } catch {
2106
2248
  }
2107
2249
  }
2108
2250
  if (isDir) walk(full, base, out);
2109
- else if (isFile) out[toPosix(path12.relative(base, full))] = hashFile(full);
2251
+ else if (isFile) out[toPosix(path13.relative(base, full))] = hashFile(full);
2110
2252
  }
2111
2253
  }
2112
2254
  function unitManifest(paths, key) {
2113
2255
  const manifest = {};
2114
2256
  if (key === SYSTEM_UNIT) {
2115
- if (fs11.existsSync(paths.config) && fs11.statSync(paths.config).isFile())
2257
+ if (fs13.existsSync(paths.config) && fs13.statSync(paths.config).isFile())
2116
2258
  manifest["design.json"] = hashFile(paths.config);
2117
2259
  walk(paths.system, paths.design, manifest);
2118
2260
  } else walk(unitRoot(paths, key), paths.design, manifest);
@@ -2121,13 +2263,13 @@ function unitManifest(paths, key) {
2121
2263
  function buildManifest(dir) {
2122
2264
  const manifest = {};
2123
2265
  const visit = (current) => {
2124
- for (const entry of fs11.readdirSync(current, { withFileTypes: true })) {
2125
- const full = path12.join(current, entry.name);
2266
+ for (const entry of fs13.readdirSync(current, { withFileTypes: true })) {
2267
+ const full = path13.join(current, entry.name);
2126
2268
  if (entry.isDirectory()) visit(full);
2127
- else if (entry.isFile()) manifest[toPosix(path12.relative(dir, full))] = hashFile(full);
2269
+ else if (entry.isFile()) manifest[toPosix(path13.relative(dir, full))] = hashFile(full);
2128
2270
  }
2129
2271
  };
2130
- if (fs11.existsSync(dir)) visit(dir);
2272
+ if (fs13.existsSync(dir)) visit(dir);
2131
2273
  return sortManifest(manifest);
2132
2274
  }
2133
2275
  function sortManifest(manifest) {
@@ -2136,18 +2278,18 @@ function sortManifest(manifest) {
2136
2278
  function scanLocal(paths) {
2137
2279
  const units = /* @__PURE__ */ new Map();
2138
2280
  const notSynced = [];
2139
- if (!fs11.existsSync(paths.design)) return { units, notSynced };
2281
+ if (!fs13.existsSync(paths.design)) return { units, notSynced };
2140
2282
  const system = unitManifest(paths, SYSTEM_UNIT);
2141
2283
  if (Object.keys(system).length) units.set(SYSTEM_UNIT, { key: SYSTEM_UNIT, kind: "system", manifest: system });
2142
- for (const entry of fs11.readdirSync(paths.design, { withFileTypes: true })) {
2284
+ for (const entry of fs13.readdirSync(paths.design, { withFileTypes: true })) {
2143
2285
  const { name } = entry;
2144
2286
  if (name.startsWith(".") || LOCAL_ONLY.has(name)) continue;
2145
2287
  if (name === "design.json" && entry.isFile()) continue;
2146
2288
  if ((name === "system" || name === "canvas") && entry.isDirectory()) continue;
2147
2289
  notSynced.push(entry.isDirectory() ? `${name}/` : name);
2148
2290
  }
2149
- if (fs11.existsSync(paths.canvases) && fs11.statSync(paths.canvases).isDirectory()) {
2150
- for (const entry of fs11.readdirSync(paths.canvases, { withFileTypes: true })) {
2291
+ if (fs13.existsSync(paths.canvases) && fs13.statSync(paths.canvases).isDirectory()) {
2292
+ for (const entry of fs13.readdirSync(paths.canvases, { withFileTypes: true })) {
2151
2293
  if (entry.name.startsWith(".")) continue;
2152
2294
  if (!entry.isDirectory() || !ID_PATTERN.test(entry.name)) {
2153
2295
  notSynced.push(`canvas/${entry.name}${entry.isDirectory() ? "/" : ""}`);
@@ -2161,11 +2303,11 @@ function scanLocal(paths) {
2161
2303
  return { units, notSynced: notSynced.sort() };
2162
2304
  }
2163
2305
  function unitBuildDir(paths, key) {
2164
- return path12.join(paths.cache, "cloud", "build", ...key.split("/"));
2306
+ return path13.join(paths.cache, "cloud", "build", ...key.split("/"));
2165
2307
  }
2166
2308
  async function buildUnit(paths, key, out = unitBuildDir(paths, key)) {
2167
2309
  const id2 = unitCanvasId(key);
2168
- fs11.rmSync(out, { recursive: true, force: true });
2310
+ fs13.rmSync(out, { recursive: true, force: true });
2169
2311
  return buildSite(paths, out, {
2170
2312
  canvases: id2 === null ? [] : [id2],
2171
2313
  includeSystem: id2 === null,
@@ -2296,7 +2438,7 @@ var CloudClient = class {
2296
2438
  }
2297
2439
  host;
2298
2440
  token;
2299
- async send(method, path26, init) {
2441
+ async send(method, path27, init) {
2300
2442
  const headers = {
2301
2443
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2302
2444
  Accept: "application/json"
@@ -2304,7 +2446,7 @@ var CloudClient = class {
2304
2446
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2305
2447
  if (init.type) headers["Content-Type"] = init.type;
2306
2448
  try {
2307
- return await fetch(`${this.host}/api${path26}`, {
2449
+ return await fetch(`${this.host}/api${path27}`, {
2308
2450
  method,
2309
2451
  headers,
2310
2452
  body: init.body,
@@ -2325,8 +2467,8 @@ var CloudClient = class {
2325
2467
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2326
2468
  }
2327
2469
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2328
- async request(method, path26, body) {
2329
- const response = await this.send(method, path26, {
2470
+ async request(method, path27, body) {
2471
+ const response = await this.send(method, path27, {
2330
2472
  body: body === void 0 ? void 0 : JSON.stringify(body),
2331
2473
  type: body === void 0 ? void 0 : "application/json",
2332
2474
  timeout: JSON_TIMEOUT
@@ -2338,7 +2480,7 @@ var CloudClient = class {
2338
2480
  try {
2339
2481
  return JSON.parse(text2);
2340
2482
  } catch {
2341
- throw new CloudError(`${this.host} answered ${method} ${path26} with something other than JSON`, 0, "bad_response");
2483
+ throw new CloudError(`${this.host} answered ${method} ${path27} with something other than JSON`, 0, "bad_response");
2342
2484
  }
2343
2485
  }
2344
2486
  /** POST /device/code. */
@@ -2472,9 +2614,9 @@ var CloudClient = class {
2472
2614
  };
2473
2615
 
2474
2616
  // src/cloud/credentials.ts
2475
- import fs12 from "fs";
2617
+ import fs14 from "fs";
2476
2618
  import os2 from "os";
2477
- import path13 from "path";
2619
+ import path14 from "path";
2478
2620
  var DEFAULT_CLOUD = "https://app.fossdesign.dev";
2479
2621
  function normalizeHost(value) {
2480
2622
  let url;
@@ -2491,14 +2633,14 @@ function cloudHost() {
2491
2633
  return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
2492
2634
  }
2493
2635
  function credentialsFile() {
2494
- const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path13.join(os2.homedir(), ".config");
2495
- return path13.join(base, "foss-design", "credentials.json");
2636
+ const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path14.join(os2.homedir(), ".config");
2637
+ return path14.join(base, "foss-design", "credentials.json");
2496
2638
  }
2497
2639
  function readAll() {
2498
2640
  const file = credentialsFile();
2499
- if (!fs12.existsSync(file)) return {};
2641
+ if (!fs14.existsSync(file)) return {};
2500
2642
  try {
2501
- const data = JSON.parse(fs12.readFileSync(file, "utf8"));
2643
+ const data = JSON.parse(fs14.readFileSync(file, "utf8"));
2502
2644
  return data && typeof data === "object" && !Array.isArray(data) ? data : {};
2503
2645
  } catch (error) {
2504
2646
  throw new CliError(`${file} is not valid JSON (${error.message}); fix or delete it`);
@@ -2506,12 +2648,12 @@ function readAll() {
2506
2648
  }
2507
2649
  function writeAll(data) {
2508
2650
  const file = credentialsFile();
2509
- fs12.mkdirSync(path13.dirname(file), { recursive: true, mode: 448 });
2651
+ fs14.mkdirSync(path14.dirname(file), { recursive: true, mode: 448 });
2510
2652
  const temp = `${file}.${process.pid}.tmp`;
2511
- fs12.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2653
+ fs14.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2512
2654
  `, { mode: 384 });
2513
- fs12.chmodSync(temp, 384);
2514
- fs12.renameSync(temp, file);
2655
+ fs14.chmodSync(temp, 384);
2656
+ fs14.renameSync(temp, file);
2515
2657
  }
2516
2658
  function getCredential(host) {
2517
2659
  const entry = readAll()[host];
@@ -2534,17 +2676,17 @@ function requireCredential(host) {
2534
2676
  }
2535
2677
 
2536
2678
  // src/cloud/state.ts
2537
- import fs13 from "fs";
2538
- import path14 from "path";
2539
- var linkFile = (paths) => path14.join(paths.design, "cloud.json");
2540
- var cloudCache = (paths) => path14.join(paths.cache, "cloud");
2541
- var incomingDir = (paths, key) => path14.join(cloudCache(paths), "incoming", ...key.split("/"));
2679
+ import fs15 from "fs";
2680
+ import path15 from "path";
2681
+ var linkFile = (paths) => path15.join(paths.design, "cloud.json");
2682
+ var cloudCache = (paths) => path15.join(paths.cache, "cloud");
2683
+ var incomingDir = (paths, key) => path15.join(cloudCache(paths), "incoming", ...key.split("/"));
2542
2684
  function readLink(paths) {
2543
2685
  const file = linkFile(paths);
2544
- if (!fs13.existsSync(file)) return null;
2686
+ if (!fs15.existsSync(file)) return null;
2545
2687
  let data;
2546
2688
  try {
2547
- data = JSON.parse(fs13.readFileSync(file, "utf8"));
2689
+ data = JSON.parse(fs15.readFileSync(file, "utf8"));
2548
2690
  } catch (error) {
2549
2691
  throw new CliError(`${file} is not valid JSON (${error.message}); run \`design link <project>\` again`);
2550
2692
  }
@@ -2574,9 +2716,9 @@ function writeLink(paths, link) {
2574
2716
  units: sorted(link.units),
2575
2717
  conflicts: sorted(link.conflicts)
2576
2718
  };
2577
- fs13.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2719
+ fs15.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2578
2720
  `);
2579
- fs13.renameSync(temp, file);
2721
+ fs15.renameSync(temp, file);
2580
2722
  }
2581
2723
  var hashes = (manifest) => Object.fromEntries(Object.entries(manifest).map(([file, entry]) => [file, entry.hash]));
2582
2724
  function diffFiles(a, b) {
@@ -2654,8 +2796,8 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
2654
2796
  var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
2655
2797
 
2656
2798
  // src/cloud/pull.ts
2657
- import fs14 from "fs";
2658
- import path15 from "path";
2799
+ import fs16 from "fs";
2800
+ import path16 from "path";
2659
2801
 
2660
2802
  // src/capture/frames.ts
2661
2803
  var AUTO_VIEWPORT_HEIGHT = 900;
@@ -2738,34 +2880,34 @@ function checkManifest(key, manifest) {
2738
2880
  }
2739
2881
  }
2740
2882
  function writeInside(root, rel, data) {
2741
- const file = path15.join(root, rel);
2883
+ const file = path16.join(root, rel);
2742
2884
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
2743
- fs14.mkdirSync(path15.dirname(file), { recursive: true });
2744
- if (!isInside(fs14.realpathSync(root), fs14.realpathSync(path15.dirname(file))))
2885
+ fs16.mkdirSync(path16.dirname(file), { recursive: true });
2886
+ if (!isInside(fs16.realpathSync(root), fs16.realpathSync(path16.dirname(file))))
2745
2887
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
2746
- if (fs14.existsSync(file) && fs14.lstatSync(file).isSymbolicLink()) fs14.unlinkSync(file);
2747
- fs14.writeFileSync(file, data);
2888
+ if (fs16.existsSync(file) && fs16.lstatSync(file).isSymbolicLink()) fs16.unlinkSync(file);
2889
+ fs16.writeFileSync(file, data);
2748
2890
  }
2749
2891
  function pruneEmpty(dir, stop) {
2750
2892
  let current = dir;
2751
2893
  while (isInside(stop, current) && current !== stop) {
2752
2894
  try {
2753
- if (fs14.readdirSync(current).length) return;
2754
- fs14.rmdirSync(current);
2895
+ if (fs16.readdirSync(current).length) return;
2896
+ fs16.rmdirSync(current);
2755
2897
  } catch {
2756
2898
  return;
2757
2899
  }
2758
- current = path15.dirname(current);
2900
+ current = path16.dirname(current);
2759
2901
  }
2760
2902
  }
2761
2903
  function applyUnit(paths, key, local, target, blobs) {
2762
2904
  const counts = { added: 0, changed: 0, removed: 0 };
2763
- const stop = key === SYSTEM_UNIT ? paths.design : path15.dirname(unitRoot(paths, key));
2905
+ const stop = key === SYSTEM_UNIT ? paths.design : path16.dirname(unitRoot(paths, key));
2764
2906
  for (const rel of Object.keys(local)) {
2765
2907
  if (rel in target) continue;
2766
- const file = path15.join(paths.design, rel);
2767
- fs14.rmSync(file, { force: true });
2768
- pruneEmpty(path15.dirname(file), stop);
2908
+ const file = path16.join(paths.design, rel);
2909
+ fs16.rmSync(file, { force: true });
2910
+ pruneEmpty(path16.dirname(file), stop);
2769
2911
  counts.removed++;
2770
2912
  }
2771
2913
  for (const [rel, entry] of Object.entries(target)) {
@@ -2828,7 +2970,7 @@ async function pull(ctx, options) {
2828
2970
  } else if (!s.ahead) {
2829
2971
  if (s.conflictRev !== void 0) {
2830
2972
  delete link.conflicts[key];
2831
- fs14.rmSync(incomingDir(paths, key), { recursive: true, force: true });
2973
+ fs16.rmSync(incomingDir(paths, key), { recursive: true, force: true });
2832
2974
  }
2833
2975
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
2834
2976
  } else if (!s.changed.length) {
@@ -2858,14 +3000,14 @@ async function pull(ctx, options) {
2858
3000
  headRev: unit.headRev,
2859
3001
  incoming: incomingDir(paths, key)
2860
3002
  };
2861
- entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path15.relative(paths.root, entry.incoming)}`;
2862
- if (s.conflictRev === unit.headRev && fs14.existsSync(entry.incoming)) report.units.push(entry);
3003
+ entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path16.relative(paths.root, entry.incoming)}`;
3004
+ if (s.conflictRev === unit.headRev && fs16.existsSync(entry.incoming)) report.units.push(entry);
2863
3005
  else plans.push({ key, kind: "incoming", remote: unit, report: entry });
2864
3006
  }
2865
3007
  }
2866
3008
  const byHash = /* @__PURE__ */ new Map();
2867
3009
  for (const unit of scan.units.values())
2868
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path15.join(paths.design, rel));
3010
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path16.join(paths.design, rel));
2869
3011
  const needed = /* @__PURE__ */ new Set();
2870
3012
  for (const plan of plans) {
2871
3013
  if (!plan.remote) continue;
@@ -2876,7 +3018,7 @@ async function pull(ctx, options) {
2876
3018
  const download = [];
2877
3019
  for (const hash of needed) {
2878
3020
  const file = byHash.get(hash);
2879
- if (file) blobs.set(hash, fs14.readFileSync(file));
3021
+ if (file) blobs.set(hash, fs16.readFileSync(file));
2880
3022
  else download.push(hash);
2881
3023
  }
2882
3024
  if (download.length) {
@@ -2902,11 +3044,11 @@ async function pull(ctx, options) {
2902
3044
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
2903
3045
  else delete link.units[key];
2904
3046
  delete link.conflicts[key];
2905
- fs14.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3047
+ fs16.rmSync(incomingDir(paths, key), { recursive: true, force: true });
2906
3048
  } else {
2907
3049
  const dir = incomingDir(paths, key);
2908
- fs14.rmSync(dir, { recursive: true, force: true });
2909
- fs14.mkdirSync(dir, { recursive: true });
3050
+ fs16.rmSync(dir, { recursive: true, force: true });
3051
+ fs16.mkdirSync(dir, { recursive: true });
2910
3052
  for (const [rel, file] of Object.entries(plan.remote.manifest))
2911
3053
  writeInside(dir, unitRelative(key, rel), blobs.get(file.hash));
2912
3054
  link.conflicts[key] = plan.remote.headRev;
@@ -2927,8 +3069,8 @@ async function pull(ctx, options) {
2927
3069
  }
2928
3070
 
2929
3071
  // src/cloud/push.ts
2930
- import fs15 from "fs";
2931
- import path16 from "path";
3072
+ import fs17 from "fs";
3073
+ import path17 from "path";
2932
3074
  function blockedReport(status, message) {
2933
3075
  return {
2934
3076
  unit: status.key,
@@ -3034,7 +3176,7 @@ async function push(ctx, options) {
3034
3176
  for (const key of toPush) {
3035
3177
  const s = status(key);
3036
3178
  if (s.conflictRev !== void 0) {
3037
- const dir = path16.relative(paths.root, incomingDir(paths, key));
3179
+ const dir = path17.relative(paths.root, incomingDir(paths, key));
3038
3180
  blocked.push(
3039
3181
  blockedReport(s, `pull left the cloud version in ${dir}; merge it, then \`design push --resolved ${key}\``)
3040
3182
  );
@@ -3078,7 +3220,7 @@ async function push(ctx, options) {
3078
3220
  const files = /* @__PURE__ */ new Map();
3079
3221
  const addFiles = (manifest, dir) => {
3080
3222
  for (const [rel, entry] of Object.entries(manifest))
3081
- if (!files.has(entry.hash)) files.set(entry.hash, path16.join(dir, rel));
3223
+ if (!files.has(entry.hash)) files.set(entry.hash, path17.join(dir, rel));
3082
3224
  };
3083
3225
  for (const key of pushing) {
3084
3226
  const id2 = unitCanvasId(key);
@@ -3110,7 +3252,7 @@ async function push(ctx, options) {
3110
3252
  if (missing.length) {
3111
3253
  const unknown = missing.find((hash) => !files.has(hash));
3112
3254
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
3113
- const bytes = missing.reduce((sum, hash) => sum + fs15.statSync(files.get(hash)).size, 0);
3255
+ const bytes = missing.reduce((sum, hash) => sum + fs17.statSync(files.get(hash)).size, 0);
3114
3256
  const bar = ctx.progress();
3115
3257
  const started = Date.now();
3116
3258
  let sentFiles = 0;
@@ -3119,12 +3261,12 @@ async function push(ctx, options) {
3119
3261
  bar.update(line());
3120
3262
  await mapLimit(missing, TRANSFERS, async (hash) => {
3121
3263
  const file = files.get(hash);
3122
- const data = fs15.readFileSync(file);
3264
+ const data = fs17.readFileSync(file);
3123
3265
  try {
3124
3266
  await client.putBlob(link.project, hash, data);
3125
3267
  } catch (error) {
3126
3268
  if (error instanceof CloudError && error.status === 413)
3127
- throw new CliError(`${path16.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
3269
+ throw new CliError(`${path17.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
3128
3270
  throw error;
3129
3271
  }
3130
3272
  sentFiles++;
@@ -3146,7 +3288,7 @@ async function push(ctx, options) {
3146
3288
  url: webUrl(link, unit.key)
3147
3289
  });
3148
3290
  }
3149
- for (const key of resolved) fs15.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3291
+ for (const key of resolved) fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3150
3292
  writeLink(paths, link);
3151
3293
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
3152
3294
  return report;
@@ -3156,8 +3298,9 @@ function refuseLegacyGo(paths, keys) {
3156
3298
  const hits = /* @__PURE__ */ new Set();
3157
3299
  for (const key of keys) {
3158
3300
  if (!key.startsWith("canvas/")) continue;
3159
- for (const screen of project.canvas(key.slice("canvas/".length))?.screens ?? []) {
3160
- for (const line of legacyGoLines(screen.file)) hits.add(`${relToRoot(paths, screen.file)}:${line}`);
3301
+ const id2 = key.slice("canvas/".length);
3302
+ for (const file of canvasCodeFiles(path17.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
3303
+ for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
3161
3304
  }
3162
3305
  }
3163
3306
  if (!hits.size) return;
@@ -3477,7 +3620,7 @@ async function runMe(paths, options) {
3477
3620
  }
3478
3621
 
3479
3622
  // src/capture/chrome.ts
3480
- import fs16 from "fs";
3623
+ import fs18 from "fs";
3481
3624
  var CANDIDATES = {
3482
3625
  darwin: [
3483
3626
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3501,8 +3644,8 @@ var CANDIDATES = {
3501
3644
  };
3502
3645
  function findChrome() {
3503
3646
  const configured = process.env.DESIGN_CHROME;
3504
- if (configured && fs16.existsSync(configured)) return configured;
3505
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs16.existsSync(candidate)) return candidate;
3647
+ if (configured && fs18.existsSync(configured)) return configured;
3648
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs18.existsSync(candidate)) return candidate;
3506
3649
  return null;
3507
3650
  }
3508
3651
  async function launchChrome() {
@@ -3515,22 +3658,40 @@ async function launchChrome() {
3515
3658
  const { chromium } = await import("playwright-core");
3516
3659
  return chromium.launch({ executablePath, headless: true, args: ["--hide-scrollbars", "--force-color-profile=srgb"] });
3517
3660
  }
3661
+ var OUTDATED_DEP = /Outdated Optimize Dep/;
3518
3662
  async function openFrame(page, url, options = {}) {
3519
- const errors = [];
3520
- page.on("pageerror", (error) => errors.push(error.message));
3663
+ let errors = [];
3664
+ let outdated = () => {
3665
+ };
3666
+ const record2 = (text2) => {
3667
+ errors.push(text2);
3668
+ if (OUTDATED_DEP.test(text2)) outdated();
3669
+ };
3670
+ page.on("pageerror", (error) => record2(error.message));
3521
3671
  page.on("console", (message) => {
3522
- if (message.type() === "error") errors.push(message.text().replace(/^(?:\s*%[osdifcO])+\s*/, ""));
3672
+ if (message.type() !== "error") return;
3673
+ let text2 = message.text().replace(/^(?:\s*%[osdifcO])+\s*/, "");
3674
+ const where = message.location().url;
3675
+ if (/^Failed to load resource/.test(text2) && where && !text2.includes(where)) text2 += ` (${where})`;
3676
+ record2(text2);
3523
3677
  });
3524
- await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3525
- let ready = options.waitForReady === false;
3526
- if (!ready) {
3527
- try {
3528
- await page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
3678
+ let ready = false;
3679
+ for (let attempt = 0; attempt < 2; attempt++) {
3680
+ errors = [];
3681
+ ready = options.waitForReady === false;
3682
+ const turnedAway = new Promise((resolve) => {
3683
+ outdated = () => resolve("outdated");
3684
+ });
3685
+ await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3686
+ if (!ready) {
3687
+ const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
3529
3688
  timeout: options.timeoutMs ?? 2e4
3689
+ }).then(() => "ready");
3690
+ signal.catch(() => {
3530
3691
  });
3531
- ready = true;
3532
- } catch {
3692
+ ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
3533
3693
  }
3694
+ if (!errors.some((error) => OUTDATED_DEP.test(error))) break;
3534
3695
  }
3535
3696
  await page.waitForTimeout(options.settleMs ?? 700);
3536
3697
  const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
@@ -3547,26 +3708,26 @@ async function openFrame(page, url, options = {}) {
3547
3708
  }
3548
3709
 
3549
3710
  // src/server/state.ts
3550
- import fs17 from "fs";
3551
- import path17 from "path";
3552
- var stateFile = (paths) => path17.join(paths.cache, "server.json");
3553
- var logFile = (paths) => path17.join(paths.cache, "server.log");
3711
+ import fs19 from "fs";
3712
+ import path18 from "path";
3713
+ var stateFile = (paths) => path18.join(paths.cache, "server.json");
3714
+ var logFile = (paths) => path18.join(paths.cache, "server.log");
3554
3715
  function readState(paths) {
3555
3716
  try {
3556
- return JSON.parse(fs17.readFileSync(stateFile(paths), "utf8"));
3717
+ return JSON.parse(fs19.readFileSync(stateFile(paths), "utf8"));
3557
3718
  } catch {
3558
3719
  return null;
3559
3720
  }
3560
3721
  }
3561
3722
  function writeState(paths, state) {
3562
- fs17.mkdirSync(paths.cache, { recursive: true });
3563
- fs17.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
3723
+ fs19.mkdirSync(paths.cache, { recursive: true });
3724
+ fs19.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
3564
3725
  `);
3565
3726
  }
3566
3727
  function clearState(paths, pid) {
3567
3728
  const state = readState(paths);
3568
3729
  if (pid !== void 0 && state && state.pid !== pid) return;
3569
- fs17.rmSync(stateFile(paths), { force: true });
3730
+ fs19.rmSync(stateFile(paths), { force: true });
3570
3731
  }
3571
3732
  function pidAlive(pid) {
3572
3733
  try {
@@ -3591,18 +3752,18 @@ async function liveServer(paths) {
3591
3752
 
3592
3753
  // src/cli/commands/preview.ts
3593
3754
  import { spawn } from "child_process";
3594
- import fs21 from "fs";
3755
+ import fs23 from "fs";
3595
3756
 
3596
3757
  // src/server/index.ts
3597
- import fs20 from "fs";
3758
+ import fs22 from "fs";
3598
3759
  import http from "http";
3599
- import path20 from "path";
3760
+ import path21 from "path";
3600
3761
  import sirv from "sirv";
3601
3762
  import { createServer, searchForWorkspaceRoot } from "vite";
3602
3763
 
3603
3764
  // src/server/api.ts
3604
- import fs18 from "fs";
3605
- import path18 from "path";
3765
+ import fs20 from "fs";
3766
+ import path19 from "path";
3606
3767
  function sendJson(res, status, body) {
3607
3768
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
3608
3769
  res.end(JSON.stringify(body));
@@ -3653,17 +3814,17 @@ async function handleApi(ctx, req, res, url) {
3653
3814
  }
3654
3815
  if (route2 === "/source") {
3655
3816
  const rel = url.searchParams.get("path") ?? "";
3656
- const file = path18.resolve(project.paths.root, rel);
3657
- if (!rel || !isInside(project.paths.root, file) || !fs18.existsSync(file) || fs18.statSync(file).isDirectory()) {
3817
+ const file = path19.resolve(project.paths.root, rel);
3818
+ if (!rel || !isInside(project.paths.root, file) || !fs20.existsSync(file) || fs20.statSync(file).isDirectory()) {
3658
3819
  sendJson(res, 404, { error: "not found" });
3659
3820
  return true;
3660
3821
  }
3661
- if (fs18.statSync(file).size > 2 * 1024 * 1024) {
3822
+ if (fs20.statSync(file).size > 2 * 1024 * 1024) {
3662
3823
  sendJson(res, 413, { error: "file too large to show" });
3663
3824
  return true;
3664
3825
  }
3665
3826
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
3666
- res.end(fs18.readFileSync(file, "utf8"));
3827
+ res.end(fs20.readFileSync(file, "utf8"));
3667
3828
  return true;
3668
3829
  }
3669
3830
  const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route2);
@@ -3693,7 +3854,7 @@ async function handleApi(ctx, req, res, url) {
3693
3854
  return true;
3694
3855
  }
3695
3856
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
3696
- fs18.createReadStream(file).pipe(res);
3857
+ fs20.createReadStream(file).pipe(res);
3697
3858
  return true;
3698
3859
  }
3699
3860
  return false;
@@ -3733,8 +3894,8 @@ var EventHub = class {
3733
3894
  };
3734
3895
 
3735
3896
  // src/server/snapshots.ts
3736
- import fs19 from "fs";
3737
- import path19 from "path";
3897
+ import fs21 from "fs";
3898
+ import path20 from "path";
3738
3899
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
3739
3900
  function isSafeSegment(value) {
3740
3901
  return SAFE.test(value);
@@ -3748,14 +3909,14 @@ var SnapshotStore = class {
3748
3909
  urlBase;
3749
3910
  meta = /* @__PURE__ */ new Map();
3750
3911
  metaFile(canvas) {
3751
- return path19.join(this.dir, canvas, "meta.json");
3912
+ return path20.join(this.dir, canvas, "meta.json");
3752
3913
  }
3753
3914
  load(canvas) {
3754
3915
  const cached = this.meta.get(canvas);
3755
3916
  if (cached) return cached;
3756
3917
  let data = {};
3757
3918
  try {
3758
- data = JSON.parse(fs19.readFileSync(this.metaFile(canvas), "utf8"));
3919
+ data = JSON.parse(fs21.readFileSync(this.metaFile(canvas), "utf8"));
3759
3920
  } catch {
3760
3921
  }
3761
3922
  this.meta.set(canvas, data);
@@ -3781,21 +3942,21 @@ var SnapshotStore = class {
3781
3942
  };
3782
3943
  save(canvas, id2, theme2, png, height) {
3783
3944
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3784
- const dir = path19.join(this.dir, canvas);
3785
- fs19.mkdirSync(dir, { recursive: true });
3786
- fs19.writeFileSync(path19.join(dir, `${id2}.${theme2}.png`), png);
3945
+ const dir = path20.join(this.dir, canvas);
3946
+ fs21.mkdirSync(dir, { recursive: true });
3947
+ fs21.writeFileSync(path20.join(dir, `${id2}.${theme2}.png`), png);
3787
3948
  const data = this.load(canvas);
3788
3949
  const version = Date.now();
3789
3950
  data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3790
- fs19.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3951
+ fs21.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3791
3952
  return { url: this.url(canvas, id2, theme2, version) };
3792
3953
  }
3793
3954
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
3794
3955
  file(canvas, name) {
3795
3956
  const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
3796
3957
  if (!isSafeSegment(canvas) || !match) return null;
3797
- const file = path19.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
3798
- if (!fs19.existsSync(file)) return null;
3958
+ const file = path20.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
3959
+ if (!fs21.existsSync(file)) return null;
3799
3960
  return match[3] ? ensureThumb(file) : file;
3800
3961
  }
3801
3962
  };
@@ -3834,8 +3995,8 @@ function notFoundPage(res, title, text2) {
3834
3995
  }
3835
3996
  async function startDevServer(options) {
3836
3997
  const paths = designPaths(options.root);
3837
- if (!fs20.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
3838
- fs20.mkdirSync(paths.cache, { recursive: true });
3998
+ if (!fs22.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
3999
+ fs22.mkdirSync(paths.cache, { recursive: true });
3839
4000
  const snapshots = new SnapshotStore(paths.snapshots);
3840
4001
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
3841
4002
  const entries = new Entries(project);
@@ -3843,6 +4004,7 @@ async function startDevServer(options) {
3843
4004
  const links = linkShippedPackages(paths);
3844
4005
  const events = new EventHub();
3845
4006
  const httpServer = http.createServer();
4007
+ dropStaleDepCache(paths);
3846
4008
  const base = baseConfig(project);
3847
4009
  const vite = await createServer({
3848
4010
  ...base,
@@ -3861,19 +4023,28 @@ async function startDevServer(options) {
3861
4023
  }
3862
4024
  },
3863
4025
  optimizeDeps: {
3864
- entries: entries.files().map((file) => toPosix(path20.relative(paths.design, file)))
4026
+ entries: entries.files().map((file) => toPosix(path21.relative(paths.design, file)))
3865
4027
  }
3866
4028
  });
3867
- const viewer = fs20.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
3868
- const viewerIndex = path20.join(PKG.viewer, "index.html");
4029
+ const publicDir = project.publicDir();
4030
+ const servePublic = publicDir ? sirv(publicDir, { dev: true }) : null;
4031
+ const publicRel = (encoded) => {
4032
+ try {
4033
+ return decodeURIComponent(encoded);
4034
+ } catch {
4035
+ return null;
4036
+ }
4037
+ };
4038
+ const viewer = fs22.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4039
+ const viewerIndex = path21.join(PKG.viewer, "index.html");
3869
4040
  const serveViewerIndex = (res) => {
3870
- const html = fs20.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4041
+ const html = fs22.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
3871
4042
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
3872
4043
  res.end(html);
3873
4044
  };
3874
4045
  const ctx = { project, events, snapshots };
3875
4046
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
3876
- const bootUrl = `/${relToDesign(paths, path20.join(entries.dir, HTML_BOOT))}`;
4047
+ const bootUrl = `/${relToDesign(paths, path21.join(entries.dir, HTML_BOOT))}`;
3877
4048
  const frameHead = (source, extra = {}) => {
3878
4049
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
3879
4050
  return {
@@ -3882,6 +4053,8 @@ async function startDevServer(options) {
3882
4053
  fonts: project.system()?.doc.fonts ?? [],
3883
4054
  snapshots: source.canvas !== SYSTEM_CANVAS,
3884
4055
  autoHeight: source.autoHeight ?? false,
4056
+ // Vite serves the public folder under the frames' base.
4057
+ public: { base: "/_fs/", files: project.publicFiles() },
3885
4058
  ...extra
3886
4059
  };
3887
4060
  };
@@ -3892,7 +4065,7 @@ async function startDevServer(options) {
3892
4065
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
3893
4066
  return;
3894
4067
  }
3895
- if (!fs20.existsSync(entries.entryFile(source))) entries.sync();
4068
+ if (!fs22.existsSync(entries.entryFile(source))) entries.sync();
3896
4069
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
3897
4070
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
3898
4071
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -3901,8 +4074,8 @@ async function startDevServer(options) {
3901
4074
  };
3902
4075
  const serveHtmlScreen = async (res, url2) => {
3903
4076
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
3904
- const file = path20.resolve(paths.design, rel);
3905
- if (!isInside(paths.design, file) || !fs20.existsSync(file)) {
4077
+ const file = path21.resolve(paths.design, rel);
4078
+ if (!isInside(paths.design, file) || !fs22.existsSync(file)) {
3906
4079
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
3907
4080
  return;
3908
4081
  }
@@ -3912,13 +4085,13 @@ async function startDevServer(options) {
3912
4085
  key: `${canvas}/${id2}`,
3913
4086
  canvas,
3914
4087
  id: id2,
3915
- title: path20.basename(file),
4088
+ title: path21.basename(file),
3916
4089
  file,
3917
4090
  format: "html",
3918
4091
  props: {},
3919
4092
  system: true
3920
4093
  };
3921
- const raw = fs20.readFileSync(file, "utf8");
4094
+ const raw = fs22.readFileSync(file, "utf8");
3922
4095
  const withSystem = source.system && !optsOutOfSystem(raw);
3923
4096
  const html = injectIntoHtml(
3924
4097
  raw,
@@ -3950,17 +4123,24 @@ async function startDevServer(options) {
3950
4123
  return;
3951
4124
  }
3952
4125
  if (url2.pathname.startsWith("/_fs/")) {
4126
+ const rel = publicRel(url2.pathname.slice("/_fs/".length));
4127
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs22.existsSync(path21.join(publicDir, rel))) {
4128
+ notFoundPage(res, "Not found", url2.pathname);
4129
+ return;
4130
+ }
3953
4131
  vite.middlewares(req, res, () => notFoundPage(res, "Not found", url2.pathname));
3954
4132
  return;
3955
4133
  }
3956
4134
  if (viewer) {
4135
+ const notViewer = () => {
4136
+ const rel = publicRel(url2.pathname.slice(1));
4137
+ if (servePublic && rel !== null && project.isPublicFile(rel))
4138
+ servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4139
+ else if (path21.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4140
+ else serveViewerIndex(res);
4141
+ };
3957
4142
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
3958
- else
3959
- viewer(
3960
- req,
3961
- res,
3962
- () => path20.extname(url2.pathname) ? notFoundPage(res, "Not found", url2.pathname) : serveViewerIndex(res)
3963
- );
4143
+ else viewer(req, res, notViewer);
3964
4144
  } else notFoundPage(res, "Viewer is not built", "Run `pnpm build` in the foss-design package.");
3965
4145
  } catch (error) {
3966
4146
  fail(error);
@@ -3983,7 +4163,8 @@ async function startDevServer(options) {
3983
4163
  }, 60);
3984
4164
  };
3985
4165
  vite.watcher.on("all", (event, file) => {
3986
- const rel = toPosix(path20.relative(paths.design, file));
4166
+ if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4167
+ const rel = toPosix(path21.relative(paths.design, file));
3987
4168
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
3988
4169
  if (rel === "design.json") {
3989
4170
  queue("config", () => {
@@ -4091,15 +4272,15 @@ async function ensureServer(paths, options) {
4091
4272
  if (stale)
4092
4273
  print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
4093
4274
  if (live2) await stopServer(paths);
4094
- fs21.mkdirSync(paths.cache, { recursive: true });
4095
- const log = fs21.openSync(logFile(paths), "w");
4275
+ fs23.mkdirSync(paths.cache, { recursive: true });
4276
+ const log = fs23.openSync(logFile(paths), "w");
4096
4277
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4097
4278
  if (options.port) args.push("--port", String(options.port));
4098
4279
  const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
4099
4280
  child.unref();
4100
4281
  const state = await waitForServer(paths, 4e4);
4101
4282
  if (!state) {
4102
- const tail = fs21.existsSync(logFile(paths)) ? fs21.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4283
+ const tail = fs23.existsSync(logFile(paths)) ? fs23.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4103
4284
  throw new CliError(`The preview server did not start.
4104
4285
  ${tail.join("\n")}`);
4105
4286
  }
@@ -4277,9 +4458,9 @@ function printLinkIssues(issues, project) {
4277
4458
  }
4278
4459
 
4279
4460
  // src/cli/commands/cloud.ts
4280
- import fs22 from "fs";
4461
+ import fs24 from "fs";
4281
4462
  import os3 from "os";
4282
- import path21 from "path";
4463
+ import path22 from "path";
4283
4464
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4284
4465
  async function runLogin() {
4285
4466
  const host = cloudHost();
@@ -4380,7 +4561,7 @@ async function runLink(paths, wanted, options) {
4380
4561
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
4381
4562
  } else {
4382
4563
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
4383
- fs22.rmSync(path21.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
4564
+ fs24.rmSync(path22.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
4384
4565
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
4385
4566
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
4386
4567
  }
@@ -4405,8 +4586,8 @@ async function runLink(paths, wanted, options) {
4405
4586
  }
4406
4587
 
4407
4588
  // src/cli/commands/scaffold.ts
4408
- import fs23 from "fs";
4409
- import path22 from "path";
4589
+ import fs25 from "fs";
4590
+ import path23 from "path";
4410
4591
 
4411
4592
  // src/cli/templates.ts
4412
4593
  var TOKENS_CSS = `/*
@@ -4617,40 +4798,40 @@ function mainScreen(canvasId) {
4617
4798
 
4618
4799
  // src/cli/commands/scaffold.ts
4619
4800
  function write(file, content, root) {
4620
- if (fs23.existsSync(file)) {
4621
- print(` ${dim("exists ")} ${path22.relative(root, file)}`);
4801
+ if (fs25.existsSync(file)) {
4802
+ print(` ${dim("exists ")} ${path23.relative(root, file)}`);
4622
4803
  return false;
4623
4804
  }
4624
- fs23.mkdirSync(path22.dirname(file), { recursive: true });
4625
- fs23.writeFileSync(file, content);
4626
- print(` ${green("created")} ${path22.relative(root, file)}`);
4805
+ fs25.mkdirSync(path23.dirname(file), { recursive: true });
4806
+ fs25.writeFileSync(file, content);
4807
+ print(` ${green("created")} ${path23.relative(root, file)}`);
4627
4808
  return true;
4628
4809
  }
4629
4810
  function ensureGitignored(root) {
4630
- const file = path22.join(root, ".gitignore");
4631
- const current = fs23.existsSync(file) ? fs23.readFileSync(file, "utf8") : "";
4811
+ const file = path23.join(root, ".gitignore");
4812
+ const current = fs25.existsSync(file) ? fs25.readFileSync(file, "utf8") : "";
4632
4813
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
4633
4814
  if (covered) return "present";
4634
4815
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
4635
- fs23.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
4816
+ fs25.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
4636
4817
  .design/
4637
4818
  `);
4638
4819
  return "added";
4639
4820
  }
4640
4821
  function projectName(root) {
4641
4822
  try {
4642
- const manifest = JSON.parse(fs23.readFileSync(path22.join(root, "package.json"), "utf8"));
4823
+ const manifest = JSON.parse(fs25.readFileSync(path23.join(root, "package.json"), "utf8"));
4643
4824
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
4644
4825
  } catch {
4645
4826
  }
4646
- return path22.basename(root);
4827
+ return path23.basename(root);
4647
4828
  }
4648
4829
  function runInit(root, options) {
4649
4830
  const paths = designPaths(root);
4650
4831
  print(`Setting up ${DESIGN_DIR} in ${root}`);
4651
4832
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
4652
4833
  `, root);
4653
- fs23.mkdirSync(paths.canvases, { recursive: true });
4834
+ fs25.mkdirSync(paths.canvases, { recursive: true });
4654
4835
  if (options.gitignore) {
4655
4836
  const result = ensureGitignored(root);
4656
4837
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -4662,44 +4843,44 @@ function runSystemInit(root, options) {
4662
4843
  const paths = designPaths(root);
4663
4844
  let base = projectName(root);
4664
4845
  try {
4665
- base = JSON.parse(fs23.readFileSync(paths.config, "utf8")).name || base;
4846
+ base = JSON.parse(fs25.readFileSync(paths.config, "utf8")).name || base;
4666
4847
  } catch {
4667
4848
  }
4668
4849
  const name = options.name ?? `${base} design system`;
4669
- print(`Scaffolding the design system in ${path22.relative(root, paths.system)}`);
4670
- write(path22.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
4850
+ print(`Scaffolding the design system in ${path23.relative(root, paths.system)}`);
4851
+ write(path23.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
4671
4852
  `, root);
4672
- write(path22.join(paths.system, "tokens.css"), TOKENS_CSS, root);
4853
+ write(path23.join(paths.system, "tokens.css"), TOKENS_CSS, root);
4673
4854
  if (!options.empty) {
4674
- write(path22.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
4675
- write(path22.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
4676
- write(path22.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
4855
+ write(path23.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
4856
+ write(path23.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
4857
+ write(path23.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
4677
4858
  }
4678
4859
  }
4679
4860
  function runNew(root, id2, options) {
4680
4861
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
4681
4862
  const paths = designPaths(root);
4682
- const dir = path22.join(paths.canvases, id2);
4683
- if (fs23.existsSync(path22.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
4863
+ const dir = path23.join(paths.canvases, id2);
4864
+ if (fs25.existsSync(path23.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
4684
4865
  print(`Creating canvas "${id2}"`);
4685
4866
  const title = options.title ?? titleize(id2);
4686
4867
  if (options.empty) {
4687
4868
  write(
4688
- path22.join(dir, "canvas.json"),
4869
+ path23.join(dir, "canvas.json"),
4689
4870
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
4690
4871
  `,
4691
4872
  root
4692
4873
  );
4693
- fs23.mkdirSync(path22.join(dir, "screens"), { recursive: true });
4874
+ fs25.mkdirSync(path23.join(dir, "screens"), { recursive: true });
4694
4875
  } else {
4695
- write(path22.join(dir, "canvas.json"), canvasJson(title), root);
4696
- write(path22.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
4876
+ write(path23.join(dir, "canvas.json"), canvasJson(title), root);
4877
+ write(path23.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
4697
4878
  }
4698
4879
  }
4699
4880
 
4700
4881
  // src/cli/commands/shot.ts
4701
- import fs24 from "fs";
4702
- import path23 from "path";
4882
+ import fs26 from "fs";
4883
+ import path24 from "path";
4703
4884
  var OVERVIEW_TIMEOUT = 9e4;
4704
4885
  async function runShot(paths, target, options) {
4705
4886
  const [canvasId = "", screenId] = target.split("/");
@@ -4715,15 +4896,15 @@ async function runShot(paths, target, options) {
4715
4896
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
4716
4897
  }
4717
4898
  const theme2 = options.theme ?? doc.theme ?? "light";
4718
- const outBase = path23.resolve(options.out ?? paths.shots);
4719
- const outDir = path23.join(outBase, canvasId);
4720
- fs24.mkdirSync(outDir, { recursive: true });
4899
+ const outBase = path24.resolve(options.out ?? paths.shots);
4900
+ const outDir = path24.join(outBase, canvasId);
4901
+ fs26.mkdirSync(outDir, { recursive: true });
4721
4902
  const base = await serverBase(paths);
4722
4903
  const browser = await launchChrome();
4723
4904
  let failed = false;
4724
4905
  try {
4725
4906
  if (options.overview) {
4726
- if (!fs24.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
4907
+ if (!fs26.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
4727
4908
  const pages = options.page ? [options.page] : screenId ? doc.pages.filter((page) => page.sections.some((section) => section.items.some((item) => item.id === screenId))).map((page) => page.id) : doc.pages.map((page) => page.id);
4728
4909
  for (const pageId of pages) {
4729
4910
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -4739,7 +4920,7 @@ async function runShot(paths, target, options) {
4739
4920
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
4740
4921
  }
4741
4922
  await page.waitForTimeout(400);
4742
- const file = path23.join(outDir, `${pageId}.overview.png`);
4923
+ const file = path24.join(outDir, `${pageId}.overview.png`);
4743
4924
  await page.screenshot({ path: file });
4744
4925
  await page.close();
4745
4926
  print(file);
@@ -4768,7 +4949,7 @@ async function shootFrames(browser, jobs, outDir) {
4768
4949
  let failed = false;
4769
4950
  const results = await mapLimit(jobs, 4, async (job) => {
4770
4951
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
4771
- const file = path23.join(outDir, `${job.id}.${job.theme}.png`);
4952
+ const file = path24.join(outDir, `${job.id}.${job.theme}.png`);
4772
4953
  try {
4773
4954
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
4774
4955
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -4800,8 +4981,8 @@ async function shootSystem(paths, project, id2, options) {
4800
4981
  if (id2 && !all.some((job) => job.id === id2)) {
4801
4982
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
4802
4983
  }
4803
- const outDir = path23.join(path23.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
4804
- fs24.mkdirSync(outDir, { recursive: true });
4984
+ const outDir = path24.join(path24.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
4985
+ fs26.mkdirSync(outDir, { recursive: true });
4805
4986
  const base = await serverBase(paths);
4806
4987
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
4807
4988
  const browser = await launchChrome();
@@ -4813,7 +4994,7 @@ async function shootSystem(paths, project, id2, options) {
4813
4994
  }
4814
4995
 
4815
4996
  // src/cli/commands/cloud-status.ts
4816
- import path24 from "path";
4997
+ import path25 from "path";
4817
4998
  var STATE_TEXT = {
4818
4999
  in_sync: green,
4819
5000
  local_changes: yellow,
@@ -4836,7 +5017,7 @@ function detail(paths, status) {
4836
5017
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
4837
5018
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
4838
5019
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
4839
- if (status.conflictRev !== void 0) parts.push(`merge ${path24.relative(paths.root, incomingDir(paths, status.key))}`);
5020
+ if (status.conflictRev !== void 0) parts.push(`merge ${path25.relative(paths.root, incomingDir(paths, status.key))}`);
4840
5021
  return parts.join(" \xB7 ");
4841
5022
  }
4842
5023
  async function printCloudStatus(paths, label) {
@@ -5010,7 +5191,7 @@ ${bold("Environment")}
5010
5191
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
5011
5192
  `;
5012
5193
  function projectPaths(rootOption, requireDesign = true) {
5013
- const found = rootOption ? path25.resolve(rootOption) : findProjectRoot();
5194
+ const found = rootOption ? path26.resolve(rootOption) : findProjectRoot();
5014
5195
  if (!found) {
5015
5196
  if (!requireDesign) return designPaths(process.cwd());
5016
5197
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -5056,7 +5237,7 @@ async function main(argv) {
5056
5237
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
5057
5238
  switch (command) {
5058
5239
  case "init":
5059
- return runInit(values.root ? path25.resolve(values.root) : process.cwd(), {
5240
+ return runInit(values.root ? path26.resolve(values.root) : process.cwd(), {
5060
5241
  name: values.name,
5061
5242
  gitignore: !values["no-gitignore"]
5062
5243
  });