scribe-cms 0.0.16 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/index.cjs +4139 -542
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +4137 -540
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1512 -232
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +1497 -231
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime.cjs +435 -33
  11. package/dist/runtime.cjs.map +1 -1
  12. package/dist/runtime.js +434 -32
  13. package/dist/runtime.js.map +1 -1
  14. package/dist/src/config/resolve-config.d.ts.map +1 -1
  15. package/dist/src/core/builtin-fields.d.ts +10 -1
  16. package/dist/src/core/builtin-fields.d.ts.map +1 -1
  17. package/dist/src/core/field.d.ts +70 -0
  18. package/dist/src/core/field.d.ts.map +1 -1
  19. package/dist/src/core/introspect-schema.d.ts +31 -1
  20. package/dist/src/core/introspect-schema.d.ts.map +1 -1
  21. package/dist/src/core/managed-roots.d.ts +15 -0
  22. package/dist/src/core/managed-roots.d.ts.map +1 -0
  23. package/dist/src/core/types.d.ts +56 -4
  24. package/dist/src/core/types.d.ts.map +1 -1
  25. package/dist/src/create-project.d.ts +11 -1
  26. package/dist/src/create-project.d.ts.map +1 -1
  27. package/dist/src/create-scribe.d.ts.map +1 -1
  28. package/dist/src/delete/execute.d.ts +21 -0
  29. package/dist/src/delete/execute.d.ts.map +1 -0
  30. package/dist/src/delete/plan.d.ts +85 -0
  31. package/dist/src/delete/plan.d.ts.map +1 -0
  32. package/dist/src/delete/render-text.d.ts +7 -0
  33. package/dist/src/delete/render-text.d.ts.map +1 -0
  34. package/dist/src/hash/page-hash.d.ts +9 -0
  35. package/dist/src/hash/page-hash.d.ts.map +1 -1
  36. package/dist/src/index.d.ts +12 -3
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/inline/resolve-tokens.d.ts +31 -0
  39. package/dist/src/inline/resolve-tokens.d.ts.map +1 -0
  40. package/dist/src/inline/tokens.d.ts +95 -0
  41. package/dist/src/inline/tokens.d.ts.map +1 -0
  42. package/dist/src/loader/create-loader.d.ts +7 -1
  43. package/dist/src/loader/create-loader.d.ts.map +1 -1
  44. package/dist/src/loader/resolve-assets.d.ts +16 -0
  45. package/dist/src/loader/resolve-assets.d.ts.map +1 -0
  46. package/dist/src/runtime.d.ts +1 -1
  47. package/dist/src/runtime.d.ts.map +1 -1
  48. package/dist/src/storage/translations.d.ts +16 -0
  49. package/dist/src/storage/translations.d.ts.map +1 -1
  50. package/dist/src/translate/resolve-translate-config.d.ts.map +1 -1
  51. package/dist/src/translate/response-schema.d.ts +7 -1
  52. package/dist/src/translate/response-schema.d.ts.map +1 -1
  53. package/dist/src/translate/translate-core.d.ts +6 -0
  54. package/dist/src/translate/translate-core.d.ts.map +1 -1
  55. package/dist/src/translate/validate-translation.d.ts.map +1 -1
  56. package/dist/src/translate/worklist.d.ts +12 -2
  57. package/dist/src/translate/worklist.d.ts.map +1 -1
  58. package/dist/src/validate/validate-assets.d.ts +26 -2
  59. package/dist/src/validate/validate-assets.d.ts.map +1 -1
  60. package/dist/src/validate/validate-inline-tokens.d.ts +10 -0
  61. package/dist/src/validate/validate-inline-tokens.d.ts.map +1 -0
  62. package/dist/src/validate/validate-project.d.ts.map +1 -1
  63. package/dist/src/validate/validate-relations.d.ts +2 -1
  64. package/dist/src/validate/validate-relations.d.ts.map +1 -1
  65. package/dist/studio/asset-serve.d.ts +39 -0
  66. package/dist/studio/asset-serve.d.ts.map +1 -0
  67. package/dist/studio/asset-views.d.ts +4 -0
  68. package/dist/studio/asset-views.d.ts.map +1 -0
  69. package/dist/studio/content-views.d.ts +85 -0
  70. package/dist/studio/content-views.d.ts.map +1 -0
  71. package/dist/studio/introspect-fields.d.ts +111 -0
  72. package/dist/studio/introspect-fields.d.ts.map +1 -0
  73. package/dist/studio/mdx-preview.d.ts +3 -0
  74. package/dist/studio/mdx-preview.d.ts.map +1 -0
  75. package/dist/studio/preview-tokens.d.ts +16 -0
  76. package/dist/studio/preview-tokens.d.ts.map +1 -0
  77. package/dist/studio/search.d.ts +34 -0
  78. package/dist/studio/search.d.ts.map +1 -0
  79. package/dist/studio/server.cjs +4806 -281
  80. package/dist/studio/server.cjs.map +1 -1
  81. package/dist/studio/server.d.ts.map +1 -1
  82. package/dist/studio/server.js +4804 -279
  83. package/dist/studio/server.js.map +1 -1
  84. package/dist/studio/shared.d.ts +20 -0
  85. package/dist/studio/shared.d.ts.map +1 -0
  86. package/dist/studio/studio-cache.d.ts +93 -0
  87. package/dist/studio/studio-cache.d.ts.map +1 -0
  88. package/package.json +2 -2
package/dist/runtime.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var path = require('path');
4
4
  var zod = require('zod');
5
- var fs2 = require('fs');
5
+ var fs3 = require('fs');
6
6
  var matter = require('gray-matter');
7
7
  var Database = require('better-sqlite3');
8
8
  var remarkMdx = require('remark-mdx');
@@ -12,7 +12,7 @@ var unified = require('unified');
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
14
14
  var path__default = /*#__PURE__*/_interopDefault(path);
15
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
15
+ var fs3__default = /*#__PURE__*/_interopDefault(fs3);
16
16
  var matter__default = /*#__PURE__*/_interopDefault(matter);
17
17
  var Database__default = /*#__PURE__*/_interopDefault(Database);
18
18
  var remarkMdx__default = /*#__PURE__*/_interopDefault(remarkMdx);
@@ -142,7 +142,15 @@ function resolveConfig(input, baseDir) {
142
142
  const projectRoot = path__default.default.resolve(process.cwd(), raw.rootDir);
143
143
  const contentRoot = path__default.default.resolve(projectRoot, raw.contentDir ?? "content");
144
144
  const storePath = path__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
145
- const assetsPath = raw.assetsDir ? path__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
145
+ let assetsPath;
146
+ let assets;
147
+ if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
148
+ const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
149
+ const publicPath = raw.assets?.publicPath ?? "/";
150
+ const managedDirs = raw.assets?.managedDirs ?? [];
151
+ assetsPath = path__default.default.resolve(projectRoot, dir);
152
+ assets = { assetsPath, publicPath, managedDirs };
153
+ }
146
154
  const seenIds = /* @__PURE__ */ new Set();
147
155
  const types = raw.types.map((type) => {
148
156
  if (seenIds.has(type.id)) {
@@ -157,13 +165,17 @@ function resolveConfig(input, baseDir) {
157
165
  contentDir: type.contentDir ?? type.id,
158
166
  label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
159
167
  slugStrategy: type.slugStrategy ?? "fixed",
160
- indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
168
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
169
+ // `body` defaults to true (fully backwards compatible). A resolved type
170
+ // always carries an explicit boolean so consumers read one source of truth.
171
+ body: type.body ?? true
161
172
  };
162
173
  });
163
174
  const config = {
164
175
  rootDir: contentRoot,
165
176
  storePath,
166
177
  assetsPath,
178
+ assets,
167
179
  locales: [...raw.locales],
168
180
  defaultLocale,
169
181
  localeRouting,
@@ -199,6 +211,7 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
199
211
  }
200
212
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
201
213
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
214
+ var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
202
215
  function getFieldKind(schema) {
203
216
  const tagged = schema;
204
217
  return tagged[FIELD_KIND] ?? "structural";
@@ -226,6 +239,29 @@ function getRelationTarget(schema) {
226
239
  }
227
240
  return null;
228
241
  }
242
+ function getAssetMeta(schema) {
243
+ let current = schema;
244
+ for (let i = 0; i < 8; i++) {
245
+ const tagged = current;
246
+ if (tagged[ASSET_META]) {
247
+ return tagged[ASSET_META];
248
+ }
249
+ if (typeof tagged.unwrap === "function") {
250
+ current = tagged.unwrap();
251
+ continue;
252
+ }
253
+ if (typeof tagged.removeDefault === "function") {
254
+ current = tagged.removeDefault();
255
+ continue;
256
+ }
257
+ if (tagged._def?.innerType) {
258
+ current = tagged._def.innerType;
259
+ continue;
260
+ }
261
+ break;
262
+ }
263
+ return null;
264
+ }
229
265
  function peelOptionalWrappers(schema) {
230
266
  let current = schema;
231
267
  for (let i = 0; i < 8; i++) {
@@ -259,7 +295,23 @@ function introspectSchema(schema, prefix = []) {
259
295
  kind: "relation",
260
296
  relationTarget: relation.typeId,
261
297
  relationMultiple: relation.multiple,
262
- relationOptional: relation.optional
298
+ relationOptional: relation.optional,
299
+ relationOnTargetDelete: relation.onTargetDelete
300
+ }
301
+ ];
302
+ }
303
+ const asset = getAssetMeta(schema);
304
+ if (asset && prefix.length > 0) {
305
+ return [
306
+ {
307
+ path: prefix,
308
+ kind: "asset",
309
+ assetDir: asset.dir,
310
+ assetTemplate: asset.template,
311
+ assetFormats: asset.formats,
312
+ assetMaxKB: asset.maxKB,
313
+ assetOptional: asset.optional,
314
+ assetOnDelete: asset.onDelete
263
315
  }
264
316
  ];
265
317
  }
@@ -337,7 +389,7 @@ function pickTranslatable(data, schema) {
337
389
  return extractByPaths(data, translatablePaths);
338
390
  }
339
391
  function pickStructural(data, schema) {
340
- const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
392
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
341
393
  return extractByPaths(data, structuralPaths);
342
394
  }
343
395
  function mergeStructuralOntoLocale(localeData, enData, schema) {
@@ -352,7 +404,7 @@ function deepMerge(base, overlay) {
352
404
  out[key] = deepMerge(out[key], value);
353
405
  } else if (Array.isArray(value) && Array.isArray(out[key])) {
354
406
  out[key] = mergeArrayOverlay(out[key], value);
355
- } else {
407
+ } else if (value !== void 0) {
356
408
  out[key] = value;
357
409
  }
358
410
  }
@@ -370,6 +422,9 @@ function mergeArrayOverlay(base, overlay) {
370
422
  function listRelationFields(schema) {
371
423
  return introspectSchema(schema).filter((f) => f.kind === "relation");
372
424
  }
425
+ function listAssetFields(schema) {
426
+ return introspectSchema(schema).filter((f) => f.kind === "asset");
427
+ }
373
428
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
374
429
  var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
375
430
  var isoDateSchema = zod.z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
@@ -450,12 +505,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
450
505
  delete rest[internalKey];
451
506
  }
452
507
  }
508
+ let vars;
509
+ if (rest.vars !== void 0) {
510
+ vars = rest.vars;
511
+ delete rest.vars;
512
+ }
453
513
  return {
454
514
  builtin: {
455
515
  publishedAt,
456
516
  updatedAt,
457
517
  noindex,
458
- canonicalPathOverride
518
+ canonicalPathOverride,
519
+ vars
459
520
  },
460
521
  rest,
461
522
  issues
@@ -485,6 +546,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
485
546
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
486
547
  out.noindex = doc.noindex;
487
548
  out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
549
+ if (doc.vars !== void 0) out.vars = doc.vars;
488
550
  return out;
489
551
  }
490
552
  function seoFieldsFromEn(enDoc) {
@@ -574,7 +636,7 @@ function resolveStorePath(config) {
574
636
  function openStore(config, mode = "readwrite") {
575
637
  const storePath = resolveStorePath(config);
576
638
  if (mode === "readwrite") {
577
- fs2__default.default.mkdirSync(path__default.default.dirname(storePath), { recursive: true });
639
+ fs3__default.default.mkdirSync(path__default.default.dirname(storePath), { recursive: true });
578
640
  }
579
641
  const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
580
642
  if (mode === "readwrite") {
@@ -613,6 +675,9 @@ function migrate(db) {
613
675
  function listTranslationsForType(db, contentType) {
614
676
  return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
615
677
  }
678
+ function bulkLoadTranslations(db) {
679
+ return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
680
+ }
616
681
 
617
682
  // src/translate/normalize-mdx-body.ts
618
683
  function needsMdxEscapeNormalization(body) {
@@ -729,6 +794,279 @@ function prepareTranslatedMdxBody(body) {
729
794
  };
730
795
  }
731
796
 
797
+ // src/loader/resolve-assets.ts
798
+ function joinPublicPath(publicPath, webPath) {
799
+ if (publicPath === "" || publicPath === "/") return webPath;
800
+ const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
801
+ return publicPath.replace(/\/+$/, "") + suffix;
802
+ }
803
+ function materializeTemplate(template, enSlug) {
804
+ return template.split("{slug}").join(enSlug);
805
+ }
806
+ function setAssetAtPath(container, path6, transform) {
807
+ const [head, ...rest] = path6;
808
+ if (head === void 0) return;
809
+ if (rest.length === 0) {
810
+ const current = container[head];
811
+ const next = transform(typeof current === "string" ? current : void 0);
812
+ if (next !== void 0) container[head] = next;
813
+ return;
814
+ }
815
+ if (rest[0] === "*") {
816
+ const arr = container[head];
817
+ if (!Array.isArray(arr)) return;
818
+ for (const item of arr) {
819
+ if (item && typeof item === "object" && !Array.isArray(item)) {
820
+ setAssetAtPath(item, rest.slice(1), transform);
821
+ }
822
+ }
823
+ return;
824
+ }
825
+ const child = container[head];
826
+ if (child && typeof child === "object" && !Array.isArray(child)) {
827
+ setAssetAtPath(child, rest, transform);
828
+ }
829
+ }
830
+ function resolveDocumentAssets(doc, assetFields, assets) {
831
+ const frontmatter = doc.frontmatter;
832
+ for (const f of assetFields) {
833
+ setAssetAtPath(frontmatter, f.path, (current) => {
834
+ let value = current;
835
+ if (value === void 0) {
836
+ if (!f.assetTemplate) return void 0;
837
+ value = materializeTemplate(f.assetTemplate, doc.enSlug);
838
+ }
839
+ return joinPublicPath(assets.publicPath, value);
840
+ });
841
+ }
842
+ }
843
+
844
+ // src/inline/tokens.ts
845
+ var TOKEN_OPEN = "${{";
846
+ var ESCAPE_SEQUENCE = "$\\{{";
847
+ var ESCAPE_RESULT = "${{";
848
+ function placeholderMarker(n) {
849
+ return `%%${n}%%`;
850
+ }
851
+ function findTokenEnd(body, start) {
852
+ let i = start;
853
+ let inString = false;
854
+ while (i < body.length) {
855
+ const ch = body[i];
856
+ if (ch === "\n") return -1;
857
+ if (inString) {
858
+ if (ch === "\\") {
859
+ i += 2;
860
+ continue;
861
+ }
862
+ if (ch === '"') inString = false;
863
+ i += 1;
864
+ continue;
865
+ }
866
+ if (ch === '"') {
867
+ inString = true;
868
+ i += 1;
869
+ continue;
870
+ }
871
+ if (ch === "}" && body[i + 1] === "}") return i;
872
+ i += 1;
873
+ }
874
+ return -1;
875
+ }
876
+ function parseTokenInner(inner) {
877
+ const colon = inner.indexOf(":");
878
+ if (colon === -1) {
879
+ return { ok: false, reason: `missing kind separator ":"` };
880
+ }
881
+ const kind = inner.slice(0, colon);
882
+ const rest = inner.slice(colon + 1);
883
+ switch (kind) {
884
+ case "static": {
885
+ let text;
886
+ try {
887
+ text = JSON.parse(rest);
888
+ } catch {
889
+ return { ok: false, reason: `static value is not a valid JSON string` };
890
+ }
891
+ if (typeof text !== "string") {
892
+ return { ok: false, reason: `static value must be a JSON string` };
893
+ }
894
+ return { ok: true, token: { kind: "static", text } };
895
+ }
896
+ case "relation": {
897
+ const parts = rest.split(":");
898
+ if (parts.length < 2 || parts.length > 3) {
899
+ return {
900
+ ok: false,
901
+ reason: `relation must be relation:<typeId>:<enSlug> or relation:<typeId>:<enSlug>:slug`
902
+ };
903
+ }
904
+ const [targetTypeId, enSlug, mode] = parts;
905
+ if (!targetTypeId || !enSlug) {
906
+ return { ok: false, reason: `relation is missing a typeId or enSlug` };
907
+ }
908
+ if (mode !== void 0 && mode !== "slug") {
909
+ return { ok: false, reason: `relation mode must be "slug" (got "${mode}")` };
910
+ }
911
+ return {
912
+ ok: true,
913
+ token: {
914
+ kind: "relation",
915
+ targetTypeId,
916
+ enSlug,
917
+ mode: mode === "slug" ? "slug" : "url"
918
+ }
919
+ };
920
+ }
921
+ case "asset": {
922
+ if (!rest.startsWith("/")) {
923
+ return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
924
+ }
925
+ return { ok: true, token: { kind: "asset", webPath: rest } };
926
+ }
927
+ case "var": {
928
+ if (!rest) return { ok: false, reason: `var is missing a key` };
929
+ return { ok: true, token: { kind: "var", key: rest } };
930
+ }
931
+ default:
932
+ return { ok: false, reason: `unknown token kind "${kind}"` };
933
+ }
934
+ }
935
+ function extractInlineTokens(body) {
936
+ const tokens = [];
937
+ const malformed = [];
938
+ let out = "";
939
+ let i = 0;
940
+ while (i < body.length) {
941
+ const open = body.indexOf(TOKEN_OPEN, i);
942
+ if (open === -1) {
943
+ out += body.slice(i);
944
+ break;
945
+ }
946
+ out += body.slice(i, open);
947
+ const contentStart = open + TOKEN_OPEN.length;
948
+ const end = findTokenEnd(body, contentStart);
949
+ if (end === -1) {
950
+ malformed.push({
951
+ raw: body.slice(open),
952
+ reason: `unterminated token (missing "}}")`,
953
+ index: open
954
+ });
955
+ out += body.slice(open);
956
+ break;
957
+ }
958
+ const raw = body.slice(open, end + 2);
959
+ const inner = body.slice(contentStart, end);
960
+ const parsed = parseTokenInner(inner);
961
+ if (parsed.ok) {
962
+ tokens.push({ ...parsed.token, raw });
963
+ out += placeholderMarker(tokens.length);
964
+ } else {
965
+ malformed.push({ raw, reason: parsed.reason, index: open });
966
+ out += raw;
967
+ }
968
+ i = end + 2;
969
+ }
970
+ return { placeholderBody: out, tokens, malformed };
971
+ }
972
+ function fillPlaceholders(body, resolvedValues) {
973
+ return body.replace(/%%(\d+)%%/g, (match, n) => {
974
+ const idx = Number(n) - 1;
975
+ return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
976
+ });
977
+ }
978
+ function unescapeInlineTokens(body) {
979
+ return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
980
+ }
981
+
982
+ // src/inline/resolve-tokens.ts
983
+ function slugKey(typeId, enSlug, locale) {
984
+ return `${typeId}\0${enSlug}\0${locale}`;
985
+ }
986
+ function isStringRecord(value) {
987
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
988
+ return Object.values(value).every((v) => typeof v === "string");
989
+ }
990
+ function createInlineResolver(config) {
991
+ const urlBuilder = createUrlBuilder(config);
992
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
993
+ const storePath = resolveStorePath(config);
994
+ let slugCache = null;
995
+ const localizedSlugIndex = () => {
996
+ let mtime = 0;
997
+ try {
998
+ mtime = fs3__default.default.statSync(storePath).mtimeMs;
999
+ } catch {
1000
+ mtime = 0;
1001
+ }
1002
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
1003
+ const index = /* @__PURE__ */ new Map();
1004
+ try {
1005
+ const db = openStore(config, "readonly");
1006
+ try {
1007
+ for (const row of bulkLoadTranslations(db)) {
1008
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1009
+ }
1010
+ } finally {
1011
+ db.close();
1012
+ }
1013
+ } catch {
1014
+ }
1015
+ slugCache = { mtime, index };
1016
+ return index;
1017
+ };
1018
+ const localizedSlug = (typeId, enSlug, locale) => {
1019
+ if (locale === config.defaultLocale) return enSlug;
1020
+ const index = localizedSlugIndex();
1021
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1022
+ if (direct) return direct;
1023
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1024
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1025
+ if (hit) return hit;
1026
+ }
1027
+ return enSlug;
1028
+ };
1029
+ return {
1030
+ resolve(token, enFrontmatter, locale) {
1031
+ switch (token.kind) {
1032
+ case "static":
1033
+ return token.text;
1034
+ case "var": {
1035
+ const vars = enFrontmatter.vars;
1036
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1037
+ return vars[token.key];
1038
+ }
1039
+ return "";
1040
+ }
1041
+ case "asset": {
1042
+ const assets = config.assets;
1043
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1044
+ }
1045
+ case "relation": {
1046
+ const type = typeById.get(token.targetTypeId);
1047
+ if (!type) return "";
1048
+ if (token.mode === "slug") return token.enSlug;
1049
+ if (!isRoutableType(type)) return "";
1050
+ const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
1051
+ return urlBuilder.resolvePath(type.path, slug, locale);
1052
+ }
1053
+ }
1054
+ }
1055
+ };
1056
+ }
1057
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1058
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1059
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1060
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1061
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1062
+ }
1063
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1064
+ const { tokens } = extractInlineTokens(enRawBody);
1065
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1066
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1067
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1068
+ }
1069
+
732
1070
  // src/loader/normalize-en.ts
733
1071
  function normalizeEnFrontmatter(data) {
734
1072
  const out = { ...data };
@@ -754,14 +1092,14 @@ function isPostFile(name) {
754
1092
  return isPublishableContentFile(name);
755
1093
  }
756
1094
  function listEnFiles(contentDir) {
757
- if (!fs2__default.default.existsSync(contentDir)) return [];
758
- return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path__default.default.join(contentDir, f));
1095
+ if (!fs3__default.default.existsSync(contentDir)) return [];
1096
+ return fs3__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path__default.default.join(contentDir, f));
759
1097
  }
760
1098
  function slugFromPath(filePath) {
761
1099
  return path__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
762
1100
  }
763
1101
  function parseEnMdx(filePath, config, type) {
764
- const raw = fs2__default.default.readFileSync(filePath, "utf8");
1102
+ const raw = fs3__default.default.readFileSync(filePath, "utf8");
765
1103
  const parsed = matter__default.default(raw);
766
1104
  const slug = slugFromPath(filePath);
767
1105
  const normalized = normalizeEnFrontmatter(parsed.data);
@@ -800,6 +1138,7 @@ function parseEnMdx(filePath, config, type) {
800
1138
  updatedAt: builtin.updatedAt,
801
1139
  noindex: builtin.noindex,
802
1140
  canonicalPathOverride: builtin.canonicalPathOverride,
1141
+ vars: builtin.vars,
803
1142
  slug,
804
1143
  locale: config.defaultLocale
805
1144
  },
@@ -824,12 +1163,15 @@ function parseEnMdx(filePath, config, type) {
824
1163
  noindex: builtin.noindex,
825
1164
  canonicalPathOverride: builtin.canonicalPathOverride,
826
1165
  frontmatter,
827
- content: parsed.content,
1166
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1167
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1168
+ // A stray body is reported separately by `scribe validate`.
1169
+ content: type.body === false ? "" : parsed.content,
828
1170
  filePath
829
1171
  };
830
1172
  return { document, issues };
831
1173
  }
832
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1174
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
833
1175
  const localeFm = JSON.parse(row.frontmatter_json);
834
1176
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
835
1177
  const seo = seoFieldsFromEn(enDoc);
@@ -840,6 +1182,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
840
1182
  config.defaultLocale,
841
1183
  config.localeRouting
842
1184
  );
1185
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1186
+ const content = inlineResolver ? fillTranslatedInlineBody(
1187
+ preparedBody,
1188
+ enDoc.content,
1189
+ enDoc.frontmatter,
1190
+ row.locale,
1191
+ inlineResolver
1192
+ ) : preparedBody;
843
1193
  return {
844
1194
  slug: row.slug,
845
1195
  enSlug: row.en_slug,
@@ -849,14 +1199,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
849
1199
  noindex: seo.noindex,
850
1200
  canonicalPathOverride: seo.canonicalPathOverride,
851
1201
  frontmatter,
852
- content: prepareTranslatedMdxBody(row.body).body
1202
+ content
853
1203
  };
854
1204
  }
855
1205
  var DEV_REVALIDATE_MS = 1500;
856
- function createContentLoader(config, type) {
1206
+ var contentVersion = 0;
1207
+ function createContentLoader(config, type, options = {}) {
857
1208
  let cached = null;
858
1209
  let signature = "";
859
1210
  let lastCheck = 0;
1211
+ let builtVersion = contentVersion;
860
1212
  const contentDir = path__default.default.join(
861
1213
  /* turbopackIgnore: true */
862
1214
  config.rootDir,
@@ -864,18 +1216,34 @@ function createContentLoader(config, type) {
864
1216
  );
865
1217
  const storePath = resolveStorePath(config);
866
1218
  const isProd = process.env.NODE_ENV === "production";
1219
+ const assets = config.assets;
1220
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1221
+ const resolveAssets = (doc) => {
1222
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1223
+ };
1224
+ const inlineResolver = options.inlineResolver;
1225
+ const resolveEnInline = (doc) => {
1226
+ if (inlineResolver) {
1227
+ doc.content = substituteEnInlineBody(
1228
+ doc.content,
1229
+ doc.frontmatter,
1230
+ config.defaultLocale,
1231
+ inlineResolver
1232
+ );
1233
+ }
1234
+ };
867
1235
  function computeSignature() {
868
1236
  const files = listEnFiles(contentDir);
869
1237
  let newest = 0;
870
1238
  for (const f of files) {
871
1239
  try {
872
- newest = Math.max(newest, fs2__default.default.statSync(f).mtimeMs);
1240
+ newest = Math.max(newest, fs3__default.default.statSync(f).mtimeMs);
873
1241
  } catch {
874
1242
  }
875
1243
  }
876
1244
  let store = 0;
877
1245
  try {
878
- store = fs2__default.default.statSync(storePath).mtimeMs;
1246
+ store = fs3__default.default.statSync(storePath).mtimeMs;
879
1247
  } catch {
880
1248
  }
881
1249
  return `${files.length}:${newest}:${store}`;
@@ -921,16 +1289,28 @@ function createContentLoader(config, type) {
921
1289
  for (const row of rowsByLocale.get(locale) ?? []) {
922
1290
  const enDoc = englishBySlug.get(row.en_slug);
923
1291
  if (!enDoc) continue;
924
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1292
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1293
+ resolveAssets(doc);
925
1294
  bySlug.set(doc.slug, doc);
926
1295
  byEnSlug.set(row.en_slug, doc);
927
1296
  }
928
1297
  out.set(locale, { bySlug, byEnSlug });
929
1298
  }
1299
+ for (const doc of englishBySlug.values()) {
1300
+ resolveAssets(doc);
1301
+ resolveEnInline(doc);
1302
+ }
930
1303
  return out;
931
1304
  }
932
1305
  return () => {
933
1306
  if (cached) {
1307
+ if (contentVersion !== builtVersion) {
1308
+ cached = build();
1309
+ builtVersion = contentVersion;
1310
+ lastCheck = Date.now();
1311
+ signature = isProd ? "" : computeSignature();
1312
+ return cached;
1313
+ }
934
1314
  if (isProd) return cached;
935
1315
  const now = Date.now();
936
1316
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -943,6 +1323,7 @@ function createContentLoader(config, type) {
943
1323
  }
944
1324
  lastCheck = Date.now();
945
1325
  cached = build();
1326
+ builtVersion = contentVersion;
946
1327
  signature = isProd ? "" : computeSignature();
947
1328
  return cached;
948
1329
  };
@@ -1032,8 +1413,11 @@ function comparatorFor(orderBy) {
1032
1413
  return (a, b) => a.slug.localeCompare(b.slug);
1033
1414
  }
1034
1415
  }
1035
- function buildRuntime(config, type, getRuntime) {
1036
- const load = createContentLoader(config, type);
1416
+ function buildRuntime(config, type, getRuntime, options = {}) {
1417
+ const load = createContentLoader(config, type, {
1418
+ resolveAssets: options.resolveAssets,
1419
+ inlineResolver: options.inlineResolver
1420
+ });
1037
1421
  const relationFields = new Map(
1038
1422
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1039
1423
  );
@@ -1050,12 +1434,12 @@ function buildRuntime(config, type, getRuntime) {
1050
1434
  id: type.id,
1051
1435
  config: type,
1052
1436
  load,
1053
- list(locale = config.defaultLocale, options = {}) {
1437
+ list(locale = config.defaultLocale, options2 = {}) {
1054
1438
  const idx = load().get(locale);
1055
1439
  if (!idx) return [];
1056
1440
  const docs = Array.from(idx.bySlug.values());
1057
- docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1058
- return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1441
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1442
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1059
1443
  },
1060
1444
  get(slug, locale = config.defaultLocale) {
1061
1445
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1068,7 +1452,7 @@ function buildRuntime(config, type, getRuntime) {
1068
1452
  load(),
1069
1453
  type,
1070
1454
  config.localeRouting,
1071
- config.localeFallbacks[locale] ?? []
1455
+ config.localeFallbacks?.[locale] ?? []
1072
1456
  );
1073
1457
  if (result.document && type.path) {
1074
1458
  return {
@@ -1082,15 +1466,15 @@ function buildRuntime(config, type, getRuntime) {
1082
1466
  }
1083
1467
  return result;
1084
1468
  },
1085
- staticParams(options = {}) {
1469
+ staticParams(options2 = {}) {
1086
1470
  assertRoutable("staticParams");
1087
1471
  const all = load();
1088
1472
  const enIdx = all.get(config.defaultLocale);
1089
1473
  if (!enIdx) return [];
1090
1474
  const params = [];
1091
- for (const locale of options.locales ?? config.locales) {
1475
+ for (const locale of options2.locales ?? config.locales) {
1092
1476
  const localeIdx = all.get(locale);
1093
- const fallbacks = config.localeFallbacks[locale] ?? [];
1477
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1094
1478
  for (const doc of enIdx.bySlug.values()) {
1095
1479
  let slug;
1096
1480
  if (locale === config.defaultLocale) {
@@ -1169,7 +1553,7 @@ function buildRuntime(config, type, getRuntime) {
1169
1553
  };
1170
1554
  return runtime;
1171
1555
  }
1172
- function createProject(config) {
1556
+ function createProject(config, options = {}) {
1173
1557
  const runtimes = /* @__PURE__ */ new Map();
1174
1558
  const getRuntime = (id) => {
1175
1559
  const runtime = runtimes.get(id);
@@ -1178,8 +1562,15 @@ function createProject(config) {
1178
1562
  }
1179
1563
  return runtime;
1180
1564
  };
1565
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config) : void 0;
1181
1566
  for (const type of config.types) {
1182
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1567
+ runtimes.set(
1568
+ type.id,
1569
+ buildRuntime(config, type, getRuntime, {
1570
+ resolveAssets: options.resolveAssets,
1571
+ inlineResolver
1572
+ })
1573
+ );
1183
1574
  }
1184
1575
  return {
1185
1576
  config,
@@ -1272,10 +1663,10 @@ function redirectsFilePath(config, type) {
1272
1663
  }
1273
1664
  function loadTypeRedirectsFile(config, type) {
1274
1665
  const filePath = redirectsFilePath(config, type);
1275
- if (!fs2__default.default.existsSync(filePath)) return null;
1666
+ if (!fs3__default.default.existsSync(filePath)) return null;
1276
1667
  let raw;
1277
1668
  try {
1278
- raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
1669
+ raw = JSON.parse(fs3__default.default.readFileSync(filePath, "utf8"));
1279
1670
  } catch (error) {
1280
1671
  throw new Error(
1281
1672
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1431,7 +1822,7 @@ async function generateSitemap(project, options) {
1431
1822
  // src/create-scribe.ts
1432
1823
  function createScribe(input) {
1433
1824
  const config = resolveConfig(input);
1434
- const project = createProject(config);
1825
+ const project = createProject(config, { resolveAssets: true, resolveInlineTokens: true });
1435
1826
  const scribe = {
1436
1827
  config,
1437
1828
  project,
@@ -1440,6 +1831,17 @@ function createScribe(input) {
1440
1831
  listRoutableTypes: project.listRoutableTypes,
1441
1832
  sitemap(options) {
1442
1833
  return generateSitemap(project, options);
1834
+ },
1835
+ assets: {
1836
+ url(ref, opts) {
1837
+ if (opts && Object.keys(opts).length > 0) {
1838
+ throw new Error(
1839
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
1840
+ );
1841
+ }
1842
+ const assetsConfig = config.assets;
1843
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
1844
+ }
1443
1845
  }
1444
1846
  };
1445
1847
  for (const type of config.types) {