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