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