scribe-cms 0.0.17 → 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 +4013 -555
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +4011 -553
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1502 -229
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +1487 -228
  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 +8 -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 +8 -1
  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 +4691 -304
  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 +4689 -302
  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/index.cjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  var zod = require('zod');
4
4
  var path3 = require('path');
5
- var fs2 = require('fs');
6
- var matter = require('gray-matter');
5
+ var fs14 = require('fs');
6
+ var matter4 = require('gray-matter');
7
7
  var Database = require('better-sqlite3');
8
8
  var remarkMdx = require('remark-mdx');
9
9
  var remarkParse = require('remark-parse');
@@ -15,8 +15,8 @@ var genai = require('@google/genai');
15
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
16
 
17
17
  var path3__default = /*#__PURE__*/_interopDefault(path3);
18
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
19
- var matter__default = /*#__PURE__*/_interopDefault(matter);
18
+ var fs14__default = /*#__PURE__*/_interopDefault(fs14);
19
+ var matter4__default = /*#__PURE__*/_interopDefault(matter4);
20
20
  var Database__default = /*#__PURE__*/_interopDefault(Database);
21
21
  var remarkMdx__default = /*#__PURE__*/_interopDefault(remarkMdx);
22
22
  var remarkParse__default = /*#__PURE__*/_interopDefault(remarkParse);
@@ -26,6 +26,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
26
26
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
27
27
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
28
28
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
29
+ var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
29
30
  function getFieldKind(schema) {
30
31
  const tagged = schema;
31
32
  return tagged[FIELD_KIND] ?? "structural";
@@ -53,6 +54,29 @@ function getRelationTarget(schema) {
53
54
  }
54
55
  return null;
55
56
  }
57
+ function getAssetMeta(schema) {
58
+ let current = schema;
59
+ for (let i = 0; i < 8; i++) {
60
+ const tagged = current;
61
+ if (tagged[ASSET_META]) {
62
+ return tagged[ASSET_META];
63
+ }
64
+ if (typeof tagged.unwrap === "function") {
65
+ current = tagged.unwrap();
66
+ continue;
67
+ }
68
+ if (typeof tagged.removeDefault === "function") {
69
+ current = tagged.removeDefault();
70
+ continue;
71
+ }
72
+ if (tagged._def?.innerType) {
73
+ current = tagged._def.innerType;
74
+ continue;
75
+ }
76
+ break;
77
+ }
78
+ return null;
79
+ }
56
80
  function mark(schema, kind) {
57
81
  Object.defineProperty(schema, FIELD_KIND, {
58
82
  value: kind,
@@ -69,6 +93,14 @@ function markRelation(schema, meta) {
69
93
  });
70
94
  return mark(schema, "structural");
71
95
  }
96
+ function markAsset(schema, meta) {
97
+ Object.defineProperty(schema, ASSET_META, {
98
+ value: meta,
99
+ enumerable: false,
100
+ configurable: true
101
+ });
102
+ return mark(schema, "structural");
103
+ }
72
104
  var field = {
73
105
  translatable: (schema) => mark(schema, "translatable"),
74
106
  structural: (schema) => mark(schema, "structural"),
@@ -89,9 +121,24 @@ var field = {
89
121
  } else {
90
122
  inner = zod.z.string().min(1);
91
123
  }
92
- markRelation(inner, { typeId, multiple, optional });
124
+ const onTargetDelete = options?.onTargetDelete ?? "restrict";
125
+ markRelation(inner, { typeId, multiple, optional, onTargetDelete });
93
126
  const schema = optional ? inner.optional() : inner;
94
127
  return schema;
128
+ },
129
+ asset: (options) => {
130
+ const optional = options?.optional ?? false;
131
+ const inner = zod.z.string().min(1);
132
+ markAsset(inner, {
133
+ dir: options?.dir,
134
+ template: options?.template,
135
+ formats: options?.formats,
136
+ maxKB: options?.maxKB,
137
+ optional,
138
+ onDelete: options?.onDelete ?? "delete"
139
+ });
140
+ const schema = optional || options?.template !== void 0 ? inner.optional() : inner;
141
+ return schema;
95
142
  }
96
143
  };
97
144
  function unwrapSchema(schema) {
@@ -131,6 +178,208 @@ function peelOptionalWrappers(schema) {
131
178
  return current;
132
179
  }
133
180
 
181
+ // src/core/introspect-schema.ts
182
+ function getArrayElement(schema) {
183
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
184
+ return schema.element;
185
+ }
186
+ return null;
187
+ }
188
+ function introspectSchema(schema, prefix = []) {
189
+ const relation = getRelationTarget(schema);
190
+ if (relation && prefix.length > 0) {
191
+ return [
192
+ {
193
+ path: prefix,
194
+ kind: "relation",
195
+ relationTarget: relation.typeId,
196
+ relationMultiple: relation.multiple,
197
+ relationOptional: relation.optional,
198
+ relationOnTargetDelete: relation.onTargetDelete
199
+ }
200
+ ];
201
+ }
202
+ const asset = getAssetMeta(schema);
203
+ if (asset && prefix.length > 0) {
204
+ return [
205
+ {
206
+ path: prefix,
207
+ kind: "asset",
208
+ assetDir: asset.dir,
209
+ assetTemplate: asset.template,
210
+ assetFormats: asset.formats,
211
+ assetMaxKB: asset.maxKB,
212
+ assetOptional: asset.optional,
213
+ assetOnDelete: asset.onDelete
214
+ }
215
+ ];
216
+ }
217
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
218
+ return [{ path: prefix, kind: "translatable" }];
219
+ }
220
+ const base = peelOptionalWrappers(schema);
221
+ if (base instanceof Object && "shape" in base) {
222
+ const shape = base.shape;
223
+ const fields = [];
224
+ for (const [key, child] of Object.entries(shape)) {
225
+ fields.push(...introspectSchema(child, [...prefix, key]));
226
+ }
227
+ return fields;
228
+ }
229
+ const element = getArrayElement(base);
230
+ if (element) {
231
+ const elementBase = peelOptionalWrappers(element);
232
+ if (elementBase instanceof Object && "shape" in elementBase) {
233
+ const shape = elementBase.shape;
234
+ const fields = [];
235
+ for (const [key, child] of Object.entries(shape)) {
236
+ fields.push(...introspectSchema(child, [...prefix, "*", key]));
237
+ }
238
+ return fields;
239
+ }
240
+ }
241
+ return [{ path: prefix, kind: getFieldKind(schema) }];
242
+ }
243
+ function buildPathTrie(paths) {
244
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
245
+ for (const path15 of paths) {
246
+ let node = root;
247
+ for (const segment of path15) {
248
+ let child = node.children.get(segment);
249
+ if (!child) {
250
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
251
+ node.children.set(segment, child);
252
+ }
253
+ node = child;
254
+ }
255
+ node.leaf = true;
256
+ }
257
+ return root;
258
+ }
259
+ function extractNode(data, trie) {
260
+ if (trie.leaf) return data;
261
+ const star = trie.children.get("*");
262
+ if (star) {
263
+ if (!Array.isArray(data)) return void 0;
264
+ return data.map(
265
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
266
+ );
267
+ }
268
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
269
+ const record = data;
270
+ const out = {};
271
+ let any = false;
272
+ for (const [key, child] of trie.children) {
273
+ const value = extractNode(record[key], child);
274
+ if (value !== void 0) {
275
+ out[key] = value;
276
+ any = true;
277
+ }
278
+ }
279
+ return any ? out : void 0;
280
+ }
281
+ function extractByPaths(data, paths) {
282
+ if (paths.length === 0) return {};
283
+ const extracted = extractNode(data, buildPathTrie(paths));
284
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
285
+ }
286
+ function pickTranslatable(data, schema) {
287
+ const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
288
+ return extractByPaths(data, translatablePaths);
289
+ }
290
+ function pruneOrphanNestedTranslations(localeData, enData, schema) {
291
+ const out = { ...localeData };
292
+ const structuralParentsWithNested = /* @__PURE__ */ new Set();
293
+ for (const meta of introspectSchema(schema)) {
294
+ if (meta.kind !== "translatable" || meta.path.length < 3 || meta.path[1] !== "*") continue;
295
+ structuralParentsWithNested.add(meta.path[0]);
296
+ }
297
+ for (const parent of structuralParentsWithNested) {
298
+ const enParent = enData[parent];
299
+ if (enParent === void 0 || enParent === null) {
300
+ delete out[parent];
301
+ }
302
+ }
303
+ return out;
304
+ }
305
+ function pickStructural(data, schema) {
306
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
307
+ return extractByPaths(data, structuralPaths);
308
+ }
309
+ function mergeStructuralOntoLocale(localeData, enData, schema) {
310
+ const structural = pickStructural(enData, schema);
311
+ const translatable = pickTranslatable(localeData, schema);
312
+ return deepMerge(structural, translatable);
313
+ }
314
+ function deepMerge(base, overlay) {
315
+ const out = { ...base };
316
+ for (const [key, value] of Object.entries(overlay)) {
317
+ if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
318
+ out[key] = deepMerge(out[key], value);
319
+ } else if (Array.isArray(value) && Array.isArray(out[key])) {
320
+ out[key] = mergeArrayOverlay(out[key], value);
321
+ } else if (value !== void 0) {
322
+ out[key] = value;
323
+ }
324
+ }
325
+ return out;
326
+ }
327
+ function mergeArrayOverlay(base, overlay) {
328
+ return base.map((item, index) => {
329
+ const overlayItem = overlay[index];
330
+ if (item && typeof item === "object" && !Array.isArray(item) && overlayItem && typeof overlayItem === "object" && !Array.isArray(overlayItem)) {
331
+ return deepMerge(item, overlayItem);
332
+ }
333
+ return overlayItem ?? item;
334
+ });
335
+ }
336
+ function listTranslatableFields(schema) {
337
+ return introspectSchema(schema).filter((f) => f.kind === "translatable");
338
+ }
339
+ function isTypeTranslatable(type) {
340
+ return listTranslatableFields(type.schema).length > 0 || type.body !== false;
341
+ }
342
+ function listRelationFields(schema) {
343
+ return introspectSchema(schema).filter((f) => f.kind === "relation");
344
+ }
345
+ function listAssetFields(schema) {
346
+ return introspectSchema(schema).filter((f) => f.kind === "asset");
347
+ }
348
+
349
+ // src/core/managed-roots.ts
350
+ function normalizeRoot(root) {
351
+ const trimmed = `/${root.replace(/^\/+|\/+$/g, "")}`;
352
+ return trimmed === "/" ? "" : trimmed;
353
+ }
354
+ function templateManagedRoot(template) {
355
+ const braceIndex = template.indexOf("{");
356
+ const staticPart = braceIndex >= 0 ? template.slice(0, braceIndex) : template;
357
+ const lastSlash = staticPart.lastIndexOf("/");
358
+ const prefix = lastSlash >= 0 ? staticPart.slice(0, lastSlash) : staticPart;
359
+ return normalizeRoot(prefix);
360
+ }
361
+ function getManagedRoots(config) {
362
+ if (!config.assets) return [];
363
+ const roots = /* @__PURE__ */ new Set();
364
+ for (const dir of config.assets.managedDirs ?? []) {
365
+ const normalized = normalizeRoot(dir);
366
+ if (normalized) roots.add(normalized);
367
+ }
368
+ for (const type of config.types) {
369
+ for (const f of listAssetFields(type.schema)) {
370
+ if (f.assetDir) {
371
+ const normalized = normalizeRoot(f.assetDir);
372
+ if (normalized) roots.add(normalized);
373
+ }
374
+ if (f.assetTemplate) {
375
+ const normalized = templateManagedRoot(f.assetTemplate);
376
+ if (normalized) roots.add(normalized);
377
+ }
378
+ }
379
+ }
380
+ return [...roots].sort();
381
+ }
382
+
134
383
  // src/core/types.ts
135
384
  function defineConfig(config) {
136
385
  return config;
@@ -144,14 +393,14 @@ var SLUG_PLACEHOLDER = "{slug}";
144
393
  function isRoutableType(type) {
145
394
  return typeof type.path === "string" && type.path.length > 0;
146
395
  }
147
- function assertValidPathTemplate(path13, typeId) {
148
- if (!path13.startsWith("/")) {
149
- throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
396
+ function assertValidPathTemplate(path15, typeId) {
397
+ if (!path15.startsWith("/")) {
398
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path15}")`);
150
399
  }
151
- const count = (path13.match(/\{slug\}/g) ?? []).length;
400
+ const count = (path15.match(/\{slug\}/g) ?? []).length;
152
401
  if (count !== 1) {
153
402
  throw new Error(
154
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
403
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path15}")`
155
404
  );
156
405
  }
157
406
  }
@@ -261,7 +510,15 @@ function resolveConfig(input, baseDir) {
261
510
  const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
262
511
  const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
263
512
  const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
264
- const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
513
+ let assetsPath;
514
+ let assets;
515
+ if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
516
+ const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
517
+ const publicPath = raw.assets?.publicPath ?? "/";
518
+ const managedDirs = raw.assets?.managedDirs ?? [];
519
+ assetsPath = path3__default.default.resolve(projectRoot, dir);
520
+ assets = { assetsPath, publicPath, managedDirs };
521
+ }
265
522
  const seenIds = /* @__PURE__ */ new Set();
266
523
  const types = raw.types.map((type) => {
267
524
  if (seenIds.has(type.id)) {
@@ -276,13 +533,17 @@ function resolveConfig(input, baseDir) {
276
533
  contentDir: type.contentDir ?? type.id,
277
534
  label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
278
535
  slugStrategy: type.slugStrategy ?? "fixed",
279
- indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
536
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
537
+ // `body` defaults to true (fully backwards compatible). A resolved type
538
+ // always carries an explicit boolean so consumers read one source of truth.
539
+ body: type.body ?? true
280
540
  };
281
541
  });
282
542
  const config = {
283
543
  rootDir: contentRoot,
284
544
  storePath,
285
545
  assetsPath,
546
+ assets,
286
547
  locales: [...raw.locales],
287
548
  defaultLocale,
288
549
  localeRouting,
@@ -316,134 +577,6 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
316
577
  }
317
578
  return fallbacks;
318
579
  }
319
-
320
- // src/core/introspect-schema.ts
321
- function getArrayElement(schema) {
322
- if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
323
- return schema.element;
324
- }
325
- return null;
326
- }
327
- function introspectSchema(schema, prefix = []) {
328
- const relation = getRelationTarget(schema);
329
- if (relation && prefix.length > 0) {
330
- return [
331
- {
332
- path: prefix,
333
- kind: "relation",
334
- relationTarget: relation.typeId,
335
- relationMultiple: relation.multiple,
336
- relationOptional: relation.optional
337
- }
338
- ];
339
- }
340
- if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
341
- return [{ path: prefix, kind: "translatable" }];
342
- }
343
- const base = peelOptionalWrappers(schema);
344
- if (base instanceof Object && "shape" in base) {
345
- const shape = base.shape;
346
- const fields = [];
347
- for (const [key, child] of Object.entries(shape)) {
348
- fields.push(...introspectSchema(child, [...prefix, key]));
349
- }
350
- return fields;
351
- }
352
- const element = getArrayElement(base);
353
- if (element) {
354
- const elementBase = peelOptionalWrappers(element);
355
- if (elementBase instanceof Object && "shape" in elementBase) {
356
- const shape = elementBase.shape;
357
- const fields = [];
358
- for (const [key, child] of Object.entries(shape)) {
359
- fields.push(...introspectSchema(child, [...prefix, "*", key]));
360
- }
361
- return fields;
362
- }
363
- }
364
- return [{ path: prefix, kind: getFieldKind(schema) }];
365
- }
366
- function buildPathTrie(paths) {
367
- const root = { leaf: false, children: /* @__PURE__ */ new Map() };
368
- for (const path13 of paths) {
369
- let node = root;
370
- for (const segment of path13) {
371
- let child = node.children.get(segment);
372
- if (!child) {
373
- child = { leaf: false, children: /* @__PURE__ */ new Map() };
374
- node.children.set(segment, child);
375
- }
376
- node = child;
377
- }
378
- node.leaf = true;
379
- }
380
- return root;
381
- }
382
- function extractNode(data, trie) {
383
- if (trie.leaf) return data;
384
- const star = trie.children.get("*");
385
- if (star) {
386
- if (!Array.isArray(data)) return void 0;
387
- return data.map(
388
- (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
389
- );
390
- }
391
- if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
392
- const record = data;
393
- const out = {};
394
- let any = false;
395
- for (const [key, child] of trie.children) {
396
- const value = extractNode(record[key], child);
397
- if (value !== void 0) {
398
- out[key] = value;
399
- any = true;
400
- }
401
- }
402
- return any ? out : void 0;
403
- }
404
- function extractByPaths(data, paths) {
405
- if (paths.length === 0) return {};
406
- const extracted = extractNode(data, buildPathTrie(paths));
407
- return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
408
- }
409
- function pickTranslatable(data, schema) {
410
- const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
411
- return extractByPaths(data, translatablePaths);
412
- }
413
- function pickStructural(data, schema) {
414
- const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
415
- return extractByPaths(data, structuralPaths);
416
- }
417
- function mergeStructuralOntoLocale(localeData, enData, schema) {
418
- const structural = pickStructural(enData, schema);
419
- const translatable = pickTranslatable(localeData, schema);
420
- return deepMerge(structural, translatable);
421
- }
422
- function deepMerge(base, overlay) {
423
- const out = { ...base };
424
- for (const [key, value] of Object.entries(overlay)) {
425
- if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
426
- out[key] = deepMerge(out[key], value);
427
- } else if (Array.isArray(value) && Array.isArray(out[key])) {
428
- out[key] = mergeArrayOverlay(out[key], value);
429
- } else {
430
- out[key] = value;
431
- }
432
- }
433
- return out;
434
- }
435
- function mergeArrayOverlay(base, overlay) {
436
- return base.map((item, index) => {
437
- const overlayItem = overlay[index];
438
- if (item && typeof item === "object" && !Array.isArray(item) && overlayItem && typeof overlayItem === "object" && !Array.isArray(overlayItem)) {
439
- return deepMerge(item, overlayItem);
440
- }
441
- return overlayItem ?? item;
442
- });
443
- }
444
- function listRelationFields(schema) {
445
- return introspectSchema(schema).filter((f) => f.kind === "relation");
446
- }
447
580
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
448
581
  var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
449
582
  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");
@@ -534,12 +667,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
534
667
  delete rest[internalKey];
535
668
  }
536
669
  }
670
+ let vars;
671
+ if (rest.vars !== void 0) {
672
+ vars = rest.vars;
673
+ delete rest.vars;
674
+ }
537
675
  return {
538
676
  builtin: {
539
677
  publishedAt,
540
678
  updatedAt,
541
679
  noindex,
542
- canonicalPathOverride
680
+ canonicalPathOverride,
681
+ vars
543
682
  },
544
683
  rest,
545
684
  issues
@@ -578,6 +717,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
578
717
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
579
718
  out.noindex = doc.noindex;
580
719
  out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
720
+ if (doc.vars !== void 0) out.vars = doc.vars;
581
721
  return out;
582
722
  }
583
723
  function seoFieldsFromEn(enDoc) {
@@ -667,7 +807,7 @@ function resolveStorePath(config) {
667
807
  function openStore(config, mode = "readwrite") {
668
808
  const storePath = resolveStorePath(config);
669
809
  if (mode === "readwrite") {
670
- fs2__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
810
+ fs14__default.default.mkdirSync(path3__default.default.dirname(storePath), { recursive: true });
671
811
  }
672
812
  const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
673
813
  if (mode === "readwrite") {
@@ -767,6 +907,26 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
767
907
  function bulkLoadTranslations(db) {
768
908
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
769
909
  }
910
+ function countTranslationsForEnSlug(db, contentType, enSlug) {
911
+ const row = db.prepare(
912
+ `SELECT COUNT(*) as count FROM translations WHERE content_type = ? AND en_slug = ?`
913
+ ).get(contentType, enSlug);
914
+ return row.count;
915
+ }
916
+ function countEnSnapshotsForEnSlug(db, contentType, enSlug) {
917
+ const row = db.prepare(
918
+ `SELECT COUNT(*) as count FROM en_snapshots WHERE content_type = ? AND en_slug = ?`
919
+ ).get(contentType, enSlug);
920
+ return row.count;
921
+ }
922
+ function deleteTranslationsForEnSlug(db, contentType, enSlug) {
923
+ const info = db.prepare(`DELETE FROM translations WHERE content_type = ? AND en_slug = ?`).run(contentType, enSlug);
924
+ return info.changes;
925
+ }
926
+ function deleteEnSnapshotsForEnSlug(db, contentType, enSlug) {
927
+ const info = db.prepare(`DELETE FROM en_snapshots WHERE content_type = ? AND en_slug = ?`).run(contentType, enSlug);
928
+ return info.changes;
929
+ }
770
930
 
771
931
  // src/translate/normalize-mdx-body.ts
772
932
  function needsMdxEscapeNormalization(body) {
@@ -900,6 +1060,312 @@ function assertValidTranslatedMdxBody(body) {
900
1060
  return prepared;
901
1061
  }
902
1062
 
1063
+ // src/loader/resolve-assets.ts
1064
+ function joinPublicPath(publicPath, webPath) {
1065
+ if (publicPath === "" || publicPath === "/") return webPath;
1066
+ const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
1067
+ return publicPath.replace(/\/+$/, "") + suffix;
1068
+ }
1069
+ function materializeTemplate(template, enSlug) {
1070
+ return template.split("{slug}").join(enSlug);
1071
+ }
1072
+ function setAssetAtPath(container, path15, transform) {
1073
+ const [head, ...rest] = path15;
1074
+ if (head === void 0) return;
1075
+ if (rest.length === 0) {
1076
+ const current = container[head];
1077
+ const next = transform(typeof current === "string" ? current : void 0);
1078
+ if (next !== void 0) container[head] = next;
1079
+ return;
1080
+ }
1081
+ if (rest[0] === "*") {
1082
+ const arr = container[head];
1083
+ if (!Array.isArray(arr)) return;
1084
+ for (const item of arr) {
1085
+ if (item && typeof item === "object" && !Array.isArray(item)) {
1086
+ setAssetAtPath(item, rest.slice(1), transform);
1087
+ }
1088
+ }
1089
+ return;
1090
+ }
1091
+ const child = container[head];
1092
+ if (child && typeof child === "object" && !Array.isArray(child)) {
1093
+ setAssetAtPath(child, rest, transform);
1094
+ }
1095
+ }
1096
+ function resolveDocumentAssets(doc, assetFields, assets) {
1097
+ const frontmatter = doc.frontmatter;
1098
+ for (const f of assetFields) {
1099
+ setAssetAtPath(frontmatter, f.path, (current) => {
1100
+ let value = current;
1101
+ if (value === void 0) {
1102
+ if (!f.assetTemplate) return void 0;
1103
+ value = materializeTemplate(f.assetTemplate, doc.enSlug);
1104
+ }
1105
+ return joinPublicPath(assets.publicPath, value);
1106
+ });
1107
+ }
1108
+ }
1109
+
1110
+ // src/inline/tokens.ts
1111
+ var TOKEN_OPEN = "${{";
1112
+ var ESCAPE_SEQUENCE = "$\\{{";
1113
+ var ESCAPE_RESULT = "${{";
1114
+ function placeholderMarker(n) {
1115
+ return `%%${n}%%`;
1116
+ }
1117
+ function findTokenEnd(body, start) {
1118
+ let i = start;
1119
+ let inString = false;
1120
+ while (i < body.length) {
1121
+ const ch = body[i];
1122
+ if (ch === "\n") return -1;
1123
+ if (inString) {
1124
+ if (ch === "\\") {
1125
+ i += 2;
1126
+ continue;
1127
+ }
1128
+ if (ch === '"') inString = false;
1129
+ i += 1;
1130
+ continue;
1131
+ }
1132
+ if (ch === '"') {
1133
+ inString = true;
1134
+ i += 1;
1135
+ continue;
1136
+ }
1137
+ if (ch === "}" && body[i + 1] === "}") return i;
1138
+ i += 1;
1139
+ }
1140
+ return -1;
1141
+ }
1142
+ function parseTokenInner(inner) {
1143
+ const colon = inner.indexOf(":");
1144
+ if (colon === -1) {
1145
+ return { ok: false, reason: `missing kind separator ":"` };
1146
+ }
1147
+ const kind = inner.slice(0, colon);
1148
+ const rest = inner.slice(colon + 1);
1149
+ switch (kind) {
1150
+ case "static": {
1151
+ let text;
1152
+ try {
1153
+ text = JSON.parse(rest);
1154
+ } catch {
1155
+ return { ok: false, reason: `static value is not a valid JSON string` };
1156
+ }
1157
+ if (typeof text !== "string") {
1158
+ return { ok: false, reason: `static value must be a JSON string` };
1159
+ }
1160
+ return { ok: true, token: { kind: "static", text } };
1161
+ }
1162
+ case "relation": {
1163
+ const parts = rest.split(":");
1164
+ if (parts.length < 2 || parts.length > 3) {
1165
+ return {
1166
+ ok: false,
1167
+ reason: `relation must be relation:<typeId>:<enSlug> or relation:<typeId>:<enSlug>:slug`
1168
+ };
1169
+ }
1170
+ const [targetTypeId, enSlug, mode] = parts;
1171
+ if (!targetTypeId || !enSlug) {
1172
+ return { ok: false, reason: `relation is missing a typeId or enSlug` };
1173
+ }
1174
+ if (mode !== void 0 && mode !== "slug") {
1175
+ return { ok: false, reason: `relation mode must be "slug" (got "${mode}")` };
1176
+ }
1177
+ return {
1178
+ ok: true,
1179
+ token: {
1180
+ kind: "relation",
1181
+ targetTypeId,
1182
+ enSlug,
1183
+ mode: mode === "slug" ? "slug" : "url"
1184
+ }
1185
+ };
1186
+ }
1187
+ case "asset": {
1188
+ if (!rest.startsWith("/")) {
1189
+ return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
1190
+ }
1191
+ return { ok: true, token: { kind: "asset", webPath: rest } };
1192
+ }
1193
+ case "var": {
1194
+ if (!rest) return { ok: false, reason: `var is missing a key` };
1195
+ return { ok: true, token: { kind: "var", key: rest } };
1196
+ }
1197
+ default:
1198
+ return { ok: false, reason: `unknown token kind "${kind}"` };
1199
+ }
1200
+ }
1201
+ function extractInlineTokens(body) {
1202
+ const tokens = [];
1203
+ const malformed = [];
1204
+ let out = "";
1205
+ let i = 0;
1206
+ while (i < body.length) {
1207
+ const open = body.indexOf(TOKEN_OPEN, i);
1208
+ if (open === -1) {
1209
+ out += body.slice(i);
1210
+ break;
1211
+ }
1212
+ out += body.slice(i, open);
1213
+ const contentStart = open + TOKEN_OPEN.length;
1214
+ const end = findTokenEnd(body, contentStart);
1215
+ if (end === -1) {
1216
+ malformed.push({
1217
+ raw: body.slice(open),
1218
+ reason: `unterminated token (missing "}}")`,
1219
+ index: open
1220
+ });
1221
+ out += body.slice(open);
1222
+ break;
1223
+ }
1224
+ const raw = body.slice(open, end + 2);
1225
+ const inner = body.slice(contentStart, end);
1226
+ const parsed = parseTokenInner(inner);
1227
+ if (parsed.ok) {
1228
+ tokens.push({ ...parsed.token, raw });
1229
+ out += placeholderMarker(tokens.length);
1230
+ } else {
1231
+ malformed.push({ raw, reason: parsed.reason, index: open });
1232
+ out += raw;
1233
+ }
1234
+ i = end + 2;
1235
+ }
1236
+ return { placeholderBody: out, tokens, malformed };
1237
+ }
1238
+ function fillPlaceholders(body, resolvedValues) {
1239
+ return body.replace(/%%(\d+)%%/g, (match, n) => {
1240
+ const idx = Number(n) - 1;
1241
+ return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
1242
+ });
1243
+ }
1244
+ function unescapeInlineTokens(body) {
1245
+ return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
1246
+ }
1247
+ function countMarkerOccurrences(body, n) {
1248
+ return body.split(placeholderMarker(n)).length - 1;
1249
+ }
1250
+ function replaceInlineSpans(body, opts) {
1251
+ let out = "";
1252
+ let i = 0;
1253
+ while (i < body.length) {
1254
+ if (body.startsWith(ESCAPE_SEQUENCE, i)) {
1255
+ out += opts.escape();
1256
+ i += ESCAPE_SEQUENCE.length;
1257
+ continue;
1258
+ }
1259
+ if (body.startsWith(TOKEN_OPEN, i)) {
1260
+ const end = findTokenEnd(body, i + TOKEN_OPEN.length);
1261
+ if (end !== -1) {
1262
+ const raw = body.slice(i, end + 2);
1263
+ const inner = body.slice(i + TOKEN_OPEN.length, end);
1264
+ out += opts.token(raw, inner);
1265
+ i = end + 2;
1266
+ continue;
1267
+ }
1268
+ }
1269
+ out += body[i];
1270
+ i += 1;
1271
+ }
1272
+ return out;
1273
+ }
1274
+ function maskInlineTokensForMdx(body) {
1275
+ return replaceInlineSpans(body, {
1276
+ token: () => "inline",
1277
+ escape: () => "$"
1278
+ });
1279
+ }
1280
+
1281
+ // src/inline/resolve-tokens.ts
1282
+ function slugKey(typeId, enSlug, locale) {
1283
+ return `${typeId}\0${enSlug}\0${locale}`;
1284
+ }
1285
+ function isStringRecord(value) {
1286
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1287
+ return Object.values(value).every((v) => typeof v === "string");
1288
+ }
1289
+ function createInlineResolver(config) {
1290
+ const urlBuilder = createUrlBuilder(config);
1291
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
1292
+ const storePath = resolveStorePath(config);
1293
+ let slugCache = null;
1294
+ const localizedSlugIndex = () => {
1295
+ let mtime = 0;
1296
+ try {
1297
+ mtime = fs14__default.default.statSync(storePath).mtimeMs;
1298
+ } catch {
1299
+ mtime = 0;
1300
+ }
1301
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
1302
+ const index = /* @__PURE__ */ new Map();
1303
+ try {
1304
+ const db = openStore(config, "readonly");
1305
+ try {
1306
+ for (const row of bulkLoadTranslations(db)) {
1307
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1308
+ }
1309
+ } finally {
1310
+ db.close();
1311
+ }
1312
+ } catch {
1313
+ }
1314
+ slugCache = { mtime, index };
1315
+ return index;
1316
+ };
1317
+ const localizedSlug2 = (typeId, enSlug, locale) => {
1318
+ if (locale === config.defaultLocale) return enSlug;
1319
+ const index = localizedSlugIndex();
1320
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1321
+ if (direct) return direct;
1322
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1323
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1324
+ if (hit) return hit;
1325
+ }
1326
+ return enSlug;
1327
+ };
1328
+ return {
1329
+ resolve(token, enFrontmatter, locale) {
1330
+ switch (token.kind) {
1331
+ case "static":
1332
+ return token.text;
1333
+ case "var": {
1334
+ const vars = enFrontmatter.vars;
1335
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1336
+ return vars[token.key];
1337
+ }
1338
+ return "";
1339
+ }
1340
+ case "asset": {
1341
+ const assets = config.assets;
1342
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1343
+ }
1344
+ case "relation": {
1345
+ const type = typeById.get(token.targetTypeId);
1346
+ if (!type) return "";
1347
+ if (token.mode === "slug") return token.enSlug;
1348
+ if (!isRoutableType(type)) return "";
1349
+ const slug = localizedSlug2(token.targetTypeId, token.enSlug, locale);
1350
+ return urlBuilder.resolvePath(type.path, slug, locale);
1351
+ }
1352
+ }
1353
+ }
1354
+ };
1355
+ }
1356
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1357
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1358
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1359
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1360
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1361
+ }
1362
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1363
+ const { tokens } = extractInlineTokens(enRawBody);
1364
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1365
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1366
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1367
+ }
1368
+
903
1369
  // src/loader/normalize-en.ts
904
1370
  function normalizeEnFrontmatter(data) {
905
1371
  const out = { ...data };
@@ -925,15 +1391,15 @@ function isPostFile(name) {
925
1391
  return isPublishableContentFile(name);
926
1392
  }
927
1393
  function listEnFiles(contentDir) {
928
- if (!fs2__default.default.existsSync(contentDir)) return [];
929
- return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path3__default.default.join(contentDir, f));
1394
+ if (!fs14__default.default.existsSync(contentDir)) return [];
1395
+ return fs14__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path3__default.default.join(contentDir, f));
930
1396
  }
931
1397
  function slugFromPath(filePath) {
932
1398
  return path3__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
933
1399
  }
934
1400
  function parseEnMdx(filePath, config, type) {
935
- const raw = fs2__default.default.readFileSync(filePath, "utf8");
936
- const parsed = matter__default.default(raw);
1401
+ const raw = fs14__default.default.readFileSync(filePath, "utf8");
1402
+ const parsed = matter4__default.default(raw);
937
1403
  const slug = slugFromPath(filePath);
938
1404
  const normalized = normalizeEnFrontmatter(parsed.data);
939
1405
  const { builtin, rest, issues: builtinIssues } = extractBuiltinEnFields(
@@ -971,6 +1437,7 @@ function parseEnMdx(filePath, config, type) {
971
1437
  updatedAt: builtin.updatedAt,
972
1438
  noindex: builtin.noindex,
973
1439
  canonicalPathOverride: builtin.canonicalPathOverride,
1440
+ vars: builtin.vars,
974
1441
  slug,
975
1442
  locale: config.defaultLocale
976
1443
  },
@@ -995,12 +1462,15 @@ function parseEnMdx(filePath, config, type) {
995
1462
  noindex: builtin.noindex,
996
1463
  canonicalPathOverride: builtin.canonicalPathOverride,
997
1464
  frontmatter,
998
- content: parsed.content,
1465
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1466
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1467
+ // A stray body is reported separately by `scribe validate`.
1468
+ content: type.body === false ? "" : parsed.content,
999
1469
  filePath
1000
1470
  };
1001
1471
  return { document: document2, issues };
1002
1472
  }
1003
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1473
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
1004
1474
  const localeFm = JSON.parse(row.frontmatter_json);
1005
1475
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
1006
1476
  const seo = seoFieldsFromEn(enDoc);
@@ -1011,6 +1481,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
1011
1481
  config.defaultLocale,
1012
1482
  config.localeRouting
1013
1483
  );
1484
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1485
+ const content = inlineResolver ? fillTranslatedInlineBody(
1486
+ preparedBody,
1487
+ enDoc.content,
1488
+ enDoc.frontmatter,
1489
+ row.locale,
1490
+ inlineResolver
1491
+ ) : preparedBody;
1014
1492
  return {
1015
1493
  slug: row.slug,
1016
1494
  enSlug: row.en_slug,
@@ -1020,14 +1498,19 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
1020
1498
  noindex: seo.noindex,
1021
1499
  canonicalPathOverride: seo.canonicalPathOverride,
1022
1500
  frontmatter,
1023
- content: prepareTranslatedMdxBody(row.body).body
1501
+ content
1024
1502
  };
1025
1503
  }
1026
1504
  var DEV_REVALIDATE_MS = 1500;
1027
- function createContentLoader(config, type) {
1505
+ var contentVersion = 0;
1506
+ function bumpContentVersion() {
1507
+ contentVersion++;
1508
+ }
1509
+ function createContentLoader(config, type, options = {}) {
1028
1510
  let cached = null;
1029
1511
  let signature = "";
1030
1512
  let lastCheck = 0;
1513
+ let builtVersion = contentVersion;
1031
1514
  const contentDir = path3__default.default.join(
1032
1515
  /* turbopackIgnore: true */
1033
1516
  config.rootDir,
@@ -1035,18 +1518,34 @@ function createContentLoader(config, type) {
1035
1518
  );
1036
1519
  const storePath = resolveStorePath(config);
1037
1520
  const isProd = process.env.NODE_ENV === "production";
1521
+ const assets = config.assets;
1522
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1523
+ const resolveAssets = (doc) => {
1524
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1525
+ };
1526
+ const inlineResolver = options.inlineResolver;
1527
+ const resolveEnInline = (doc) => {
1528
+ if (inlineResolver) {
1529
+ doc.content = substituteEnInlineBody(
1530
+ doc.content,
1531
+ doc.frontmatter,
1532
+ config.defaultLocale,
1533
+ inlineResolver
1534
+ );
1535
+ }
1536
+ };
1038
1537
  function computeSignature() {
1039
1538
  const files = listEnFiles(contentDir);
1040
1539
  let newest = 0;
1041
1540
  for (const f of files) {
1042
1541
  try {
1043
- newest = Math.max(newest, fs2__default.default.statSync(f).mtimeMs);
1542
+ newest = Math.max(newest, fs14__default.default.statSync(f).mtimeMs);
1044
1543
  } catch {
1045
1544
  }
1046
1545
  }
1047
1546
  let store = 0;
1048
1547
  try {
1049
- store = fs2__default.default.statSync(storePath).mtimeMs;
1548
+ store = fs14__default.default.statSync(storePath).mtimeMs;
1050
1549
  } catch {
1051
1550
  }
1052
1551
  return `${files.length}:${newest}:${store}`;
@@ -1092,16 +1591,28 @@ function createContentLoader(config, type) {
1092
1591
  for (const row of rowsByLocale.get(locale) ?? []) {
1093
1592
  const enDoc = englishBySlug.get(row.en_slug);
1094
1593
  if (!enDoc) continue;
1095
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1594
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1595
+ resolveAssets(doc);
1096
1596
  bySlug.set(doc.slug, doc);
1097
1597
  byEnSlug.set(row.en_slug, doc);
1098
1598
  }
1099
1599
  out.set(locale, { bySlug, byEnSlug });
1100
1600
  }
1601
+ for (const doc of englishBySlug.values()) {
1602
+ resolveAssets(doc);
1603
+ resolveEnInline(doc);
1604
+ }
1101
1605
  return out;
1102
1606
  }
1103
1607
  return () => {
1104
1608
  if (cached) {
1609
+ if (contentVersion !== builtVersion) {
1610
+ cached = build();
1611
+ builtVersion = contentVersion;
1612
+ lastCheck = Date.now();
1613
+ signature = isProd ? "" : computeSignature();
1614
+ return cached;
1615
+ }
1105
1616
  if (isProd) return cached;
1106
1617
  const now = Date.now();
1107
1618
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -1114,6 +1625,7 @@ function createContentLoader(config, type) {
1114
1625
  }
1115
1626
  lastCheck = Date.now();
1116
1627
  cached = build();
1628
+ builtVersion = contentVersion;
1117
1629
  signature = isProd ? "" : computeSignature();
1118
1630
  return cached;
1119
1631
  };
@@ -1126,7 +1638,7 @@ function readEnDocument(config, type, enSlug) {
1126
1638
  );
1127
1639
  for (const ext of [".mdx", ".md"]) {
1128
1640
  const filePath = path3__default.default.join(contentDir, `${enSlug}${ext}`);
1129
- if (!fs2__default.default.existsSync(filePath)) continue;
1641
+ if (!fs14__default.default.existsSync(filePath)) continue;
1130
1642
  const { document: document2 } = parseEnMdx(filePath, config, type);
1131
1643
  return document2;
1132
1644
  }
@@ -1135,7 +1647,9 @@ function readEnDocument(config, type, enSlug) {
1135
1647
  function getTranslatablePayload(doc, type) {
1136
1648
  return {
1137
1649
  frontmatter: pickTranslatable(doc.frontmatter, type.schema),
1138
- body: doc.content
1650
+ // Bodyless types never contribute a body to any translation payload, hash,
1651
+ // or snapshot — regardless of what a document's `content` happens to hold.
1652
+ body: type.body === false ? "" : doc.content
1139
1653
  };
1140
1654
  }
1141
1655
 
@@ -1223,8 +1737,11 @@ function comparatorFor(orderBy) {
1223
1737
  return (a, b) => a.slug.localeCompare(b.slug);
1224
1738
  }
1225
1739
  }
1226
- function buildRuntime(config, type, getRuntime) {
1227
- const load = createContentLoader(config, type);
1740
+ function buildRuntime(config, type, getRuntime, options = {}) {
1741
+ const load = createContentLoader(config, type, {
1742
+ resolveAssets: options.resolveAssets,
1743
+ inlineResolver: options.inlineResolver
1744
+ });
1228
1745
  const relationFields = new Map(
1229
1746
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1230
1747
  );
@@ -1241,12 +1758,12 @@ function buildRuntime(config, type, getRuntime) {
1241
1758
  id: type.id,
1242
1759
  config: type,
1243
1760
  load,
1244
- list(locale = config.defaultLocale, options = {}) {
1761
+ list(locale = config.defaultLocale, options2 = {}) {
1245
1762
  const idx = load().get(locale);
1246
1763
  if (!idx) return [];
1247
1764
  const docs = Array.from(idx.bySlug.values());
1248
- docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1249
- return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1765
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1766
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1250
1767
  },
1251
1768
  get(slug, locale = config.defaultLocale) {
1252
1769
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1259,7 +1776,7 @@ function buildRuntime(config, type, getRuntime) {
1259
1776
  load(),
1260
1777
  type,
1261
1778
  config.localeRouting,
1262
- config.localeFallbacks[locale] ?? []
1779
+ config.localeFallbacks?.[locale] ?? []
1263
1780
  );
1264
1781
  if (result.document && type.path) {
1265
1782
  return {
@@ -1273,15 +1790,15 @@ function buildRuntime(config, type, getRuntime) {
1273
1790
  }
1274
1791
  return result;
1275
1792
  },
1276
- staticParams(options = {}) {
1793
+ staticParams(options2 = {}) {
1277
1794
  assertRoutable("staticParams");
1278
1795
  const all = load();
1279
1796
  const enIdx = all.get(config.defaultLocale);
1280
1797
  if (!enIdx) return [];
1281
1798
  const params = [];
1282
- for (const locale of options.locales ?? config.locales) {
1799
+ for (const locale of options2.locales ?? config.locales) {
1283
1800
  const localeIdx = all.get(locale);
1284
- const fallbacks = config.localeFallbacks[locale] ?? [];
1801
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1285
1802
  for (const doc of enIdx.bySlug.values()) {
1286
1803
  let slug;
1287
1804
  if (locale === config.defaultLocale) {
@@ -1360,7 +1877,7 @@ function buildRuntime(config, type, getRuntime) {
1360
1877
  };
1361
1878
  return runtime;
1362
1879
  }
1363
- function createProject(config) {
1880
+ function createProject(config, options = {}) {
1364
1881
  const runtimes = /* @__PURE__ */ new Map();
1365
1882
  const getRuntime = (id) => {
1366
1883
  const runtime = runtimes.get(id);
@@ -1369,8 +1886,15 @@ function createProject(config) {
1369
1886
  }
1370
1887
  return runtime;
1371
1888
  };
1889
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config) : void 0;
1372
1890
  for (const type of config.types) {
1373
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1891
+ runtimes.set(
1892
+ type.id,
1893
+ buildRuntime(config, type, getRuntime, {
1894
+ resolveAssets: options.resolveAssets,
1895
+ inlineResolver
1896
+ })
1897
+ );
1374
1898
  }
1375
1899
  return {
1376
1900
  config,
@@ -1387,12 +1911,12 @@ function createProject(config) {
1387
1911
  }
1388
1912
  function enFileExists(config, type, enSlug) {
1389
1913
  const dir = path3__default.default.join(config.rootDir, type.contentDir);
1390
- return fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs2__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
1914
+ return fs14__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.mdx`)) || fs14__default.default.existsSync(path3__default.default.join(dir, `${enSlug}.md`));
1391
1915
  }
1392
1916
  function listEnSlugs(rootDir, contentDir) {
1393
1917
  const dir = path3__default.default.join(rootDir, contentDir);
1394
- if (!fs2__default.default.existsSync(dir)) return [];
1395
- return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1918
+ if (!fs14__default.default.existsSync(dir)) return [];
1919
+ return fs14__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1396
1920
  }
1397
1921
 
1398
1922
  // src/i18n/translation-index.ts
@@ -1488,10 +2012,10 @@ function redirectsFilePath(config, type) {
1488
2012
  }
1489
2013
  function loadTypeRedirectsFile(config, type) {
1490
2014
  const filePath = redirectsFilePath(config, type);
1491
- if (!fs2__default.default.existsSync(filePath)) return null;
2015
+ if (!fs14__default.default.existsSync(filePath)) return null;
1492
2016
  let raw;
1493
2017
  try {
1494
- raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
2018
+ raw = JSON.parse(fs14__default.default.readFileSync(filePath, "utf8"));
1495
2019
  } catch (error) {
1496
2020
  throw new Error(
1497
2021
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1799,8 +2323,8 @@ function getRedirectSourceSlugs(project) {
1799
2323
  // src/sitemap/join-base-url.ts
1800
2324
  function joinBaseUrl(baseUrl, pathname) {
1801
2325
  const origin = baseUrl.replace(/\/$/, "");
1802
- const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1803
- return `${origin}${path13}`;
2326
+ const path15 = pathname.startsWith("/") ? pathname : `/${pathname}`;
2327
+ return `${origin}${path15}`;
1804
2328
  }
1805
2329
 
1806
2330
  // src/sitemap/generate-sitemap.ts
@@ -1885,7 +2409,7 @@ async function generateSitemap(project, options) {
1885
2409
  // src/create-scribe.ts
1886
2410
  function createScribe(input) {
1887
2411
  const config = resolveConfig(input);
1888
- const project = createProject(config);
2412
+ const project = createProject(config, { resolveAssets: true, resolveInlineTokens: true });
1889
2413
  const scribe = {
1890
2414
  config,
1891
2415
  project,
@@ -1894,6 +2418,17 @@ function createScribe(input) {
1894
2418
  listRoutableTypes: project.listRoutableTypes,
1895
2419
  sitemap(options) {
1896
2420
  return generateSitemap(project, options);
2421
+ },
2422
+ assets: {
2423
+ url(ref, opts) {
2424
+ if (opts && Object.keys(opts).length > 0) {
2425
+ throw new Error(
2426
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
2427
+ );
2428
+ }
2429
+ const assetsConfig = config.assets;
2430
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
2431
+ }
1897
2432
  }
1898
2433
  };
1899
2434
  for (const type of config.types) {
@@ -1920,7 +2455,7 @@ function findConfigPath(cwd) {
1920
2455
  )
1921
2456
  ];
1922
2457
  for (const candidate of candidates) {
1923
- if (fs2__default.default.existsSync(candidate) && fs2__default.default.statSync(candidate).isFile()) {
2458
+ if (fs14__default.default.existsSync(candidate) && fs14__default.default.statSync(candidate).isFile()) {
1924
2459
  return candidate;
1925
2460
  }
1926
2461
  }
@@ -1972,7 +2507,7 @@ function validateTypeRedirects(project) {
1972
2507
  const globalFrom = /* @__PURE__ */ new Map();
1973
2508
  for (const type of project.config.types) {
1974
2509
  const filePath = path3__default.default.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1975
- if (!fs2__default.default.existsSync(filePath)) continue;
2510
+ if (!fs14__default.default.existsSync(filePath)) continue;
1976
2511
  let loaded;
1977
2512
  try {
1978
2513
  loaded = loadTypeRedirectsFile(project.config, type);
@@ -2125,8 +2660,8 @@ function buildEnSlugIndex(config) {
2125
2660
  }
2126
2661
  function listEnSlugsForType(rootDir, contentDir) {
2127
2662
  const dir = path3__default.default.join(rootDir, contentDir);
2128
- if (!fs2__default.default.existsSync(dir)) return [];
2129
- return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2663
+ if (!fs14__default.default.existsSync(dir)) return [];
2664
+ return fs14__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2130
2665
  }
2131
2666
  function validateRelations(project) {
2132
2667
  const { config } = project;
@@ -2234,13 +2769,14 @@ function collectImagePaths(frontmatter, body) {
2234
2769
  function assetFilePath(assetsPath, webPath) {
2235
2770
  return path3__default.default.join(assetsPath, webPath.replace(/^\//, ""));
2236
2771
  }
2237
- function validateDocumentAssets(config, input) {
2772
+ function validateDocumentAssets(config, input, skipPaths) {
2238
2773
  const assetsPath = config.assetsPath;
2239
2774
  if (!assetsPath) return [];
2240
2775
  const issues = [];
2241
2776
  for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
2777
+ if (skipPaths?.has(webPath)) continue;
2242
2778
  const filePath = assetFilePath(assetsPath, webPath);
2243
- if (fs2__default.default.existsSync(filePath)) continue;
2779
+ if (fs14__default.default.existsSync(filePath)) continue;
2244
2780
  issues.push({
2245
2781
  level: "warning",
2246
2782
  contentType: input.contentType,
@@ -2252,6 +2788,115 @@ function validateDocumentAssets(config, input) {
2252
2788
  }
2253
2789
  return issues;
2254
2790
  }
2791
+ function collectAssetValues(container, path15, out, fieldPathSoFar = []) {
2792
+ const [head, ...rest] = path15;
2793
+ if (head === void 0) return;
2794
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
2795
+ const record = container;
2796
+ if (rest.length === 0) {
2797
+ const value = record[head];
2798
+ out.push({
2799
+ fieldPath: [...fieldPathSoFar, head].join("."),
2800
+ value: typeof value === "string" ? value : void 0
2801
+ });
2802
+ return;
2803
+ }
2804
+ if (rest[0] === "*") {
2805
+ const arr = record[head];
2806
+ if (!Array.isArray(arr)) {
2807
+ out.push({ fieldPath: [...fieldPathSoFar, head, "*", ...rest.slice(1)].join("."), value: void 0 });
2808
+ return;
2809
+ }
2810
+ arr.forEach((item) => {
2811
+ collectAssetValues(item, rest.slice(1), out, [...fieldPathSoFar, head, "*"]);
2812
+ });
2813
+ return;
2814
+ }
2815
+ collectAssetValues(record[head], rest, out, [...fieldPathSoFar, head]);
2816
+ }
2817
+ function normalizeDir(dir) {
2818
+ return `/${dir.replace(/^\/+|\/+$/g, "")}`;
2819
+ }
2820
+ function validateDeclaredAssetFields(config, input) {
2821
+ const assetsPath = config.assetsPath;
2822
+ if (!assetsPath) return [];
2823
+ const issues = [];
2824
+ const attrib = (fieldPath, level, message) => issues.push({
2825
+ level,
2826
+ contentType: input.contentType,
2827
+ enSlug: input.enSlug,
2828
+ locale: input.locale,
2829
+ field: fieldPath,
2830
+ message
2831
+ });
2832
+ for (const f of listAssetFields(input.schema)) {
2833
+ const locations = [];
2834
+ collectAssetValues(input.frontmatter, f.path, locations);
2835
+ for (const { fieldPath, value } of locations) {
2836
+ let effective = value;
2837
+ if (effective === void 0 && f.assetTemplate) {
2838
+ effective = f.assetTemplate.split("{slug}").join(input.enSlug);
2839
+ }
2840
+ if (effective === void 0) {
2841
+ if (!f.assetOptional) {
2842
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} is required but missing`);
2843
+ }
2844
+ continue;
2845
+ }
2846
+ if (f.assetDir) {
2847
+ const dir = normalizeDir(f.assetDir);
2848
+ if (!(effective === dir || effective.startsWith(`${dir}/`))) {
2849
+ attrib(
2850
+ fieldPath,
2851
+ "error",
2852
+ `${input.contentType}/${input.enSlug}: ${fieldPath} value ${effective} is outside declared dir ${dir}`
2853
+ );
2854
+ }
2855
+ }
2856
+ const filePath = assetFilePath(assetsPath, effective);
2857
+ if (!fs14__default.default.existsSync(filePath)) {
2858
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} not found`);
2859
+ continue;
2860
+ }
2861
+ if (f.assetFormats && f.assetFormats.length > 0) {
2862
+ const ext = path3__default.default.extname(effective).slice(1).toLowerCase();
2863
+ if (!f.assetFormats.includes(ext)) {
2864
+ attrib(
2865
+ fieldPath,
2866
+ "warning",
2867
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} extension .${ext} not in formats [${f.assetFormats.join(", ")}]`
2868
+ );
2869
+ }
2870
+ }
2871
+ if (f.assetMaxKB !== void 0) {
2872
+ try {
2873
+ const sizeKB = fs14__default.default.statSync(filePath).size / 1024;
2874
+ if (sizeKB > f.assetMaxKB) {
2875
+ attrib(
2876
+ fieldPath,
2877
+ "warning",
2878
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} is ${Math.round(sizeKB)}KB, over the ${f.assetMaxKB}KB budget`
2879
+ );
2880
+ }
2881
+ } catch {
2882
+ }
2883
+ }
2884
+ }
2885
+ }
2886
+ return issues;
2887
+ }
2888
+ function collectDeclaredAssetPaths(frontmatter, enSlug, schema, fields) {
2889
+ const out = /* @__PURE__ */ new Set();
2890
+ for (const f of listAssetFields(schema)) {
2891
+ const locations = [];
2892
+ collectAssetValues(frontmatter, f.path, locations);
2893
+ for (const { value } of locations) {
2894
+ if (value !== void 0) out.add(value);
2895
+ else if (f.assetTemplate) out.add(f.assetTemplate.split("{slug}").join(enSlug));
2896
+ }
2897
+ }
2898
+ return out;
2899
+ }
2255
2900
 
2256
2901
  // src/core/localized-slug.ts
2257
2902
  function findLocaleSuffixInSlug(slug, localeCodes) {
@@ -2294,15 +2939,151 @@ function validateTranslationSlugSuffixes(config, db) {
2294
2939
  }
2295
2940
  return issues;
2296
2941
  }
2942
+ function listEnSlugs3(rootDir, contentDir) {
2943
+ const dir = path3__default.default.join(rootDir, contentDir);
2944
+ if (!fs14__default.default.existsSync(dir)) return [];
2945
+ return fs14__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2946
+ }
2947
+ function isStringRecord2(value) {
2948
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2949
+ return Object.values(value).every((v) => typeof v === "string");
2950
+ }
2951
+ function assetFileExists(config, webPath) {
2952
+ const assetsPath = config.assetsPath;
2953
+ if (!assetsPath) return true;
2954
+ return fs14__default.default.existsSync(path3__default.default.join(assetsPath, webPath.replace(/^\//, "")));
2955
+ }
2956
+ function validateInlineTokens(config) {
2957
+ const issues = [];
2958
+ const slugIndex = buildEnSlugIndex(config);
2959
+ const typeIds = new Set(config.types.map((t) => t.id));
2960
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
2961
+ for (const type of config.types) {
2962
+ for (const enSlug of listEnSlugs3(config.rootDir, type.contentDir)) {
2963
+ const doc = readEnDocument(config, type, enSlug);
2964
+ if (!doc) continue;
2965
+ const { tokens, malformed } = extractInlineTokens(doc.content);
2966
+ for (const bad of malformed) {
2967
+ issues.push({
2968
+ level: "error",
2969
+ contentType: type.id,
2970
+ enSlug,
2971
+ field: "body",
2972
+ message: `Malformed inline token (${bad.reason}): ${bad.raw}`
2973
+ });
2974
+ }
2975
+ let varsReported = false;
2976
+ const vars = doc.frontmatter.vars;
2977
+ for (const token of tokens) {
2978
+ switch (token.kind) {
2979
+ case "static":
2980
+ break;
2981
+ case "relation": {
2982
+ if (!typeIds.has(token.targetTypeId)) {
2983
+ issues.push({
2984
+ level: "error",
2985
+ contentType: type.id,
2986
+ enSlug,
2987
+ field: "body",
2988
+ message: `Inline relation token targets unknown type "${token.targetTypeId}"`
2989
+ });
2990
+ break;
2991
+ }
2992
+ const targetSlugs = slugIndex.get(token.targetTypeId) ?? /* @__PURE__ */ new Set();
2993
+ if (!targetSlugs.has(token.enSlug)) {
2994
+ issues.push({
2995
+ level: "error",
2996
+ contentType: type.id,
2997
+ enSlug,
2998
+ field: "body",
2999
+ message: `Inline relation token references "${token.enSlug}", but no ${token.targetTypeId} doc has that slug`
3000
+ });
3001
+ break;
3002
+ }
3003
+ if (token.mode === "url") {
3004
+ const targetType = typeById.get(token.targetTypeId);
3005
+ if (!isRoutableType(targetType)) {
3006
+ issues.push({
3007
+ level: "error",
3008
+ contentType: type.id,
3009
+ enSlug,
3010
+ field: "body",
3011
+ message: `Inline relation token resolves a URL for "${token.targetTypeId}", which has no path template (not routable)`
3012
+ });
3013
+ }
3014
+ }
3015
+ break;
3016
+ }
3017
+ case "asset": {
3018
+ if (!assetFileExists(config, token.webPath)) {
3019
+ issues.push({
3020
+ level: "error",
3021
+ contentType: type.id,
3022
+ enSlug,
3023
+ field: "body",
3024
+ message: `Inline asset token references ${token.webPath}, which is missing on disk`
3025
+ });
3026
+ }
3027
+ break;
3028
+ }
3029
+ case "var": {
3030
+ if (vars !== void 0 && !isStringRecord2(vars)) {
3031
+ if (!varsReported) {
3032
+ varsReported = true;
3033
+ issues.push({
3034
+ level: "error",
3035
+ contentType: type.id,
3036
+ enSlug,
3037
+ field: "vars",
3038
+ message: `Frontmatter "vars" must be a string-to-string map`
3039
+ });
3040
+ }
3041
+ break;
3042
+ }
3043
+ if (!isStringRecord2(vars) || !(token.key in vars)) {
3044
+ issues.push({
3045
+ level: "error",
3046
+ contentType: type.id,
3047
+ enSlug,
3048
+ field: "body",
3049
+ message: `Inline var token references "${token.key}", absent from this document's vars map`
3050
+ });
3051
+ }
3052
+ break;
3053
+ }
3054
+ }
3055
+ }
3056
+ }
3057
+ }
3058
+ return issues;
3059
+ }
2297
3060
 
2298
3061
  // src/validate/validate-project.ts
2299
- function listEnSlugs3(rootDir, contentDir) {
3062
+ function listEnSlugs4(rootDir, contentDir) {
2300
3063
  const dir = path3__default.default.join(rootDir, contentDir);
2301
- if (!fs2__default.default.existsSync(dir)) return [];
2302
- return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3064
+ if (!fs14__default.default.existsSync(dir)) return [];
3065
+ return fs14__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3066
+ }
3067
+ function validateBodylessEntry(issues, config, type, enSlug) {
3068
+ const contentDir = path3__default.default.join(config.rootDir, type.contentDir);
3069
+ for (const ext of [".mdx", ".md"]) {
3070
+ const filePath = path3__default.default.join(contentDir, `${enSlug}${ext}`);
3071
+ if (!fs14__default.default.existsSync(filePath)) continue;
3072
+ const body = matter4__default.default(fs14__default.default.readFileSync(filePath, "utf8")).content;
3073
+ if (body.trim().length > 0) {
3074
+ issues.push({
3075
+ level: "error",
3076
+ contentType: type.id,
3077
+ enSlug,
3078
+ field: "body",
3079
+ message: `type "${type.id}" is frontmatter-only (body: false) but the entry has body content`
3080
+ });
3081
+ }
3082
+ return;
3083
+ }
2303
3084
  }
2304
3085
  function validateDocumentMdxBody(issues, input) {
2305
- const preparedBody = prepareTranslatedMdxBody(input.body).body;
3086
+ const preparedBody = prepareTranslatedMdxBody(maskInlineTokensForMdx(input.body)).body;
2306
3087
  const mdxValidation = validateMdxBody(preparedBody);
2307
3088
  if (!mdxValidation.ok) {
2308
3089
  issues.push({
@@ -2319,7 +3100,7 @@ function validateProject(config) {
2319
3100
  const issues = [];
2320
3101
  const project = createProject(config);
2321
3102
  const storePath = project.storePath;
2322
- if (!fs2__default.default.existsSync(storePath)) {
3103
+ if (!fs14__default.default.existsSync(storePath)) {
2323
3104
  issues.push({
2324
3105
  level: "error",
2325
3106
  message: `Missing store.sqlite at ${storePath}`
@@ -2328,7 +3109,7 @@ function validateProject(config) {
2328
3109
  }
2329
3110
  const db = openStore(config, "readonly");
2330
3111
  for (const type of config.types) {
2331
- const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
3112
+ const enSlugs = listEnSlugs4(config.rootDir, type.contentDir);
2332
3113
  const englishSlugs = new Set(enSlugs);
2333
3114
  if (!type.translate && isRoutableType(type)) {
2334
3115
  issues.push({
@@ -2365,20 +3146,41 @@ function validateProject(config) {
2365
3146
  message: issue.message
2366
3147
  });
2367
3148
  }
2368
- for (const issue of validateDocumentAssets(config, {
3149
+ for (const issue of validateDeclaredAssetFields(config, {
2369
3150
  contentType: type.id,
2370
3151
  enSlug,
2371
3152
  frontmatter: enDoc.frontmatter,
2372
- body: enDoc.content
3153
+ schema: type.schema
2373
3154
  })) {
2374
3155
  issues.push(issue);
2375
3156
  }
2376
- validateDocumentMdxBody(issues, {
2377
- contentType: type.id,
3157
+ const declaredAssetPaths = collectDeclaredAssetPaths(
3158
+ enDoc.frontmatter,
2378
3159
  enSlug,
2379
- locale: config.defaultLocale,
2380
- body: enDoc.content
2381
- });
3160
+ type.schema
3161
+ );
3162
+ for (const issue of validateDocumentAssets(
3163
+ config,
3164
+ {
3165
+ contentType: type.id,
3166
+ enSlug,
3167
+ frontmatter: enDoc.frontmatter,
3168
+ body: enDoc.content
3169
+ },
3170
+ declaredAssetPaths
3171
+ )) {
3172
+ issues.push(issue);
3173
+ }
3174
+ if (type.body === false) {
3175
+ validateBodylessEntry(issues, config, type, enSlug);
3176
+ } else {
3177
+ validateDocumentMdxBody(issues, {
3178
+ contentType: type.id,
3179
+ enSlug,
3180
+ locale: config.defaultLocale,
3181
+ body: enDoc.content
3182
+ });
3183
+ }
2382
3184
  for (const locale of config.locales) {
2383
3185
  if (locale === config.defaultLocale) continue;
2384
3186
  const row = getTranslation(db, type.id, enSlug, locale);
@@ -2395,21 +3197,27 @@ function validateProject(config) {
2395
3197
  });
2396
3198
  }
2397
3199
  const preparedBody = prepareTranslatedMdxBody(row.body).body;
2398
- for (const issue of validateDocumentAssets(config, {
2399
- contentType: type.id,
2400
- enSlug,
2401
- locale,
2402
- frontmatter: localeFm,
2403
- body: preparedBody
2404
- })) {
3200
+ for (const issue of validateDocumentAssets(
3201
+ config,
3202
+ {
3203
+ contentType: type.id,
3204
+ enSlug,
3205
+ locale,
3206
+ frontmatter: localeFm,
3207
+ body: preparedBody
3208
+ },
3209
+ declaredAssetPaths
3210
+ )) {
2405
3211
  issues.push(issue);
2406
3212
  }
2407
- validateDocumentMdxBody(issues, {
2408
- contentType: type.id,
2409
- enSlug,
2410
- locale,
2411
- body: row.body
2412
- });
3213
+ if (type.body !== false) {
3214
+ validateDocumentMdxBody(issues, {
3215
+ contentType: type.id,
3216
+ enSlug,
3217
+ locale,
3218
+ body: row.body
3219
+ });
3220
+ }
2413
3221
  }
2414
3222
  }
2415
3223
  try {
@@ -2435,6 +3243,9 @@ function validateProject(config) {
2435
3243
  message: issue.message
2436
3244
  });
2437
3245
  }
3246
+ for (const issue of validateInlineTokens(config)) {
3247
+ issues.push(issue);
3248
+ }
2438
3249
  const dbForSuffix = openStore(config, "readonly");
2439
3250
  try {
2440
3251
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
@@ -2459,6 +3270,10 @@ function computePageEnHash(translatableFrontmatter, body) {
2459
3270
  const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
2460
3271
  return sha256(payload);
2461
3272
  }
3273
+ function computeTranslationEnHash(translatableFrontmatter, body) {
3274
+ const { placeholderBody } = extractInlineTokens(body);
3275
+ return computePageEnHash(translatableFrontmatter, placeholderBody);
3276
+ }
2462
3277
 
2463
3278
  // src/translate/worklist.ts
2464
3279
  function parseContentTypeFilter(contentType) {
@@ -2466,10 +3281,10 @@ function parseContentTypeFilter(contentType) {
2466
3281
  const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2467
3282
  return ids.length > 0 ? new Set(ids) : void 0;
2468
3283
  }
2469
- function listEnSlugs4(rootDir, contentDir) {
3284
+ function listEnSlugs5(rootDir, contentDir) {
2470
3285
  const dir = path3__default.default.join(rootDir, contentDir);
2471
- if (!fs2__default.default.existsSync(dir)) return [];
2472
- return fs2__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3286
+ if (!fs14__default.default.existsSync(dir)) return [];
3287
+ return fs14__default.default.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2473
3288
  }
2474
3289
  function buildWorklist(config, options = {}) {
2475
3290
  const db = openStore(config, "readonly");
@@ -2478,12 +3293,16 @@ function buildWorklist(config, options = {}) {
2478
3293
  const contentTypes = parseContentTypeFilter(options.contentType);
2479
3294
  for (const type of config.types) {
2480
3295
  if (contentTypes && !contentTypes.has(type.id)) continue;
2481
- const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
3296
+ if (!isTypeTranslatable(type)) {
3297
+ options.onSkipType?.(type);
3298
+ continue;
3299
+ }
3300
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs5(config.rootDir, type.contentDir);
2482
3301
  for (const enSlug of enSlugs) {
2483
3302
  const enDoc = readEnDocument(config, type, enSlug);
2484
3303
  if (!enDoc) continue;
2485
3304
  const payload = getTranslatablePayload(enDoc, type);
2486
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3305
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
2487
3306
  for (const locale of locales) {
2488
3307
  if (locale === config.defaultLocale) continue;
2489
3308
  const existing = getTranslation(db, type.id, enSlug, locale);
@@ -2945,9 +3764,22 @@ function buildTranslatableSubschema(schema) {
2945
3764
  if (Object.keys(out).length === 0) return null;
2946
3765
  return zod.z.object(out);
2947
3766
  }
2948
- function buildGeminiResponseSchema(schema, slugStrategy) {
3767
+ function buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) {
3768
+ const full = buildTranslatableSubschema(schema);
3769
+ if (!full) return null;
3770
+ const filtered = {};
3771
+ for (const [key, childSchema] of Object.entries(full.shape)) {
3772
+ if (!(key in translatableFrontmatter)) continue;
3773
+ const value = translatableFrontmatter[key];
3774
+ if (value === void 0 || value === null) continue;
3775
+ filtered[key] = childSchema;
3776
+ }
3777
+ if (Object.keys(filtered).length === 0) return null;
3778
+ return zod.z.object(filtered);
3779
+ }
3780
+ function buildGeminiResponseSchema(schema, slugStrategy, translatableFrontmatter) {
2949
3781
  try {
2950
- const translatable = buildTranslatableSubschema(schema);
3782
+ const translatable = translatableFrontmatter !== void 0 ? buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) : buildTranslatableSubschema(schema);
2951
3783
  const responseShape = {
2952
3784
  ...translatable ? { frontmatter: translatable } : {},
2953
3785
  body: zod.z.string()
@@ -2969,9 +3801,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2969
3801
 
2970
3802
  // src/translate/resolve-translate-config.ts
2971
3803
  var BUILTIN_TRANSLATE_RULES = [
3804
+ "Placeholders written as %%1%%, %%2%%, %%3%% (double percent signs around a number) are immutable. Reproduce each placeholder that appears in the source EXACTLY as-is, exactly once, and never translate, edit, renumber, or add spaces inside it. You may move a placeholder to a different position within its sentence when the target grammar requires it.",
2972
3805
  "Do not translate brand or product names unless the brand has a well-known localized name in the target market (rare). Keep the original spelling and capitalization.",
2973
3806
  "Return the MDX body with real line breaks; do not use JSON escape sequences like \\n or \\t in the body string.",
2974
- 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".'
3807
+ 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".',
3808
+ "Match the EN source format: when the EN body uses Markdown/MDX (headings, paragraphs, GFM tables, links), keep that structure in the translation. Do not convert markdown elements into raw HTML (<p>, <h2>, <table>, etc.).",
3809
+ "Otherwise match the format and components used in the EN source."
2975
3810
  ];
2976
3811
  function slugStrategyRules(slugStrategy) {
2977
3812
  if (slugStrategy === "localized") {
@@ -3015,12 +3850,13 @@ function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
3015
3850
  return pickTranslatable(rawFrontmatter, schema);
3016
3851
  }
3017
3852
  function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
3018
- const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
3019
- const merged = mergeStructuralOntoLocale(
3020
- frontmatter,
3021
- enDoc.frontmatter,
3853
+ const enFrontmatter = enDoc.frontmatter;
3854
+ const frontmatter = pruneOrphanNestedTranslations(
3855
+ sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema),
3856
+ enFrontmatter,
3022
3857
  typeSchema
3023
3858
  );
3859
+ const merged = mergeStructuralOntoLocale(frontmatter, enFrontmatter, typeSchema);
3024
3860
  const parsed = typeSchema.safeParse(merged);
3025
3861
  if (!parsed.success) {
3026
3862
  return { ok: false, error: formatZodIssues(parsed.error) };
@@ -3069,6 +3905,22 @@ function formatTranslateError(error) {
3069
3905
  }
3070
3906
  return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
3071
3907
  }
3908
+ function verifyInlineMarkers(enRawBody, translatedBody) {
3909
+ const { tokens } = extractInlineTokens(enRawBody);
3910
+ if (tokens.length === 0) return;
3911
+ const missing = [];
3912
+ const duplicated = [];
3913
+ for (let i = 1; i <= tokens.length; i++) {
3914
+ const count = countMarkerOccurrences(translatedBody, i);
3915
+ if (count === 0) missing.push(placeholderMarker(i));
3916
+ else if (count > 1) duplicated.push(placeholderMarker(i));
3917
+ }
3918
+ if (missing.length === 0 && duplicated.length === 0) return;
3919
+ const parts = [];
3920
+ if (missing.length > 0) parts.push(`missing ${missing.join(", ")}`);
3921
+ if (duplicated.length > 0) parts.push(`duplicated ${duplicated.join(", ")}`);
3922
+ throw new Error(`Inline token markers mismatch: ${parts.join("; ")}`);
3923
+ }
3072
3924
  function resolveContextLabel(enDoc, enSlug) {
3073
3925
  const fm = enDoc.frontmatter;
3074
3926
  for (const key of ["title", "name", "h1"]) {
@@ -3090,7 +3942,8 @@ function prepareTranslation(config, item, options, startedAt) {
3090
3942
  const enDoc = readEnDocument(config, type, item.enSlug);
3091
3943
  if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
3092
3944
  const payload = getTranslatablePayload(enDoc, type);
3093
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3945
+ const { placeholderBody } = extractInlineTokens(payload.body);
3946
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
3094
3947
  const db = openStore(config, "readonly");
3095
3948
  const existing = getTranslation(db, type.id, item.enSlug, item.locale);
3096
3949
  db.close();
@@ -3112,11 +3965,15 @@ function prepareTranslation(config, item, options, startedAt) {
3112
3965
  targetLocale: item.locale,
3113
3966
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
3114
3967
  translatableFrontmatter: payload.frontmatter,
3115
- enBody: payload.body,
3968
+ enBody: placeholderBody,
3116
3969
  slugStrategy: type.slugStrategy,
3117
3970
  previousError: item.previousError
3118
3971
  });
3119
- const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
3972
+ const responseSchema = buildGeminiResponseSchema(
3973
+ type.schema,
3974
+ type.slugStrategy,
3975
+ payload.frontmatter
3976
+ );
3120
3977
  return {
3121
3978
  status: "ready",
3122
3979
  prepared: {
@@ -3144,9 +4001,8 @@ function finalizeTranslation(config, prepared, output, options) {
3144
4001
  if (!validated.ok) {
3145
4002
  throw new Error(`Translation validation failed: ${validated.error}`);
3146
4003
  }
3147
- const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
3148
- output.parsed.body
3149
- );
4004
+ const { body: translatedBody, adjusted: mdxAdjusted } = type.body === false ? { body: "", adjusted: false } : assertValidTranslatedMdxBody(output.parsed.body);
4005
+ verifyInlineMarkers(payload.body, translatedBody);
3150
4006
  const writeDb = openStore(config, "readwrite");
3151
4007
  const snapshotId = options.snapshotId ?? recordEnSnapshot(
3152
4008
  config,
@@ -3860,7 +4716,7 @@ async function resumeTranslationJobs(config, options = {}) {
3860
4716
  return results;
3861
4717
  }
3862
4718
  function serializeMdx(frontmatter, content) {
3863
- return matter__default.default.stringify(content, frontmatter);
4719
+ return matter4__default.default.stringify(content, frontmatter);
3864
4720
  }
3865
4721
 
3866
4722
  // src/export/build-static-raw-exports.ts
@@ -3927,8 +4783,8 @@ function buildStaticRawExports(project, options = {}) {
3927
4783
  return out;
3928
4784
  }
3929
4785
  function rmDirIfExists(dir) {
3930
- if (fs2__default.default.existsSync(dir)) {
3931
- fs2__default.default.rmSync(dir, { recursive: true, force: true });
4786
+ if (fs14__default.default.existsSync(dir)) {
4787
+ fs14__default.default.rmSync(dir, { recursive: true, force: true });
3932
4788
  }
3933
4789
  }
3934
4790
  function writeStaticRawExports(project, options = {}) {
@@ -3944,8 +4800,8 @@ function writeStaticRawExports(project, options = {}) {
3944
4800
  const counts = /* @__PURE__ */ new Map();
3945
4801
  for (const item of exports) {
3946
4802
  const filePath = path3__default.default.join(outDir, item.relativePath);
3947
- fs2__default.default.mkdirSync(path3__default.default.dirname(filePath), { recursive: true });
3948
- fs2__default.default.writeFileSync(filePath, item.source, "utf8");
4803
+ fs14__default.default.mkdirSync(path3__default.default.dirname(filePath), { recursive: true });
4804
+ fs14__default.default.writeFileSync(filePath, item.source, "utf8");
3949
4805
  const key = `${item.typeId}/${item.locale}`;
3950
4806
  counts.set(key, (counts.get(key) ?? 0) + 1);
3951
4807
  }
@@ -3955,7 +4811,411 @@ function writeStaticRawExports(project, options = {}) {
3955
4811
  return { exports, written: exports.length };
3956
4812
  }
3957
4813
 
4814
+ // src/delete/plan.ts
4815
+ function isPlanBlocked(plan) {
4816
+ return plan.blocked.length > 0;
4817
+ }
4818
+ function deletedDocs(plan) {
4819
+ return [
4820
+ ...plan.roots.map((r) => ({ typeId: r.typeId, enSlug: r.enSlug })),
4821
+ ...plan.cascades.map((c) => ({ typeId: c.typeId, enSlug: c.enSlug }))
4822
+ ];
4823
+ }
4824
+ function docKey(typeId, enSlug) {
4825
+ return `${typeId}\0${enSlug}`;
4826
+ }
4827
+ function relationSlugsAt(frontmatter, path15) {
4828
+ const out = [];
4829
+ const walk = (container, remaining) => {
4830
+ const [head, ...rest] = remaining;
4831
+ if (head === void 0) return;
4832
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4833
+ const record = container;
4834
+ if (rest.length === 0) {
4835
+ const value = record[head];
4836
+ if (Array.isArray(value)) {
4837
+ for (const slug of value) if (typeof slug === "string" && slug) out.push(slug);
4838
+ } else if (typeof value === "string" && value) {
4839
+ out.push(value);
4840
+ }
4841
+ return;
4842
+ }
4843
+ if (rest[0] === "*") {
4844
+ const arr = record[head];
4845
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4846
+ return;
4847
+ }
4848
+ walk(record[head], rest);
4849
+ };
4850
+ walk(frontmatter, path15);
4851
+ return out;
4852
+ }
4853
+ function assetValuesAt(frontmatter, field2, enSlug) {
4854
+ const out = [];
4855
+ const walk = (container, remaining) => {
4856
+ const [head, ...rest] = remaining;
4857
+ if (head === void 0) return;
4858
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4859
+ const record = container;
4860
+ if (rest.length === 0) {
4861
+ const value = record[head];
4862
+ if (typeof value === "string" && value) out.push(value);
4863
+ else if (value === void 0 && field2.assetTemplate) {
4864
+ out.push(field2.assetTemplate.split("{slug}").join(enSlug));
4865
+ }
4866
+ return;
4867
+ }
4868
+ if (rest[0] === "*") {
4869
+ const arr = record[head];
4870
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4871
+ return;
4872
+ }
4873
+ walk(record[head], rest);
4874
+ };
4875
+ walk(frontmatter, field2.path);
4876
+ return out;
4877
+ }
4878
+ function buildDeletionPlan(project, typeId, enSlug) {
4879
+ const rootType = (() => {
4880
+ try {
4881
+ return project.getType(typeId);
4882
+ } catch {
4883
+ return null;
4884
+ }
4885
+ })();
4886
+ if (!rootType) {
4887
+ throw new Error(`Unknown content type "${typeId}"`);
4888
+ }
4889
+ const rootDoc = rootType.get(enSlug);
4890
+ if (!rootDoc) {
4891
+ throw new Error(`No ${typeId} entry "${enSlug}"`);
4892
+ }
4893
+ const docByKey = /* @__PURE__ */ new Map();
4894
+ const relationFieldsByType = /* @__PURE__ */ new Map();
4895
+ const assetFieldsByType = /* @__PURE__ */ new Map();
4896
+ const referrers = /* @__PURE__ */ new Map();
4897
+ const assetRefs = /* @__PURE__ */ new Map();
4898
+ const bodyRelationRefs = [];
4899
+ for (const type of project.listTypes()) {
4900
+ const relFields = listRelationFields(type.config.schema);
4901
+ const assetFields = listAssetFields(type.config.schema);
4902
+ relationFieldsByType.set(type.id, relFields);
4903
+ assetFieldsByType.set(type.id, assetFields);
4904
+ for (const doc of type.list()) {
4905
+ const frontmatter = doc.frontmatter;
4906
+ docByKey.set(docKey(type.id, doc.enSlug), {
4907
+ typeId: type.id,
4908
+ enSlug: doc.enSlug,
4909
+ frontmatter
4910
+ });
4911
+ for (const field2 of relFields) {
4912
+ const target = field2.relationTarget;
4913
+ if (!target) continue;
4914
+ for (const slug of relationSlugsAt(frontmatter, field2.path)) {
4915
+ const list = referrers.get(docKey(target, slug)) ?? [];
4916
+ list.push({
4917
+ typeId: type.id,
4918
+ enSlug: doc.enSlug,
4919
+ fieldPath: field2.path.join("."),
4920
+ multiple: Boolean(field2.relationMultiple),
4921
+ optional: Boolean(field2.relationOptional),
4922
+ onTargetDelete: field2.relationOnTargetDelete ?? "restrict"
4923
+ });
4924
+ referrers.set(docKey(target, slug), list);
4925
+ }
4926
+ }
4927
+ for (const field2 of assetFields) {
4928
+ for (const webPath of assetValuesAt(frontmatter, field2, doc.enSlug)) {
4929
+ const set = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
4930
+ set.add(docKey(type.id, doc.enSlug));
4931
+ assetRefs.set(webPath, set);
4932
+ }
4933
+ }
4934
+ for (const token of extractInlineTokens(doc.content).tokens) {
4935
+ if (token.kind === "relation") {
4936
+ bodyRelationRefs.push({
4937
+ typeId: type.id,
4938
+ enSlug: doc.enSlug,
4939
+ targetTypeId: token.targetTypeId,
4940
+ targetEnSlug: token.enSlug
4941
+ });
4942
+ }
4943
+ }
4944
+ }
4945
+ }
4946
+ const deleted = /* @__PURE__ */ new Map();
4947
+ deleted.set(docKey(typeId, enSlug), { typeId, enSlug });
4948
+ const queue = [{ typeId, enSlug }];
4949
+ while (queue.length > 0) {
4950
+ const current = queue.shift();
4951
+ for (const ref of referrers.get(docKey(current.typeId, current.enSlug)) ?? []) {
4952
+ if (ref.onTargetDelete !== "cascade") continue;
4953
+ const refKey = docKey(ref.typeId, ref.enSlug);
4954
+ if (deleted.has(refKey)) continue;
4955
+ deleted.set(refKey, {
4956
+ typeId: ref.typeId,
4957
+ enSlug: ref.enSlug,
4958
+ via: `${ref.fieldPath}=${current.enSlug}`
4959
+ });
4960
+ queue.push({ typeId: ref.typeId, enSlug: ref.enSlug });
4961
+ }
4962
+ }
4963
+ const detaches = [];
4964
+ const blocked = [];
4965
+ const seenDetach = /* @__PURE__ */ new Set();
4966
+ const seenBlock = /* @__PURE__ */ new Set();
4967
+ for (const del of deleted.values()) {
4968
+ for (const ref of referrers.get(docKey(del.typeId, del.enSlug)) ?? []) {
4969
+ if (deleted.has(docKey(ref.typeId, ref.enSlug))) continue;
4970
+ if (ref.onTargetDelete === "restrict") {
4971
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0restrict`;
4972
+ if (!seenBlock.has(k)) {
4973
+ seenBlock.add(k);
4974
+ blocked.push({
4975
+ typeId: ref.typeId,
4976
+ enSlug: ref.enSlug,
4977
+ fieldPath: ref.fieldPath,
4978
+ reason: "restrict"
4979
+ });
4980
+ }
4981
+ } else if (ref.onTargetDelete === "detach") {
4982
+ const requiredSingle = !ref.multiple && !ref.optional;
4983
+ if (requiredSingle) {
4984
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0required-single`;
4985
+ if (!seenBlock.has(k)) {
4986
+ seenBlock.add(k);
4987
+ blocked.push({
4988
+ typeId: ref.typeId,
4989
+ enSlug: ref.enSlug,
4990
+ fieldPath: ref.fieldPath,
4991
+ reason: "required-single"
4992
+ });
4993
+ }
4994
+ } else {
4995
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0${del.enSlug}`;
4996
+ if (!seenDetach.has(k)) {
4997
+ seenDetach.add(k);
4998
+ detaches.push({
4999
+ typeId: ref.typeId,
5000
+ enSlug: ref.enSlug,
5001
+ fieldPath: ref.fieldPath,
5002
+ removedSlug: del.enSlug
5003
+ });
5004
+ }
5005
+ }
5006
+ }
5007
+ }
5008
+ }
5009
+ const assets = [];
5010
+ const seenAsset = /* @__PURE__ */ new Set();
5011
+ for (const del of deleted.values()) {
5012
+ const record = docByKey.get(docKey(del.typeId, del.enSlug));
5013
+ if (!record) continue;
5014
+ for (const field2 of assetFieldsByType.get(del.typeId) ?? []) {
5015
+ for (const webPath of assetValuesAt(record.frontmatter, field2, del.enSlug)) {
5016
+ const dedupeKey = `${del.typeId}\0${del.enSlug}\0${webPath}`;
5017
+ if (seenAsset.has(dedupeKey)) continue;
5018
+ seenAsset.add(dedupeKey);
5019
+ if (field2.assetOnDelete === "keep") {
5020
+ assets.push({
5021
+ path: webPath,
5022
+ ownerTypeId: del.typeId,
5023
+ ownerEnSlug: del.enSlug,
5024
+ action: "keep",
5025
+ reason: "config-keep"
5026
+ });
5027
+ continue;
5028
+ }
5029
+ const refs = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
5030
+ let sharedOutside = false;
5031
+ for (const owner of refs) {
5032
+ if (!deleted.has(owner)) {
5033
+ sharedOutside = true;
5034
+ break;
5035
+ }
5036
+ }
5037
+ assets.push(
5038
+ sharedOutside ? {
5039
+ path: webPath,
5040
+ ownerTypeId: del.typeId,
5041
+ ownerEnSlug: del.enSlug,
5042
+ action: "keep",
5043
+ reason: "shared"
5044
+ } : {
5045
+ path: webPath,
5046
+ ownerTypeId: del.typeId,
5047
+ ownerEnSlug: del.enSlug,
5048
+ action: "delete"
5049
+ }
5050
+ );
5051
+ }
5052
+ }
5053
+ }
5054
+ const store = [];
5055
+ try {
5056
+ const db = openStore(project.config, "readonly");
5057
+ try {
5058
+ for (const del of deleted.values()) {
5059
+ store.push({
5060
+ typeId: del.typeId,
5061
+ enSlug: del.enSlug,
5062
+ translations: countTranslationsForEnSlug(db, del.typeId, del.enSlug),
5063
+ snapshots: countEnSnapshotsForEnSlug(db, del.typeId, del.enSlug)
5064
+ });
5065
+ }
5066
+ } finally {
5067
+ db.close();
5068
+ }
5069
+ } catch {
5070
+ for (const del of deleted.values()) {
5071
+ store.push({ typeId: del.typeId, enSlug: del.enSlug, translations: 0, snapshots: 0 });
5072
+ }
5073
+ }
5074
+ const rootTitle = (() => {
5075
+ const title = rootDoc.frontmatter.title;
5076
+ return typeof title === "string" && title.trim() ? title : void 0;
5077
+ })();
5078
+ const cascades = [];
5079
+ for (const del of deleted.values()) {
5080
+ if (del.typeId === typeId && del.enSlug === enSlug) continue;
5081
+ cascades.push({ typeId: del.typeId, enSlug: del.enSlug, via: del.via ?? "" });
5082
+ }
5083
+ const bodyRefWarnings = bodyRelationRefs.filter(
5084
+ (ref) => deleted.has(docKey(ref.targetTypeId, ref.targetEnSlug)) && !deleted.has(docKey(ref.typeId, ref.enSlug))
5085
+ );
5086
+ return {
5087
+ roots: [{ typeId, enSlug, title: rootTitle }],
5088
+ cascades,
5089
+ detaches,
5090
+ blocked,
5091
+ assets,
5092
+ store,
5093
+ bodyRefWarnings
5094
+ };
5095
+ }
5096
+ function enFilePath(rootDir, contentDir, enSlug) {
5097
+ for (const ext of [".mdx", ".md"]) {
5098
+ const candidate = path3__default.default.join(rootDir, contentDir, `${enSlug}${ext}`);
5099
+ if (fs14__default.default.existsSync(candidate)) return candidate;
5100
+ }
5101
+ return null;
5102
+ }
5103
+ function rewriteFrontmatter(raw, data) {
5104
+ const match = raw.match(/^()?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/);
5105
+ if (!match) {
5106
+ throw new Error("Cannot rewrite frontmatter: no leading YAML block found");
5107
+ }
5108
+ const bom = match[1] ?? "";
5109
+ const body = raw.slice(match[0].length);
5110
+ const serialized = matter4__default.default.stringify("", data);
5111
+ const blockMatch = serialized.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/);
5112
+ const block = blockMatch ? blockMatch[0] : serialized;
5113
+ return `${bom}${block}${body}`;
5114
+ }
5115
+ function removeSlugAtPath(container, remaining, removedSlug, multiple) {
5116
+ const [head, ...rest] = remaining;
5117
+ if (head === void 0) return;
5118
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
5119
+ const record = container;
5120
+ if (rest.length === 0) {
5121
+ if (multiple) {
5122
+ const value = record[head];
5123
+ if (Array.isArray(value)) {
5124
+ record[head] = value.filter((slug) => slug !== removedSlug);
5125
+ }
5126
+ } else if (record[head] === removedSlug) {
5127
+ delete record[head];
5128
+ }
5129
+ return;
5130
+ }
5131
+ if (rest[0] === "*") {
5132
+ const arr = record[head];
5133
+ if (Array.isArray(arr)) {
5134
+ for (const item of arr) removeSlugAtPath(item, rest.slice(1), removedSlug, multiple);
5135
+ }
5136
+ return;
5137
+ }
5138
+ removeSlugAtPath(record[head], rest, removedSlug, multiple);
5139
+ }
5140
+ function executeDeletionPlan(project, plan) {
5141
+ if (isPlanBlocked(plan)) {
5142
+ throw new Error("Refusing to execute a blocked deletion plan");
5143
+ }
5144
+ const config = project.config;
5145
+ const result = {
5146
+ deletedFiles: [],
5147
+ deletedAssets: [],
5148
+ detachedFiles: [],
5149
+ translationsDeleted: 0,
5150
+ snapshotsDeleted: 0
5151
+ };
5152
+ const detachesByFile = /* @__PURE__ */ new Map();
5153
+ for (const detach of plan.detaches) {
5154
+ const key = `${detach.typeId} ${detach.enSlug}`;
5155
+ const list = detachesByFile.get(key) ?? [];
5156
+ list.push(detach);
5157
+ detachesByFile.set(key, list);
5158
+ }
5159
+ for (const [, detaches] of detachesByFile) {
5160
+ const { typeId, enSlug } = detaches[0];
5161
+ const type = project.getType(typeId);
5162
+ const file = enFilePath(config.rootDir, type.config.contentDir, enSlug);
5163
+ if (!file) continue;
5164
+ const fieldByPath = new Map(
5165
+ listRelationFields(type.config.schema).map((f) => [f.path.join("."), f])
5166
+ );
5167
+ const raw = fs14__default.default.readFileSync(file, "utf8");
5168
+ const data = structuredClone(matter4__default.default(raw).data);
5169
+ for (const detach of detaches) {
5170
+ const field2 = fieldByPath.get(detach.fieldPath);
5171
+ if (!field2) continue;
5172
+ removeSlugAtPath(
5173
+ data,
5174
+ detach.fieldPath.split("."),
5175
+ detach.removedSlug,
5176
+ Boolean(field2.relationMultiple)
5177
+ );
5178
+ }
5179
+ fs14__default.default.writeFileSync(file, rewriteFrontmatter(raw, data), "utf8");
5180
+ result.detachedFiles.push(file);
5181
+ }
5182
+ const assetsDir = config.assets?.assetsPath ?? config.assetsPath;
5183
+ for (const asset of plan.assets) {
5184
+ if (asset.action !== "delete") continue;
5185
+ if (!assetsDir) continue;
5186
+ const root = path3__default.default.resolve(assetsDir);
5187
+ const relative = asset.path.replace(/^\/+/, "");
5188
+ const abs = path3__default.default.resolve(root, relative);
5189
+ const rootWithSep = root.endsWith(path3__default.default.sep) ? root : root + path3__default.default.sep;
5190
+ if (abs !== root && !abs.startsWith(rootWithSep)) continue;
5191
+ if (fs14__default.default.existsSync(abs)) {
5192
+ fs14__default.default.rmSync(abs, { force: true });
5193
+ result.deletedAssets.push(abs);
5194
+ }
5195
+ }
5196
+ for (const doc of deletedDocs(plan)) {
5197
+ const type = project.getType(doc.typeId);
5198
+ const file = enFilePath(config.rootDir, type.config.contentDir, doc.enSlug);
5199
+ if (file && fs14__default.default.existsSync(file)) {
5200
+ fs14__default.default.rmSync(file, { force: true });
5201
+ result.deletedFiles.push(file);
5202
+ }
5203
+ }
5204
+ const db = openStore(config, "readwrite");
5205
+ try {
5206
+ for (const doc of deletedDocs(plan)) {
5207
+ result.translationsDeleted += deleteTranslationsForEnSlug(db, doc.typeId, doc.enSlug);
5208
+ result.snapshotsDeleted += deleteEnSnapshotsForEnSlug(db, doc.typeId, doc.enSlug);
5209
+ }
5210
+ } finally {
5211
+ db.close();
5212
+ }
5213
+ bumpContentVersion();
5214
+ return result;
5215
+ }
5216
+
3958
5217
  exports.buildAllContentRedirects = buildAllContentRedirects;
5218
+ exports.buildDeletionPlan = buildDeletionPlan;
3959
5219
  exports.buildStaticRawExports = buildStaticRawExports;
3960
5220
  exports.buildWorklist = buildWorklist;
3961
5221
  exports.createProject = createProject;
@@ -3963,23 +5223,36 @@ exports.createScribe = createScribe;
3963
5223
  exports.createUrlBuilder = createUrlBuilder;
3964
5224
  exports.defineConfig = defineConfig;
3965
5225
  exports.defineContentType = defineContentType;
5226
+ exports.executeDeletionPlan = executeDeletionPlan;
3966
5227
  exports.exportDirSegment = exportDirSegment;
5228
+ exports.extractInlineTokens = extractInlineTokens;
3967
5229
  exports.field = field;
5230
+ exports.fillPlaceholders = fillPlaceholders;
3968
5231
  exports.findConfigPath = findConfigPath;
3969
5232
  exports.generateSitemap = generateSitemap;
5233
+ exports.getAssetMeta = getAssetMeta;
3970
5234
  exports.getFieldKind = getFieldKind;
5235
+ exports.getManagedRoots = getManagedRoots;
3971
5236
  exports.getRedirectSourceSlugs = getRedirectSourceSlugs;
3972
5237
  exports.getRelationTarget = getRelationTarget;
3973
5238
  exports.getStaticExportRoots = getStaticExportRoots;
5239
+ exports.introspectSchema = introspectSchema;
3974
5240
  exports.isResolvedConfig = isResolvedConfig;
3975
5241
  exports.isRoutableType = isRoutableType;
5242
+ exports.isTypeTranslatable = isTypeTranslatable;
5243
+ exports.listAssetFields = listAssetFields;
5244
+ exports.listRelationFields = listRelationFields;
5245
+ exports.listTranslatableFields = listTranslatableFields;
3976
5246
  exports.loadConfigSync = loadConfigSync;
5247
+ exports.placeholderMarker = placeholderMarker;
3977
5248
  exports.resolveConfig = resolveConfig;
3978
5249
  exports.resolveLocalesFromPreset = resolveLocalesFromPreset;
3979
5250
  exports.resumeTranslationJobs = resumeTranslationJobs;
3980
5251
  exports.serializeMdx = serializeMdx;
5252
+ exports.templateManagedRoot = templateManagedRoot;
3981
5253
  exports.translatePage = translatePage;
3982
5254
  exports.translateWorklist = translateWorklist;
5255
+ exports.unescapeInlineTokens = unescapeInlineTokens;
3983
5256
  exports.unwrapSchema = unwrapSchema;
3984
5257
  exports.validateProject = validateProject;
3985
5258
  exports.writeStaticRawExports = writeStaticRawExports;