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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import path3 from 'path';
3
- import fs2 from 'fs';
4
- import matter from 'gray-matter';
3
+ import fs14 from 'fs';
4
+ import matter4 from 'gray-matter';
5
5
  import Database from 'better-sqlite3';
6
6
  import remarkMdx from 'remark-mdx';
7
7
  import remarkParse from 'remark-parse';
@@ -13,6 +13,7 @@ import { GoogleGenAI, ThinkingLevel, ApiError } from '@google/genai';
13
13
  // src/core/field.ts
14
14
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
15
15
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
16
+ var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
16
17
  function getFieldKind(schema) {
17
18
  const tagged = schema;
18
19
  return tagged[FIELD_KIND] ?? "structural";
@@ -40,6 +41,29 @@ function getRelationTarget(schema) {
40
41
  }
41
42
  return null;
42
43
  }
44
+ function getAssetMeta(schema) {
45
+ let current = schema;
46
+ for (let i = 0; i < 8; i++) {
47
+ const tagged = current;
48
+ if (tagged[ASSET_META]) {
49
+ return tagged[ASSET_META];
50
+ }
51
+ if (typeof tagged.unwrap === "function") {
52
+ current = tagged.unwrap();
53
+ continue;
54
+ }
55
+ if (typeof tagged.removeDefault === "function") {
56
+ current = tagged.removeDefault();
57
+ continue;
58
+ }
59
+ if (tagged._def?.innerType) {
60
+ current = tagged._def.innerType;
61
+ continue;
62
+ }
63
+ break;
64
+ }
65
+ return null;
66
+ }
43
67
  function mark(schema, kind) {
44
68
  Object.defineProperty(schema, FIELD_KIND, {
45
69
  value: kind,
@@ -56,6 +80,14 @@ function markRelation(schema, meta) {
56
80
  });
57
81
  return mark(schema, "structural");
58
82
  }
83
+ function markAsset(schema, meta) {
84
+ Object.defineProperty(schema, ASSET_META, {
85
+ value: meta,
86
+ enumerable: false,
87
+ configurable: true
88
+ });
89
+ return mark(schema, "structural");
90
+ }
59
91
  var field = {
60
92
  translatable: (schema) => mark(schema, "translatable"),
61
93
  structural: (schema) => mark(schema, "structural"),
@@ -76,9 +108,24 @@ var field = {
76
108
  } else {
77
109
  inner = z.string().min(1);
78
110
  }
79
- markRelation(inner, { typeId, multiple, optional });
111
+ const onTargetDelete = options?.onTargetDelete ?? "restrict";
112
+ markRelation(inner, { typeId, multiple, optional, onTargetDelete });
80
113
  const schema = optional ? inner.optional() : inner;
81
114
  return schema;
115
+ },
116
+ asset: (options) => {
117
+ const optional = options?.optional ?? false;
118
+ const inner = z.string().min(1);
119
+ markAsset(inner, {
120
+ dir: options?.dir,
121
+ template: options?.template,
122
+ formats: options?.formats,
123
+ maxKB: options?.maxKB,
124
+ optional,
125
+ onDelete: options?.onDelete ?? "delete"
126
+ });
127
+ const schema = optional || options?.template !== void 0 ? inner.optional() : inner;
128
+ return schema;
82
129
  }
83
130
  };
84
131
  function unwrapSchema(schema) {
@@ -118,6 +165,208 @@ function peelOptionalWrappers(schema) {
118
165
  return current;
119
166
  }
120
167
 
168
+ // src/core/introspect-schema.ts
169
+ function getArrayElement(schema) {
170
+ if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
171
+ return schema.element;
172
+ }
173
+ return null;
174
+ }
175
+ function introspectSchema(schema, prefix = []) {
176
+ const relation = getRelationTarget(schema);
177
+ if (relation && prefix.length > 0) {
178
+ return [
179
+ {
180
+ path: prefix,
181
+ kind: "relation",
182
+ relationTarget: relation.typeId,
183
+ relationMultiple: relation.multiple,
184
+ relationOptional: relation.optional,
185
+ relationOnTargetDelete: relation.onTargetDelete
186
+ }
187
+ ];
188
+ }
189
+ const asset = getAssetMeta(schema);
190
+ if (asset && prefix.length > 0) {
191
+ return [
192
+ {
193
+ path: prefix,
194
+ kind: "asset",
195
+ assetDir: asset.dir,
196
+ assetTemplate: asset.template,
197
+ assetFormats: asset.formats,
198
+ assetMaxKB: asset.maxKB,
199
+ assetOptional: asset.optional,
200
+ assetOnDelete: asset.onDelete
201
+ }
202
+ ];
203
+ }
204
+ if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
205
+ return [{ path: prefix, kind: "translatable" }];
206
+ }
207
+ const base = peelOptionalWrappers(schema);
208
+ if (base instanceof Object && "shape" in base) {
209
+ const shape = base.shape;
210
+ const fields = [];
211
+ for (const [key, child] of Object.entries(shape)) {
212
+ fields.push(...introspectSchema(child, [...prefix, key]));
213
+ }
214
+ return fields;
215
+ }
216
+ const element = getArrayElement(base);
217
+ if (element) {
218
+ const elementBase = peelOptionalWrappers(element);
219
+ if (elementBase instanceof Object && "shape" in elementBase) {
220
+ const shape = elementBase.shape;
221
+ const fields = [];
222
+ for (const [key, child] of Object.entries(shape)) {
223
+ fields.push(...introspectSchema(child, [...prefix, "*", key]));
224
+ }
225
+ return fields;
226
+ }
227
+ }
228
+ return [{ path: prefix, kind: getFieldKind(schema) }];
229
+ }
230
+ function buildPathTrie(paths) {
231
+ const root = { leaf: false, children: /* @__PURE__ */ new Map() };
232
+ for (const path15 of paths) {
233
+ let node = root;
234
+ for (const segment of path15) {
235
+ let child = node.children.get(segment);
236
+ if (!child) {
237
+ child = { leaf: false, children: /* @__PURE__ */ new Map() };
238
+ node.children.set(segment, child);
239
+ }
240
+ node = child;
241
+ }
242
+ node.leaf = true;
243
+ }
244
+ return root;
245
+ }
246
+ function extractNode(data, trie) {
247
+ if (trie.leaf) return data;
248
+ const star = trie.children.get("*");
249
+ if (star) {
250
+ if (!Array.isArray(data)) return void 0;
251
+ return data.map(
252
+ (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
253
+ );
254
+ }
255
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
256
+ const record = data;
257
+ const out = {};
258
+ let any = false;
259
+ for (const [key, child] of trie.children) {
260
+ const value = extractNode(record[key], child);
261
+ if (value !== void 0) {
262
+ out[key] = value;
263
+ any = true;
264
+ }
265
+ }
266
+ return any ? out : void 0;
267
+ }
268
+ function extractByPaths(data, paths) {
269
+ if (paths.length === 0) return {};
270
+ const extracted = extractNode(data, buildPathTrie(paths));
271
+ return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
272
+ }
273
+ function pickTranslatable(data, schema) {
274
+ const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
275
+ return extractByPaths(data, translatablePaths);
276
+ }
277
+ function pruneOrphanNestedTranslations(localeData, enData, schema) {
278
+ const out = { ...localeData };
279
+ const structuralParentsWithNested = /* @__PURE__ */ new Set();
280
+ for (const meta of introspectSchema(schema)) {
281
+ if (meta.kind !== "translatable" || meta.path.length < 3 || meta.path[1] !== "*") continue;
282
+ structuralParentsWithNested.add(meta.path[0]);
283
+ }
284
+ for (const parent of structuralParentsWithNested) {
285
+ const enParent = enData[parent];
286
+ if (enParent === void 0 || enParent === null) {
287
+ delete out[parent];
288
+ }
289
+ }
290
+ return out;
291
+ }
292
+ function pickStructural(data, schema) {
293
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
294
+ return extractByPaths(data, structuralPaths);
295
+ }
296
+ function mergeStructuralOntoLocale(localeData, enData, schema) {
297
+ const structural = pickStructural(enData, schema);
298
+ const translatable = pickTranslatable(localeData, schema);
299
+ return deepMerge(structural, translatable);
300
+ }
301
+ function deepMerge(base, overlay) {
302
+ const out = { ...base };
303
+ for (const [key, value] of Object.entries(overlay)) {
304
+ if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
305
+ out[key] = deepMerge(out[key], value);
306
+ } else if (Array.isArray(value) && Array.isArray(out[key])) {
307
+ out[key] = mergeArrayOverlay(out[key], value);
308
+ } else if (value !== void 0) {
309
+ out[key] = value;
310
+ }
311
+ }
312
+ return out;
313
+ }
314
+ function mergeArrayOverlay(base, overlay) {
315
+ return base.map((item, index) => {
316
+ const overlayItem = overlay[index];
317
+ if (item && typeof item === "object" && !Array.isArray(item) && overlayItem && typeof overlayItem === "object" && !Array.isArray(overlayItem)) {
318
+ return deepMerge(item, overlayItem);
319
+ }
320
+ return overlayItem ?? item;
321
+ });
322
+ }
323
+ function listTranslatableFields(schema) {
324
+ return introspectSchema(schema).filter((f) => f.kind === "translatable");
325
+ }
326
+ function isTypeTranslatable(type) {
327
+ return listTranslatableFields(type.schema).length > 0 || type.body !== false;
328
+ }
329
+ function listRelationFields(schema) {
330
+ return introspectSchema(schema).filter((f) => f.kind === "relation");
331
+ }
332
+ function listAssetFields(schema) {
333
+ return introspectSchema(schema).filter((f) => f.kind === "asset");
334
+ }
335
+
336
+ // src/core/managed-roots.ts
337
+ function normalizeRoot(root) {
338
+ const trimmed = `/${root.replace(/^\/+|\/+$/g, "")}`;
339
+ return trimmed === "/" ? "" : trimmed;
340
+ }
341
+ function templateManagedRoot(template) {
342
+ const braceIndex = template.indexOf("{");
343
+ const staticPart = braceIndex >= 0 ? template.slice(0, braceIndex) : template;
344
+ const lastSlash = staticPart.lastIndexOf("/");
345
+ const prefix = lastSlash >= 0 ? staticPart.slice(0, lastSlash) : staticPart;
346
+ return normalizeRoot(prefix);
347
+ }
348
+ function getManagedRoots(config) {
349
+ if (!config.assets) return [];
350
+ const roots = /* @__PURE__ */ new Set();
351
+ for (const dir of config.assets.managedDirs ?? []) {
352
+ const normalized = normalizeRoot(dir);
353
+ if (normalized) roots.add(normalized);
354
+ }
355
+ for (const type of config.types) {
356
+ for (const f of listAssetFields(type.schema)) {
357
+ if (f.assetDir) {
358
+ const normalized = normalizeRoot(f.assetDir);
359
+ if (normalized) roots.add(normalized);
360
+ }
361
+ if (f.assetTemplate) {
362
+ const normalized = templateManagedRoot(f.assetTemplate);
363
+ if (normalized) roots.add(normalized);
364
+ }
365
+ }
366
+ }
367
+ return [...roots].sort();
368
+ }
369
+
121
370
  // src/core/types.ts
122
371
  function defineConfig(config) {
123
372
  return config;
@@ -131,14 +380,14 @@ var SLUG_PLACEHOLDER = "{slug}";
131
380
  function isRoutableType(type) {
132
381
  return typeof type.path === "string" && type.path.length > 0;
133
382
  }
134
- function assertValidPathTemplate(path13, typeId) {
135
- if (!path13.startsWith("/")) {
136
- throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
383
+ function assertValidPathTemplate(path15, typeId) {
384
+ if (!path15.startsWith("/")) {
385
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path15}")`);
137
386
  }
138
- const count = (path13.match(/\{slug\}/g) ?? []).length;
387
+ const count = (path15.match(/\{slug\}/g) ?? []).length;
139
388
  if (count !== 1) {
140
389
  throw new Error(
141
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
390
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path15}")`
142
391
  );
143
392
  }
144
393
  }
@@ -248,7 +497,15 @@ function resolveConfig(input, baseDir) {
248
497
  const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
249
498
  const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
250
499
  const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
251
- const assetsPath = raw.assetsDir ? path3.resolve(projectRoot, raw.assetsDir) : void 0;
500
+ let assetsPath;
501
+ let assets;
502
+ if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
503
+ const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
504
+ const publicPath = raw.assets?.publicPath ?? "/";
505
+ const managedDirs = raw.assets?.managedDirs ?? [];
506
+ assetsPath = path3.resolve(projectRoot, dir);
507
+ assets = { assetsPath, publicPath, managedDirs };
508
+ }
252
509
  const seenIds = /* @__PURE__ */ new Set();
253
510
  const types = raw.types.map((type) => {
254
511
  if (seenIds.has(type.id)) {
@@ -263,13 +520,17 @@ function resolveConfig(input, baseDir) {
263
520
  contentDir: type.contentDir ?? type.id,
264
521
  label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
265
522
  slugStrategy: type.slugStrategy ?? "fixed",
266
- indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
523
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
524
+ // `body` defaults to true (fully backwards compatible). A resolved type
525
+ // always carries an explicit boolean so consumers read one source of truth.
526
+ body: type.body ?? true
267
527
  };
268
528
  });
269
529
  const config = {
270
530
  rootDir: contentRoot,
271
531
  storePath,
272
532
  assetsPath,
533
+ assets,
273
534
  locales: [...raw.locales],
274
535
  defaultLocale,
275
536
  localeRouting,
@@ -303,134 +564,6 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
303
564
  }
304
565
  return fallbacks;
305
566
  }
306
-
307
- // src/core/introspect-schema.ts
308
- function getArrayElement(schema) {
309
- if (schema instanceof Object && "element" in schema && schema._def?.type === "array") {
310
- return schema.element;
311
- }
312
- return null;
313
- }
314
- function introspectSchema(schema, prefix = []) {
315
- const relation = getRelationTarget(schema);
316
- if (relation && prefix.length > 0) {
317
- return [
318
- {
319
- path: prefix,
320
- kind: "relation",
321
- relationTarget: relation.typeId,
322
- relationMultiple: relation.multiple,
323
- relationOptional: relation.optional
324
- }
325
- ];
326
- }
327
- if (prefix.length > 0 && getFieldKind(schema) === "translatable") {
328
- return [{ path: prefix, kind: "translatable" }];
329
- }
330
- const base = peelOptionalWrappers(schema);
331
- if (base instanceof Object && "shape" in base) {
332
- const shape = base.shape;
333
- const fields = [];
334
- for (const [key, child] of Object.entries(shape)) {
335
- fields.push(...introspectSchema(child, [...prefix, key]));
336
- }
337
- return fields;
338
- }
339
- const element = getArrayElement(base);
340
- if (element) {
341
- const elementBase = peelOptionalWrappers(element);
342
- if (elementBase instanceof Object && "shape" in elementBase) {
343
- const shape = elementBase.shape;
344
- const fields = [];
345
- for (const [key, child] of Object.entries(shape)) {
346
- fields.push(...introspectSchema(child, [...prefix, "*", key]));
347
- }
348
- return fields;
349
- }
350
- }
351
- return [{ path: prefix, kind: getFieldKind(schema) }];
352
- }
353
- function buildPathTrie(paths) {
354
- const root = { leaf: false, children: /* @__PURE__ */ new Map() };
355
- for (const path13 of paths) {
356
- let node = root;
357
- for (const segment of path13) {
358
- let child = node.children.get(segment);
359
- if (!child) {
360
- child = { leaf: false, children: /* @__PURE__ */ new Map() };
361
- node.children.set(segment, child);
362
- }
363
- node = child;
364
- }
365
- node.leaf = true;
366
- }
367
- return root;
368
- }
369
- function extractNode(data, trie) {
370
- if (trie.leaf) return data;
371
- const star = trie.children.get("*");
372
- if (star) {
373
- if (!Array.isArray(data)) return void 0;
374
- return data.map(
375
- (item) => typeof item === "object" && item !== null ? extractNode(item, star) ?? {} : {}
376
- );
377
- }
378
- if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
379
- const record = data;
380
- const out = {};
381
- let any = false;
382
- for (const [key, child] of trie.children) {
383
- const value = extractNode(record[key], child);
384
- if (value !== void 0) {
385
- out[key] = value;
386
- any = true;
387
- }
388
- }
389
- return any ? out : void 0;
390
- }
391
- function extractByPaths(data, paths) {
392
- if (paths.length === 0) return {};
393
- const extracted = extractNode(data, buildPathTrie(paths));
394
- return extracted && typeof extracted === "object" && !Array.isArray(extracted) ? extracted : {};
395
- }
396
- function pickTranslatable(data, schema) {
397
- const translatablePaths = introspectSchema(schema).filter((f) => f.kind === "translatable").map((f) => f.path);
398
- return extractByPaths(data, translatablePaths);
399
- }
400
- function pickStructural(data, schema) {
401
- const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
402
- return extractByPaths(data, structuralPaths);
403
- }
404
- function mergeStructuralOntoLocale(localeData, enData, schema) {
405
- const structural = pickStructural(enData, schema);
406
- const translatable = pickTranslatable(localeData, schema);
407
- return deepMerge(structural, translatable);
408
- }
409
- function deepMerge(base, overlay) {
410
- const out = { ...base };
411
- for (const [key, value] of Object.entries(overlay)) {
412
- if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
413
- out[key] = deepMerge(out[key], value);
414
- } else if (Array.isArray(value) && Array.isArray(out[key])) {
415
- out[key] = mergeArrayOverlay(out[key], value);
416
- } else {
417
- out[key] = value;
418
- }
419
- }
420
- return out;
421
- }
422
- function mergeArrayOverlay(base, overlay) {
423
- return base.map((item, index) => {
424
- const overlayItem = overlay[index];
425
- if (item && typeof item === "object" && !Array.isArray(item) && overlayItem && typeof overlayItem === "object" && !Array.isArray(overlayItem)) {
426
- return deepMerge(item, overlayItem);
427
- }
428
- return overlayItem ?? item;
429
- });
430
- }
431
- function listRelationFields(schema) {
432
- return introspectSchema(schema).filter((f) => f.kind === "relation");
433
- }
434
567
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
435
568
  var slugPatternSchema = z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
436
569
  var isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
@@ -521,12 +654,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
521
654
  delete rest[internalKey];
522
655
  }
523
656
  }
657
+ let vars;
658
+ if (rest.vars !== void 0) {
659
+ vars = rest.vars;
660
+ delete rest.vars;
661
+ }
524
662
  return {
525
663
  builtin: {
526
664
  publishedAt,
527
665
  updatedAt,
528
666
  noindex,
529
- canonicalPathOverride
667
+ canonicalPathOverride,
668
+ vars
530
669
  },
531
670
  rest,
532
671
  issues
@@ -565,6 +704,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
565
704
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
566
705
  out.noindex = doc.noindex;
567
706
  out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
707
+ if (doc.vars !== void 0) out.vars = doc.vars;
568
708
  return out;
569
709
  }
570
710
  function seoFieldsFromEn(enDoc) {
@@ -654,7 +794,7 @@ function resolveStorePath(config) {
654
794
  function openStore(config, mode = "readwrite") {
655
795
  const storePath = resolveStorePath(config);
656
796
  if (mode === "readwrite") {
657
- fs2.mkdirSync(path3.dirname(storePath), { recursive: true });
797
+ fs14.mkdirSync(path3.dirname(storePath), { recursive: true });
658
798
  }
659
799
  const db = new Database(storePath, { readonly: mode === "readonly" });
660
800
  if (mode === "readwrite") {
@@ -754,6 +894,26 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
754
894
  function bulkLoadTranslations(db) {
755
895
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
756
896
  }
897
+ function countTranslationsForEnSlug(db, contentType, enSlug) {
898
+ const row = db.prepare(
899
+ `SELECT COUNT(*) as count FROM translations WHERE content_type = ? AND en_slug = ?`
900
+ ).get(contentType, enSlug);
901
+ return row.count;
902
+ }
903
+ function countEnSnapshotsForEnSlug(db, contentType, enSlug) {
904
+ const row = db.prepare(
905
+ `SELECT COUNT(*) as count FROM en_snapshots WHERE content_type = ? AND en_slug = ?`
906
+ ).get(contentType, enSlug);
907
+ return row.count;
908
+ }
909
+ function deleteTranslationsForEnSlug(db, contentType, enSlug) {
910
+ const info = db.prepare(`DELETE FROM translations WHERE content_type = ? AND en_slug = ?`).run(contentType, enSlug);
911
+ return info.changes;
912
+ }
913
+ function deleteEnSnapshotsForEnSlug(db, contentType, enSlug) {
914
+ const info = db.prepare(`DELETE FROM en_snapshots WHERE content_type = ? AND en_slug = ?`).run(contentType, enSlug);
915
+ return info.changes;
916
+ }
757
917
 
758
918
  // src/translate/normalize-mdx-body.ts
759
919
  function needsMdxEscapeNormalization(body) {
@@ -887,6 +1047,312 @@ function assertValidTranslatedMdxBody(body) {
887
1047
  return prepared;
888
1048
  }
889
1049
 
1050
+ // src/loader/resolve-assets.ts
1051
+ function joinPublicPath(publicPath, webPath) {
1052
+ if (publicPath === "" || publicPath === "/") return webPath;
1053
+ const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
1054
+ return publicPath.replace(/\/+$/, "") + suffix;
1055
+ }
1056
+ function materializeTemplate(template, enSlug) {
1057
+ return template.split("{slug}").join(enSlug);
1058
+ }
1059
+ function setAssetAtPath(container, path15, transform) {
1060
+ const [head, ...rest] = path15;
1061
+ if (head === void 0) return;
1062
+ if (rest.length === 0) {
1063
+ const current = container[head];
1064
+ const next = transform(typeof current === "string" ? current : void 0);
1065
+ if (next !== void 0) container[head] = next;
1066
+ return;
1067
+ }
1068
+ if (rest[0] === "*") {
1069
+ const arr = container[head];
1070
+ if (!Array.isArray(arr)) return;
1071
+ for (const item of arr) {
1072
+ if (item && typeof item === "object" && !Array.isArray(item)) {
1073
+ setAssetAtPath(item, rest.slice(1), transform);
1074
+ }
1075
+ }
1076
+ return;
1077
+ }
1078
+ const child = container[head];
1079
+ if (child && typeof child === "object" && !Array.isArray(child)) {
1080
+ setAssetAtPath(child, rest, transform);
1081
+ }
1082
+ }
1083
+ function resolveDocumentAssets(doc, assetFields, assets) {
1084
+ const frontmatter = doc.frontmatter;
1085
+ for (const f of assetFields) {
1086
+ setAssetAtPath(frontmatter, f.path, (current) => {
1087
+ let value = current;
1088
+ if (value === void 0) {
1089
+ if (!f.assetTemplate) return void 0;
1090
+ value = materializeTemplate(f.assetTemplate, doc.enSlug);
1091
+ }
1092
+ return joinPublicPath(assets.publicPath, value);
1093
+ });
1094
+ }
1095
+ }
1096
+
1097
+ // src/inline/tokens.ts
1098
+ var TOKEN_OPEN = "${{";
1099
+ var ESCAPE_SEQUENCE = "$\\{{";
1100
+ var ESCAPE_RESULT = "${{";
1101
+ function placeholderMarker(n) {
1102
+ return `%%${n}%%`;
1103
+ }
1104
+ function findTokenEnd(body, start) {
1105
+ let i = start;
1106
+ let inString = false;
1107
+ while (i < body.length) {
1108
+ const ch = body[i];
1109
+ if (ch === "\n") return -1;
1110
+ if (inString) {
1111
+ if (ch === "\\") {
1112
+ i += 2;
1113
+ continue;
1114
+ }
1115
+ if (ch === '"') inString = false;
1116
+ i += 1;
1117
+ continue;
1118
+ }
1119
+ if (ch === '"') {
1120
+ inString = true;
1121
+ i += 1;
1122
+ continue;
1123
+ }
1124
+ if (ch === "}" && body[i + 1] === "}") return i;
1125
+ i += 1;
1126
+ }
1127
+ return -1;
1128
+ }
1129
+ function parseTokenInner(inner) {
1130
+ const colon = inner.indexOf(":");
1131
+ if (colon === -1) {
1132
+ return { ok: false, reason: `missing kind separator ":"` };
1133
+ }
1134
+ const kind = inner.slice(0, colon);
1135
+ const rest = inner.slice(colon + 1);
1136
+ switch (kind) {
1137
+ case "static": {
1138
+ let text;
1139
+ try {
1140
+ text = JSON.parse(rest);
1141
+ } catch {
1142
+ return { ok: false, reason: `static value is not a valid JSON string` };
1143
+ }
1144
+ if (typeof text !== "string") {
1145
+ return { ok: false, reason: `static value must be a JSON string` };
1146
+ }
1147
+ return { ok: true, token: { kind: "static", text } };
1148
+ }
1149
+ case "relation": {
1150
+ const parts = rest.split(":");
1151
+ if (parts.length < 2 || parts.length > 3) {
1152
+ return {
1153
+ ok: false,
1154
+ reason: `relation must be relation:<typeId>:<enSlug> or relation:<typeId>:<enSlug>:slug`
1155
+ };
1156
+ }
1157
+ const [targetTypeId, enSlug, mode] = parts;
1158
+ if (!targetTypeId || !enSlug) {
1159
+ return { ok: false, reason: `relation is missing a typeId or enSlug` };
1160
+ }
1161
+ if (mode !== void 0 && mode !== "slug") {
1162
+ return { ok: false, reason: `relation mode must be "slug" (got "${mode}")` };
1163
+ }
1164
+ return {
1165
+ ok: true,
1166
+ token: {
1167
+ kind: "relation",
1168
+ targetTypeId,
1169
+ enSlug,
1170
+ mode: mode === "slug" ? "slug" : "url"
1171
+ }
1172
+ };
1173
+ }
1174
+ case "asset": {
1175
+ if (!rest.startsWith("/")) {
1176
+ return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
1177
+ }
1178
+ return { ok: true, token: { kind: "asset", webPath: rest } };
1179
+ }
1180
+ case "var": {
1181
+ if (!rest) return { ok: false, reason: `var is missing a key` };
1182
+ return { ok: true, token: { kind: "var", key: rest } };
1183
+ }
1184
+ default:
1185
+ return { ok: false, reason: `unknown token kind "${kind}"` };
1186
+ }
1187
+ }
1188
+ function extractInlineTokens(body) {
1189
+ const tokens = [];
1190
+ const malformed = [];
1191
+ let out = "";
1192
+ let i = 0;
1193
+ while (i < body.length) {
1194
+ const open = body.indexOf(TOKEN_OPEN, i);
1195
+ if (open === -1) {
1196
+ out += body.slice(i);
1197
+ break;
1198
+ }
1199
+ out += body.slice(i, open);
1200
+ const contentStart = open + TOKEN_OPEN.length;
1201
+ const end = findTokenEnd(body, contentStart);
1202
+ if (end === -1) {
1203
+ malformed.push({
1204
+ raw: body.slice(open),
1205
+ reason: `unterminated token (missing "}}")`,
1206
+ index: open
1207
+ });
1208
+ out += body.slice(open);
1209
+ break;
1210
+ }
1211
+ const raw = body.slice(open, end + 2);
1212
+ const inner = body.slice(contentStart, end);
1213
+ const parsed = parseTokenInner(inner);
1214
+ if (parsed.ok) {
1215
+ tokens.push({ ...parsed.token, raw });
1216
+ out += placeholderMarker(tokens.length);
1217
+ } else {
1218
+ malformed.push({ raw, reason: parsed.reason, index: open });
1219
+ out += raw;
1220
+ }
1221
+ i = end + 2;
1222
+ }
1223
+ return { placeholderBody: out, tokens, malformed };
1224
+ }
1225
+ function fillPlaceholders(body, resolvedValues) {
1226
+ return body.replace(/%%(\d+)%%/g, (match, n) => {
1227
+ const idx = Number(n) - 1;
1228
+ return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
1229
+ });
1230
+ }
1231
+ function unescapeInlineTokens(body) {
1232
+ return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
1233
+ }
1234
+ function countMarkerOccurrences(body, n) {
1235
+ return body.split(placeholderMarker(n)).length - 1;
1236
+ }
1237
+ function replaceInlineSpans(body, opts) {
1238
+ let out = "";
1239
+ let i = 0;
1240
+ while (i < body.length) {
1241
+ if (body.startsWith(ESCAPE_SEQUENCE, i)) {
1242
+ out += opts.escape();
1243
+ i += ESCAPE_SEQUENCE.length;
1244
+ continue;
1245
+ }
1246
+ if (body.startsWith(TOKEN_OPEN, i)) {
1247
+ const end = findTokenEnd(body, i + TOKEN_OPEN.length);
1248
+ if (end !== -1) {
1249
+ const raw = body.slice(i, end + 2);
1250
+ const inner = body.slice(i + TOKEN_OPEN.length, end);
1251
+ out += opts.token(raw, inner);
1252
+ i = end + 2;
1253
+ continue;
1254
+ }
1255
+ }
1256
+ out += body[i];
1257
+ i += 1;
1258
+ }
1259
+ return out;
1260
+ }
1261
+ function maskInlineTokensForMdx(body) {
1262
+ return replaceInlineSpans(body, {
1263
+ token: () => "inline",
1264
+ escape: () => "$"
1265
+ });
1266
+ }
1267
+
1268
+ // src/inline/resolve-tokens.ts
1269
+ function slugKey(typeId, enSlug, locale) {
1270
+ return `${typeId}\0${enSlug}\0${locale}`;
1271
+ }
1272
+ function isStringRecord(value) {
1273
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1274
+ return Object.values(value).every((v) => typeof v === "string");
1275
+ }
1276
+ function createInlineResolver(config) {
1277
+ const urlBuilder = createUrlBuilder(config);
1278
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
1279
+ const storePath = resolveStorePath(config);
1280
+ let slugCache = null;
1281
+ const localizedSlugIndex = () => {
1282
+ let mtime = 0;
1283
+ try {
1284
+ mtime = fs14.statSync(storePath).mtimeMs;
1285
+ } catch {
1286
+ mtime = 0;
1287
+ }
1288
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
1289
+ const index = /* @__PURE__ */ new Map();
1290
+ try {
1291
+ const db = openStore(config, "readonly");
1292
+ try {
1293
+ for (const row of bulkLoadTranslations(db)) {
1294
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1295
+ }
1296
+ } finally {
1297
+ db.close();
1298
+ }
1299
+ } catch {
1300
+ }
1301
+ slugCache = { mtime, index };
1302
+ return index;
1303
+ };
1304
+ const localizedSlug2 = (typeId, enSlug, locale) => {
1305
+ if (locale === config.defaultLocale) return enSlug;
1306
+ const index = localizedSlugIndex();
1307
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1308
+ if (direct) return direct;
1309
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1310
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1311
+ if (hit) return hit;
1312
+ }
1313
+ return enSlug;
1314
+ };
1315
+ return {
1316
+ resolve(token, enFrontmatter, locale) {
1317
+ switch (token.kind) {
1318
+ case "static":
1319
+ return token.text;
1320
+ case "var": {
1321
+ const vars = enFrontmatter.vars;
1322
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1323
+ return vars[token.key];
1324
+ }
1325
+ return "";
1326
+ }
1327
+ case "asset": {
1328
+ const assets = config.assets;
1329
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1330
+ }
1331
+ case "relation": {
1332
+ const type = typeById.get(token.targetTypeId);
1333
+ if (!type) return "";
1334
+ if (token.mode === "slug") return token.enSlug;
1335
+ if (!isRoutableType(type)) return "";
1336
+ const slug = localizedSlug2(token.targetTypeId, token.enSlug, locale);
1337
+ return urlBuilder.resolvePath(type.path, slug, locale);
1338
+ }
1339
+ }
1340
+ }
1341
+ };
1342
+ }
1343
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1344
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1345
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1346
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1347
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1348
+ }
1349
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1350
+ const { tokens } = extractInlineTokens(enRawBody);
1351
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1352
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1353
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1354
+ }
1355
+
890
1356
  // src/loader/normalize-en.ts
891
1357
  function normalizeEnFrontmatter(data) {
892
1358
  const out = { ...data };
@@ -912,15 +1378,15 @@ function isPostFile(name) {
912
1378
  return isPublishableContentFile(name);
913
1379
  }
914
1380
  function listEnFiles(contentDir) {
915
- if (!fs2.existsSync(contentDir)) return [];
916
- return fs2.readdirSync(contentDir).filter(isPostFile).map((f) => path3.join(contentDir, f));
1381
+ if (!fs14.existsSync(contentDir)) return [];
1382
+ return fs14.readdirSync(contentDir).filter(isPostFile).map((f) => path3.join(contentDir, f));
917
1383
  }
918
1384
  function slugFromPath(filePath) {
919
1385
  return path3.basename(filePath).replace(/\.(md|mdx)$/, "");
920
1386
  }
921
1387
  function parseEnMdx(filePath, config, type) {
922
- const raw = fs2.readFileSync(filePath, "utf8");
923
- const parsed = matter(raw);
1388
+ const raw = fs14.readFileSync(filePath, "utf8");
1389
+ const parsed = matter4(raw);
924
1390
  const slug = slugFromPath(filePath);
925
1391
  const normalized = normalizeEnFrontmatter(parsed.data);
926
1392
  const { builtin, rest, issues: builtinIssues } = extractBuiltinEnFields(
@@ -958,6 +1424,7 @@ function parseEnMdx(filePath, config, type) {
958
1424
  updatedAt: builtin.updatedAt,
959
1425
  noindex: builtin.noindex,
960
1426
  canonicalPathOverride: builtin.canonicalPathOverride,
1427
+ vars: builtin.vars,
961
1428
  slug,
962
1429
  locale: config.defaultLocale
963
1430
  },
@@ -982,12 +1449,15 @@ function parseEnMdx(filePath, config, type) {
982
1449
  noindex: builtin.noindex,
983
1450
  canonicalPathOverride: builtin.canonicalPathOverride,
984
1451
  frontmatter,
985
- content: parsed.content,
1452
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1453
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1454
+ // A stray body is reported separately by `scribe validate`.
1455
+ content: type.body === false ? "" : parsed.content,
986
1456
  filePath
987
1457
  };
988
1458
  return { document, issues };
989
1459
  }
990
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1460
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
991
1461
  const localeFm = JSON.parse(row.frontmatter_json);
992
1462
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
993
1463
  const seo = seoFieldsFromEn(enDoc);
@@ -998,6 +1468,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
998
1468
  config.defaultLocale,
999
1469
  config.localeRouting
1000
1470
  );
1471
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1472
+ const content = inlineResolver ? fillTranslatedInlineBody(
1473
+ preparedBody,
1474
+ enDoc.content,
1475
+ enDoc.frontmatter,
1476
+ row.locale,
1477
+ inlineResolver
1478
+ ) : preparedBody;
1001
1479
  return {
1002
1480
  slug: row.slug,
1003
1481
  enSlug: row.en_slug,
@@ -1007,14 +1485,19 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
1007
1485
  noindex: seo.noindex,
1008
1486
  canonicalPathOverride: seo.canonicalPathOverride,
1009
1487
  frontmatter,
1010
- content: prepareTranslatedMdxBody(row.body).body
1488
+ content
1011
1489
  };
1012
1490
  }
1013
1491
  var DEV_REVALIDATE_MS = 1500;
1014
- function createContentLoader(config, type) {
1492
+ var contentVersion = 0;
1493
+ function bumpContentVersion() {
1494
+ contentVersion++;
1495
+ }
1496
+ function createContentLoader(config, type, options = {}) {
1015
1497
  let cached = null;
1016
1498
  let signature = "";
1017
1499
  let lastCheck = 0;
1500
+ let builtVersion = contentVersion;
1018
1501
  const contentDir = path3.join(
1019
1502
  /* turbopackIgnore: true */
1020
1503
  config.rootDir,
@@ -1022,18 +1505,34 @@ function createContentLoader(config, type) {
1022
1505
  );
1023
1506
  const storePath = resolveStorePath(config);
1024
1507
  const isProd = process.env.NODE_ENV === "production";
1508
+ const assets = config.assets;
1509
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1510
+ const resolveAssets = (doc) => {
1511
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1512
+ };
1513
+ const inlineResolver = options.inlineResolver;
1514
+ const resolveEnInline = (doc) => {
1515
+ if (inlineResolver) {
1516
+ doc.content = substituteEnInlineBody(
1517
+ doc.content,
1518
+ doc.frontmatter,
1519
+ config.defaultLocale,
1520
+ inlineResolver
1521
+ );
1522
+ }
1523
+ };
1025
1524
  function computeSignature() {
1026
1525
  const files = listEnFiles(contentDir);
1027
1526
  let newest = 0;
1028
1527
  for (const f of files) {
1029
1528
  try {
1030
- newest = Math.max(newest, fs2.statSync(f).mtimeMs);
1529
+ newest = Math.max(newest, fs14.statSync(f).mtimeMs);
1031
1530
  } catch {
1032
1531
  }
1033
1532
  }
1034
1533
  let store = 0;
1035
1534
  try {
1036
- store = fs2.statSync(storePath).mtimeMs;
1535
+ store = fs14.statSync(storePath).mtimeMs;
1037
1536
  } catch {
1038
1537
  }
1039
1538
  return `${files.length}:${newest}:${store}`;
@@ -1079,16 +1578,28 @@ function createContentLoader(config, type) {
1079
1578
  for (const row of rowsByLocale.get(locale) ?? []) {
1080
1579
  const enDoc = englishBySlug.get(row.en_slug);
1081
1580
  if (!enDoc) continue;
1082
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1581
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1582
+ resolveAssets(doc);
1083
1583
  bySlug.set(doc.slug, doc);
1084
1584
  byEnSlug.set(row.en_slug, doc);
1085
1585
  }
1086
1586
  out.set(locale, { bySlug, byEnSlug });
1087
1587
  }
1588
+ for (const doc of englishBySlug.values()) {
1589
+ resolveAssets(doc);
1590
+ resolveEnInline(doc);
1591
+ }
1088
1592
  return out;
1089
1593
  }
1090
1594
  return () => {
1091
1595
  if (cached) {
1596
+ if (contentVersion !== builtVersion) {
1597
+ cached = build();
1598
+ builtVersion = contentVersion;
1599
+ lastCheck = Date.now();
1600
+ signature = isProd ? "" : computeSignature();
1601
+ return cached;
1602
+ }
1092
1603
  if (isProd) return cached;
1093
1604
  const now = Date.now();
1094
1605
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -1101,6 +1612,7 @@ function createContentLoader(config, type) {
1101
1612
  }
1102
1613
  lastCheck = Date.now();
1103
1614
  cached = build();
1615
+ builtVersion = contentVersion;
1104
1616
  signature = isProd ? "" : computeSignature();
1105
1617
  return cached;
1106
1618
  };
@@ -1113,7 +1625,7 @@ function readEnDocument(config, type, enSlug) {
1113
1625
  );
1114
1626
  for (const ext of [".mdx", ".md"]) {
1115
1627
  const filePath = path3.join(contentDir, `${enSlug}${ext}`);
1116
- if (!fs2.existsSync(filePath)) continue;
1628
+ if (!fs14.existsSync(filePath)) continue;
1117
1629
  const { document } = parseEnMdx(filePath, config, type);
1118
1630
  return document;
1119
1631
  }
@@ -1122,7 +1634,9 @@ function readEnDocument(config, type, enSlug) {
1122
1634
  function getTranslatablePayload(doc, type) {
1123
1635
  return {
1124
1636
  frontmatter: pickTranslatable(doc.frontmatter, type.schema),
1125
- body: doc.content
1637
+ // Bodyless types never contribute a body to any translation payload, hash,
1638
+ // or snapshot — regardless of what a document's `content` happens to hold.
1639
+ body: type.body === false ? "" : doc.content
1126
1640
  };
1127
1641
  }
1128
1642
 
@@ -1210,8 +1724,11 @@ function comparatorFor(orderBy) {
1210
1724
  return (a, b) => a.slug.localeCompare(b.slug);
1211
1725
  }
1212
1726
  }
1213
- function buildRuntime(config, type, getRuntime) {
1214
- const load = createContentLoader(config, type);
1727
+ function buildRuntime(config, type, getRuntime, options = {}) {
1728
+ const load = createContentLoader(config, type, {
1729
+ resolveAssets: options.resolveAssets,
1730
+ inlineResolver: options.inlineResolver
1731
+ });
1215
1732
  const relationFields = new Map(
1216
1733
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1217
1734
  );
@@ -1228,12 +1745,12 @@ function buildRuntime(config, type, getRuntime) {
1228
1745
  id: type.id,
1229
1746
  config: type,
1230
1747
  load,
1231
- list(locale = config.defaultLocale, options = {}) {
1748
+ list(locale = config.defaultLocale, options2 = {}) {
1232
1749
  const idx = load().get(locale);
1233
1750
  if (!idx) return [];
1234
1751
  const docs = Array.from(idx.bySlug.values());
1235
- docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1236
- return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1752
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1753
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1237
1754
  },
1238
1755
  get(slug, locale = config.defaultLocale) {
1239
1756
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1246,7 +1763,7 @@ function buildRuntime(config, type, getRuntime) {
1246
1763
  load(),
1247
1764
  type,
1248
1765
  config.localeRouting,
1249
- config.localeFallbacks[locale] ?? []
1766
+ config.localeFallbacks?.[locale] ?? []
1250
1767
  );
1251
1768
  if (result.document && type.path) {
1252
1769
  return {
@@ -1260,15 +1777,15 @@ function buildRuntime(config, type, getRuntime) {
1260
1777
  }
1261
1778
  return result;
1262
1779
  },
1263
- staticParams(options = {}) {
1780
+ staticParams(options2 = {}) {
1264
1781
  assertRoutable("staticParams");
1265
1782
  const all = load();
1266
1783
  const enIdx = all.get(config.defaultLocale);
1267
1784
  if (!enIdx) return [];
1268
1785
  const params = [];
1269
- for (const locale of options.locales ?? config.locales) {
1786
+ for (const locale of options2.locales ?? config.locales) {
1270
1787
  const localeIdx = all.get(locale);
1271
- const fallbacks = config.localeFallbacks[locale] ?? [];
1788
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1272
1789
  for (const doc of enIdx.bySlug.values()) {
1273
1790
  let slug;
1274
1791
  if (locale === config.defaultLocale) {
@@ -1347,7 +1864,7 @@ function buildRuntime(config, type, getRuntime) {
1347
1864
  };
1348
1865
  return runtime;
1349
1866
  }
1350
- function createProject(config) {
1867
+ function createProject(config, options = {}) {
1351
1868
  const runtimes = /* @__PURE__ */ new Map();
1352
1869
  const getRuntime = (id) => {
1353
1870
  const runtime = runtimes.get(id);
@@ -1356,8 +1873,15 @@ function createProject(config) {
1356
1873
  }
1357
1874
  return runtime;
1358
1875
  };
1876
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config) : void 0;
1359
1877
  for (const type of config.types) {
1360
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1878
+ runtimes.set(
1879
+ type.id,
1880
+ buildRuntime(config, type, getRuntime, {
1881
+ resolveAssets: options.resolveAssets,
1882
+ inlineResolver
1883
+ })
1884
+ );
1361
1885
  }
1362
1886
  return {
1363
1887
  config,
@@ -1374,12 +1898,12 @@ function createProject(config) {
1374
1898
  }
1375
1899
  function enFileExists(config, type, enSlug) {
1376
1900
  const dir = path3.join(config.rootDir, type.contentDir);
1377
- return fs2.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs2.existsSync(path3.join(dir, `${enSlug}.md`));
1901
+ return fs14.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs14.existsSync(path3.join(dir, `${enSlug}.md`));
1378
1902
  }
1379
1903
  function listEnSlugs(rootDir, contentDir) {
1380
1904
  const dir = path3.join(rootDir, contentDir);
1381
- if (!fs2.existsSync(dir)) return [];
1382
- return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1905
+ if (!fs14.existsSync(dir)) return [];
1906
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1383
1907
  }
1384
1908
 
1385
1909
  // src/i18n/translation-index.ts
@@ -1475,10 +1999,10 @@ function redirectsFilePath(config, type) {
1475
1999
  }
1476
2000
  function loadTypeRedirectsFile(config, type) {
1477
2001
  const filePath = redirectsFilePath(config, type);
1478
- if (!fs2.existsSync(filePath)) return null;
2002
+ if (!fs14.existsSync(filePath)) return null;
1479
2003
  let raw;
1480
2004
  try {
1481
- raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
2005
+ raw = JSON.parse(fs14.readFileSync(filePath, "utf8"));
1482
2006
  } catch (error) {
1483
2007
  throw new Error(
1484
2008
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1786,8 +2310,8 @@ function getRedirectSourceSlugs(project) {
1786
2310
  // src/sitemap/join-base-url.ts
1787
2311
  function joinBaseUrl(baseUrl, pathname) {
1788
2312
  const origin = baseUrl.replace(/\/$/, "");
1789
- const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1790
- return `${origin}${path13}`;
2313
+ const path15 = pathname.startsWith("/") ? pathname : `/${pathname}`;
2314
+ return `${origin}${path15}`;
1791
2315
  }
1792
2316
 
1793
2317
  // src/sitemap/generate-sitemap.ts
@@ -1872,7 +2396,7 @@ async function generateSitemap(project, options) {
1872
2396
  // src/create-scribe.ts
1873
2397
  function createScribe(input) {
1874
2398
  const config = resolveConfig(input);
1875
- const project = createProject(config);
2399
+ const project = createProject(config, { resolveAssets: true, resolveInlineTokens: true });
1876
2400
  const scribe = {
1877
2401
  config,
1878
2402
  project,
@@ -1881,6 +2405,17 @@ function createScribe(input) {
1881
2405
  listRoutableTypes: project.listRoutableTypes,
1882
2406
  sitemap(options) {
1883
2407
  return generateSitemap(project, options);
2408
+ },
2409
+ assets: {
2410
+ url(ref, opts) {
2411
+ if (opts && Object.keys(opts).length > 0) {
2412
+ throw new Error(
2413
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
2414
+ );
2415
+ }
2416
+ const assetsConfig = config.assets;
2417
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
2418
+ }
1884
2419
  }
1885
2420
  };
1886
2421
  for (const type of config.types) {
@@ -1907,7 +2442,7 @@ function findConfigPath(cwd) {
1907
2442
  )
1908
2443
  ];
1909
2444
  for (const candidate of candidates) {
1910
- if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
2445
+ if (fs14.existsSync(candidate) && fs14.statSync(candidate).isFile()) {
1911
2446
  return candidate;
1912
2447
  }
1913
2448
  }
@@ -1959,7 +2494,7 @@ function validateTypeRedirects(project) {
1959
2494
  const globalFrom = /* @__PURE__ */ new Map();
1960
2495
  for (const type of project.config.types) {
1961
2496
  const filePath = path3.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1962
- if (!fs2.existsSync(filePath)) continue;
2497
+ if (!fs14.existsSync(filePath)) continue;
1963
2498
  let loaded;
1964
2499
  try {
1965
2500
  loaded = loadTypeRedirectsFile(project.config, type);
@@ -2112,8 +2647,8 @@ function buildEnSlugIndex(config) {
2112
2647
  }
2113
2648
  function listEnSlugsForType(rootDir, contentDir) {
2114
2649
  const dir = path3.join(rootDir, contentDir);
2115
- if (!fs2.existsSync(dir)) return [];
2116
- return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2650
+ if (!fs14.existsSync(dir)) return [];
2651
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2117
2652
  }
2118
2653
  function validateRelations(project) {
2119
2654
  const { config } = project;
@@ -2221,13 +2756,14 @@ function collectImagePaths(frontmatter, body) {
2221
2756
  function assetFilePath(assetsPath, webPath) {
2222
2757
  return path3.join(assetsPath, webPath.replace(/^\//, ""));
2223
2758
  }
2224
- function validateDocumentAssets(config, input) {
2759
+ function validateDocumentAssets(config, input, skipPaths) {
2225
2760
  const assetsPath = config.assetsPath;
2226
2761
  if (!assetsPath) return [];
2227
2762
  const issues = [];
2228
2763
  for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
2764
+ if (skipPaths?.has(webPath)) continue;
2229
2765
  const filePath = assetFilePath(assetsPath, webPath);
2230
- if (fs2.existsSync(filePath)) continue;
2766
+ if (fs14.existsSync(filePath)) continue;
2231
2767
  issues.push({
2232
2768
  level: "warning",
2233
2769
  contentType: input.contentType,
@@ -2239,6 +2775,115 @@ function validateDocumentAssets(config, input) {
2239
2775
  }
2240
2776
  return issues;
2241
2777
  }
2778
+ function collectAssetValues(container, path15, out, fieldPathSoFar = []) {
2779
+ const [head, ...rest] = path15;
2780
+ if (head === void 0) return;
2781
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
2782
+ const record = container;
2783
+ if (rest.length === 0) {
2784
+ const value = record[head];
2785
+ out.push({
2786
+ fieldPath: [...fieldPathSoFar, head].join("."),
2787
+ value: typeof value === "string" ? value : void 0
2788
+ });
2789
+ return;
2790
+ }
2791
+ if (rest[0] === "*") {
2792
+ const arr = record[head];
2793
+ if (!Array.isArray(arr)) {
2794
+ out.push({ fieldPath: [...fieldPathSoFar, head, "*", ...rest.slice(1)].join("."), value: void 0 });
2795
+ return;
2796
+ }
2797
+ arr.forEach((item) => {
2798
+ collectAssetValues(item, rest.slice(1), out, [...fieldPathSoFar, head, "*"]);
2799
+ });
2800
+ return;
2801
+ }
2802
+ collectAssetValues(record[head], rest, out, [...fieldPathSoFar, head]);
2803
+ }
2804
+ function normalizeDir(dir) {
2805
+ return `/${dir.replace(/^\/+|\/+$/g, "")}`;
2806
+ }
2807
+ function validateDeclaredAssetFields(config, input) {
2808
+ const assetsPath = config.assetsPath;
2809
+ if (!assetsPath) return [];
2810
+ const issues = [];
2811
+ const attrib = (fieldPath, level, message) => issues.push({
2812
+ level,
2813
+ contentType: input.contentType,
2814
+ enSlug: input.enSlug,
2815
+ locale: input.locale,
2816
+ field: fieldPath,
2817
+ message
2818
+ });
2819
+ for (const f of listAssetFields(input.schema)) {
2820
+ const locations = [];
2821
+ collectAssetValues(input.frontmatter, f.path, locations);
2822
+ for (const { fieldPath, value } of locations) {
2823
+ let effective = value;
2824
+ if (effective === void 0 && f.assetTemplate) {
2825
+ effective = f.assetTemplate.split("{slug}").join(input.enSlug);
2826
+ }
2827
+ if (effective === void 0) {
2828
+ if (!f.assetOptional) {
2829
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} is required but missing`);
2830
+ }
2831
+ continue;
2832
+ }
2833
+ if (f.assetDir) {
2834
+ const dir = normalizeDir(f.assetDir);
2835
+ if (!(effective === dir || effective.startsWith(`${dir}/`))) {
2836
+ attrib(
2837
+ fieldPath,
2838
+ "error",
2839
+ `${input.contentType}/${input.enSlug}: ${fieldPath} value ${effective} is outside declared dir ${dir}`
2840
+ );
2841
+ }
2842
+ }
2843
+ const filePath = assetFilePath(assetsPath, effective);
2844
+ if (!fs14.existsSync(filePath)) {
2845
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} not found`);
2846
+ continue;
2847
+ }
2848
+ if (f.assetFormats && f.assetFormats.length > 0) {
2849
+ const ext = path3.extname(effective).slice(1).toLowerCase();
2850
+ if (!f.assetFormats.includes(ext)) {
2851
+ attrib(
2852
+ fieldPath,
2853
+ "warning",
2854
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} extension .${ext} not in formats [${f.assetFormats.join(", ")}]`
2855
+ );
2856
+ }
2857
+ }
2858
+ if (f.assetMaxKB !== void 0) {
2859
+ try {
2860
+ const sizeKB = fs14.statSync(filePath).size / 1024;
2861
+ if (sizeKB > f.assetMaxKB) {
2862
+ attrib(
2863
+ fieldPath,
2864
+ "warning",
2865
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} is ${Math.round(sizeKB)}KB, over the ${f.assetMaxKB}KB budget`
2866
+ );
2867
+ }
2868
+ } catch {
2869
+ }
2870
+ }
2871
+ }
2872
+ }
2873
+ return issues;
2874
+ }
2875
+ function collectDeclaredAssetPaths(frontmatter, enSlug, schema, fields) {
2876
+ const out = /* @__PURE__ */ new Set();
2877
+ for (const f of listAssetFields(schema)) {
2878
+ const locations = [];
2879
+ collectAssetValues(frontmatter, f.path, locations);
2880
+ for (const { value } of locations) {
2881
+ if (value !== void 0) out.add(value);
2882
+ else if (f.assetTemplate) out.add(f.assetTemplate.split("{slug}").join(enSlug));
2883
+ }
2884
+ }
2885
+ return out;
2886
+ }
2242
2887
 
2243
2888
  // src/core/localized-slug.ts
2244
2889
  function findLocaleSuffixInSlug(slug, localeCodes) {
@@ -2281,15 +2926,151 @@ function validateTranslationSlugSuffixes(config, db) {
2281
2926
  }
2282
2927
  return issues;
2283
2928
  }
2929
+ function listEnSlugs3(rootDir, contentDir) {
2930
+ const dir = path3.join(rootDir, contentDir);
2931
+ if (!fs14.existsSync(dir)) return [];
2932
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2933
+ }
2934
+ function isStringRecord2(value) {
2935
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2936
+ return Object.values(value).every((v) => typeof v === "string");
2937
+ }
2938
+ function assetFileExists(config, webPath) {
2939
+ const assetsPath = config.assetsPath;
2940
+ if (!assetsPath) return true;
2941
+ return fs14.existsSync(path3.join(assetsPath, webPath.replace(/^\//, "")));
2942
+ }
2943
+ function validateInlineTokens(config) {
2944
+ const issues = [];
2945
+ const slugIndex = buildEnSlugIndex(config);
2946
+ const typeIds = new Set(config.types.map((t) => t.id));
2947
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
2948
+ for (const type of config.types) {
2949
+ for (const enSlug of listEnSlugs3(config.rootDir, type.contentDir)) {
2950
+ const doc = readEnDocument(config, type, enSlug);
2951
+ if (!doc) continue;
2952
+ const { tokens, malformed } = extractInlineTokens(doc.content);
2953
+ for (const bad of malformed) {
2954
+ issues.push({
2955
+ level: "error",
2956
+ contentType: type.id,
2957
+ enSlug,
2958
+ field: "body",
2959
+ message: `Malformed inline token (${bad.reason}): ${bad.raw}`
2960
+ });
2961
+ }
2962
+ let varsReported = false;
2963
+ const vars = doc.frontmatter.vars;
2964
+ for (const token of tokens) {
2965
+ switch (token.kind) {
2966
+ case "static":
2967
+ break;
2968
+ case "relation": {
2969
+ if (!typeIds.has(token.targetTypeId)) {
2970
+ issues.push({
2971
+ level: "error",
2972
+ contentType: type.id,
2973
+ enSlug,
2974
+ field: "body",
2975
+ message: `Inline relation token targets unknown type "${token.targetTypeId}"`
2976
+ });
2977
+ break;
2978
+ }
2979
+ const targetSlugs = slugIndex.get(token.targetTypeId) ?? /* @__PURE__ */ new Set();
2980
+ if (!targetSlugs.has(token.enSlug)) {
2981
+ issues.push({
2982
+ level: "error",
2983
+ contentType: type.id,
2984
+ enSlug,
2985
+ field: "body",
2986
+ message: `Inline relation token references "${token.enSlug}", but no ${token.targetTypeId} doc has that slug`
2987
+ });
2988
+ break;
2989
+ }
2990
+ if (token.mode === "url") {
2991
+ const targetType = typeById.get(token.targetTypeId);
2992
+ if (!isRoutableType(targetType)) {
2993
+ issues.push({
2994
+ level: "error",
2995
+ contentType: type.id,
2996
+ enSlug,
2997
+ field: "body",
2998
+ message: `Inline relation token resolves a URL for "${token.targetTypeId}", which has no path template (not routable)`
2999
+ });
3000
+ }
3001
+ }
3002
+ break;
3003
+ }
3004
+ case "asset": {
3005
+ if (!assetFileExists(config, token.webPath)) {
3006
+ issues.push({
3007
+ level: "error",
3008
+ contentType: type.id,
3009
+ enSlug,
3010
+ field: "body",
3011
+ message: `Inline asset token references ${token.webPath}, which is missing on disk`
3012
+ });
3013
+ }
3014
+ break;
3015
+ }
3016
+ case "var": {
3017
+ if (vars !== void 0 && !isStringRecord2(vars)) {
3018
+ if (!varsReported) {
3019
+ varsReported = true;
3020
+ issues.push({
3021
+ level: "error",
3022
+ contentType: type.id,
3023
+ enSlug,
3024
+ field: "vars",
3025
+ message: `Frontmatter "vars" must be a string-to-string map`
3026
+ });
3027
+ }
3028
+ break;
3029
+ }
3030
+ if (!isStringRecord2(vars) || !(token.key in vars)) {
3031
+ issues.push({
3032
+ level: "error",
3033
+ contentType: type.id,
3034
+ enSlug,
3035
+ field: "body",
3036
+ message: `Inline var token references "${token.key}", absent from this document's vars map`
3037
+ });
3038
+ }
3039
+ break;
3040
+ }
3041
+ }
3042
+ }
3043
+ }
3044
+ }
3045
+ return issues;
3046
+ }
2284
3047
 
2285
3048
  // src/validate/validate-project.ts
2286
- function listEnSlugs3(rootDir, contentDir) {
3049
+ function listEnSlugs4(rootDir, contentDir) {
2287
3050
  const dir = path3.join(rootDir, contentDir);
2288
- if (!fs2.existsSync(dir)) return [];
2289
- return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3051
+ if (!fs14.existsSync(dir)) return [];
3052
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3053
+ }
3054
+ function validateBodylessEntry(issues, config, type, enSlug) {
3055
+ const contentDir = path3.join(config.rootDir, type.contentDir);
3056
+ for (const ext of [".mdx", ".md"]) {
3057
+ const filePath = path3.join(contentDir, `${enSlug}${ext}`);
3058
+ if (!fs14.existsSync(filePath)) continue;
3059
+ const body = matter4(fs14.readFileSync(filePath, "utf8")).content;
3060
+ if (body.trim().length > 0) {
3061
+ issues.push({
3062
+ level: "error",
3063
+ contentType: type.id,
3064
+ enSlug,
3065
+ field: "body",
3066
+ message: `type "${type.id}" is frontmatter-only (body: false) but the entry has body content`
3067
+ });
3068
+ }
3069
+ return;
3070
+ }
2290
3071
  }
2291
3072
  function validateDocumentMdxBody(issues, input) {
2292
- const preparedBody = prepareTranslatedMdxBody(input.body).body;
3073
+ const preparedBody = prepareTranslatedMdxBody(maskInlineTokensForMdx(input.body)).body;
2293
3074
  const mdxValidation = validateMdxBody(preparedBody);
2294
3075
  if (!mdxValidation.ok) {
2295
3076
  issues.push({
@@ -2306,7 +3087,7 @@ function validateProject(config) {
2306
3087
  const issues = [];
2307
3088
  const project = createProject(config);
2308
3089
  const storePath = project.storePath;
2309
- if (!fs2.existsSync(storePath)) {
3090
+ if (!fs14.existsSync(storePath)) {
2310
3091
  issues.push({
2311
3092
  level: "error",
2312
3093
  message: `Missing store.sqlite at ${storePath}`
@@ -2315,7 +3096,7 @@ function validateProject(config) {
2315
3096
  }
2316
3097
  const db = openStore(config, "readonly");
2317
3098
  for (const type of config.types) {
2318
- const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
3099
+ const enSlugs = listEnSlugs4(config.rootDir, type.contentDir);
2319
3100
  const englishSlugs = new Set(enSlugs);
2320
3101
  if (!type.translate && isRoutableType(type)) {
2321
3102
  issues.push({
@@ -2352,20 +3133,41 @@ function validateProject(config) {
2352
3133
  message: issue.message
2353
3134
  });
2354
3135
  }
2355
- for (const issue of validateDocumentAssets(config, {
3136
+ for (const issue of validateDeclaredAssetFields(config, {
2356
3137
  contentType: type.id,
2357
3138
  enSlug,
2358
3139
  frontmatter: enDoc.frontmatter,
2359
- body: enDoc.content
3140
+ schema: type.schema
2360
3141
  })) {
2361
3142
  issues.push(issue);
2362
3143
  }
2363
- validateDocumentMdxBody(issues, {
2364
- contentType: type.id,
3144
+ const declaredAssetPaths = collectDeclaredAssetPaths(
3145
+ enDoc.frontmatter,
2365
3146
  enSlug,
2366
- locale: config.defaultLocale,
2367
- body: enDoc.content
2368
- });
3147
+ type.schema
3148
+ );
3149
+ for (const issue of validateDocumentAssets(
3150
+ config,
3151
+ {
3152
+ contentType: type.id,
3153
+ enSlug,
3154
+ frontmatter: enDoc.frontmatter,
3155
+ body: enDoc.content
3156
+ },
3157
+ declaredAssetPaths
3158
+ )) {
3159
+ issues.push(issue);
3160
+ }
3161
+ if (type.body === false) {
3162
+ validateBodylessEntry(issues, config, type, enSlug);
3163
+ } else {
3164
+ validateDocumentMdxBody(issues, {
3165
+ contentType: type.id,
3166
+ enSlug,
3167
+ locale: config.defaultLocale,
3168
+ body: enDoc.content
3169
+ });
3170
+ }
2369
3171
  for (const locale of config.locales) {
2370
3172
  if (locale === config.defaultLocale) continue;
2371
3173
  const row = getTranslation(db, type.id, enSlug, locale);
@@ -2382,21 +3184,27 @@ function validateProject(config) {
2382
3184
  });
2383
3185
  }
2384
3186
  const preparedBody = prepareTranslatedMdxBody(row.body).body;
2385
- for (const issue of validateDocumentAssets(config, {
2386
- contentType: type.id,
2387
- enSlug,
2388
- locale,
2389
- frontmatter: localeFm,
2390
- body: preparedBody
2391
- })) {
3187
+ for (const issue of validateDocumentAssets(
3188
+ config,
3189
+ {
3190
+ contentType: type.id,
3191
+ enSlug,
3192
+ locale,
3193
+ frontmatter: localeFm,
3194
+ body: preparedBody
3195
+ },
3196
+ declaredAssetPaths
3197
+ )) {
2392
3198
  issues.push(issue);
2393
3199
  }
2394
- validateDocumentMdxBody(issues, {
2395
- contentType: type.id,
2396
- enSlug,
2397
- locale,
2398
- body: row.body
2399
- });
3200
+ if (type.body !== false) {
3201
+ validateDocumentMdxBody(issues, {
3202
+ contentType: type.id,
3203
+ enSlug,
3204
+ locale,
3205
+ body: row.body
3206
+ });
3207
+ }
2400
3208
  }
2401
3209
  }
2402
3210
  try {
@@ -2422,6 +3230,9 @@ function validateProject(config) {
2422
3230
  message: issue.message
2423
3231
  });
2424
3232
  }
3233
+ for (const issue of validateInlineTokens(config)) {
3234
+ issues.push(issue);
3235
+ }
2425
3236
  const dbForSuffix = openStore(config, "readonly");
2426
3237
  try {
2427
3238
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
@@ -2446,6 +3257,10 @@ function computePageEnHash(translatableFrontmatter, body) {
2446
3257
  const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
2447
3258
  return sha256(payload);
2448
3259
  }
3260
+ function computeTranslationEnHash(translatableFrontmatter, body) {
3261
+ const { placeholderBody } = extractInlineTokens(body);
3262
+ return computePageEnHash(translatableFrontmatter, placeholderBody);
3263
+ }
2449
3264
 
2450
3265
  // src/translate/worklist.ts
2451
3266
  function parseContentTypeFilter(contentType) {
@@ -2453,10 +3268,10 @@ function parseContentTypeFilter(contentType) {
2453
3268
  const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2454
3269
  return ids.length > 0 ? new Set(ids) : void 0;
2455
3270
  }
2456
- function listEnSlugs4(rootDir, contentDir) {
3271
+ function listEnSlugs5(rootDir, contentDir) {
2457
3272
  const dir = path3.join(rootDir, contentDir);
2458
- if (!fs2.existsSync(dir)) return [];
2459
- return fs2.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3273
+ if (!fs14.existsSync(dir)) return [];
3274
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2460
3275
  }
2461
3276
  function buildWorklist(config, options = {}) {
2462
3277
  const db = openStore(config, "readonly");
@@ -2465,12 +3280,16 @@ function buildWorklist(config, options = {}) {
2465
3280
  const contentTypes = parseContentTypeFilter(options.contentType);
2466
3281
  for (const type of config.types) {
2467
3282
  if (contentTypes && !contentTypes.has(type.id)) continue;
2468
- const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
3283
+ if (!isTypeTranslatable(type)) {
3284
+ options.onSkipType?.(type);
3285
+ continue;
3286
+ }
3287
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs5(config.rootDir, type.contentDir);
2469
3288
  for (const enSlug of enSlugs) {
2470
3289
  const enDoc = readEnDocument(config, type, enSlug);
2471
3290
  if (!enDoc) continue;
2472
3291
  const payload = getTranslatablePayload(enDoc, type);
2473
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3292
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
2474
3293
  for (const locale of locales) {
2475
3294
  if (locale === config.defaultLocale) continue;
2476
3295
  const existing = getTranslation(db, type.id, enSlug, locale);
@@ -2932,9 +3751,22 @@ function buildTranslatableSubschema(schema) {
2932
3751
  if (Object.keys(out).length === 0) return null;
2933
3752
  return z.object(out);
2934
3753
  }
2935
- function buildGeminiResponseSchema(schema, slugStrategy) {
3754
+ function buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) {
3755
+ const full = buildTranslatableSubschema(schema);
3756
+ if (!full) return null;
3757
+ const filtered = {};
3758
+ for (const [key, childSchema] of Object.entries(full.shape)) {
3759
+ if (!(key in translatableFrontmatter)) continue;
3760
+ const value = translatableFrontmatter[key];
3761
+ if (value === void 0 || value === null) continue;
3762
+ filtered[key] = childSchema;
3763
+ }
3764
+ if (Object.keys(filtered).length === 0) return null;
3765
+ return z.object(filtered);
3766
+ }
3767
+ function buildGeminiResponseSchema(schema, slugStrategy, translatableFrontmatter) {
2936
3768
  try {
2937
- const translatable = buildTranslatableSubschema(schema);
3769
+ const translatable = translatableFrontmatter !== void 0 ? buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) : buildTranslatableSubschema(schema);
2938
3770
  const responseShape = {
2939
3771
  ...translatable ? { frontmatter: translatable } : {},
2940
3772
  body: z.string()
@@ -2956,9 +3788,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2956
3788
 
2957
3789
  // src/translate/resolve-translate-config.ts
2958
3790
  var BUILTIN_TRANSLATE_RULES = [
3791
+ "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.",
2959
3792
  "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.",
2960
3793
  "Return the MDX body with real line breaks; do not use JSON escape sequences like \\n or \\t in the body string.",
2961
- '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 \\".'
3794
+ '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 \\".',
3795
+ "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.).",
3796
+ "Otherwise match the format and components used in the EN source."
2962
3797
  ];
2963
3798
  function slugStrategyRules(slugStrategy) {
2964
3799
  if (slugStrategy === "localized") {
@@ -3002,12 +3837,13 @@ function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
3002
3837
  return pickTranslatable(rawFrontmatter, schema);
3003
3838
  }
3004
3839
  function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
3005
- const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
3006
- const merged = mergeStructuralOntoLocale(
3007
- frontmatter,
3008
- enDoc.frontmatter,
3840
+ const enFrontmatter = enDoc.frontmatter;
3841
+ const frontmatter = pruneOrphanNestedTranslations(
3842
+ sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema),
3843
+ enFrontmatter,
3009
3844
  typeSchema
3010
3845
  );
3846
+ const merged = mergeStructuralOntoLocale(frontmatter, enFrontmatter, typeSchema);
3011
3847
  const parsed = typeSchema.safeParse(merged);
3012
3848
  if (!parsed.success) {
3013
3849
  return { ok: false, error: formatZodIssues(parsed.error) };
@@ -3056,6 +3892,22 @@ function formatTranslateError(error) {
3056
3892
  }
3057
3893
  return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
3058
3894
  }
3895
+ function verifyInlineMarkers(enRawBody, translatedBody) {
3896
+ const { tokens } = extractInlineTokens(enRawBody);
3897
+ if (tokens.length === 0) return;
3898
+ const missing = [];
3899
+ const duplicated = [];
3900
+ for (let i = 1; i <= tokens.length; i++) {
3901
+ const count = countMarkerOccurrences(translatedBody, i);
3902
+ if (count === 0) missing.push(placeholderMarker(i));
3903
+ else if (count > 1) duplicated.push(placeholderMarker(i));
3904
+ }
3905
+ if (missing.length === 0 && duplicated.length === 0) return;
3906
+ const parts = [];
3907
+ if (missing.length > 0) parts.push(`missing ${missing.join(", ")}`);
3908
+ if (duplicated.length > 0) parts.push(`duplicated ${duplicated.join(", ")}`);
3909
+ throw new Error(`Inline token markers mismatch: ${parts.join("; ")}`);
3910
+ }
3059
3911
  function resolveContextLabel(enDoc, enSlug) {
3060
3912
  const fm = enDoc.frontmatter;
3061
3913
  for (const key of ["title", "name", "h1"]) {
@@ -3077,7 +3929,8 @@ function prepareTranslation(config, item, options, startedAt) {
3077
3929
  const enDoc = readEnDocument(config, type, item.enSlug);
3078
3930
  if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
3079
3931
  const payload = getTranslatablePayload(enDoc, type);
3080
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3932
+ const { placeholderBody } = extractInlineTokens(payload.body);
3933
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
3081
3934
  const db = openStore(config, "readonly");
3082
3935
  const existing = getTranslation(db, type.id, item.enSlug, item.locale);
3083
3936
  db.close();
@@ -3099,11 +3952,15 @@ function prepareTranslation(config, item, options, startedAt) {
3099
3952
  targetLocale: item.locale,
3100
3953
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
3101
3954
  translatableFrontmatter: payload.frontmatter,
3102
- enBody: payload.body,
3955
+ enBody: placeholderBody,
3103
3956
  slugStrategy: type.slugStrategy,
3104
3957
  previousError: item.previousError
3105
3958
  });
3106
- const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
3959
+ const responseSchema = buildGeminiResponseSchema(
3960
+ type.schema,
3961
+ type.slugStrategy,
3962
+ payload.frontmatter
3963
+ );
3107
3964
  return {
3108
3965
  status: "ready",
3109
3966
  prepared: {
@@ -3131,9 +3988,8 @@ function finalizeTranslation(config, prepared, output, options) {
3131
3988
  if (!validated.ok) {
3132
3989
  throw new Error(`Translation validation failed: ${validated.error}`);
3133
3990
  }
3134
- const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
3135
- output.parsed.body
3136
- );
3991
+ const { body: translatedBody, adjusted: mdxAdjusted } = type.body === false ? { body: "", adjusted: false } : assertValidTranslatedMdxBody(output.parsed.body);
3992
+ verifyInlineMarkers(payload.body, translatedBody);
3137
3993
  const writeDb = openStore(config, "readwrite");
3138
3994
  const snapshotId = options.snapshotId ?? recordEnSnapshot(
3139
3995
  config,
@@ -3847,7 +4703,7 @@ async function resumeTranslationJobs(config, options = {}) {
3847
4703
  return results;
3848
4704
  }
3849
4705
  function serializeMdx(frontmatter, content) {
3850
- return matter.stringify(content, frontmatter);
4706
+ return matter4.stringify(content, frontmatter);
3851
4707
  }
3852
4708
 
3853
4709
  // src/export/build-static-raw-exports.ts
@@ -3914,8 +4770,8 @@ function buildStaticRawExports(project, options = {}) {
3914
4770
  return out;
3915
4771
  }
3916
4772
  function rmDirIfExists(dir) {
3917
- if (fs2.existsSync(dir)) {
3918
- fs2.rmSync(dir, { recursive: true, force: true });
4773
+ if (fs14.existsSync(dir)) {
4774
+ fs14.rmSync(dir, { recursive: true, force: true });
3919
4775
  }
3920
4776
  }
3921
4777
  function writeStaticRawExports(project, options = {}) {
@@ -3931,8 +4787,8 @@ function writeStaticRawExports(project, options = {}) {
3931
4787
  const counts = /* @__PURE__ */ new Map();
3932
4788
  for (const item of exports) {
3933
4789
  const filePath = path3.join(outDir, item.relativePath);
3934
- fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
3935
- fs2.writeFileSync(filePath, item.source, "utf8");
4790
+ fs14.mkdirSync(path3.dirname(filePath), { recursive: true });
4791
+ fs14.writeFileSync(filePath, item.source, "utf8");
3936
4792
  const key = `${item.typeId}/${item.locale}`;
3937
4793
  counts.set(key, (counts.get(key) ?? 0) + 1);
3938
4794
  }
@@ -3942,6 +4798,409 @@ function writeStaticRawExports(project, options = {}) {
3942
4798
  return { exports, written: exports.length };
3943
4799
  }
3944
4800
 
3945
- export { buildAllContentRedirects, buildStaticRawExports, buildWorklist, createProject, createScribe, createUrlBuilder, defineConfig, defineContentType, exportDirSegment, field, findConfigPath, generateSitemap, getFieldKind, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, isResolvedConfig, isRoutableType, loadConfigSync, resolveConfig, resolveLocalesFromPreset, resumeTranslationJobs, serializeMdx, translatePage, translateWorklist, unwrapSchema, validateProject, writeStaticRawExports };
4801
+ // src/delete/plan.ts
4802
+ function isPlanBlocked(plan) {
4803
+ return plan.blocked.length > 0;
4804
+ }
4805
+ function deletedDocs(plan) {
4806
+ return [
4807
+ ...plan.roots.map((r) => ({ typeId: r.typeId, enSlug: r.enSlug })),
4808
+ ...plan.cascades.map((c) => ({ typeId: c.typeId, enSlug: c.enSlug }))
4809
+ ];
4810
+ }
4811
+ function docKey(typeId, enSlug) {
4812
+ return `${typeId}\0${enSlug}`;
4813
+ }
4814
+ function relationSlugsAt(frontmatter, path15) {
4815
+ const out = [];
4816
+ const walk = (container, remaining) => {
4817
+ const [head, ...rest] = remaining;
4818
+ if (head === void 0) return;
4819
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4820
+ const record = container;
4821
+ if (rest.length === 0) {
4822
+ const value = record[head];
4823
+ if (Array.isArray(value)) {
4824
+ for (const slug of value) if (typeof slug === "string" && slug) out.push(slug);
4825
+ } else if (typeof value === "string" && value) {
4826
+ out.push(value);
4827
+ }
4828
+ return;
4829
+ }
4830
+ if (rest[0] === "*") {
4831
+ const arr = record[head];
4832
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4833
+ return;
4834
+ }
4835
+ walk(record[head], rest);
4836
+ };
4837
+ walk(frontmatter, path15);
4838
+ return out;
4839
+ }
4840
+ function assetValuesAt(frontmatter, field2, enSlug) {
4841
+ const out = [];
4842
+ const walk = (container, remaining) => {
4843
+ const [head, ...rest] = remaining;
4844
+ if (head === void 0) return;
4845
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4846
+ const record = container;
4847
+ if (rest.length === 0) {
4848
+ const value = record[head];
4849
+ if (typeof value === "string" && value) out.push(value);
4850
+ else if (value === void 0 && field2.assetTemplate) {
4851
+ out.push(field2.assetTemplate.split("{slug}").join(enSlug));
4852
+ }
4853
+ return;
4854
+ }
4855
+ if (rest[0] === "*") {
4856
+ const arr = record[head];
4857
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4858
+ return;
4859
+ }
4860
+ walk(record[head], rest);
4861
+ };
4862
+ walk(frontmatter, field2.path);
4863
+ return out;
4864
+ }
4865
+ function buildDeletionPlan(project, typeId, enSlug) {
4866
+ const rootType = (() => {
4867
+ try {
4868
+ return project.getType(typeId);
4869
+ } catch {
4870
+ return null;
4871
+ }
4872
+ })();
4873
+ if (!rootType) {
4874
+ throw new Error(`Unknown content type "${typeId}"`);
4875
+ }
4876
+ const rootDoc = rootType.get(enSlug);
4877
+ if (!rootDoc) {
4878
+ throw new Error(`No ${typeId} entry "${enSlug}"`);
4879
+ }
4880
+ const docByKey = /* @__PURE__ */ new Map();
4881
+ const relationFieldsByType = /* @__PURE__ */ new Map();
4882
+ const assetFieldsByType = /* @__PURE__ */ new Map();
4883
+ const referrers = /* @__PURE__ */ new Map();
4884
+ const assetRefs = /* @__PURE__ */ new Map();
4885
+ const bodyRelationRefs = [];
4886
+ for (const type of project.listTypes()) {
4887
+ const relFields = listRelationFields(type.config.schema);
4888
+ const assetFields = listAssetFields(type.config.schema);
4889
+ relationFieldsByType.set(type.id, relFields);
4890
+ assetFieldsByType.set(type.id, assetFields);
4891
+ for (const doc of type.list()) {
4892
+ const frontmatter = doc.frontmatter;
4893
+ docByKey.set(docKey(type.id, doc.enSlug), {
4894
+ typeId: type.id,
4895
+ enSlug: doc.enSlug,
4896
+ frontmatter
4897
+ });
4898
+ for (const field2 of relFields) {
4899
+ const target = field2.relationTarget;
4900
+ if (!target) continue;
4901
+ for (const slug of relationSlugsAt(frontmatter, field2.path)) {
4902
+ const list = referrers.get(docKey(target, slug)) ?? [];
4903
+ list.push({
4904
+ typeId: type.id,
4905
+ enSlug: doc.enSlug,
4906
+ fieldPath: field2.path.join("."),
4907
+ multiple: Boolean(field2.relationMultiple),
4908
+ optional: Boolean(field2.relationOptional),
4909
+ onTargetDelete: field2.relationOnTargetDelete ?? "restrict"
4910
+ });
4911
+ referrers.set(docKey(target, slug), list);
4912
+ }
4913
+ }
4914
+ for (const field2 of assetFields) {
4915
+ for (const webPath of assetValuesAt(frontmatter, field2, doc.enSlug)) {
4916
+ const set = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
4917
+ set.add(docKey(type.id, doc.enSlug));
4918
+ assetRefs.set(webPath, set);
4919
+ }
4920
+ }
4921
+ for (const token of extractInlineTokens(doc.content).tokens) {
4922
+ if (token.kind === "relation") {
4923
+ bodyRelationRefs.push({
4924
+ typeId: type.id,
4925
+ enSlug: doc.enSlug,
4926
+ targetTypeId: token.targetTypeId,
4927
+ targetEnSlug: token.enSlug
4928
+ });
4929
+ }
4930
+ }
4931
+ }
4932
+ }
4933
+ const deleted = /* @__PURE__ */ new Map();
4934
+ deleted.set(docKey(typeId, enSlug), { typeId, enSlug });
4935
+ const queue = [{ typeId, enSlug }];
4936
+ while (queue.length > 0) {
4937
+ const current = queue.shift();
4938
+ for (const ref of referrers.get(docKey(current.typeId, current.enSlug)) ?? []) {
4939
+ if (ref.onTargetDelete !== "cascade") continue;
4940
+ const refKey = docKey(ref.typeId, ref.enSlug);
4941
+ if (deleted.has(refKey)) continue;
4942
+ deleted.set(refKey, {
4943
+ typeId: ref.typeId,
4944
+ enSlug: ref.enSlug,
4945
+ via: `${ref.fieldPath}=${current.enSlug}`
4946
+ });
4947
+ queue.push({ typeId: ref.typeId, enSlug: ref.enSlug });
4948
+ }
4949
+ }
4950
+ const detaches = [];
4951
+ const blocked = [];
4952
+ const seenDetach = /* @__PURE__ */ new Set();
4953
+ const seenBlock = /* @__PURE__ */ new Set();
4954
+ for (const del of deleted.values()) {
4955
+ for (const ref of referrers.get(docKey(del.typeId, del.enSlug)) ?? []) {
4956
+ if (deleted.has(docKey(ref.typeId, ref.enSlug))) continue;
4957
+ if (ref.onTargetDelete === "restrict") {
4958
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0restrict`;
4959
+ if (!seenBlock.has(k)) {
4960
+ seenBlock.add(k);
4961
+ blocked.push({
4962
+ typeId: ref.typeId,
4963
+ enSlug: ref.enSlug,
4964
+ fieldPath: ref.fieldPath,
4965
+ reason: "restrict"
4966
+ });
4967
+ }
4968
+ } else if (ref.onTargetDelete === "detach") {
4969
+ const requiredSingle = !ref.multiple && !ref.optional;
4970
+ if (requiredSingle) {
4971
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0required-single`;
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: "required-single"
4979
+ });
4980
+ }
4981
+ } else {
4982
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0${del.enSlug}`;
4983
+ if (!seenDetach.has(k)) {
4984
+ seenDetach.add(k);
4985
+ detaches.push({
4986
+ typeId: ref.typeId,
4987
+ enSlug: ref.enSlug,
4988
+ fieldPath: ref.fieldPath,
4989
+ removedSlug: del.enSlug
4990
+ });
4991
+ }
4992
+ }
4993
+ }
4994
+ }
4995
+ }
4996
+ const assets = [];
4997
+ const seenAsset = /* @__PURE__ */ new Set();
4998
+ for (const del of deleted.values()) {
4999
+ const record = docByKey.get(docKey(del.typeId, del.enSlug));
5000
+ if (!record) continue;
5001
+ for (const field2 of assetFieldsByType.get(del.typeId) ?? []) {
5002
+ for (const webPath of assetValuesAt(record.frontmatter, field2, del.enSlug)) {
5003
+ const dedupeKey = `${del.typeId}\0${del.enSlug}\0${webPath}`;
5004
+ if (seenAsset.has(dedupeKey)) continue;
5005
+ seenAsset.add(dedupeKey);
5006
+ if (field2.assetOnDelete === "keep") {
5007
+ assets.push({
5008
+ path: webPath,
5009
+ ownerTypeId: del.typeId,
5010
+ ownerEnSlug: del.enSlug,
5011
+ action: "keep",
5012
+ reason: "config-keep"
5013
+ });
5014
+ continue;
5015
+ }
5016
+ const refs = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
5017
+ let sharedOutside = false;
5018
+ for (const owner of refs) {
5019
+ if (!deleted.has(owner)) {
5020
+ sharedOutside = true;
5021
+ break;
5022
+ }
5023
+ }
5024
+ assets.push(
5025
+ sharedOutside ? {
5026
+ path: webPath,
5027
+ ownerTypeId: del.typeId,
5028
+ ownerEnSlug: del.enSlug,
5029
+ action: "keep",
5030
+ reason: "shared"
5031
+ } : {
5032
+ path: webPath,
5033
+ ownerTypeId: del.typeId,
5034
+ ownerEnSlug: del.enSlug,
5035
+ action: "delete"
5036
+ }
5037
+ );
5038
+ }
5039
+ }
5040
+ }
5041
+ const store = [];
5042
+ try {
5043
+ const db = openStore(project.config, "readonly");
5044
+ try {
5045
+ for (const del of deleted.values()) {
5046
+ store.push({
5047
+ typeId: del.typeId,
5048
+ enSlug: del.enSlug,
5049
+ translations: countTranslationsForEnSlug(db, del.typeId, del.enSlug),
5050
+ snapshots: countEnSnapshotsForEnSlug(db, del.typeId, del.enSlug)
5051
+ });
5052
+ }
5053
+ } finally {
5054
+ db.close();
5055
+ }
5056
+ } catch {
5057
+ for (const del of deleted.values()) {
5058
+ store.push({ typeId: del.typeId, enSlug: del.enSlug, translations: 0, snapshots: 0 });
5059
+ }
5060
+ }
5061
+ const rootTitle = (() => {
5062
+ const title = rootDoc.frontmatter.title;
5063
+ return typeof title === "string" && title.trim() ? title : void 0;
5064
+ })();
5065
+ const cascades = [];
5066
+ for (const del of deleted.values()) {
5067
+ if (del.typeId === typeId && del.enSlug === enSlug) continue;
5068
+ cascades.push({ typeId: del.typeId, enSlug: del.enSlug, via: del.via ?? "" });
5069
+ }
5070
+ const bodyRefWarnings = bodyRelationRefs.filter(
5071
+ (ref) => deleted.has(docKey(ref.targetTypeId, ref.targetEnSlug)) && !deleted.has(docKey(ref.typeId, ref.enSlug))
5072
+ );
5073
+ return {
5074
+ roots: [{ typeId, enSlug, title: rootTitle }],
5075
+ cascades,
5076
+ detaches,
5077
+ blocked,
5078
+ assets,
5079
+ store,
5080
+ bodyRefWarnings
5081
+ };
5082
+ }
5083
+ function enFilePath(rootDir, contentDir, enSlug) {
5084
+ for (const ext of [".mdx", ".md"]) {
5085
+ const candidate = path3.join(rootDir, contentDir, `${enSlug}${ext}`);
5086
+ if (fs14.existsSync(candidate)) return candidate;
5087
+ }
5088
+ return null;
5089
+ }
5090
+ function rewriteFrontmatter(raw, data) {
5091
+ const match = raw.match(/^()?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/);
5092
+ if (!match) {
5093
+ throw new Error("Cannot rewrite frontmatter: no leading YAML block found");
5094
+ }
5095
+ const bom = match[1] ?? "";
5096
+ const body = raw.slice(match[0].length);
5097
+ const serialized = matter4.stringify("", data);
5098
+ const blockMatch = serialized.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/);
5099
+ const block = blockMatch ? blockMatch[0] : serialized;
5100
+ return `${bom}${block}${body}`;
5101
+ }
5102
+ function removeSlugAtPath(container, remaining, removedSlug, multiple) {
5103
+ const [head, ...rest] = remaining;
5104
+ if (head === void 0) return;
5105
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
5106
+ const record = container;
5107
+ if (rest.length === 0) {
5108
+ if (multiple) {
5109
+ const value = record[head];
5110
+ if (Array.isArray(value)) {
5111
+ record[head] = value.filter((slug) => slug !== removedSlug);
5112
+ }
5113
+ } else if (record[head] === removedSlug) {
5114
+ delete record[head];
5115
+ }
5116
+ return;
5117
+ }
5118
+ if (rest[0] === "*") {
5119
+ const arr = record[head];
5120
+ if (Array.isArray(arr)) {
5121
+ for (const item of arr) removeSlugAtPath(item, rest.slice(1), removedSlug, multiple);
5122
+ }
5123
+ return;
5124
+ }
5125
+ removeSlugAtPath(record[head], rest, removedSlug, multiple);
5126
+ }
5127
+ function executeDeletionPlan(project, plan) {
5128
+ if (isPlanBlocked(plan)) {
5129
+ throw new Error("Refusing to execute a blocked deletion plan");
5130
+ }
5131
+ const config = project.config;
5132
+ const result = {
5133
+ deletedFiles: [],
5134
+ deletedAssets: [],
5135
+ detachedFiles: [],
5136
+ translationsDeleted: 0,
5137
+ snapshotsDeleted: 0
5138
+ };
5139
+ const detachesByFile = /* @__PURE__ */ new Map();
5140
+ for (const detach of plan.detaches) {
5141
+ const key = `${detach.typeId} ${detach.enSlug}`;
5142
+ const list = detachesByFile.get(key) ?? [];
5143
+ list.push(detach);
5144
+ detachesByFile.set(key, list);
5145
+ }
5146
+ for (const [, detaches] of detachesByFile) {
5147
+ const { typeId, enSlug } = detaches[0];
5148
+ const type = project.getType(typeId);
5149
+ const file = enFilePath(config.rootDir, type.config.contentDir, enSlug);
5150
+ if (!file) continue;
5151
+ const fieldByPath = new Map(
5152
+ listRelationFields(type.config.schema).map((f) => [f.path.join("."), f])
5153
+ );
5154
+ const raw = fs14.readFileSync(file, "utf8");
5155
+ const data = structuredClone(matter4(raw).data);
5156
+ for (const detach of detaches) {
5157
+ const field2 = fieldByPath.get(detach.fieldPath);
5158
+ if (!field2) continue;
5159
+ removeSlugAtPath(
5160
+ data,
5161
+ detach.fieldPath.split("."),
5162
+ detach.removedSlug,
5163
+ Boolean(field2.relationMultiple)
5164
+ );
5165
+ }
5166
+ fs14.writeFileSync(file, rewriteFrontmatter(raw, data), "utf8");
5167
+ result.detachedFiles.push(file);
5168
+ }
5169
+ const assetsDir = config.assets?.assetsPath ?? config.assetsPath;
5170
+ for (const asset of plan.assets) {
5171
+ if (asset.action !== "delete") continue;
5172
+ if (!assetsDir) continue;
5173
+ const root = path3.resolve(assetsDir);
5174
+ const relative = asset.path.replace(/^\/+/, "");
5175
+ const abs = path3.resolve(root, relative);
5176
+ const rootWithSep = root.endsWith(path3.sep) ? root : root + path3.sep;
5177
+ if (abs !== root && !abs.startsWith(rootWithSep)) continue;
5178
+ if (fs14.existsSync(abs)) {
5179
+ fs14.rmSync(abs, { force: true });
5180
+ result.deletedAssets.push(abs);
5181
+ }
5182
+ }
5183
+ for (const doc of deletedDocs(plan)) {
5184
+ const type = project.getType(doc.typeId);
5185
+ const file = enFilePath(config.rootDir, type.config.contentDir, doc.enSlug);
5186
+ if (file && fs14.existsSync(file)) {
5187
+ fs14.rmSync(file, { force: true });
5188
+ result.deletedFiles.push(file);
5189
+ }
5190
+ }
5191
+ const db = openStore(config, "readwrite");
5192
+ try {
5193
+ for (const doc of deletedDocs(plan)) {
5194
+ result.translationsDeleted += deleteTranslationsForEnSlug(db, doc.typeId, doc.enSlug);
5195
+ result.snapshotsDeleted += deleteEnSnapshotsForEnSlug(db, doc.typeId, doc.enSlug);
5196
+ }
5197
+ } finally {
5198
+ db.close();
5199
+ }
5200
+ bumpContentVersion();
5201
+ return result;
5202
+ }
5203
+
5204
+ export { buildAllContentRedirects, buildDeletionPlan, buildStaticRawExports, buildWorklist, createProject, createScribe, createUrlBuilder, defineConfig, defineContentType, executeDeletionPlan, exportDirSegment, extractInlineTokens, field, fillPlaceholders, findConfigPath, generateSitemap, getAssetMeta, getFieldKind, getManagedRoots, getRedirectSourceSlugs, getRelationTarget, getStaticExportRoots, introspectSchema, isResolvedConfig, isRoutableType, isTypeTranslatable, listAssetFields, listRelationFields, listTranslatableFields, loadConfigSync, placeholderMarker, resolveConfig, resolveLocalesFromPreset, resumeTranslationJobs, serializeMdx, templateManagedRoot, translatePage, translateWorklist, unescapeInlineTokens, unwrapSchema, validateProject, writeStaticRawExports };
3946
5205
  //# sourceMappingURL=index.js.map
3947
5206
  //# sourceMappingURL=index.js.map