scribe-cms 0.0.17 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli/index.cjs +4013 -555
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +4011 -553
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1502 -229
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1487 -228
- 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 +8 -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 +8 -1
- 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 +4691 -304
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +4689 -302
- 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/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import
|
|
3
|
+
import fs3 from 'fs';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
6
|
import remarkMdx from 'remark-mdx';
|
|
@@ -131,7 +131,15 @@ function resolveConfig(input, baseDir) {
|
|
|
131
131
|
const projectRoot = path.resolve(process.cwd(), raw.rootDir);
|
|
132
132
|
const contentRoot = path.resolve(projectRoot, raw.contentDir ?? "content");
|
|
133
133
|
const storePath = path.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
134
|
-
|
|
134
|
+
let assetsPath;
|
|
135
|
+
let assets;
|
|
136
|
+
if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
|
|
137
|
+
const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
|
|
138
|
+
const publicPath = raw.assets?.publicPath ?? "/";
|
|
139
|
+
const managedDirs = raw.assets?.managedDirs ?? [];
|
|
140
|
+
assetsPath = path.resolve(projectRoot, dir);
|
|
141
|
+
assets = { assetsPath, publicPath, managedDirs };
|
|
142
|
+
}
|
|
135
143
|
const seenIds = /* @__PURE__ */ new Set();
|
|
136
144
|
const types = raw.types.map((type) => {
|
|
137
145
|
if (seenIds.has(type.id)) {
|
|
@@ -146,13 +154,17 @@ function resolveConfig(input, baseDir) {
|
|
|
146
154
|
contentDir: type.contentDir ?? type.id,
|
|
147
155
|
label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
|
|
148
156
|
slugStrategy: type.slugStrategy ?? "fixed",
|
|
149
|
-
indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
|
|
157
|
+
indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
|
|
158
|
+
// `body` defaults to true (fully backwards compatible). A resolved type
|
|
159
|
+
// always carries an explicit boolean so consumers read one source of truth.
|
|
160
|
+
body: type.body ?? true
|
|
150
161
|
};
|
|
151
162
|
});
|
|
152
163
|
const config = {
|
|
153
164
|
rootDir: contentRoot,
|
|
154
165
|
storePath,
|
|
155
166
|
assetsPath,
|
|
167
|
+
assets,
|
|
156
168
|
locales: [...raw.locales],
|
|
157
169
|
defaultLocale,
|
|
158
170
|
localeRouting,
|
|
@@ -188,6 +200,7 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
|
|
|
188
200
|
}
|
|
189
201
|
var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
|
|
190
202
|
var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
|
|
203
|
+
var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
|
|
191
204
|
function getFieldKind(schema) {
|
|
192
205
|
const tagged = schema;
|
|
193
206
|
return tagged[FIELD_KIND] ?? "structural";
|
|
@@ -215,6 +228,29 @@ function getRelationTarget(schema) {
|
|
|
215
228
|
}
|
|
216
229
|
return null;
|
|
217
230
|
}
|
|
231
|
+
function getAssetMeta(schema) {
|
|
232
|
+
let current = schema;
|
|
233
|
+
for (let i = 0; i < 8; i++) {
|
|
234
|
+
const tagged = current;
|
|
235
|
+
if (tagged[ASSET_META]) {
|
|
236
|
+
return tagged[ASSET_META];
|
|
237
|
+
}
|
|
238
|
+
if (typeof tagged.unwrap === "function") {
|
|
239
|
+
current = tagged.unwrap();
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (typeof tagged.removeDefault === "function") {
|
|
243
|
+
current = tagged.removeDefault();
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (tagged._def?.innerType) {
|
|
247
|
+
current = tagged._def.innerType;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
218
254
|
function peelOptionalWrappers(schema) {
|
|
219
255
|
let current = schema;
|
|
220
256
|
for (let i = 0; i < 8; i++) {
|
|
@@ -248,7 +284,23 @@ function introspectSchema(schema, prefix = []) {
|
|
|
248
284
|
kind: "relation",
|
|
249
285
|
relationTarget: relation.typeId,
|
|
250
286
|
relationMultiple: relation.multiple,
|
|
251
|
-
relationOptional: relation.optional
|
|
287
|
+
relationOptional: relation.optional,
|
|
288
|
+
relationOnTargetDelete: relation.onTargetDelete
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
const asset = getAssetMeta(schema);
|
|
293
|
+
if (asset && prefix.length > 0) {
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
path: prefix,
|
|
297
|
+
kind: "asset",
|
|
298
|
+
assetDir: asset.dir,
|
|
299
|
+
assetTemplate: asset.template,
|
|
300
|
+
assetFormats: asset.formats,
|
|
301
|
+
assetMaxKB: asset.maxKB,
|
|
302
|
+
assetOptional: asset.optional,
|
|
303
|
+
assetOnDelete: asset.onDelete
|
|
252
304
|
}
|
|
253
305
|
];
|
|
254
306
|
}
|
|
@@ -326,7 +378,7 @@ function pickTranslatable(data, schema) {
|
|
|
326
378
|
return extractByPaths(data, translatablePaths);
|
|
327
379
|
}
|
|
328
380
|
function pickStructural(data, schema) {
|
|
329
|
-
const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
|
|
381
|
+
const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
|
|
330
382
|
return extractByPaths(data, structuralPaths);
|
|
331
383
|
}
|
|
332
384
|
function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
@@ -341,7 +393,7 @@ function deepMerge(base, overlay) {
|
|
|
341
393
|
out[key] = deepMerge(out[key], value);
|
|
342
394
|
} else if (Array.isArray(value) && Array.isArray(out[key])) {
|
|
343
395
|
out[key] = mergeArrayOverlay(out[key], value);
|
|
344
|
-
} else {
|
|
396
|
+
} else if (value !== void 0) {
|
|
345
397
|
out[key] = value;
|
|
346
398
|
}
|
|
347
399
|
}
|
|
@@ -359,6 +411,9 @@ function mergeArrayOverlay(base, overlay) {
|
|
|
359
411
|
function listRelationFields(schema) {
|
|
360
412
|
return introspectSchema(schema).filter((f) => f.kind === "relation");
|
|
361
413
|
}
|
|
414
|
+
function listAssetFields(schema) {
|
|
415
|
+
return introspectSchema(schema).filter((f) => f.kind === "asset");
|
|
416
|
+
}
|
|
362
417
|
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
363
418
|
var slugPatternSchema = z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
|
|
364
419
|
var isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
|
|
@@ -439,12 +494,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
|
|
|
439
494
|
delete rest[internalKey];
|
|
440
495
|
}
|
|
441
496
|
}
|
|
497
|
+
let vars;
|
|
498
|
+
if (rest.vars !== void 0) {
|
|
499
|
+
vars = rest.vars;
|
|
500
|
+
delete rest.vars;
|
|
501
|
+
}
|
|
442
502
|
return {
|
|
443
503
|
builtin: {
|
|
444
504
|
publishedAt,
|
|
445
505
|
updatedAt,
|
|
446
506
|
noindex,
|
|
447
|
-
canonicalPathOverride
|
|
507
|
+
canonicalPathOverride,
|
|
508
|
+
vars
|
|
448
509
|
},
|
|
449
510
|
rest,
|
|
450
511
|
issues
|
|
@@ -474,6 +535,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
|
|
|
474
535
|
if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
|
|
475
536
|
out.noindex = doc.noindex;
|
|
476
537
|
out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
|
|
538
|
+
if (doc.vars !== void 0) out.vars = doc.vars;
|
|
477
539
|
return out;
|
|
478
540
|
}
|
|
479
541
|
function seoFieldsFromEn(enDoc) {
|
|
@@ -563,7 +625,7 @@ function resolveStorePath(config) {
|
|
|
563
625
|
function openStore(config, mode = "readwrite") {
|
|
564
626
|
const storePath = resolveStorePath(config);
|
|
565
627
|
if (mode === "readwrite") {
|
|
566
|
-
|
|
628
|
+
fs3.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
567
629
|
}
|
|
568
630
|
const db = new Database(storePath, { readonly: mode === "readonly" });
|
|
569
631
|
if (mode === "readwrite") {
|
|
@@ -602,6 +664,9 @@ function migrate(db) {
|
|
|
602
664
|
function listTranslationsForType(db, contentType) {
|
|
603
665
|
return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
|
|
604
666
|
}
|
|
667
|
+
function bulkLoadTranslations(db) {
|
|
668
|
+
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
669
|
+
}
|
|
605
670
|
|
|
606
671
|
// src/translate/normalize-mdx-body.ts
|
|
607
672
|
function needsMdxEscapeNormalization(body) {
|
|
@@ -718,6 +783,279 @@ function prepareTranslatedMdxBody(body) {
|
|
|
718
783
|
};
|
|
719
784
|
}
|
|
720
785
|
|
|
786
|
+
// src/loader/resolve-assets.ts
|
|
787
|
+
function joinPublicPath(publicPath, webPath) {
|
|
788
|
+
if (publicPath === "" || publicPath === "/") return webPath;
|
|
789
|
+
const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
|
|
790
|
+
return publicPath.replace(/\/+$/, "") + suffix;
|
|
791
|
+
}
|
|
792
|
+
function materializeTemplate(template, enSlug) {
|
|
793
|
+
return template.split("{slug}").join(enSlug);
|
|
794
|
+
}
|
|
795
|
+
function setAssetAtPath(container, path6, transform) {
|
|
796
|
+
const [head, ...rest] = path6;
|
|
797
|
+
if (head === void 0) return;
|
|
798
|
+
if (rest.length === 0) {
|
|
799
|
+
const current = container[head];
|
|
800
|
+
const next = transform(typeof current === "string" ? current : void 0);
|
|
801
|
+
if (next !== void 0) container[head] = next;
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (rest[0] === "*") {
|
|
805
|
+
const arr = container[head];
|
|
806
|
+
if (!Array.isArray(arr)) return;
|
|
807
|
+
for (const item of arr) {
|
|
808
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
809
|
+
setAssetAtPath(item, rest.slice(1), transform);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const child = container[head];
|
|
815
|
+
if (child && typeof child === "object" && !Array.isArray(child)) {
|
|
816
|
+
setAssetAtPath(child, rest, transform);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function resolveDocumentAssets(doc, assetFields, assets) {
|
|
820
|
+
const frontmatter = doc.frontmatter;
|
|
821
|
+
for (const f of assetFields) {
|
|
822
|
+
setAssetAtPath(frontmatter, f.path, (current) => {
|
|
823
|
+
let value = current;
|
|
824
|
+
if (value === void 0) {
|
|
825
|
+
if (!f.assetTemplate) return void 0;
|
|
826
|
+
value = materializeTemplate(f.assetTemplate, doc.enSlug);
|
|
827
|
+
}
|
|
828
|
+
return joinPublicPath(assets.publicPath, value);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/inline/tokens.ts
|
|
834
|
+
var TOKEN_OPEN = "${{";
|
|
835
|
+
var ESCAPE_SEQUENCE = "$\\{{";
|
|
836
|
+
var ESCAPE_RESULT = "${{";
|
|
837
|
+
function placeholderMarker(n) {
|
|
838
|
+
return `%%${n}%%`;
|
|
839
|
+
}
|
|
840
|
+
function findTokenEnd(body, start) {
|
|
841
|
+
let i = start;
|
|
842
|
+
let inString = false;
|
|
843
|
+
while (i < body.length) {
|
|
844
|
+
const ch = body[i];
|
|
845
|
+
if (ch === "\n") return -1;
|
|
846
|
+
if (inString) {
|
|
847
|
+
if (ch === "\\") {
|
|
848
|
+
i += 2;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (ch === '"') inString = false;
|
|
852
|
+
i += 1;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (ch === '"') {
|
|
856
|
+
inString = true;
|
|
857
|
+
i += 1;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (ch === "}" && body[i + 1] === "}") return i;
|
|
861
|
+
i += 1;
|
|
862
|
+
}
|
|
863
|
+
return -1;
|
|
864
|
+
}
|
|
865
|
+
function parseTokenInner(inner) {
|
|
866
|
+
const colon = inner.indexOf(":");
|
|
867
|
+
if (colon === -1) {
|
|
868
|
+
return { ok: false, reason: `missing kind separator ":"` };
|
|
869
|
+
}
|
|
870
|
+
const kind = inner.slice(0, colon);
|
|
871
|
+
const rest = inner.slice(colon + 1);
|
|
872
|
+
switch (kind) {
|
|
873
|
+
case "static": {
|
|
874
|
+
let text;
|
|
875
|
+
try {
|
|
876
|
+
text = JSON.parse(rest);
|
|
877
|
+
} catch {
|
|
878
|
+
return { ok: false, reason: `static value is not a valid JSON string` };
|
|
879
|
+
}
|
|
880
|
+
if (typeof text !== "string") {
|
|
881
|
+
return { ok: false, reason: `static value must be a JSON string` };
|
|
882
|
+
}
|
|
883
|
+
return { ok: true, token: { kind: "static", text } };
|
|
884
|
+
}
|
|
885
|
+
case "relation": {
|
|
886
|
+
const parts = rest.split(":");
|
|
887
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
888
|
+
return {
|
|
889
|
+
ok: false,
|
|
890
|
+
reason: `relation must be relation:<typeId>:<enSlug> or relation:<typeId>:<enSlug>:slug`
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
const [targetTypeId, enSlug, mode] = parts;
|
|
894
|
+
if (!targetTypeId || !enSlug) {
|
|
895
|
+
return { ok: false, reason: `relation is missing a typeId or enSlug` };
|
|
896
|
+
}
|
|
897
|
+
if (mode !== void 0 && mode !== "slug") {
|
|
898
|
+
return { ok: false, reason: `relation mode must be "slug" (got "${mode}")` };
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
ok: true,
|
|
902
|
+
token: {
|
|
903
|
+
kind: "relation",
|
|
904
|
+
targetTypeId,
|
|
905
|
+
enSlug,
|
|
906
|
+
mode: mode === "slug" ? "slug" : "url"
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
case "asset": {
|
|
911
|
+
if (!rest.startsWith("/")) {
|
|
912
|
+
return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
|
|
913
|
+
}
|
|
914
|
+
return { ok: true, token: { kind: "asset", webPath: rest } };
|
|
915
|
+
}
|
|
916
|
+
case "var": {
|
|
917
|
+
if (!rest) return { ok: false, reason: `var is missing a key` };
|
|
918
|
+
return { ok: true, token: { kind: "var", key: rest } };
|
|
919
|
+
}
|
|
920
|
+
default:
|
|
921
|
+
return { ok: false, reason: `unknown token kind "${kind}"` };
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function extractInlineTokens(body) {
|
|
925
|
+
const tokens = [];
|
|
926
|
+
const malformed = [];
|
|
927
|
+
let out = "";
|
|
928
|
+
let i = 0;
|
|
929
|
+
while (i < body.length) {
|
|
930
|
+
const open = body.indexOf(TOKEN_OPEN, i);
|
|
931
|
+
if (open === -1) {
|
|
932
|
+
out += body.slice(i);
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
out += body.slice(i, open);
|
|
936
|
+
const contentStart = open + TOKEN_OPEN.length;
|
|
937
|
+
const end = findTokenEnd(body, contentStart);
|
|
938
|
+
if (end === -1) {
|
|
939
|
+
malformed.push({
|
|
940
|
+
raw: body.slice(open),
|
|
941
|
+
reason: `unterminated token (missing "}}")`,
|
|
942
|
+
index: open
|
|
943
|
+
});
|
|
944
|
+
out += body.slice(open);
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
const raw = body.slice(open, end + 2);
|
|
948
|
+
const inner = body.slice(contentStart, end);
|
|
949
|
+
const parsed = parseTokenInner(inner);
|
|
950
|
+
if (parsed.ok) {
|
|
951
|
+
tokens.push({ ...parsed.token, raw });
|
|
952
|
+
out += placeholderMarker(tokens.length);
|
|
953
|
+
} else {
|
|
954
|
+
malformed.push({ raw, reason: parsed.reason, index: open });
|
|
955
|
+
out += raw;
|
|
956
|
+
}
|
|
957
|
+
i = end + 2;
|
|
958
|
+
}
|
|
959
|
+
return { placeholderBody: out, tokens, malformed };
|
|
960
|
+
}
|
|
961
|
+
function fillPlaceholders(body, resolvedValues) {
|
|
962
|
+
return body.replace(/%%(\d+)%%/g, (match, n) => {
|
|
963
|
+
const idx = Number(n) - 1;
|
|
964
|
+
return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function unescapeInlineTokens(body) {
|
|
968
|
+
return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/inline/resolve-tokens.ts
|
|
972
|
+
function slugKey(typeId, enSlug, locale) {
|
|
973
|
+
return `${typeId}\0${enSlug}\0${locale}`;
|
|
974
|
+
}
|
|
975
|
+
function isStringRecord(value) {
|
|
976
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
977
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
978
|
+
}
|
|
979
|
+
function createInlineResolver(config) {
|
|
980
|
+
const urlBuilder = createUrlBuilder(config);
|
|
981
|
+
const typeById = new Map(config.types.map((t) => [t.id, t]));
|
|
982
|
+
const storePath = resolveStorePath(config);
|
|
983
|
+
let slugCache = null;
|
|
984
|
+
const localizedSlugIndex = () => {
|
|
985
|
+
let mtime = 0;
|
|
986
|
+
try {
|
|
987
|
+
mtime = fs3.statSync(storePath).mtimeMs;
|
|
988
|
+
} catch {
|
|
989
|
+
mtime = 0;
|
|
990
|
+
}
|
|
991
|
+
if (slugCache && slugCache.mtime === mtime) return slugCache.index;
|
|
992
|
+
const index = /* @__PURE__ */ new Map();
|
|
993
|
+
try {
|
|
994
|
+
const db = openStore(config, "readonly");
|
|
995
|
+
try {
|
|
996
|
+
for (const row of bulkLoadTranslations(db)) {
|
|
997
|
+
index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
|
|
998
|
+
}
|
|
999
|
+
} finally {
|
|
1000
|
+
db.close();
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
slugCache = { mtime, index };
|
|
1005
|
+
return index;
|
|
1006
|
+
};
|
|
1007
|
+
const localizedSlug = (typeId, enSlug, locale) => {
|
|
1008
|
+
if (locale === config.defaultLocale) return enSlug;
|
|
1009
|
+
const index = localizedSlugIndex();
|
|
1010
|
+
const direct = index.get(slugKey(typeId, enSlug, locale));
|
|
1011
|
+
if (direct) return direct;
|
|
1012
|
+
for (const fb of config.localeFallbacks?.[locale] ?? []) {
|
|
1013
|
+
const hit = index.get(slugKey(typeId, enSlug, fb));
|
|
1014
|
+
if (hit) return hit;
|
|
1015
|
+
}
|
|
1016
|
+
return enSlug;
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
resolve(token, enFrontmatter, locale) {
|
|
1020
|
+
switch (token.kind) {
|
|
1021
|
+
case "static":
|
|
1022
|
+
return token.text;
|
|
1023
|
+
case "var": {
|
|
1024
|
+
const vars = enFrontmatter.vars;
|
|
1025
|
+
if (isStringRecord(vars) && typeof vars[token.key] === "string") {
|
|
1026
|
+
return vars[token.key];
|
|
1027
|
+
}
|
|
1028
|
+
return "";
|
|
1029
|
+
}
|
|
1030
|
+
case "asset": {
|
|
1031
|
+
const assets = config.assets;
|
|
1032
|
+
return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
|
|
1033
|
+
}
|
|
1034
|
+
case "relation": {
|
|
1035
|
+
const type = typeById.get(token.targetTypeId);
|
|
1036
|
+
if (!type) return "";
|
|
1037
|
+
if (token.mode === "slug") return token.enSlug;
|
|
1038
|
+
if (!isRoutableType(type)) return "";
|
|
1039
|
+
const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
|
|
1040
|
+
return urlBuilder.resolvePath(type.path, slug, locale);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
|
|
1047
|
+
const { placeholderBody, tokens } = extractInlineTokens(rawBody);
|
|
1048
|
+
if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
|
|
1049
|
+
const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
|
|
1050
|
+
return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
|
|
1051
|
+
}
|
|
1052
|
+
function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
|
|
1053
|
+
const { tokens } = extractInlineTokens(enRawBody);
|
|
1054
|
+
if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
|
|
1055
|
+
const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
|
|
1056
|
+
return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
721
1059
|
// src/loader/normalize-en.ts
|
|
722
1060
|
function normalizeEnFrontmatter(data) {
|
|
723
1061
|
const out = { ...data };
|
|
@@ -743,14 +1081,14 @@ function isPostFile(name) {
|
|
|
743
1081
|
return isPublishableContentFile(name);
|
|
744
1082
|
}
|
|
745
1083
|
function listEnFiles(contentDir) {
|
|
746
|
-
if (!
|
|
747
|
-
return
|
|
1084
|
+
if (!fs3.existsSync(contentDir)) return [];
|
|
1085
|
+
return fs3.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
|
|
748
1086
|
}
|
|
749
1087
|
function slugFromPath(filePath) {
|
|
750
1088
|
return path.basename(filePath).replace(/\.(md|mdx)$/, "");
|
|
751
1089
|
}
|
|
752
1090
|
function parseEnMdx(filePath, config, type) {
|
|
753
|
-
const raw =
|
|
1091
|
+
const raw = fs3.readFileSync(filePath, "utf8");
|
|
754
1092
|
const parsed = matter(raw);
|
|
755
1093
|
const slug = slugFromPath(filePath);
|
|
756
1094
|
const normalized = normalizeEnFrontmatter(parsed.data);
|
|
@@ -789,6 +1127,7 @@ function parseEnMdx(filePath, config, type) {
|
|
|
789
1127
|
updatedAt: builtin.updatedAt,
|
|
790
1128
|
noindex: builtin.noindex,
|
|
791
1129
|
canonicalPathOverride: builtin.canonicalPathOverride,
|
|
1130
|
+
vars: builtin.vars,
|
|
792
1131
|
slug,
|
|
793
1132
|
locale: config.defaultLocale
|
|
794
1133
|
},
|
|
@@ -813,12 +1152,15 @@ function parseEnMdx(filePath, config, type) {
|
|
|
813
1152
|
noindex: builtin.noindex,
|
|
814
1153
|
canonicalPathOverride: builtin.canonicalPathOverride,
|
|
815
1154
|
frontmatter,
|
|
816
|
-
|
|
1155
|
+
// Bodyless types (`body: false`) never carry a body: the loader skips the
|
|
1156
|
+
// MDX body entirely so runtimes, exports, and hashing all see an empty body.
|
|
1157
|
+
// A stray body is reported separately by `scribe validate`.
|
|
1158
|
+
content: type.body === false ? "" : parsed.content,
|
|
817
1159
|
filePath
|
|
818
1160
|
};
|
|
819
1161
|
return { document, issues };
|
|
820
1162
|
}
|
|
821
|
-
function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
1163
|
+
function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
|
|
822
1164
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
823
1165
|
const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
|
|
824
1166
|
const seo = seoFieldsFromEn(enDoc);
|
|
@@ -829,6 +1171,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
829
1171
|
config.defaultLocale,
|
|
830
1172
|
config.localeRouting
|
|
831
1173
|
);
|
|
1174
|
+
const preparedBody = prepareTranslatedMdxBody(row.body).body;
|
|
1175
|
+
const content = inlineResolver ? fillTranslatedInlineBody(
|
|
1176
|
+
preparedBody,
|
|
1177
|
+
enDoc.content,
|
|
1178
|
+
enDoc.frontmatter,
|
|
1179
|
+
row.locale,
|
|
1180
|
+
inlineResolver
|
|
1181
|
+
) : preparedBody;
|
|
832
1182
|
return {
|
|
833
1183
|
slug: row.slug,
|
|
834
1184
|
enSlug: row.en_slug,
|
|
@@ -838,14 +1188,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
838
1188
|
noindex: seo.noindex,
|
|
839
1189
|
canonicalPathOverride: seo.canonicalPathOverride,
|
|
840
1190
|
frontmatter,
|
|
841
|
-
content
|
|
1191
|
+
content
|
|
842
1192
|
};
|
|
843
1193
|
}
|
|
844
1194
|
var DEV_REVALIDATE_MS = 1500;
|
|
845
|
-
|
|
1195
|
+
var contentVersion = 0;
|
|
1196
|
+
function createContentLoader(config, type, options = {}) {
|
|
846
1197
|
let cached = null;
|
|
847
1198
|
let signature = "";
|
|
848
1199
|
let lastCheck = 0;
|
|
1200
|
+
let builtVersion = contentVersion;
|
|
849
1201
|
const contentDir = path.join(
|
|
850
1202
|
/* turbopackIgnore: true */
|
|
851
1203
|
config.rootDir,
|
|
@@ -853,18 +1205,34 @@ function createContentLoader(config, type) {
|
|
|
853
1205
|
);
|
|
854
1206
|
const storePath = resolveStorePath(config);
|
|
855
1207
|
const isProd = process.env.NODE_ENV === "production";
|
|
1208
|
+
const assets = config.assets;
|
|
1209
|
+
const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
|
|
1210
|
+
const resolveAssets = (doc) => {
|
|
1211
|
+
if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
|
|
1212
|
+
};
|
|
1213
|
+
const inlineResolver = options.inlineResolver;
|
|
1214
|
+
const resolveEnInline = (doc) => {
|
|
1215
|
+
if (inlineResolver) {
|
|
1216
|
+
doc.content = substituteEnInlineBody(
|
|
1217
|
+
doc.content,
|
|
1218
|
+
doc.frontmatter,
|
|
1219
|
+
config.defaultLocale,
|
|
1220
|
+
inlineResolver
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
856
1224
|
function computeSignature() {
|
|
857
1225
|
const files = listEnFiles(contentDir);
|
|
858
1226
|
let newest = 0;
|
|
859
1227
|
for (const f of files) {
|
|
860
1228
|
try {
|
|
861
|
-
newest = Math.max(newest,
|
|
1229
|
+
newest = Math.max(newest, fs3.statSync(f).mtimeMs);
|
|
862
1230
|
} catch {
|
|
863
1231
|
}
|
|
864
1232
|
}
|
|
865
1233
|
let store = 0;
|
|
866
1234
|
try {
|
|
867
|
-
store =
|
|
1235
|
+
store = fs3.statSync(storePath).mtimeMs;
|
|
868
1236
|
} catch {
|
|
869
1237
|
}
|
|
870
1238
|
return `${files.length}:${newest}:${store}`;
|
|
@@ -910,16 +1278,28 @@ function createContentLoader(config, type) {
|
|
|
910
1278
|
for (const row of rowsByLocale.get(locale) ?? []) {
|
|
911
1279
|
const enDoc = englishBySlug.get(row.en_slug);
|
|
912
1280
|
if (!enDoc) continue;
|
|
913
|
-
const doc = buildDocumentFromTranslation(row, enDoc, type, config);
|
|
1281
|
+
const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
|
|
1282
|
+
resolveAssets(doc);
|
|
914
1283
|
bySlug.set(doc.slug, doc);
|
|
915
1284
|
byEnSlug.set(row.en_slug, doc);
|
|
916
1285
|
}
|
|
917
1286
|
out.set(locale, { bySlug, byEnSlug });
|
|
918
1287
|
}
|
|
1288
|
+
for (const doc of englishBySlug.values()) {
|
|
1289
|
+
resolveAssets(doc);
|
|
1290
|
+
resolveEnInline(doc);
|
|
1291
|
+
}
|
|
919
1292
|
return out;
|
|
920
1293
|
}
|
|
921
1294
|
return () => {
|
|
922
1295
|
if (cached) {
|
|
1296
|
+
if (contentVersion !== builtVersion) {
|
|
1297
|
+
cached = build();
|
|
1298
|
+
builtVersion = contentVersion;
|
|
1299
|
+
lastCheck = Date.now();
|
|
1300
|
+
signature = isProd ? "" : computeSignature();
|
|
1301
|
+
return cached;
|
|
1302
|
+
}
|
|
923
1303
|
if (isProd) return cached;
|
|
924
1304
|
const now = Date.now();
|
|
925
1305
|
if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
|
|
@@ -932,6 +1312,7 @@ function createContentLoader(config, type) {
|
|
|
932
1312
|
}
|
|
933
1313
|
lastCheck = Date.now();
|
|
934
1314
|
cached = build();
|
|
1315
|
+
builtVersion = contentVersion;
|
|
935
1316
|
signature = isProd ? "" : computeSignature();
|
|
936
1317
|
return cached;
|
|
937
1318
|
};
|
|
@@ -1021,8 +1402,11 @@ function comparatorFor(orderBy) {
|
|
|
1021
1402
|
return (a, b) => a.slug.localeCompare(b.slug);
|
|
1022
1403
|
}
|
|
1023
1404
|
}
|
|
1024
|
-
function buildRuntime(config, type, getRuntime) {
|
|
1025
|
-
const load = createContentLoader(config, type
|
|
1405
|
+
function buildRuntime(config, type, getRuntime, options = {}) {
|
|
1406
|
+
const load = createContentLoader(config, type, {
|
|
1407
|
+
resolveAssets: options.resolveAssets,
|
|
1408
|
+
inlineResolver: options.inlineResolver
|
|
1409
|
+
});
|
|
1026
1410
|
const relationFields = new Map(
|
|
1027
1411
|
listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
|
|
1028
1412
|
);
|
|
@@ -1039,12 +1423,12 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1039
1423
|
id: type.id,
|
|
1040
1424
|
config: type,
|
|
1041
1425
|
load,
|
|
1042
|
-
list(locale = config.defaultLocale,
|
|
1426
|
+
list(locale = config.defaultLocale, options2 = {}) {
|
|
1043
1427
|
const idx = load().get(locale);
|
|
1044
1428
|
if (!idx) return [];
|
|
1045
1429
|
const docs = Array.from(idx.bySlug.values());
|
|
1046
|
-
docs.sort(comparatorFor(
|
|
1047
|
-
return
|
|
1430
|
+
docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
|
|
1431
|
+
return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
|
|
1048
1432
|
},
|
|
1049
1433
|
get(slug, locale = config.defaultLocale) {
|
|
1050
1434
|
return load().get(locale)?.bySlug.get(slug) ?? null;
|
|
@@ -1057,7 +1441,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1057
1441
|
load(),
|
|
1058
1442
|
type,
|
|
1059
1443
|
config.localeRouting,
|
|
1060
|
-
config.localeFallbacks[locale] ?? []
|
|
1444
|
+
config.localeFallbacks?.[locale] ?? []
|
|
1061
1445
|
);
|
|
1062
1446
|
if (result.document && type.path) {
|
|
1063
1447
|
return {
|
|
@@ -1071,15 +1455,15 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1071
1455
|
}
|
|
1072
1456
|
return result;
|
|
1073
1457
|
},
|
|
1074
|
-
staticParams(
|
|
1458
|
+
staticParams(options2 = {}) {
|
|
1075
1459
|
assertRoutable("staticParams");
|
|
1076
1460
|
const all = load();
|
|
1077
1461
|
const enIdx = all.get(config.defaultLocale);
|
|
1078
1462
|
if (!enIdx) return [];
|
|
1079
1463
|
const params = [];
|
|
1080
|
-
for (const locale of
|
|
1464
|
+
for (const locale of options2.locales ?? config.locales) {
|
|
1081
1465
|
const localeIdx = all.get(locale);
|
|
1082
|
-
const fallbacks = config.localeFallbacks[locale] ?? [];
|
|
1466
|
+
const fallbacks = config.localeFallbacks?.[locale] ?? [];
|
|
1083
1467
|
for (const doc of enIdx.bySlug.values()) {
|
|
1084
1468
|
let slug;
|
|
1085
1469
|
if (locale === config.defaultLocale) {
|
|
@@ -1158,7 +1542,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1158
1542
|
};
|
|
1159
1543
|
return runtime;
|
|
1160
1544
|
}
|
|
1161
|
-
function createProject(config) {
|
|
1545
|
+
function createProject(config, options = {}) {
|
|
1162
1546
|
const runtimes = /* @__PURE__ */ new Map();
|
|
1163
1547
|
const getRuntime = (id) => {
|
|
1164
1548
|
const runtime = runtimes.get(id);
|
|
@@ -1167,8 +1551,15 @@ function createProject(config) {
|
|
|
1167
1551
|
}
|
|
1168
1552
|
return runtime;
|
|
1169
1553
|
};
|
|
1554
|
+
const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config) : void 0;
|
|
1170
1555
|
for (const type of config.types) {
|
|
1171
|
-
runtimes.set(
|
|
1556
|
+
runtimes.set(
|
|
1557
|
+
type.id,
|
|
1558
|
+
buildRuntime(config, type, getRuntime, {
|
|
1559
|
+
resolveAssets: options.resolveAssets,
|
|
1560
|
+
inlineResolver
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1172
1563
|
}
|
|
1173
1564
|
return {
|
|
1174
1565
|
config,
|
|
@@ -1261,10 +1652,10 @@ function redirectsFilePath(config, type) {
|
|
|
1261
1652
|
}
|
|
1262
1653
|
function loadTypeRedirectsFile(config, type) {
|
|
1263
1654
|
const filePath = redirectsFilePath(config, type);
|
|
1264
|
-
if (!
|
|
1655
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
1265
1656
|
let raw;
|
|
1266
1657
|
try {
|
|
1267
|
-
raw = JSON.parse(
|
|
1658
|
+
raw = JSON.parse(fs3.readFileSync(filePath, "utf8"));
|
|
1268
1659
|
} catch (error) {
|
|
1269
1660
|
throw new Error(
|
|
1270
1661
|
`${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1420,7 +1811,7 @@ async function generateSitemap(project, options) {
|
|
|
1420
1811
|
// src/create-scribe.ts
|
|
1421
1812
|
function createScribe(input) {
|
|
1422
1813
|
const config = resolveConfig(input);
|
|
1423
|
-
const project = createProject(config);
|
|
1814
|
+
const project = createProject(config, { resolveAssets: true, resolveInlineTokens: true });
|
|
1424
1815
|
const scribe = {
|
|
1425
1816
|
config,
|
|
1426
1817
|
project,
|
|
@@ -1429,6 +1820,17 @@ function createScribe(input) {
|
|
|
1429
1820
|
listRoutableTypes: project.listRoutableTypes,
|
|
1430
1821
|
sitemap(options) {
|
|
1431
1822
|
return generateSitemap(project, options);
|
|
1823
|
+
},
|
|
1824
|
+
assets: {
|
|
1825
|
+
url(ref, opts) {
|
|
1826
|
+
if (opts && Object.keys(opts).length > 0) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
`scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
const assetsConfig = config.assets;
|
|
1832
|
+
return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
|
|
1833
|
+
}
|
|
1432
1834
|
}
|
|
1433
1835
|
};
|
|
1434
1836
|
for (const type of config.types) {
|