@motion-proto/live-tokens 0.25.1 → 0.26.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/README.md CHANGED
@@ -11,7 +11,7 @@ A foundational design system for quickly styling and building Svelte + Vite micr
11
11
  - **Theme editor** (`/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
12
  - **Per-component editor** (`/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
13
13
  - **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
14
- - **Preset bundles** — capture a whole site configuration (active theme + every component's active config) as a single portable artifact. Drop a preset into a new project to restore the full styling in one step.
14
+ - **Manifests** — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
15
15
  - **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
16
16
  - **Claude Code skill suite** — three bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. One command to install all three: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
17
17
 
@@ -477,6 +477,8 @@ function versionedFileResourceServer(opts) {
477
477
  const activePath = import_path.default.join(dir, "_active.json");
478
478
  const productionPath = import_path.default.join(dir, "_production.json");
479
479
  const defaultName = opts.defaultName ?? "default";
480
+ const resolvedDir = import_path.default.resolve(dir);
481
+ const resolvedPackageDir = opts.packageDir ? import_path.default.resolve(opts.packageDir) : null;
480
482
  function ensureDir() {
481
483
  if (!import_fs.default.existsSync(dir)) {
482
484
  import_fs.default.mkdirSync(dir, { recursive: true });
@@ -493,6 +495,37 @@ function versionedFileResourceServer(opts) {
493
495
  function filePath(name) {
494
496
  return import_path.default.join(dir, `${name}.json`);
495
497
  }
498
+ function existingPath(name) {
499
+ const local = filePath(name);
500
+ if (import_fs.default.existsSync(local)) return local;
501
+ if (resolvedPackageDir) {
502
+ const pkg = import_path.default.join(resolvedPackageDir, `${name}.json`);
503
+ if (import_fs.default.existsSync(pkg)) return pkg;
504
+ }
505
+ return null;
506
+ }
507
+ function readJson(name) {
508
+ const resolved = existingPath(name);
509
+ if (!resolved) return null;
510
+ return JSON.parse(import_fs.default.readFileSync(resolved, "utf-8"));
511
+ }
512
+ function listNames() {
513
+ const names = [];
514
+ const seen = /* @__PURE__ */ new Set();
515
+ const collect = (d) => {
516
+ if (!d || !import_fs.default.existsSync(d)) return;
517
+ for (const f of import_fs.default.readdirSync(d)) {
518
+ if (!f.endsWith(".json") || f.startsWith("_")) continue;
519
+ const name = f.slice(0, -".json".length);
520
+ if (seen.has(name)) continue;
521
+ seen.add(name);
522
+ names.push(name);
523
+ }
524
+ };
525
+ collect(resolvedDir);
526
+ if (resolvedPackageDir && resolvedPackageDir !== resolvedDir) collect(resolvedPackageDir);
527
+ return names;
528
+ }
496
529
  function getActiveName() {
497
530
  try {
498
531
  const data = JSON.parse(import_fs.default.readFileSync(activePath, "utf-8"));
@@ -522,6 +555,9 @@ function versionedFileResourceServer(opts) {
522
555
  ensureDir,
523
556
  ensureMeta,
524
557
  filePath,
558
+ existingPath,
559
+ readJson,
560
+ listNames,
525
561
  getActiveName,
526
562
  getProductionName,
527
563
  setActiveName,
@@ -826,36 +862,42 @@ function themeFileApi(opts) {
826
862
  "system",
827
863
  "components"
828
864
  );
865
+ const packageDataDir = import_path3.default.resolve(
866
+ import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
867
+ "..",
868
+ "src",
869
+ "live-tokens",
870
+ "data"
871
+ );
872
+ const packageThemesDir = import_path3.default.join(packageDataDir, "themes");
873
+ const packageManifestsDir = import_path3.default.join(packageDataDir, "manifests");
874
+ const packageComponentConfigsDir = import_path3.default.join(packageDataDir, "component-configs");
829
875
  const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
830
876
  if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
831
877
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
832
878
  }
833
- const LEGACY_PRESETS_DIR = import_path3.default.resolve("presets");
834
879
  const themesResource = versionedFileResourceServer({
835
- dir: THEMES_DIR
880
+ dir: THEMES_DIR,
881
+ packageDir: packageThemesDir
836
882
  });
837
883
  const componentResourceCache = /* @__PURE__ */ new Map();
838
884
  function componentResource(comp) {
839
885
  let r = componentResourceCache.get(comp);
840
886
  if (!r) {
841
- r = versionedFileResourceServer({ dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp) });
887
+ r = versionedFileResourceServer({
888
+ dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp),
889
+ packageDir: import_path3.default.join(packageComponentConfigsDir, comp)
890
+ });
842
891
  componentResourceCache.set(comp, r);
843
892
  }
844
893
  return r;
845
894
  }
846
- const manifestsResource = versionedFileResourceServer({ dir: MANIFESTS_DIR });
895
+ const manifestsResource = versionedFileResourceServer({
896
+ dir: MANIFESTS_DIR,
897
+ packageDir: packageManifestsDir
898
+ });
847
899
  function ensureThemesDir() {
848
900
  themesResource.ensureDir();
849
- if (!import_fs3.default.existsSync(import_path3.default.join(THEMES_DIR, "default.json"))) {
850
- const defaultTheme = {
851
- name: "Default Theme",
852
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
853
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
854
- editorConfigs: {},
855
- cssVariables: {}
856
- };
857
- import_fs3.default.writeFileSync(import_path3.default.join(THEMES_DIR, "default.json"), JSON.stringify(defaultTheme, null, 2));
858
- }
859
901
  themesResource.ensureMeta();
860
902
  }
861
903
  function readBody(req) {
@@ -919,10 +961,9 @@ function themeFileApi(opts) {
919
961
  lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
920
962
  lines.push("");
921
963
  const productionThemeName = themesResource.getProductionName();
922
- const themePath = import_path3.default.join(THEMES_DIR, `${productionThemeName}.json`);
964
+ const themeData = themesResource.readJson(productionThemeName);
923
965
  let themeVarCount = 0;
924
- if (import_fs3.default.existsSync(themePath)) {
925
- const themeData = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
966
+ if (themeData) {
926
967
  const cssVars = { ...themeData.cssVariables || {} };
927
968
  Object.assign(cssVars, palettesToVars(themeData.editorConfigs ?? {}));
928
969
  const resolvedFontVars = resolveFontStacks(themeData);
@@ -987,9 +1028,8 @@ function themeFileApi(opts) {
987
1028
  regenerateTokensCss();
988
1029
  }
989
1030
  function syncFontsToCss(fileName) {
990
- const themePath = import_path3.default.join(THEMES_DIR, `${fileName}.json`);
991
- if (!import_fs3.default.existsSync(themePath)) return;
992
- const themeData = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
1031
+ const themeData = themesResource.readJson(fileName);
1032
+ if (!themeData) return;
993
1033
  const sources = themeData.fontSources;
994
1034
  if (!sources) return;
995
1035
  const lines = [];
@@ -1149,28 +1189,7 @@ ${lines.join("\n")}
1149
1189
  }
1150
1190
  }
1151
1191
  function ensureManifestsDir() {
1152
- if (!import_fs3.default.existsSync(MANIFESTS_DIR) && import_fs3.default.existsSync(LEGACY_PRESETS_DIR)) {
1153
- import_fs3.default.renameSync(LEGACY_PRESETS_DIR, MANIFESTS_DIR);
1154
- const legacyProd = import_path3.default.join(MANIFESTS_DIR, "_production.json");
1155
- if (import_fs3.default.existsSync(legacyProd)) import_fs3.default.unlinkSync(legacyProd);
1156
- }
1157
1192
  manifestsResource.ensureDir();
1158
- const defaultPath = import_path3.default.join(MANIFESTS_DIR, "default.json");
1159
- if (!import_fs3.default.existsSync(defaultPath)) {
1160
- const componentConfigs = {};
1161
- for (const comp of listComponentNames()) {
1162
- componentConfigs[comp] = componentResource(comp).getActiveName();
1163
- }
1164
- const now = (/* @__PURE__ */ new Date()).toISOString();
1165
- const defaultManifest = {
1166
- name: "Default",
1167
- createdAt: now,
1168
- updatedAt: now,
1169
- theme: themesResource.getActiveName(),
1170
- componentConfigs
1171
- };
1172
- import_fs3.default.writeFileSync(defaultPath, JSON.stringify(defaultManifest, null, 2));
1173
- }
1174
1193
  if (!import_fs3.default.existsSync(manifestsResource.activePath)) {
1175
1194
  import_fs3.default.writeFileSync(
1176
1195
  manifestsResource.activePath,
@@ -1178,12 +1197,10 @@ ${lines.join("\n")}
1178
1197
  );
1179
1198
  } else {
1180
1199
  const activeName = manifestsResource.getActiveName();
1181
- if (!import_fs3.default.existsSync(manifestsResource.filePath(activeName))) {
1200
+ if (manifestsResource.existingPath(activeName) === null) {
1182
1201
  manifestsResource.setActiveName("default");
1183
1202
  }
1184
1203
  }
1185
- const stragglerProd = import_path3.default.join(MANIFESTS_DIR, "_production.json");
1186
- if (import_fs3.default.existsSync(stragglerProd)) import_fs3.default.unlinkSync(stragglerProd);
1187
1204
  }
1188
1205
  function patchActiveManifest(field, comp, fileName) {
1189
1206
  const activeFile = manifestsResource.getActiveName();
@@ -1234,10 +1251,8 @@ ${lines.join("\n")}
1234
1251
  return JSON.stringify(a) === JSON.stringify(b);
1235
1252
  }
1236
1253
  function readComponentConfig(comp, name) {
1237
- const filePath = componentResource(comp).filePath(name);
1238
- if (!import_fs3.default.existsSync(filePath)) return null;
1239
1254
  try {
1240
- return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1255
+ return componentResource(comp).readJson(name);
1241
1256
  } catch {
1242
1257
  return null;
1243
1258
  }
@@ -1263,14 +1278,12 @@ ${lines.join("\n")}
1263
1278
  const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
1264
1279
  async function handleListThemes(_ctx) {
1265
1280
  const activeFile = themesResource.getActiveName();
1266
- const files = import_fs3.default.readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1267
- const filePath = import_path3.default.join(THEMES_DIR, f);
1268
- const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1269
- const fileName = f.replace(".json", "");
1281
+ const files = themesResource.listNames().map((fileName) => {
1282
+ const data = themesResource.readJson(fileName);
1270
1283
  return {
1271
- name: data.name || fileName,
1284
+ name: data?.name || fileName,
1272
1285
  fileName,
1273
- updatedAt: data.updatedAt || "",
1286
+ updatedAt: data?.updatedAt || "",
1274
1287
  isActive: fileName === activeFile
1275
1288
  };
1276
1289
  });
@@ -1278,19 +1291,19 @@ ${lines.join("\n")}
1278
1291
  }
1279
1292
  async function handleGetActiveTheme({ res }) {
1280
1293
  const activeFile = themesResource.getActiveName();
1281
- const filePath = themesResource.filePath(activeFile);
1282
- if (!import_fs3.default.existsSync(filePath)) {
1294
+ const raw = themesResource.readJson(activeFile);
1295
+ if (!raw) {
1283
1296
  jsonResponse(res, 404, { error: "Active theme not found" });
1284
1297
  return;
1285
1298
  }
1286
- const data = normalizeTheme(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
1299
+ const data = normalizeTheme(raw);
1287
1300
  data._fileName = activeFile;
1288
1301
  jsonResponse(res, 200, data);
1289
1302
  }
1290
1303
  async function handleSetActiveTheme({ req, res }) {
1291
1304
  const body = JSON.parse(await readBody(req));
1292
1305
  const fileName = sanitizeFileName(body.name || "default");
1293
- if (!import_fs3.default.existsSync(themesResource.filePath(fileName))) {
1306
+ if (themesResource.existingPath(fileName) === null) {
1294
1307
  jsonResponse(res, 404, { error: "Theme not found" });
1295
1308
  return;
1296
1309
  }
@@ -1299,12 +1312,12 @@ ${lines.join("\n")}
1299
1312
  }
1300
1313
  async function handleGetProductionTheme({ res }) {
1301
1314
  const prodFile = themesResource.getProductionName();
1302
- const filePath = themesResource.filePath(prodFile);
1303
- if (!import_fs3.default.existsSync(filePath)) {
1315
+ const raw = themesResource.readJson(prodFile);
1316
+ if (!raw) {
1304
1317
  jsonResponse(res, 200, { fileName: prodFile, name: prodFile, cssVariables: {} });
1305
1318
  return;
1306
1319
  }
1307
- const data = normalizeTheme(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
1320
+ const data = normalizeTheme(raw);
1308
1321
  jsonResponse(res, 200, {
1309
1322
  fileName: prodFile,
1310
1323
  name: data.name || prodFile,
@@ -1315,7 +1328,7 @@ ${lines.join("\n")}
1315
1328
  async function handleSetProductionTheme({ req, res }) {
1316
1329
  const body = JSON.parse(await readBody(req));
1317
1330
  const fileName = sanitizeFileName(body.name || "default");
1318
- if (!import_fs3.default.existsSync(themesResource.filePath(fileName))) {
1331
+ if (themesResource.existingPath(fileName) === null) {
1319
1332
  jsonResponse(res, 404, { error: "Theme not found" });
1320
1333
  return;
1321
1334
  }
@@ -1331,28 +1344,33 @@ ${lines.join("\n")}
1331
1344
  syncFontsToCss(fileName);
1332
1345
  syncComponentsToCss();
1333
1346
  patchActiveManifest("theme", null, fileName);
1334
- const data = JSON.parse(import_fs3.default.readFileSync(themesResource.filePath(fileName), "utf-8"));
1347
+ const data = themesResource.readJson(fileName);
1335
1348
  jsonResponse(res, 200, {
1336
1349
  ok: true,
1337
1350
  fileName,
1338
- name: data.name || fileName,
1339
- updatedAt: data.updatedAt || ""
1351
+ name: data?.name || fileName,
1352
+ updatedAt: data?.updatedAt || ""
1340
1353
  });
1341
1354
  }
1342
1355
  async function handleThemeByName({ params, req, res }) {
1343
1356
  const [fileName] = params;
1344
1357
  const filePath = themesResource.filePath(fileName);
1345
1358
  if (req.method === "GET") {
1346
- if (!import_fs3.default.existsSync(filePath)) {
1359
+ const raw = themesResource.readJson(fileName);
1360
+ if (!raw) {
1347
1361
  jsonResponse(res, 404, { error: "Not found" });
1348
1362
  return;
1349
1363
  }
1350
- const data = normalizeTheme(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
1364
+ const data = normalizeTheme(raw);
1351
1365
  data._fileName = fileName;
1352
1366
  jsonResponse(res, 200, data);
1353
1367
  return;
1354
1368
  }
1355
1369
  if (req.method === "PUT") {
1370
+ if (fileName === "default") {
1371
+ jsonResponse(res, 403, { error: "Cannot overwrite the default theme (live from package)" });
1372
+ return;
1373
+ }
1356
1374
  const body = JSON.parse(await readBody(req));
1357
1375
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1358
1376
  if (import_fs3.default.existsSync(filePath)) {
@@ -1421,8 +1439,7 @@ ${lines.join("\n")}
1421
1439
  const body = JSON.parse(await readBody(req));
1422
1440
  const fileName = sanitizeFileName(body.name || "default");
1423
1441
  const r = componentResource(comp);
1424
- const configPath = r.filePath(fileName);
1425
- if (!import_fs3.default.existsSync(configPath)) {
1442
+ if (r.existingPath(fileName) === null) {
1426
1443
  jsonResponse(res, 404, { error: "Config not found" });
1427
1444
  return;
1428
1445
  }
@@ -1446,8 +1463,7 @@ ${lines.join("\n")}
1446
1463
  const body = JSON.parse(await readBody(req));
1447
1464
  const fileName = sanitizeFileName(body.name || "default");
1448
1465
  const r = componentResource(comp);
1449
- const configPath = r.filePath(fileName);
1450
- if (!import_fs3.default.existsSync(configPath)) {
1466
+ if (r.existingPath(fileName) === null) {
1451
1467
  jsonResponse(res, 404, { error: "Config not found" });
1452
1468
  return;
1453
1469
  }
@@ -1475,11 +1491,12 @@ ${lines.join("\n")}
1475
1491
  const r = componentResource(comp);
1476
1492
  const configPath = r.filePath(name);
1477
1493
  if (req.method === "GET") {
1478
- if (!import_fs3.default.existsSync(configPath)) {
1494
+ const raw = r.readJson(name);
1495
+ if (!raw) {
1479
1496
  jsonResponse(res, 404, { error: "Not found" });
1480
1497
  return;
1481
1498
  }
1482
- const data = JSON.parse(import_fs3.default.readFileSync(configPath, "utf-8"));
1499
+ const data = raw;
1483
1500
  data._fileName = name;
1484
1501
  jsonResponse(res, 200, data);
1485
1502
  return;
@@ -1537,14 +1554,12 @@ ${lines.join("\n")}
1537
1554
  }
1538
1555
  const activeFile = r.getActiveName();
1539
1556
  const productionFile = r.getProductionName();
1540
- const files = import_fs3.default.readdirSync(r.dir).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1541
- const filePath = import_path3.default.join(r.dir, f);
1542
- const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1543
- const fileName = f.replace(".json", "");
1557
+ const files = r.listNames().map((fileName) => {
1558
+ const data = r.readJson(fileName);
1544
1559
  return {
1545
- name: data.name || fileName,
1560
+ name: data?.name || fileName,
1546
1561
  fileName,
1547
- updatedAt: data.updatedAt || "",
1562
+ updatedAt: data?.updatedAt || "",
1548
1563
  isActive: fileName === activeFile,
1549
1564
  isProduction: fileName === productionFile
1550
1565
  };
@@ -1553,14 +1568,12 @@ ${lines.join("\n")}
1553
1568
  }
1554
1569
  async function handleListManifests({ res }) {
1555
1570
  const activeFile = manifestsResource.getActiveName();
1556
- const files = import_fs3.default.readdirSync(MANIFESTS_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1557
- const filePath = import_path3.default.join(MANIFESTS_DIR, f);
1558
- const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1559
- const fileName = f.replace(".json", "");
1571
+ const files = manifestsResource.listNames().map((fileName) => {
1572
+ const data = manifestsResource.readJson(fileName);
1560
1573
  return {
1561
- name: data.name || fileName,
1574
+ name: data?.name || fileName,
1562
1575
  fileName,
1563
- updatedAt: data.updatedAt || "",
1576
+ updatedAt: data?.updatedAt || "",
1564
1577
  isActive: fileName === activeFile,
1565
1578
  isProtected: fileName === "default"
1566
1579
  };
@@ -1569,19 +1582,19 @@ ${lines.join("\n")}
1569
1582
  }
1570
1583
  async function handleGetActiveManifest({ res }) {
1571
1584
  const activeFile = manifestsResource.getActiveName();
1572
- const filePath = manifestsResource.filePath(activeFile);
1573
- if (!import_fs3.default.existsSync(filePath)) {
1585
+ const raw = manifestsResource.readJson(activeFile);
1586
+ if (!raw) {
1574
1587
  jsonResponse(res, 404, { error: "Active manifest not found" });
1575
1588
  return;
1576
1589
  }
1577
- const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1590
+ const data = raw;
1578
1591
  data._fileName = activeFile;
1579
1592
  jsonResponse(res, 200, data);
1580
1593
  }
1581
1594
  async function handleSetActiveManifest({ req, res }) {
1582
1595
  const body = JSON.parse(await readBody(req));
1583
1596
  const fileName = sanitizeFileName(body.name || "default");
1584
- if (!import_fs3.default.existsSync(manifestsResource.filePath(fileName))) {
1597
+ if (manifestsResource.existingPath(fileName) === null) {
1585
1598
  jsonResponse(res, 404, { error: "Manifest not found" });
1586
1599
  return;
1587
1600
  }
@@ -1592,11 +1605,12 @@ ${lines.join("\n")}
1592
1605
  const [fileName] = params;
1593
1606
  const filePath = manifestsResource.filePath(fileName);
1594
1607
  if (req.method === "GET") {
1595
- if (!import_fs3.default.existsSync(filePath)) {
1608
+ const raw = manifestsResource.readJson(fileName);
1609
+ if (!raw) {
1596
1610
  jsonResponse(res, 404, { error: "Not found" });
1597
1611
  return;
1598
1612
  }
1599
- const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1613
+ const data = raw;
1600
1614
  data._fileName = fileName;
1601
1615
  jsonResponse(res, 200, data);
1602
1616
  return;
@@ -1637,15 +1651,13 @@ ${lines.join("\n")}
1637
1651
  }
1638
1652
  async function handleApplyManifest({ params, res }) {
1639
1653
  const [fileName] = params;
1640
- const manifestPath = manifestsResource.filePath(fileName);
1641
- if (!import_fs3.default.existsSync(manifestPath)) {
1654
+ const manifest = manifestsResource.readJson(fileName);
1655
+ if (!manifest) {
1642
1656
  jsonResponse(res, 404, { error: "Manifest not found" });
1643
1657
  return;
1644
1658
  }
1645
- const manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
1646
1659
  const themeName = sanitizeFileName(manifest.theme || "default");
1647
- const themePath = themesResource.filePath(themeName);
1648
- if (!import_fs3.default.existsSync(themePath)) {
1660
+ if (themesResource.existingPath(themeName) === null) {
1649
1661
  jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
1650
1662
  return;
1651
1663
  }
@@ -1657,8 +1669,7 @@ ${lines.join("\n")}
1657
1669
  if (!knownComponents.has(comp)) continue;
1658
1670
  const sanitized = sanitizeFileName(String(configFile) || "default");
1659
1671
  const r = componentResource(comp);
1660
- const cfgPath = r.filePath(sanitized);
1661
- if (!import_fs3.default.existsSync(cfgPath)) {
1672
+ if (r.existingPath(sanitized) === null) {
1662
1673
  jsonResponse(res, 422, {
1663
1674
  error: `Manifest references missing config: ${comp}/${sanitized}`
1664
1675
  });
@@ -1670,7 +1681,7 @@ ${lines.join("\n")}
1670
1681
  themesResource.setProductionName(themeName);
1671
1682
  syncTokensToCss(themeName);
1672
1683
  syncFontsToCss(themeName);
1673
- const themeData = normalizeTheme(JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8")));
1684
+ const themeData = normalizeTheme(themesResource.readJson(themeName));
1674
1685
  themeData._fileName = themeName;
1675
1686
  for (const [comp, configFile] of apply) {
1676
1687
  const r = componentResource(comp);
@@ -1696,33 +1707,30 @@ ${lines.join("\n")}
1696
1707
  }
1697
1708
  async function handleExportManifest({ params, res }) {
1698
1709
  const [fileName] = params;
1699
- const manifestPath = manifestsResource.filePath(fileName);
1700
- if (!import_fs3.default.existsSync(manifestPath)) {
1710
+ const manifest = manifestsResource.readJson(fileName);
1711
+ if (!manifest) {
1701
1712
  jsonResponse(res, 404, { error: "Manifest not found" });
1702
1713
  return;
1703
1714
  }
1704
- const manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
1705
1715
  const themeName = sanitizeFileName(manifest.theme || "default");
1706
- const themePath = themesResource.filePath(themeName);
1707
- if (!import_fs3.default.existsSync(themePath)) {
1716
+ const theme = themesResource.readJson(themeName);
1717
+ if (!theme) {
1708
1718
  jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
1709
1719
  return;
1710
1720
  }
1711
- const theme = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
1712
1721
  const knownComponents = new Set(listComponentNames());
1713
1722
  const componentConfigs = {};
1714
1723
  for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
1715
1724
  if (!knownComponents.has(comp)) continue;
1716
1725
  const sanitized = sanitizeFileName(String(configFile) || "default");
1717
1726
  if (sanitized === "default") continue;
1718
- const cfgPath = componentResource(comp).filePath(sanitized);
1719
- if (!import_fs3.default.existsSync(cfgPath)) {
1727
+ const cfg = componentResource(comp).readJson(sanitized);
1728
+ if (!cfg) {
1720
1729
  jsonResponse(res, 422, {
1721
1730
  error: `Manifest references missing config: ${comp}/${sanitized}`
1722
1731
  });
1723
1732
  return;
1724
1733
  }
1725
- const cfg = JSON.parse(import_fs3.default.readFileSync(cfgPath, "utf-8"));
1726
1734
  componentConfigs[`${comp}/${sanitized}`] = cfg;
1727
1735
  }
1728
1736
  const bundle = {
@@ -1742,11 +1750,8 @@ ${lines.join("\n")}
1742
1750
  );
1743
1751
  res.end(JSON.stringify(bundle, null, 2));
1744
1752
  }
1745
- function nextAvailableName2(resourceFilePath, baseName) {
1746
- return nextAvailableName(
1747
- (n) => import_fs3.default.existsSync(resourceFilePath(n)),
1748
- sanitizeFileName(baseName)
1749
- );
1753
+ function nextAvailableName2(exists, baseName) {
1754
+ return nextAvailableName(exists, sanitizeFileName(baseName));
1750
1755
  }
1751
1756
  async function handleImportManifest({ req, res }) {
1752
1757
  let bundle;
@@ -1776,7 +1781,7 @@ ${lines.join("\n")}
1776
1781
  const now = (/* @__PURE__ */ new Date()).toISOString();
1777
1782
  const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
1778
1783
  const finalThemeName = nextAvailableName2(
1779
- (n) => themesResource.filePath(n),
1784
+ (n) => themesResource.existingPath(n) !== null,
1780
1785
  originalThemeName
1781
1786
  );
1782
1787
  if (finalThemeName !== originalThemeName) {
@@ -1795,7 +1800,7 @@ ${lines.join("\n")}
1795
1800
  if (!knownComponents.has(comp)) continue;
1796
1801
  const r = componentResource(comp);
1797
1802
  const finalName = nextAvailableName2(
1798
- (n) => r.filePath(n),
1803
+ (n) => r.existingPath(n) !== null,
1799
1804
  originalName
1800
1805
  );
1801
1806
  if (finalName !== originalName) {
@@ -1828,7 +1833,7 @@ ${lines.join("\n")}
1828
1833
  if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
1829
1834
  const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
1830
1835
  const finalManifestName = nextAvailableName2(
1831
- (n) => manifestsResource.filePath(n),
1836
+ (n) => manifestsResource.existingPath(n) !== null,
1832
1837
  originalManifestName
1833
1838
  );
1834
1839
  if (finalManifestName !== originalManifestName) {