scribe-cms 0.0.17 → 0.0.21

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 +4021 -555
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +4019 -553
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1514 -229
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +1499 -228
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime.cjs +447 -33
  11. package/dist/runtime.cjs.map +1 -1
  12. package/dist/runtime.js +446 -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 +17 -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 +39 -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 +4699 -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 +4697 -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,317 @@ 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 !== 3) {
1152
+ return {
1153
+ ok: false,
1154
+ reason: `relation must be relation:<typeId>:<enSlug>:href 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 !== "href" && mode !== "slug") {
1162
+ return { ok: false, reason: `relation mode must be "href" or "slug" (got "${mode}")` };
1163
+ }
1164
+ return {
1165
+ ok: true,
1166
+ token: {
1167
+ kind: "relation",
1168
+ targetTypeId,
1169
+ enSlug,
1170
+ mode
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, options = {}) {
1277
+ const linkStyle = options.linkStyle ?? "app";
1278
+ const exportExtension = options.exportExtension ?? ".md";
1279
+ const urlBuilder = createUrlBuilder(config);
1280
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
1281
+ const storePath = resolveStorePath(config);
1282
+ let slugCache = null;
1283
+ const localizedSlugIndex = () => {
1284
+ let mtime = 0;
1285
+ try {
1286
+ mtime = fs14.statSync(storePath).mtimeMs;
1287
+ } catch {
1288
+ mtime = 0;
1289
+ }
1290
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
1291
+ const index = /* @__PURE__ */ new Map();
1292
+ try {
1293
+ const db = openStore(config, "readonly");
1294
+ try {
1295
+ for (const row of bulkLoadTranslations(db)) {
1296
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1297
+ }
1298
+ } finally {
1299
+ db.close();
1300
+ }
1301
+ } catch {
1302
+ }
1303
+ slugCache = { mtime, index };
1304
+ return index;
1305
+ };
1306
+ const localizedSlug2 = (typeId, enSlug, locale) => {
1307
+ if (locale === config.defaultLocale) return enSlug;
1308
+ const index = localizedSlugIndex();
1309
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1310
+ if (direct) return direct;
1311
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1312
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1313
+ if (hit) return hit;
1314
+ }
1315
+ return enSlug;
1316
+ };
1317
+ return {
1318
+ resolve(token, enFrontmatter, locale) {
1319
+ switch (token.kind) {
1320
+ case "static":
1321
+ return token.text;
1322
+ case "var": {
1323
+ const vars = enFrontmatter.vars;
1324
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1325
+ return vars[token.key];
1326
+ }
1327
+ return "";
1328
+ }
1329
+ case "asset": {
1330
+ const assets = config.assets;
1331
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1332
+ }
1333
+ case "relation": {
1334
+ const type = typeById.get(token.targetTypeId);
1335
+ if (!type) return "";
1336
+ if (token.mode === "slug") return token.enSlug;
1337
+ if (!isRoutableType(type)) return "";
1338
+ const slug = localizedSlug2(token.targetTypeId, token.enSlug, locale);
1339
+ if (linkStyle === "export") {
1340
+ return urlBuilder.resolvePath(type.path, `${slug}${exportExtension}`, locale);
1341
+ }
1342
+ return type.path.replace("{slug}", slug);
1343
+ }
1344
+ }
1345
+ }
1346
+ };
1347
+ }
1348
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1349
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1350
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1351
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1352
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1353
+ }
1354
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1355
+ const { tokens } = extractInlineTokens(enRawBody);
1356
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1357
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1358
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1359
+ }
1360
+
890
1361
  // src/loader/normalize-en.ts
891
1362
  function normalizeEnFrontmatter(data) {
892
1363
  const out = { ...data };
@@ -912,15 +1383,15 @@ function isPostFile(name) {
912
1383
  return isPublishableContentFile(name);
913
1384
  }
914
1385
  function listEnFiles(contentDir) {
915
- if (!fs2.existsSync(contentDir)) return [];
916
- return fs2.readdirSync(contentDir).filter(isPostFile).map((f) => path3.join(contentDir, f));
1386
+ if (!fs14.existsSync(contentDir)) return [];
1387
+ return fs14.readdirSync(contentDir).filter(isPostFile).map((f) => path3.join(contentDir, f));
917
1388
  }
918
1389
  function slugFromPath(filePath) {
919
1390
  return path3.basename(filePath).replace(/\.(md|mdx)$/, "");
920
1391
  }
921
1392
  function parseEnMdx(filePath, config, type) {
922
- const raw = fs2.readFileSync(filePath, "utf8");
923
- const parsed = matter(raw);
1393
+ const raw = fs14.readFileSync(filePath, "utf8");
1394
+ const parsed = matter4(raw);
924
1395
  const slug = slugFromPath(filePath);
925
1396
  const normalized = normalizeEnFrontmatter(parsed.data);
926
1397
  const { builtin, rest, issues: builtinIssues } = extractBuiltinEnFields(
@@ -958,6 +1429,7 @@ function parseEnMdx(filePath, config, type) {
958
1429
  updatedAt: builtin.updatedAt,
959
1430
  noindex: builtin.noindex,
960
1431
  canonicalPathOverride: builtin.canonicalPathOverride,
1432
+ vars: builtin.vars,
961
1433
  slug,
962
1434
  locale: config.defaultLocale
963
1435
  },
@@ -982,12 +1454,15 @@ function parseEnMdx(filePath, config, type) {
982
1454
  noindex: builtin.noindex,
983
1455
  canonicalPathOverride: builtin.canonicalPathOverride,
984
1456
  frontmatter,
985
- content: parsed.content,
1457
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1458
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1459
+ // A stray body is reported separately by `scribe validate`.
1460
+ content: type.body === false ? "" : parsed.content,
986
1461
  filePath
987
1462
  };
988
1463
  return { document, issues };
989
1464
  }
990
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1465
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
991
1466
  const localeFm = JSON.parse(row.frontmatter_json);
992
1467
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
993
1468
  const seo = seoFieldsFromEn(enDoc);
@@ -998,6 +1473,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
998
1473
  config.defaultLocale,
999
1474
  config.localeRouting
1000
1475
  );
1476
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1477
+ const content = inlineResolver ? fillTranslatedInlineBody(
1478
+ preparedBody,
1479
+ enDoc.content,
1480
+ enDoc.frontmatter,
1481
+ row.locale,
1482
+ inlineResolver
1483
+ ) : preparedBody;
1001
1484
  return {
1002
1485
  slug: row.slug,
1003
1486
  enSlug: row.en_slug,
@@ -1007,14 +1490,19 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
1007
1490
  noindex: seo.noindex,
1008
1491
  canonicalPathOverride: seo.canonicalPathOverride,
1009
1492
  frontmatter,
1010
- content: prepareTranslatedMdxBody(row.body).body
1493
+ content
1011
1494
  };
1012
1495
  }
1013
1496
  var DEV_REVALIDATE_MS = 1500;
1014
- function createContentLoader(config, type) {
1497
+ var contentVersion = 0;
1498
+ function bumpContentVersion() {
1499
+ contentVersion++;
1500
+ }
1501
+ function createContentLoader(config, type, options = {}) {
1015
1502
  let cached = null;
1016
1503
  let signature = "";
1017
1504
  let lastCheck = 0;
1505
+ let builtVersion = contentVersion;
1018
1506
  const contentDir = path3.join(
1019
1507
  /* turbopackIgnore: true */
1020
1508
  config.rootDir,
@@ -1022,18 +1510,34 @@ function createContentLoader(config, type) {
1022
1510
  );
1023
1511
  const storePath = resolveStorePath(config);
1024
1512
  const isProd = process.env.NODE_ENV === "production";
1513
+ const assets = config.assets;
1514
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1515
+ const resolveAssets = (doc) => {
1516
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1517
+ };
1518
+ const inlineResolver = options.inlineResolver;
1519
+ const resolveEnInline = (doc) => {
1520
+ if (inlineResolver) {
1521
+ doc.content = substituteEnInlineBody(
1522
+ doc.content,
1523
+ doc.frontmatter,
1524
+ config.defaultLocale,
1525
+ inlineResolver
1526
+ );
1527
+ }
1528
+ };
1025
1529
  function computeSignature() {
1026
1530
  const files = listEnFiles(contentDir);
1027
1531
  let newest = 0;
1028
1532
  for (const f of files) {
1029
1533
  try {
1030
- newest = Math.max(newest, fs2.statSync(f).mtimeMs);
1534
+ newest = Math.max(newest, fs14.statSync(f).mtimeMs);
1031
1535
  } catch {
1032
1536
  }
1033
1537
  }
1034
1538
  let store = 0;
1035
1539
  try {
1036
- store = fs2.statSync(storePath).mtimeMs;
1540
+ store = fs14.statSync(storePath).mtimeMs;
1037
1541
  } catch {
1038
1542
  }
1039
1543
  return `${files.length}:${newest}:${store}`;
@@ -1079,16 +1583,28 @@ function createContentLoader(config, type) {
1079
1583
  for (const row of rowsByLocale.get(locale) ?? []) {
1080
1584
  const enDoc = englishBySlug.get(row.en_slug);
1081
1585
  if (!enDoc) continue;
1082
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1586
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1587
+ resolveAssets(doc);
1083
1588
  bySlug.set(doc.slug, doc);
1084
1589
  byEnSlug.set(row.en_slug, doc);
1085
1590
  }
1086
1591
  out.set(locale, { bySlug, byEnSlug });
1087
1592
  }
1593
+ for (const doc of englishBySlug.values()) {
1594
+ resolveAssets(doc);
1595
+ resolveEnInline(doc);
1596
+ }
1088
1597
  return out;
1089
1598
  }
1090
1599
  return () => {
1091
1600
  if (cached) {
1601
+ if (contentVersion !== builtVersion) {
1602
+ cached = build();
1603
+ builtVersion = contentVersion;
1604
+ lastCheck = Date.now();
1605
+ signature = isProd ? "" : computeSignature();
1606
+ return cached;
1607
+ }
1092
1608
  if (isProd) return cached;
1093
1609
  const now = Date.now();
1094
1610
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -1101,6 +1617,7 @@ function createContentLoader(config, type) {
1101
1617
  }
1102
1618
  lastCheck = Date.now();
1103
1619
  cached = build();
1620
+ builtVersion = contentVersion;
1104
1621
  signature = isProd ? "" : computeSignature();
1105
1622
  return cached;
1106
1623
  };
@@ -1113,7 +1630,7 @@ function readEnDocument(config, type, enSlug) {
1113
1630
  );
1114
1631
  for (const ext of [".mdx", ".md"]) {
1115
1632
  const filePath = path3.join(contentDir, `${enSlug}${ext}`);
1116
- if (!fs2.existsSync(filePath)) continue;
1633
+ if (!fs14.existsSync(filePath)) continue;
1117
1634
  const { document } = parseEnMdx(filePath, config, type);
1118
1635
  return document;
1119
1636
  }
@@ -1122,7 +1639,9 @@ function readEnDocument(config, type, enSlug) {
1122
1639
  function getTranslatablePayload(doc, type) {
1123
1640
  return {
1124
1641
  frontmatter: pickTranslatable(doc.frontmatter, type.schema),
1125
- body: doc.content
1642
+ // Bodyless types never contribute a body to any translation payload, hash,
1643
+ // or snapshot — regardless of what a document's `content` happens to hold.
1644
+ body: type.body === false ? "" : doc.content
1126
1645
  };
1127
1646
  }
1128
1647
 
@@ -1210,8 +1729,11 @@ function comparatorFor(orderBy) {
1210
1729
  return (a, b) => a.slug.localeCompare(b.slug);
1211
1730
  }
1212
1731
  }
1213
- function buildRuntime(config, type, getRuntime) {
1214
- const load = createContentLoader(config, type);
1732
+ function buildRuntime(config, type, getRuntime, options = {}) {
1733
+ const load = createContentLoader(config, type, {
1734
+ resolveAssets: options.resolveAssets,
1735
+ inlineResolver: options.inlineResolver
1736
+ });
1215
1737
  const relationFields = new Map(
1216
1738
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1217
1739
  );
@@ -1228,12 +1750,12 @@ function buildRuntime(config, type, getRuntime) {
1228
1750
  id: type.id,
1229
1751
  config: type,
1230
1752
  load,
1231
- list(locale = config.defaultLocale, options = {}) {
1753
+ list(locale = config.defaultLocale, options2 = {}) {
1232
1754
  const idx = load().get(locale);
1233
1755
  if (!idx) return [];
1234
1756
  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;
1757
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1758
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1237
1759
  },
1238
1760
  get(slug, locale = config.defaultLocale) {
1239
1761
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1246,7 +1768,7 @@ function buildRuntime(config, type, getRuntime) {
1246
1768
  load(),
1247
1769
  type,
1248
1770
  config.localeRouting,
1249
- config.localeFallbacks[locale] ?? []
1771
+ config.localeFallbacks?.[locale] ?? []
1250
1772
  );
1251
1773
  if (result.document && type.path) {
1252
1774
  return {
@@ -1260,15 +1782,15 @@ function buildRuntime(config, type, getRuntime) {
1260
1782
  }
1261
1783
  return result;
1262
1784
  },
1263
- staticParams(options = {}) {
1785
+ staticParams(options2 = {}) {
1264
1786
  assertRoutable("staticParams");
1265
1787
  const all = load();
1266
1788
  const enIdx = all.get(config.defaultLocale);
1267
1789
  if (!enIdx) return [];
1268
1790
  const params = [];
1269
- for (const locale of options.locales ?? config.locales) {
1791
+ for (const locale of options2.locales ?? config.locales) {
1270
1792
  const localeIdx = all.get(locale);
1271
- const fallbacks = config.localeFallbacks[locale] ?? [];
1793
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1272
1794
  for (const doc of enIdx.bySlug.values()) {
1273
1795
  let slug;
1274
1796
  if (locale === config.defaultLocale) {
@@ -1347,7 +1869,7 @@ function buildRuntime(config, type, getRuntime) {
1347
1869
  };
1348
1870
  return runtime;
1349
1871
  }
1350
- function createProject(config) {
1872
+ function createProject(config, options = {}) {
1351
1873
  const runtimes = /* @__PURE__ */ new Map();
1352
1874
  const getRuntime = (id) => {
1353
1875
  const runtime = runtimes.get(id);
@@ -1356,8 +1878,18 @@ function createProject(config) {
1356
1878
  }
1357
1879
  return runtime;
1358
1880
  };
1881
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config, {
1882
+ linkStyle: options.inlineLinkStyle,
1883
+ exportExtension: options.exportLinkExtension
1884
+ }) : void 0;
1359
1885
  for (const type of config.types) {
1360
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1886
+ runtimes.set(
1887
+ type.id,
1888
+ buildRuntime(config, type, getRuntime, {
1889
+ resolveAssets: options.resolveAssets,
1890
+ inlineResolver
1891
+ })
1892
+ );
1361
1893
  }
1362
1894
  return {
1363
1895
  config,
@@ -1374,12 +1906,12 @@ function createProject(config) {
1374
1906
  }
1375
1907
  function enFileExists(config, type, enSlug) {
1376
1908
  const dir = path3.join(config.rootDir, type.contentDir);
1377
- return fs2.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs2.existsSync(path3.join(dir, `${enSlug}.md`));
1909
+ return fs14.existsSync(path3.join(dir, `${enSlug}.mdx`)) || fs14.existsSync(path3.join(dir, `${enSlug}.md`));
1378
1910
  }
1379
1911
  function listEnSlugs(rootDir, contentDir) {
1380
1912
  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)$/, ""));
1913
+ if (!fs14.existsSync(dir)) return [];
1914
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
1383
1915
  }
1384
1916
 
1385
1917
  // src/i18n/translation-index.ts
@@ -1475,10 +2007,10 @@ function redirectsFilePath(config, type) {
1475
2007
  }
1476
2008
  function loadTypeRedirectsFile(config, type) {
1477
2009
  const filePath = redirectsFilePath(config, type);
1478
- if (!fs2.existsSync(filePath)) return null;
2010
+ if (!fs14.existsSync(filePath)) return null;
1479
2011
  let raw;
1480
2012
  try {
1481
- raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
2013
+ raw = JSON.parse(fs14.readFileSync(filePath, "utf8"));
1482
2014
  } catch (error) {
1483
2015
  throw new Error(
1484
2016
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1786,8 +2318,8 @@ function getRedirectSourceSlugs(project) {
1786
2318
  // src/sitemap/join-base-url.ts
1787
2319
  function joinBaseUrl(baseUrl, pathname) {
1788
2320
  const origin = baseUrl.replace(/\/$/, "");
1789
- const path13 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1790
- return `${origin}${path13}`;
2321
+ const path15 = pathname.startsWith("/") ? pathname : `/${pathname}`;
2322
+ return `${origin}${path15}`;
1791
2323
  }
1792
2324
 
1793
2325
  // src/sitemap/generate-sitemap.ts
@@ -1872,7 +2404,11 @@ async function generateSitemap(project, options) {
1872
2404
  // src/create-scribe.ts
1873
2405
  function createScribe(input) {
1874
2406
  const config = resolveConfig(input);
1875
- const project = createProject(config);
2407
+ const project = createProject(config, {
2408
+ resolveAssets: true,
2409
+ resolveInlineTokens: true,
2410
+ inlineLinkStyle: "app"
2411
+ });
1876
2412
  const scribe = {
1877
2413
  config,
1878
2414
  project,
@@ -1881,6 +2417,17 @@ function createScribe(input) {
1881
2417
  listRoutableTypes: project.listRoutableTypes,
1882
2418
  sitemap(options) {
1883
2419
  return generateSitemap(project, options);
2420
+ },
2421
+ assets: {
2422
+ url(ref, opts) {
2423
+ if (opts && Object.keys(opts).length > 0) {
2424
+ throw new Error(
2425
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
2426
+ );
2427
+ }
2428
+ const assetsConfig = config.assets;
2429
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
2430
+ }
1884
2431
  }
1885
2432
  };
1886
2433
  for (const type of config.types) {
@@ -1907,7 +2454,7 @@ function findConfigPath(cwd) {
1907
2454
  )
1908
2455
  ];
1909
2456
  for (const candidate of candidates) {
1910
- if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
2457
+ if (fs14.existsSync(candidate) && fs14.statSync(candidate).isFile()) {
1911
2458
  return candidate;
1912
2459
  }
1913
2460
  }
@@ -1959,7 +2506,7 @@ function validateTypeRedirects(project) {
1959
2506
  const globalFrom = /* @__PURE__ */ new Map();
1960
2507
  for (const type of project.config.types) {
1961
2508
  const filePath = path3.join(project.config.rootDir, type.contentDir, TYPE_REDIRECTS_FILENAME);
1962
- if (!fs2.existsSync(filePath)) continue;
2509
+ if (!fs14.existsSync(filePath)) continue;
1963
2510
  let loaded;
1964
2511
  try {
1965
2512
  loaded = loadTypeRedirectsFile(project.config, type);
@@ -2112,8 +2659,8 @@ function buildEnSlugIndex(config) {
2112
2659
  }
2113
2660
  function listEnSlugsForType(rootDir, contentDir) {
2114
2661
  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)$/, ""));
2662
+ if (!fs14.existsSync(dir)) return [];
2663
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2117
2664
  }
2118
2665
  function validateRelations(project) {
2119
2666
  const { config } = project;
@@ -2221,13 +2768,14 @@ function collectImagePaths(frontmatter, body) {
2221
2768
  function assetFilePath(assetsPath, webPath) {
2222
2769
  return path3.join(assetsPath, webPath.replace(/^\//, ""));
2223
2770
  }
2224
- function validateDocumentAssets(config, input) {
2771
+ function validateDocumentAssets(config, input, skipPaths) {
2225
2772
  const assetsPath = config.assetsPath;
2226
2773
  if (!assetsPath) return [];
2227
2774
  const issues = [];
2228
2775
  for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
2776
+ if (skipPaths?.has(webPath)) continue;
2229
2777
  const filePath = assetFilePath(assetsPath, webPath);
2230
- if (fs2.existsSync(filePath)) continue;
2778
+ if (fs14.existsSync(filePath)) continue;
2231
2779
  issues.push({
2232
2780
  level: "warning",
2233
2781
  contentType: input.contentType,
@@ -2239,6 +2787,115 @@ function validateDocumentAssets(config, input) {
2239
2787
  }
2240
2788
  return issues;
2241
2789
  }
2790
+ function collectAssetValues(container, path15, out, fieldPathSoFar = []) {
2791
+ const [head, ...rest] = path15;
2792
+ if (head === void 0) return;
2793
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
2794
+ const record = container;
2795
+ if (rest.length === 0) {
2796
+ const value = record[head];
2797
+ out.push({
2798
+ fieldPath: [...fieldPathSoFar, head].join("."),
2799
+ value: typeof value === "string" ? value : void 0
2800
+ });
2801
+ return;
2802
+ }
2803
+ if (rest[0] === "*") {
2804
+ const arr = record[head];
2805
+ if (!Array.isArray(arr)) {
2806
+ out.push({ fieldPath: [...fieldPathSoFar, head, "*", ...rest.slice(1)].join("."), value: void 0 });
2807
+ return;
2808
+ }
2809
+ arr.forEach((item) => {
2810
+ collectAssetValues(item, rest.slice(1), out, [...fieldPathSoFar, head, "*"]);
2811
+ });
2812
+ return;
2813
+ }
2814
+ collectAssetValues(record[head], rest, out, [...fieldPathSoFar, head]);
2815
+ }
2816
+ function normalizeDir(dir) {
2817
+ return `/${dir.replace(/^\/+|\/+$/g, "")}`;
2818
+ }
2819
+ function validateDeclaredAssetFields(config, input) {
2820
+ const assetsPath = config.assetsPath;
2821
+ if (!assetsPath) return [];
2822
+ const issues = [];
2823
+ const attrib = (fieldPath, level, message) => issues.push({
2824
+ level,
2825
+ contentType: input.contentType,
2826
+ enSlug: input.enSlug,
2827
+ locale: input.locale,
2828
+ field: fieldPath,
2829
+ message
2830
+ });
2831
+ for (const f of listAssetFields(input.schema)) {
2832
+ const locations = [];
2833
+ collectAssetValues(input.frontmatter, f.path, locations);
2834
+ for (const { fieldPath, value } of locations) {
2835
+ let effective = value;
2836
+ if (effective === void 0 && f.assetTemplate) {
2837
+ effective = f.assetTemplate.split("{slug}").join(input.enSlug);
2838
+ }
2839
+ if (effective === void 0) {
2840
+ if (!f.assetOptional) {
2841
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} is required but missing`);
2842
+ }
2843
+ continue;
2844
+ }
2845
+ if (f.assetDir) {
2846
+ const dir = normalizeDir(f.assetDir);
2847
+ if (!(effective === dir || effective.startsWith(`${dir}/`))) {
2848
+ attrib(
2849
+ fieldPath,
2850
+ "error",
2851
+ `${input.contentType}/${input.enSlug}: ${fieldPath} value ${effective} is outside declared dir ${dir}`
2852
+ );
2853
+ }
2854
+ }
2855
+ const filePath = assetFilePath(assetsPath, effective);
2856
+ if (!fs14.existsSync(filePath)) {
2857
+ attrib(fieldPath, "error", `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} not found`);
2858
+ continue;
2859
+ }
2860
+ if (f.assetFormats && f.assetFormats.length > 0) {
2861
+ const ext = path3.extname(effective).slice(1).toLowerCase();
2862
+ if (!f.assetFormats.includes(ext)) {
2863
+ attrib(
2864
+ fieldPath,
2865
+ "warning",
2866
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} extension .${ext} not in formats [${f.assetFormats.join(", ")}]`
2867
+ );
2868
+ }
2869
+ }
2870
+ if (f.assetMaxKB !== void 0) {
2871
+ try {
2872
+ const sizeKB = fs14.statSync(filePath).size / 1024;
2873
+ if (sizeKB > f.assetMaxKB) {
2874
+ attrib(
2875
+ fieldPath,
2876
+ "warning",
2877
+ `${input.contentType}/${input.enSlug}: ${fieldPath} \u2192 ${effective} is ${Math.round(sizeKB)}KB, over the ${f.assetMaxKB}KB budget`
2878
+ );
2879
+ }
2880
+ } catch {
2881
+ }
2882
+ }
2883
+ }
2884
+ }
2885
+ return issues;
2886
+ }
2887
+ function collectDeclaredAssetPaths(frontmatter, enSlug, schema, fields) {
2888
+ const out = /* @__PURE__ */ new Set();
2889
+ for (const f of listAssetFields(schema)) {
2890
+ const locations = [];
2891
+ collectAssetValues(frontmatter, f.path, locations);
2892
+ for (const { value } of locations) {
2893
+ if (value !== void 0) out.add(value);
2894
+ else if (f.assetTemplate) out.add(f.assetTemplate.split("{slug}").join(enSlug));
2895
+ }
2896
+ }
2897
+ return out;
2898
+ }
2242
2899
 
2243
2900
  // src/core/localized-slug.ts
2244
2901
  function findLocaleSuffixInSlug(slug, localeCodes) {
@@ -2281,15 +2938,151 @@ function validateTranslationSlugSuffixes(config, db) {
2281
2938
  }
2282
2939
  return issues;
2283
2940
  }
2941
+ function listEnSlugs3(rootDir, contentDir) {
2942
+ const dir = path3.join(rootDir, contentDir);
2943
+ if (!fs14.existsSync(dir)) return [];
2944
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2945
+ }
2946
+ function isStringRecord2(value) {
2947
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2948
+ return Object.values(value).every((v) => typeof v === "string");
2949
+ }
2950
+ function assetFileExists(config, webPath) {
2951
+ const assetsPath = config.assetsPath;
2952
+ if (!assetsPath) return true;
2953
+ return fs14.existsSync(path3.join(assetsPath, webPath.replace(/^\//, "")));
2954
+ }
2955
+ function validateInlineTokens(config) {
2956
+ const issues = [];
2957
+ const slugIndex = buildEnSlugIndex(config);
2958
+ const typeIds = new Set(config.types.map((t) => t.id));
2959
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
2960
+ for (const type of config.types) {
2961
+ for (const enSlug of listEnSlugs3(config.rootDir, type.contentDir)) {
2962
+ const doc = readEnDocument(config, type, enSlug);
2963
+ if (!doc) continue;
2964
+ const { tokens, malformed } = extractInlineTokens(doc.content);
2965
+ for (const bad of malformed) {
2966
+ issues.push({
2967
+ level: "error",
2968
+ contentType: type.id,
2969
+ enSlug,
2970
+ field: "body",
2971
+ message: `Malformed inline token (${bad.reason}): ${bad.raw}`
2972
+ });
2973
+ }
2974
+ let varsReported = false;
2975
+ const vars = doc.frontmatter.vars;
2976
+ for (const token of tokens) {
2977
+ switch (token.kind) {
2978
+ case "static":
2979
+ break;
2980
+ case "relation": {
2981
+ if (!typeIds.has(token.targetTypeId)) {
2982
+ issues.push({
2983
+ level: "error",
2984
+ contentType: type.id,
2985
+ enSlug,
2986
+ field: "body",
2987
+ message: `Inline relation token targets unknown type "${token.targetTypeId}"`
2988
+ });
2989
+ break;
2990
+ }
2991
+ const targetSlugs = slugIndex.get(token.targetTypeId) ?? /* @__PURE__ */ new Set();
2992
+ if (!targetSlugs.has(token.enSlug)) {
2993
+ issues.push({
2994
+ level: "error",
2995
+ contentType: type.id,
2996
+ enSlug,
2997
+ field: "body",
2998
+ message: `Inline relation token references "${token.enSlug}", but no ${token.targetTypeId} doc has that slug`
2999
+ });
3000
+ break;
3001
+ }
3002
+ if (token.mode === "href") {
3003
+ const targetType = typeById.get(token.targetTypeId);
3004
+ if (!isRoutableType(targetType)) {
3005
+ issues.push({
3006
+ level: "error",
3007
+ contentType: type.id,
3008
+ enSlug,
3009
+ field: "body",
3010
+ message: `Inline relation token resolves a URL for "${token.targetTypeId}", which has no path template (not routable)`
3011
+ });
3012
+ }
3013
+ }
3014
+ break;
3015
+ }
3016
+ case "asset": {
3017
+ if (!assetFileExists(config, token.webPath)) {
3018
+ issues.push({
3019
+ level: "error",
3020
+ contentType: type.id,
3021
+ enSlug,
3022
+ field: "body",
3023
+ message: `Inline asset token references ${token.webPath}, which is missing on disk`
3024
+ });
3025
+ }
3026
+ break;
3027
+ }
3028
+ case "var": {
3029
+ if (vars !== void 0 && !isStringRecord2(vars)) {
3030
+ if (!varsReported) {
3031
+ varsReported = true;
3032
+ issues.push({
3033
+ level: "error",
3034
+ contentType: type.id,
3035
+ enSlug,
3036
+ field: "vars",
3037
+ message: `Frontmatter "vars" must be a string-to-string map`
3038
+ });
3039
+ }
3040
+ break;
3041
+ }
3042
+ if (!isStringRecord2(vars) || !(token.key in vars)) {
3043
+ issues.push({
3044
+ level: "error",
3045
+ contentType: type.id,
3046
+ enSlug,
3047
+ field: "body",
3048
+ message: `Inline var token references "${token.key}", absent from this document's vars map`
3049
+ });
3050
+ }
3051
+ break;
3052
+ }
3053
+ }
3054
+ }
3055
+ }
3056
+ }
3057
+ return issues;
3058
+ }
2284
3059
 
2285
3060
  // src/validate/validate-project.ts
2286
- function listEnSlugs3(rootDir, contentDir) {
3061
+ function listEnSlugs4(rootDir, contentDir) {
2287
3062
  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)$/, ""));
3063
+ if (!fs14.existsSync(dir)) return [];
3064
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
3065
+ }
3066
+ function validateBodylessEntry(issues, config, type, enSlug) {
3067
+ const contentDir = path3.join(config.rootDir, type.contentDir);
3068
+ for (const ext of [".mdx", ".md"]) {
3069
+ const filePath = path3.join(contentDir, `${enSlug}${ext}`);
3070
+ if (!fs14.existsSync(filePath)) continue;
3071
+ const body = matter4(fs14.readFileSync(filePath, "utf8")).content;
3072
+ if (body.trim().length > 0) {
3073
+ issues.push({
3074
+ level: "error",
3075
+ contentType: type.id,
3076
+ enSlug,
3077
+ field: "body",
3078
+ message: `type "${type.id}" is frontmatter-only (body: false) but the entry has body content`
3079
+ });
3080
+ }
3081
+ return;
3082
+ }
2290
3083
  }
2291
3084
  function validateDocumentMdxBody(issues, input) {
2292
- const preparedBody = prepareTranslatedMdxBody(input.body).body;
3085
+ const preparedBody = prepareTranslatedMdxBody(maskInlineTokensForMdx(input.body)).body;
2293
3086
  const mdxValidation = validateMdxBody(preparedBody);
2294
3087
  if (!mdxValidation.ok) {
2295
3088
  issues.push({
@@ -2306,7 +3099,7 @@ function validateProject(config) {
2306
3099
  const issues = [];
2307
3100
  const project = createProject(config);
2308
3101
  const storePath = project.storePath;
2309
- if (!fs2.existsSync(storePath)) {
3102
+ if (!fs14.existsSync(storePath)) {
2310
3103
  issues.push({
2311
3104
  level: "error",
2312
3105
  message: `Missing store.sqlite at ${storePath}`
@@ -2315,7 +3108,7 @@ function validateProject(config) {
2315
3108
  }
2316
3109
  const db = openStore(config, "readonly");
2317
3110
  for (const type of config.types) {
2318
- const enSlugs = listEnSlugs3(config.rootDir, type.contentDir);
3111
+ const enSlugs = listEnSlugs4(config.rootDir, type.contentDir);
2319
3112
  const englishSlugs = new Set(enSlugs);
2320
3113
  if (!type.translate && isRoutableType(type)) {
2321
3114
  issues.push({
@@ -2352,20 +3145,41 @@ function validateProject(config) {
2352
3145
  message: issue.message
2353
3146
  });
2354
3147
  }
2355
- for (const issue of validateDocumentAssets(config, {
3148
+ for (const issue of validateDeclaredAssetFields(config, {
2356
3149
  contentType: type.id,
2357
3150
  enSlug,
2358
3151
  frontmatter: enDoc.frontmatter,
2359
- body: enDoc.content
3152
+ schema: type.schema
2360
3153
  })) {
2361
3154
  issues.push(issue);
2362
3155
  }
2363
- validateDocumentMdxBody(issues, {
2364
- contentType: type.id,
3156
+ const declaredAssetPaths = collectDeclaredAssetPaths(
3157
+ enDoc.frontmatter,
2365
3158
  enSlug,
2366
- locale: config.defaultLocale,
2367
- body: enDoc.content
2368
- });
3159
+ type.schema
3160
+ );
3161
+ for (const issue of validateDocumentAssets(
3162
+ config,
3163
+ {
3164
+ contentType: type.id,
3165
+ enSlug,
3166
+ frontmatter: enDoc.frontmatter,
3167
+ body: enDoc.content
3168
+ },
3169
+ declaredAssetPaths
3170
+ )) {
3171
+ issues.push(issue);
3172
+ }
3173
+ if (type.body === false) {
3174
+ validateBodylessEntry(issues, config, type, enSlug);
3175
+ } else {
3176
+ validateDocumentMdxBody(issues, {
3177
+ contentType: type.id,
3178
+ enSlug,
3179
+ locale: config.defaultLocale,
3180
+ body: enDoc.content
3181
+ });
3182
+ }
2369
3183
  for (const locale of config.locales) {
2370
3184
  if (locale === config.defaultLocale) continue;
2371
3185
  const row = getTranslation(db, type.id, enSlug, locale);
@@ -2382,21 +3196,27 @@ function validateProject(config) {
2382
3196
  });
2383
3197
  }
2384
3198
  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
- })) {
3199
+ for (const issue of validateDocumentAssets(
3200
+ config,
3201
+ {
3202
+ contentType: type.id,
3203
+ enSlug,
3204
+ locale,
3205
+ frontmatter: localeFm,
3206
+ body: preparedBody
3207
+ },
3208
+ declaredAssetPaths
3209
+ )) {
2392
3210
  issues.push(issue);
2393
3211
  }
2394
- validateDocumentMdxBody(issues, {
2395
- contentType: type.id,
2396
- enSlug,
2397
- locale,
2398
- body: row.body
2399
- });
3212
+ if (type.body !== false) {
3213
+ validateDocumentMdxBody(issues, {
3214
+ contentType: type.id,
3215
+ enSlug,
3216
+ locale,
3217
+ body: row.body
3218
+ });
3219
+ }
2400
3220
  }
2401
3221
  }
2402
3222
  try {
@@ -2422,6 +3242,9 @@ function validateProject(config) {
2422
3242
  message: issue.message
2423
3243
  });
2424
3244
  }
3245
+ for (const issue of validateInlineTokens(config)) {
3246
+ issues.push(issue);
3247
+ }
2425
3248
  const dbForSuffix = openStore(config, "readonly");
2426
3249
  try {
2427
3250
  for (const issue of validateTranslationSlugSuffixes(config, dbForSuffix)) {
@@ -2446,6 +3269,10 @@ function computePageEnHash(translatableFrontmatter, body) {
2446
3269
  const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
2447
3270
  return sha256(payload);
2448
3271
  }
3272
+ function computeTranslationEnHash(translatableFrontmatter, body) {
3273
+ const { placeholderBody } = extractInlineTokens(body);
3274
+ return computePageEnHash(translatableFrontmatter, placeholderBody);
3275
+ }
2449
3276
 
2450
3277
  // src/translate/worklist.ts
2451
3278
  function parseContentTypeFilter(contentType) {
@@ -2453,10 +3280,10 @@ function parseContentTypeFilter(contentType) {
2453
3280
  const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2454
3281
  return ids.length > 0 ? new Set(ids) : void 0;
2455
3282
  }
2456
- function listEnSlugs4(rootDir, contentDir) {
3283
+ function listEnSlugs5(rootDir, contentDir) {
2457
3284
  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)$/, ""));
3285
+ if (!fs14.existsSync(dir)) return [];
3286
+ return fs14.readdirSync(dir).filter(isPublishableContentFile).map((f) => f.replace(/\.(md|mdx)$/, ""));
2460
3287
  }
2461
3288
  function buildWorklist(config, options = {}) {
2462
3289
  const db = openStore(config, "readonly");
@@ -2465,12 +3292,16 @@ function buildWorklist(config, options = {}) {
2465
3292
  const contentTypes = parseContentTypeFilter(options.contentType);
2466
3293
  for (const type of config.types) {
2467
3294
  if (contentTypes && !contentTypes.has(type.id)) continue;
2468
- const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
3295
+ if (!isTypeTranslatable(type)) {
3296
+ options.onSkipType?.(type);
3297
+ continue;
3298
+ }
3299
+ const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs5(config.rootDir, type.contentDir);
2469
3300
  for (const enSlug of enSlugs) {
2470
3301
  const enDoc = readEnDocument(config, type, enSlug);
2471
3302
  if (!enDoc) continue;
2472
3303
  const payload = getTranslatablePayload(enDoc, type);
2473
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3304
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
2474
3305
  for (const locale of locales) {
2475
3306
  if (locale === config.defaultLocale) continue;
2476
3307
  const existing = getTranslation(db, type.id, enSlug, locale);
@@ -2932,9 +3763,22 @@ function buildTranslatableSubschema(schema) {
2932
3763
  if (Object.keys(out).length === 0) return null;
2933
3764
  return z.object(out);
2934
3765
  }
2935
- function buildGeminiResponseSchema(schema, slugStrategy) {
3766
+ function buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) {
3767
+ const full = buildTranslatableSubschema(schema);
3768
+ if (!full) return null;
3769
+ const filtered = {};
3770
+ for (const [key, childSchema] of Object.entries(full.shape)) {
3771
+ if (!(key in translatableFrontmatter)) continue;
3772
+ const value = translatableFrontmatter[key];
3773
+ if (value === void 0 || value === null) continue;
3774
+ filtered[key] = childSchema;
3775
+ }
3776
+ if (Object.keys(filtered).length === 0) return null;
3777
+ return z.object(filtered);
3778
+ }
3779
+ function buildGeminiResponseSchema(schema, slugStrategy, translatableFrontmatter) {
2936
3780
  try {
2937
- const translatable = buildTranslatableSubschema(schema);
3781
+ const translatable = translatableFrontmatter !== void 0 ? buildTranslatableSubschemaForPayload(schema, translatableFrontmatter) : buildTranslatableSubschema(schema);
2938
3782
  const responseShape = {
2939
3783
  ...translatable ? { frontmatter: translatable } : {},
2940
3784
  body: z.string()
@@ -2956,9 +3800,12 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2956
3800
 
2957
3801
  // src/translate/resolve-translate-config.ts
2958
3802
  var BUILTIN_TRANSLATE_RULES = [
3803
+ "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
3804
  "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
3805
  "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 \\".'
3806
+ 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".',
3807
+ "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.).",
3808
+ "Otherwise match the format and components used in the EN source."
2962
3809
  ];
2963
3810
  function slugStrategyRules(slugStrategy) {
2964
3811
  if (slugStrategy === "localized") {
@@ -3002,12 +3849,13 @@ function sanitizeTranslatedFrontmatter(rawFrontmatter, schema) {
3002
3849
  return pickTranslatable(rawFrontmatter, schema);
3003
3850
  }
3004
3851
  function validateTranslatedFrontmatter(enDoc, localeFrontmatter, typeSchema) {
3005
- const frontmatter = sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema);
3006
- const merged = mergeStructuralOntoLocale(
3007
- frontmatter,
3008
- enDoc.frontmatter,
3852
+ const enFrontmatter = enDoc.frontmatter;
3853
+ const frontmatter = pruneOrphanNestedTranslations(
3854
+ sanitizeTranslatedFrontmatter(localeFrontmatter, typeSchema),
3855
+ enFrontmatter,
3009
3856
  typeSchema
3010
3857
  );
3858
+ const merged = mergeStructuralOntoLocale(frontmatter, enFrontmatter, typeSchema);
3011
3859
  const parsed = typeSchema.safeParse(merged);
3012
3860
  if (!parsed.success) {
3013
3861
  return { ok: false, error: formatZodIssues(parsed.error) };
@@ -3056,6 +3904,22 @@ function formatTranslateError(error) {
3056
3904
  }
3057
3905
  return message.length > 160 ? `${message.slice(0, 157)}\u2026` : message;
3058
3906
  }
3907
+ function verifyInlineMarkers(enRawBody, translatedBody) {
3908
+ const { tokens } = extractInlineTokens(enRawBody);
3909
+ if (tokens.length === 0) return;
3910
+ const missing = [];
3911
+ const duplicated = [];
3912
+ for (let i = 1; i <= tokens.length; i++) {
3913
+ const count = countMarkerOccurrences(translatedBody, i);
3914
+ if (count === 0) missing.push(placeholderMarker(i));
3915
+ else if (count > 1) duplicated.push(placeholderMarker(i));
3916
+ }
3917
+ if (missing.length === 0 && duplicated.length === 0) return;
3918
+ const parts = [];
3919
+ if (missing.length > 0) parts.push(`missing ${missing.join(", ")}`);
3920
+ if (duplicated.length > 0) parts.push(`duplicated ${duplicated.join(", ")}`);
3921
+ throw new Error(`Inline token markers mismatch: ${parts.join("; ")}`);
3922
+ }
3059
3923
  function resolveContextLabel(enDoc, enSlug) {
3060
3924
  const fm = enDoc.frontmatter;
3061
3925
  for (const key of ["title", "name", "h1"]) {
@@ -3077,7 +3941,8 @@ function prepareTranslation(config, item, options, startedAt) {
3077
3941
  const enDoc = readEnDocument(config, type, item.enSlug);
3078
3942
  if (!enDoc) throw new Error(`EN document not found: ${item.enSlug}`);
3079
3943
  const payload = getTranslatablePayload(enDoc, type);
3080
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
3944
+ const { placeholderBody } = extractInlineTokens(payload.body);
3945
+ const currentEnHash = computeTranslationEnHash(payload.frontmatter, payload.body);
3081
3946
  const db = openStore(config, "readonly");
3082
3947
  const existing = getTranslation(db, type.id, item.enSlug, item.locale);
3083
3948
  db.close();
@@ -3099,11 +3964,15 @@ function prepareTranslation(config, item, options, startedAt) {
3099
3964
  targetLocale: item.locale,
3100
3965
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
3101
3966
  translatableFrontmatter: payload.frontmatter,
3102
- enBody: payload.body,
3967
+ enBody: placeholderBody,
3103
3968
  slugStrategy: type.slugStrategy,
3104
3969
  previousError: item.previousError
3105
3970
  });
3106
- const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
3971
+ const responseSchema = buildGeminiResponseSchema(
3972
+ type.schema,
3973
+ type.slugStrategy,
3974
+ payload.frontmatter
3975
+ );
3107
3976
  return {
3108
3977
  status: "ready",
3109
3978
  prepared: {
@@ -3131,9 +4000,8 @@ function finalizeTranslation(config, prepared, output, options) {
3131
4000
  if (!validated.ok) {
3132
4001
  throw new Error(`Translation validation failed: ${validated.error}`);
3133
4002
  }
3134
- const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
3135
- output.parsed.body
3136
- );
4003
+ const { body: translatedBody, adjusted: mdxAdjusted } = type.body === false ? { body: "", adjusted: false } : assertValidTranslatedMdxBody(output.parsed.body);
4004
+ verifyInlineMarkers(payload.body, translatedBody);
3137
4005
  const writeDb = openStore(config, "readwrite");
3138
4006
  const snapshotId = options.snapshotId ?? recordEnSnapshot(
3139
4007
  config,
@@ -3847,7 +4715,7 @@ async function resumeTranslationJobs(config, options = {}) {
3847
4715
  return results;
3848
4716
  }
3849
4717
  function serializeMdx(frontmatter, content) {
3850
- return matter.stringify(content, frontmatter);
4718
+ return matter4.stringify(content, frontmatter);
3851
4719
  }
3852
4720
 
3853
4721
  // src/export/build-static-raw-exports.ts
@@ -3914,8 +4782,8 @@ function buildStaticRawExports(project, options = {}) {
3914
4782
  return out;
3915
4783
  }
3916
4784
  function rmDirIfExists(dir) {
3917
- if (fs2.existsSync(dir)) {
3918
- fs2.rmSync(dir, { recursive: true, force: true });
4785
+ if (fs14.existsSync(dir)) {
4786
+ fs14.rmSync(dir, { recursive: true, force: true });
3919
4787
  }
3920
4788
  }
3921
4789
  function writeStaticRawExports(project, options = {}) {
@@ -3931,8 +4799,8 @@ function writeStaticRawExports(project, options = {}) {
3931
4799
  const counts = /* @__PURE__ */ new Map();
3932
4800
  for (const item of exports) {
3933
4801
  const filePath = path3.join(outDir, item.relativePath);
3934
- fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
3935
- fs2.writeFileSync(filePath, item.source, "utf8");
4802
+ fs14.mkdirSync(path3.dirname(filePath), { recursive: true });
4803
+ fs14.writeFileSync(filePath, item.source, "utf8");
3936
4804
  const key = `${item.typeId}/${item.locale}`;
3937
4805
  counts.set(key, (counts.get(key) ?? 0) + 1);
3938
4806
  }
@@ -3942,6 +4810,409 @@ function writeStaticRawExports(project, options = {}) {
3942
4810
  return { exports, written: exports.length };
3943
4811
  }
3944
4812
 
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 };
4813
+ // src/delete/plan.ts
4814
+ function isPlanBlocked(plan) {
4815
+ return plan.blocked.length > 0;
4816
+ }
4817
+ function deletedDocs(plan) {
4818
+ return [
4819
+ ...plan.roots.map((r) => ({ typeId: r.typeId, enSlug: r.enSlug })),
4820
+ ...plan.cascades.map((c) => ({ typeId: c.typeId, enSlug: c.enSlug }))
4821
+ ];
4822
+ }
4823
+ function docKey(typeId, enSlug) {
4824
+ return `${typeId}\0${enSlug}`;
4825
+ }
4826
+ function relationSlugsAt(frontmatter, path15) {
4827
+ const out = [];
4828
+ const walk = (container, remaining) => {
4829
+ const [head, ...rest] = remaining;
4830
+ if (head === void 0) return;
4831
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4832
+ const record = container;
4833
+ if (rest.length === 0) {
4834
+ const value = record[head];
4835
+ if (Array.isArray(value)) {
4836
+ for (const slug of value) if (typeof slug === "string" && slug) out.push(slug);
4837
+ } else if (typeof value === "string" && value) {
4838
+ out.push(value);
4839
+ }
4840
+ return;
4841
+ }
4842
+ if (rest[0] === "*") {
4843
+ const arr = record[head];
4844
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4845
+ return;
4846
+ }
4847
+ walk(record[head], rest);
4848
+ };
4849
+ walk(frontmatter, path15);
4850
+ return out;
4851
+ }
4852
+ function assetValuesAt(frontmatter, field2, enSlug) {
4853
+ const out = [];
4854
+ const walk = (container, remaining) => {
4855
+ const [head, ...rest] = remaining;
4856
+ if (head === void 0) return;
4857
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
4858
+ const record = container;
4859
+ if (rest.length === 0) {
4860
+ const value = record[head];
4861
+ if (typeof value === "string" && value) out.push(value);
4862
+ else if (value === void 0 && field2.assetTemplate) {
4863
+ out.push(field2.assetTemplate.split("{slug}").join(enSlug));
4864
+ }
4865
+ return;
4866
+ }
4867
+ if (rest[0] === "*") {
4868
+ const arr = record[head];
4869
+ if (Array.isArray(arr)) for (const item of arr) walk(item, rest.slice(1));
4870
+ return;
4871
+ }
4872
+ walk(record[head], rest);
4873
+ };
4874
+ walk(frontmatter, field2.path);
4875
+ return out;
4876
+ }
4877
+ function buildDeletionPlan(project, typeId, enSlug) {
4878
+ const rootType = (() => {
4879
+ try {
4880
+ return project.getType(typeId);
4881
+ } catch {
4882
+ return null;
4883
+ }
4884
+ })();
4885
+ if (!rootType) {
4886
+ throw new Error(`Unknown content type "${typeId}"`);
4887
+ }
4888
+ const rootDoc = rootType.get(enSlug);
4889
+ if (!rootDoc) {
4890
+ throw new Error(`No ${typeId} entry "${enSlug}"`);
4891
+ }
4892
+ const docByKey = /* @__PURE__ */ new Map();
4893
+ const relationFieldsByType = /* @__PURE__ */ new Map();
4894
+ const assetFieldsByType = /* @__PURE__ */ new Map();
4895
+ const referrers = /* @__PURE__ */ new Map();
4896
+ const assetRefs = /* @__PURE__ */ new Map();
4897
+ const bodyRelationRefs = [];
4898
+ for (const type of project.listTypes()) {
4899
+ const relFields = listRelationFields(type.config.schema);
4900
+ const assetFields = listAssetFields(type.config.schema);
4901
+ relationFieldsByType.set(type.id, relFields);
4902
+ assetFieldsByType.set(type.id, assetFields);
4903
+ for (const doc of type.list()) {
4904
+ const frontmatter = doc.frontmatter;
4905
+ docByKey.set(docKey(type.id, doc.enSlug), {
4906
+ typeId: type.id,
4907
+ enSlug: doc.enSlug,
4908
+ frontmatter
4909
+ });
4910
+ for (const field2 of relFields) {
4911
+ const target = field2.relationTarget;
4912
+ if (!target) continue;
4913
+ for (const slug of relationSlugsAt(frontmatter, field2.path)) {
4914
+ const list = referrers.get(docKey(target, slug)) ?? [];
4915
+ list.push({
4916
+ typeId: type.id,
4917
+ enSlug: doc.enSlug,
4918
+ fieldPath: field2.path.join("."),
4919
+ multiple: Boolean(field2.relationMultiple),
4920
+ optional: Boolean(field2.relationOptional),
4921
+ onTargetDelete: field2.relationOnTargetDelete ?? "restrict"
4922
+ });
4923
+ referrers.set(docKey(target, slug), list);
4924
+ }
4925
+ }
4926
+ for (const field2 of assetFields) {
4927
+ for (const webPath of assetValuesAt(frontmatter, field2, doc.enSlug)) {
4928
+ const set = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
4929
+ set.add(docKey(type.id, doc.enSlug));
4930
+ assetRefs.set(webPath, set);
4931
+ }
4932
+ }
4933
+ for (const token of extractInlineTokens(doc.content).tokens) {
4934
+ if (token.kind === "relation") {
4935
+ bodyRelationRefs.push({
4936
+ typeId: type.id,
4937
+ enSlug: doc.enSlug,
4938
+ targetTypeId: token.targetTypeId,
4939
+ targetEnSlug: token.enSlug
4940
+ });
4941
+ }
4942
+ }
4943
+ }
4944
+ }
4945
+ const deleted = /* @__PURE__ */ new Map();
4946
+ deleted.set(docKey(typeId, enSlug), { typeId, enSlug });
4947
+ const queue = [{ typeId, enSlug }];
4948
+ while (queue.length > 0) {
4949
+ const current = queue.shift();
4950
+ for (const ref of referrers.get(docKey(current.typeId, current.enSlug)) ?? []) {
4951
+ if (ref.onTargetDelete !== "cascade") continue;
4952
+ const refKey = docKey(ref.typeId, ref.enSlug);
4953
+ if (deleted.has(refKey)) continue;
4954
+ deleted.set(refKey, {
4955
+ typeId: ref.typeId,
4956
+ enSlug: ref.enSlug,
4957
+ via: `${ref.fieldPath}=${current.enSlug}`
4958
+ });
4959
+ queue.push({ typeId: ref.typeId, enSlug: ref.enSlug });
4960
+ }
4961
+ }
4962
+ const detaches = [];
4963
+ const blocked = [];
4964
+ const seenDetach = /* @__PURE__ */ new Set();
4965
+ const seenBlock = /* @__PURE__ */ new Set();
4966
+ for (const del of deleted.values()) {
4967
+ for (const ref of referrers.get(docKey(del.typeId, del.enSlug)) ?? []) {
4968
+ if (deleted.has(docKey(ref.typeId, ref.enSlug))) continue;
4969
+ if (ref.onTargetDelete === "restrict") {
4970
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0restrict`;
4971
+ if (!seenBlock.has(k)) {
4972
+ seenBlock.add(k);
4973
+ blocked.push({
4974
+ typeId: ref.typeId,
4975
+ enSlug: ref.enSlug,
4976
+ fieldPath: ref.fieldPath,
4977
+ reason: "restrict"
4978
+ });
4979
+ }
4980
+ } else if (ref.onTargetDelete === "detach") {
4981
+ const requiredSingle = !ref.multiple && !ref.optional;
4982
+ if (requiredSingle) {
4983
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0required-single`;
4984
+ if (!seenBlock.has(k)) {
4985
+ seenBlock.add(k);
4986
+ blocked.push({
4987
+ typeId: ref.typeId,
4988
+ enSlug: ref.enSlug,
4989
+ fieldPath: ref.fieldPath,
4990
+ reason: "required-single"
4991
+ });
4992
+ }
4993
+ } else {
4994
+ const k = `${ref.typeId}\0${ref.enSlug}\0${ref.fieldPath}\0${del.enSlug}`;
4995
+ if (!seenDetach.has(k)) {
4996
+ seenDetach.add(k);
4997
+ detaches.push({
4998
+ typeId: ref.typeId,
4999
+ enSlug: ref.enSlug,
5000
+ fieldPath: ref.fieldPath,
5001
+ removedSlug: del.enSlug
5002
+ });
5003
+ }
5004
+ }
5005
+ }
5006
+ }
5007
+ }
5008
+ const assets = [];
5009
+ const seenAsset = /* @__PURE__ */ new Set();
5010
+ for (const del of deleted.values()) {
5011
+ const record = docByKey.get(docKey(del.typeId, del.enSlug));
5012
+ if (!record) continue;
5013
+ for (const field2 of assetFieldsByType.get(del.typeId) ?? []) {
5014
+ for (const webPath of assetValuesAt(record.frontmatter, field2, del.enSlug)) {
5015
+ const dedupeKey = `${del.typeId}\0${del.enSlug}\0${webPath}`;
5016
+ if (seenAsset.has(dedupeKey)) continue;
5017
+ seenAsset.add(dedupeKey);
5018
+ if (field2.assetOnDelete === "keep") {
5019
+ assets.push({
5020
+ path: webPath,
5021
+ ownerTypeId: del.typeId,
5022
+ ownerEnSlug: del.enSlug,
5023
+ action: "keep",
5024
+ reason: "config-keep"
5025
+ });
5026
+ continue;
5027
+ }
5028
+ const refs = assetRefs.get(webPath) ?? /* @__PURE__ */ new Set();
5029
+ let sharedOutside = false;
5030
+ for (const owner of refs) {
5031
+ if (!deleted.has(owner)) {
5032
+ sharedOutside = true;
5033
+ break;
5034
+ }
5035
+ }
5036
+ assets.push(
5037
+ sharedOutside ? {
5038
+ path: webPath,
5039
+ ownerTypeId: del.typeId,
5040
+ ownerEnSlug: del.enSlug,
5041
+ action: "keep",
5042
+ reason: "shared"
5043
+ } : {
5044
+ path: webPath,
5045
+ ownerTypeId: del.typeId,
5046
+ ownerEnSlug: del.enSlug,
5047
+ action: "delete"
5048
+ }
5049
+ );
5050
+ }
5051
+ }
5052
+ }
5053
+ const store = [];
5054
+ try {
5055
+ const db = openStore(project.config, "readonly");
5056
+ try {
5057
+ for (const del of deleted.values()) {
5058
+ store.push({
5059
+ typeId: del.typeId,
5060
+ enSlug: del.enSlug,
5061
+ translations: countTranslationsForEnSlug(db, del.typeId, del.enSlug),
5062
+ snapshots: countEnSnapshotsForEnSlug(db, del.typeId, del.enSlug)
5063
+ });
5064
+ }
5065
+ } finally {
5066
+ db.close();
5067
+ }
5068
+ } catch {
5069
+ for (const del of deleted.values()) {
5070
+ store.push({ typeId: del.typeId, enSlug: del.enSlug, translations: 0, snapshots: 0 });
5071
+ }
5072
+ }
5073
+ const rootTitle = (() => {
5074
+ const title = rootDoc.frontmatter.title;
5075
+ return typeof title === "string" && title.trim() ? title : void 0;
5076
+ })();
5077
+ const cascades = [];
5078
+ for (const del of deleted.values()) {
5079
+ if (del.typeId === typeId && del.enSlug === enSlug) continue;
5080
+ cascades.push({ typeId: del.typeId, enSlug: del.enSlug, via: del.via ?? "" });
5081
+ }
5082
+ const bodyRefWarnings = bodyRelationRefs.filter(
5083
+ (ref) => deleted.has(docKey(ref.targetTypeId, ref.targetEnSlug)) && !deleted.has(docKey(ref.typeId, ref.enSlug))
5084
+ );
5085
+ return {
5086
+ roots: [{ typeId, enSlug, title: rootTitle }],
5087
+ cascades,
5088
+ detaches,
5089
+ blocked,
5090
+ assets,
5091
+ store,
5092
+ bodyRefWarnings
5093
+ };
5094
+ }
5095
+ function enFilePath(rootDir, contentDir, enSlug) {
5096
+ for (const ext of [".mdx", ".md"]) {
5097
+ const candidate = path3.join(rootDir, contentDir, `${enSlug}${ext}`);
5098
+ if (fs14.existsSync(candidate)) return candidate;
5099
+ }
5100
+ return null;
5101
+ }
5102
+ function rewriteFrontmatter(raw, data) {
5103
+ const match = raw.match(/^()?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/);
5104
+ if (!match) {
5105
+ throw new Error("Cannot rewrite frontmatter: no leading YAML block found");
5106
+ }
5107
+ const bom = match[1] ?? "";
5108
+ const body = raw.slice(match[0].length);
5109
+ const serialized = matter4.stringify("", data);
5110
+ const blockMatch = serialized.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/);
5111
+ const block = blockMatch ? blockMatch[0] : serialized;
5112
+ return `${bom}${block}${body}`;
5113
+ }
5114
+ function removeSlugAtPath(container, remaining, removedSlug, multiple) {
5115
+ const [head, ...rest] = remaining;
5116
+ if (head === void 0) return;
5117
+ if (typeof container !== "object" || container === null || Array.isArray(container)) return;
5118
+ const record = container;
5119
+ if (rest.length === 0) {
5120
+ if (multiple) {
5121
+ const value = record[head];
5122
+ if (Array.isArray(value)) {
5123
+ record[head] = value.filter((slug) => slug !== removedSlug);
5124
+ }
5125
+ } else if (record[head] === removedSlug) {
5126
+ delete record[head];
5127
+ }
5128
+ return;
5129
+ }
5130
+ if (rest[0] === "*") {
5131
+ const arr = record[head];
5132
+ if (Array.isArray(arr)) {
5133
+ for (const item of arr) removeSlugAtPath(item, rest.slice(1), removedSlug, multiple);
5134
+ }
5135
+ return;
5136
+ }
5137
+ removeSlugAtPath(record[head], rest, removedSlug, multiple);
5138
+ }
5139
+ function executeDeletionPlan(project, plan) {
5140
+ if (isPlanBlocked(plan)) {
5141
+ throw new Error("Refusing to execute a blocked deletion plan");
5142
+ }
5143
+ const config = project.config;
5144
+ const result = {
5145
+ deletedFiles: [],
5146
+ deletedAssets: [],
5147
+ detachedFiles: [],
5148
+ translationsDeleted: 0,
5149
+ snapshotsDeleted: 0
5150
+ };
5151
+ const detachesByFile = /* @__PURE__ */ new Map();
5152
+ for (const detach of plan.detaches) {
5153
+ const key = `${detach.typeId} ${detach.enSlug}`;
5154
+ const list = detachesByFile.get(key) ?? [];
5155
+ list.push(detach);
5156
+ detachesByFile.set(key, list);
5157
+ }
5158
+ for (const [, detaches] of detachesByFile) {
5159
+ const { typeId, enSlug } = detaches[0];
5160
+ const type = project.getType(typeId);
5161
+ const file = enFilePath(config.rootDir, type.config.contentDir, enSlug);
5162
+ if (!file) continue;
5163
+ const fieldByPath = new Map(
5164
+ listRelationFields(type.config.schema).map((f) => [f.path.join("."), f])
5165
+ );
5166
+ const raw = fs14.readFileSync(file, "utf8");
5167
+ const data = structuredClone(matter4(raw).data);
5168
+ for (const detach of detaches) {
5169
+ const field2 = fieldByPath.get(detach.fieldPath);
5170
+ if (!field2) continue;
5171
+ removeSlugAtPath(
5172
+ data,
5173
+ detach.fieldPath.split("."),
5174
+ detach.removedSlug,
5175
+ Boolean(field2.relationMultiple)
5176
+ );
5177
+ }
5178
+ fs14.writeFileSync(file, rewriteFrontmatter(raw, data), "utf8");
5179
+ result.detachedFiles.push(file);
5180
+ }
5181
+ const assetsDir = config.assets?.assetsPath ?? config.assetsPath;
5182
+ for (const asset of plan.assets) {
5183
+ if (asset.action !== "delete") continue;
5184
+ if (!assetsDir) continue;
5185
+ const root = path3.resolve(assetsDir);
5186
+ const relative = asset.path.replace(/^\/+/, "");
5187
+ const abs = path3.resolve(root, relative);
5188
+ const rootWithSep = root.endsWith(path3.sep) ? root : root + path3.sep;
5189
+ if (abs !== root && !abs.startsWith(rootWithSep)) continue;
5190
+ if (fs14.existsSync(abs)) {
5191
+ fs14.rmSync(abs, { force: true });
5192
+ result.deletedAssets.push(abs);
5193
+ }
5194
+ }
5195
+ for (const doc of deletedDocs(plan)) {
5196
+ const type = project.getType(doc.typeId);
5197
+ const file = enFilePath(config.rootDir, type.config.contentDir, doc.enSlug);
5198
+ if (file && fs14.existsSync(file)) {
5199
+ fs14.rmSync(file, { force: true });
5200
+ result.deletedFiles.push(file);
5201
+ }
5202
+ }
5203
+ const db = openStore(config, "readwrite");
5204
+ try {
5205
+ for (const doc of deletedDocs(plan)) {
5206
+ result.translationsDeleted += deleteTranslationsForEnSlug(db, doc.typeId, doc.enSlug);
5207
+ result.snapshotsDeleted += deleteEnSnapshotsForEnSlug(db, doc.typeId, doc.enSlug);
5208
+ }
5209
+ } finally {
5210
+ db.close();
5211
+ }
5212
+ bumpContentVersion();
5213
+ return result;
5214
+ }
5215
+
5216
+ 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
5217
  //# sourceMappingURL=index.js.map
3947
5218
  //# sourceMappingURL=index.js.map