scribe-cms 0.0.17 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli/index.cjs +4021 -555
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +4019 -553
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1514 -229
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1499 -228
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +447 -33
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +446 -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 +17 -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 +39 -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 +4699 -304
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +4697 -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,284 @@ 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 !== 3) {
|
|
888
|
+
return {
|
|
889
|
+
ok: false,
|
|
890
|
+
reason: `relation must be relation:<typeId>:<enSlug>:href 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 !== "href" && mode !== "slug") {
|
|
898
|
+
return { ok: false, reason: `relation mode must be "href" or "slug" (got "${mode}")` };
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
ok: true,
|
|
902
|
+
token: {
|
|
903
|
+
kind: "relation",
|
|
904
|
+
targetTypeId,
|
|
905
|
+
enSlug,
|
|
906
|
+
mode
|
|
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, options = {}) {
|
|
980
|
+
const linkStyle = options.linkStyle ?? "app";
|
|
981
|
+
const exportExtension = options.exportExtension ?? ".md";
|
|
982
|
+
const urlBuilder = createUrlBuilder(config);
|
|
983
|
+
const typeById = new Map(config.types.map((t) => [t.id, t]));
|
|
984
|
+
const storePath = resolveStorePath(config);
|
|
985
|
+
let slugCache = null;
|
|
986
|
+
const localizedSlugIndex = () => {
|
|
987
|
+
let mtime = 0;
|
|
988
|
+
try {
|
|
989
|
+
mtime = fs3.statSync(storePath).mtimeMs;
|
|
990
|
+
} catch {
|
|
991
|
+
mtime = 0;
|
|
992
|
+
}
|
|
993
|
+
if (slugCache && slugCache.mtime === mtime) return slugCache.index;
|
|
994
|
+
const index = /* @__PURE__ */ new Map();
|
|
995
|
+
try {
|
|
996
|
+
const db = openStore(config, "readonly");
|
|
997
|
+
try {
|
|
998
|
+
for (const row of bulkLoadTranslations(db)) {
|
|
999
|
+
index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
|
|
1000
|
+
}
|
|
1001
|
+
} finally {
|
|
1002
|
+
db.close();
|
|
1003
|
+
}
|
|
1004
|
+
} catch {
|
|
1005
|
+
}
|
|
1006
|
+
slugCache = { mtime, index };
|
|
1007
|
+
return index;
|
|
1008
|
+
};
|
|
1009
|
+
const localizedSlug = (typeId, enSlug, locale) => {
|
|
1010
|
+
if (locale === config.defaultLocale) return enSlug;
|
|
1011
|
+
const index = localizedSlugIndex();
|
|
1012
|
+
const direct = index.get(slugKey(typeId, enSlug, locale));
|
|
1013
|
+
if (direct) return direct;
|
|
1014
|
+
for (const fb of config.localeFallbacks?.[locale] ?? []) {
|
|
1015
|
+
const hit = index.get(slugKey(typeId, enSlug, fb));
|
|
1016
|
+
if (hit) return hit;
|
|
1017
|
+
}
|
|
1018
|
+
return enSlug;
|
|
1019
|
+
};
|
|
1020
|
+
return {
|
|
1021
|
+
resolve(token, enFrontmatter, locale) {
|
|
1022
|
+
switch (token.kind) {
|
|
1023
|
+
case "static":
|
|
1024
|
+
return token.text;
|
|
1025
|
+
case "var": {
|
|
1026
|
+
const vars = enFrontmatter.vars;
|
|
1027
|
+
if (isStringRecord(vars) && typeof vars[token.key] === "string") {
|
|
1028
|
+
return vars[token.key];
|
|
1029
|
+
}
|
|
1030
|
+
return "";
|
|
1031
|
+
}
|
|
1032
|
+
case "asset": {
|
|
1033
|
+
const assets = config.assets;
|
|
1034
|
+
return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
|
|
1035
|
+
}
|
|
1036
|
+
case "relation": {
|
|
1037
|
+
const type = typeById.get(token.targetTypeId);
|
|
1038
|
+
if (!type) return "";
|
|
1039
|
+
if (token.mode === "slug") return token.enSlug;
|
|
1040
|
+
if (!isRoutableType(type)) return "";
|
|
1041
|
+
const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
|
|
1042
|
+
if (linkStyle === "export") {
|
|
1043
|
+
return urlBuilder.resolvePath(type.path, `${slug}${exportExtension}`, locale);
|
|
1044
|
+
}
|
|
1045
|
+
return type.path.replace("{slug}", slug);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
|
|
1052
|
+
const { placeholderBody, tokens } = extractInlineTokens(rawBody);
|
|
1053
|
+
if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
|
|
1054
|
+
const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
|
|
1055
|
+
return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
|
|
1056
|
+
}
|
|
1057
|
+
function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
|
|
1058
|
+
const { tokens } = extractInlineTokens(enRawBody);
|
|
1059
|
+
if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
|
|
1060
|
+
const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
|
|
1061
|
+
return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
721
1064
|
// src/loader/normalize-en.ts
|
|
722
1065
|
function normalizeEnFrontmatter(data) {
|
|
723
1066
|
const out = { ...data };
|
|
@@ -743,14 +1086,14 @@ function isPostFile(name) {
|
|
|
743
1086
|
return isPublishableContentFile(name);
|
|
744
1087
|
}
|
|
745
1088
|
function listEnFiles(contentDir) {
|
|
746
|
-
if (!
|
|
747
|
-
return
|
|
1089
|
+
if (!fs3.existsSync(contentDir)) return [];
|
|
1090
|
+
return fs3.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
|
|
748
1091
|
}
|
|
749
1092
|
function slugFromPath(filePath) {
|
|
750
1093
|
return path.basename(filePath).replace(/\.(md|mdx)$/, "");
|
|
751
1094
|
}
|
|
752
1095
|
function parseEnMdx(filePath, config, type) {
|
|
753
|
-
const raw =
|
|
1096
|
+
const raw = fs3.readFileSync(filePath, "utf8");
|
|
754
1097
|
const parsed = matter(raw);
|
|
755
1098
|
const slug = slugFromPath(filePath);
|
|
756
1099
|
const normalized = normalizeEnFrontmatter(parsed.data);
|
|
@@ -789,6 +1132,7 @@ function parseEnMdx(filePath, config, type) {
|
|
|
789
1132
|
updatedAt: builtin.updatedAt,
|
|
790
1133
|
noindex: builtin.noindex,
|
|
791
1134
|
canonicalPathOverride: builtin.canonicalPathOverride,
|
|
1135
|
+
vars: builtin.vars,
|
|
792
1136
|
slug,
|
|
793
1137
|
locale: config.defaultLocale
|
|
794
1138
|
},
|
|
@@ -813,12 +1157,15 @@ function parseEnMdx(filePath, config, type) {
|
|
|
813
1157
|
noindex: builtin.noindex,
|
|
814
1158
|
canonicalPathOverride: builtin.canonicalPathOverride,
|
|
815
1159
|
frontmatter,
|
|
816
|
-
|
|
1160
|
+
// Bodyless types (`body: false`) never carry a body: the loader skips the
|
|
1161
|
+
// MDX body entirely so runtimes, exports, and hashing all see an empty body.
|
|
1162
|
+
// A stray body is reported separately by `scribe validate`.
|
|
1163
|
+
content: type.body === false ? "" : parsed.content,
|
|
817
1164
|
filePath
|
|
818
1165
|
};
|
|
819
1166
|
return { document, issues };
|
|
820
1167
|
}
|
|
821
|
-
function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
1168
|
+
function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
|
|
822
1169
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
823
1170
|
const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
|
|
824
1171
|
const seo = seoFieldsFromEn(enDoc);
|
|
@@ -829,6 +1176,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
829
1176
|
config.defaultLocale,
|
|
830
1177
|
config.localeRouting
|
|
831
1178
|
);
|
|
1179
|
+
const preparedBody = prepareTranslatedMdxBody(row.body).body;
|
|
1180
|
+
const content = inlineResolver ? fillTranslatedInlineBody(
|
|
1181
|
+
preparedBody,
|
|
1182
|
+
enDoc.content,
|
|
1183
|
+
enDoc.frontmatter,
|
|
1184
|
+
row.locale,
|
|
1185
|
+
inlineResolver
|
|
1186
|
+
) : preparedBody;
|
|
832
1187
|
return {
|
|
833
1188
|
slug: row.slug,
|
|
834
1189
|
enSlug: row.en_slug,
|
|
@@ -838,14 +1193,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
|
|
|
838
1193
|
noindex: seo.noindex,
|
|
839
1194
|
canonicalPathOverride: seo.canonicalPathOverride,
|
|
840
1195
|
frontmatter,
|
|
841
|
-
content
|
|
1196
|
+
content
|
|
842
1197
|
};
|
|
843
1198
|
}
|
|
844
1199
|
var DEV_REVALIDATE_MS = 1500;
|
|
845
|
-
|
|
1200
|
+
var contentVersion = 0;
|
|
1201
|
+
function createContentLoader(config, type, options = {}) {
|
|
846
1202
|
let cached = null;
|
|
847
1203
|
let signature = "";
|
|
848
1204
|
let lastCheck = 0;
|
|
1205
|
+
let builtVersion = contentVersion;
|
|
849
1206
|
const contentDir = path.join(
|
|
850
1207
|
/* turbopackIgnore: true */
|
|
851
1208
|
config.rootDir,
|
|
@@ -853,18 +1210,34 @@ function createContentLoader(config, type) {
|
|
|
853
1210
|
);
|
|
854
1211
|
const storePath = resolveStorePath(config);
|
|
855
1212
|
const isProd = process.env.NODE_ENV === "production";
|
|
1213
|
+
const assets = config.assets;
|
|
1214
|
+
const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
|
|
1215
|
+
const resolveAssets = (doc) => {
|
|
1216
|
+
if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
|
|
1217
|
+
};
|
|
1218
|
+
const inlineResolver = options.inlineResolver;
|
|
1219
|
+
const resolveEnInline = (doc) => {
|
|
1220
|
+
if (inlineResolver) {
|
|
1221
|
+
doc.content = substituteEnInlineBody(
|
|
1222
|
+
doc.content,
|
|
1223
|
+
doc.frontmatter,
|
|
1224
|
+
config.defaultLocale,
|
|
1225
|
+
inlineResolver
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
856
1229
|
function computeSignature() {
|
|
857
1230
|
const files = listEnFiles(contentDir);
|
|
858
1231
|
let newest = 0;
|
|
859
1232
|
for (const f of files) {
|
|
860
1233
|
try {
|
|
861
|
-
newest = Math.max(newest,
|
|
1234
|
+
newest = Math.max(newest, fs3.statSync(f).mtimeMs);
|
|
862
1235
|
} catch {
|
|
863
1236
|
}
|
|
864
1237
|
}
|
|
865
1238
|
let store = 0;
|
|
866
1239
|
try {
|
|
867
|
-
store =
|
|
1240
|
+
store = fs3.statSync(storePath).mtimeMs;
|
|
868
1241
|
} catch {
|
|
869
1242
|
}
|
|
870
1243
|
return `${files.length}:${newest}:${store}`;
|
|
@@ -910,16 +1283,28 @@ function createContentLoader(config, type) {
|
|
|
910
1283
|
for (const row of rowsByLocale.get(locale) ?? []) {
|
|
911
1284
|
const enDoc = englishBySlug.get(row.en_slug);
|
|
912
1285
|
if (!enDoc) continue;
|
|
913
|
-
const doc = buildDocumentFromTranslation(row, enDoc, type, config);
|
|
1286
|
+
const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
|
|
1287
|
+
resolveAssets(doc);
|
|
914
1288
|
bySlug.set(doc.slug, doc);
|
|
915
1289
|
byEnSlug.set(row.en_slug, doc);
|
|
916
1290
|
}
|
|
917
1291
|
out.set(locale, { bySlug, byEnSlug });
|
|
918
1292
|
}
|
|
1293
|
+
for (const doc of englishBySlug.values()) {
|
|
1294
|
+
resolveAssets(doc);
|
|
1295
|
+
resolveEnInline(doc);
|
|
1296
|
+
}
|
|
919
1297
|
return out;
|
|
920
1298
|
}
|
|
921
1299
|
return () => {
|
|
922
1300
|
if (cached) {
|
|
1301
|
+
if (contentVersion !== builtVersion) {
|
|
1302
|
+
cached = build();
|
|
1303
|
+
builtVersion = contentVersion;
|
|
1304
|
+
lastCheck = Date.now();
|
|
1305
|
+
signature = isProd ? "" : computeSignature();
|
|
1306
|
+
return cached;
|
|
1307
|
+
}
|
|
923
1308
|
if (isProd) return cached;
|
|
924
1309
|
const now = Date.now();
|
|
925
1310
|
if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
|
|
@@ -932,6 +1317,7 @@ function createContentLoader(config, type) {
|
|
|
932
1317
|
}
|
|
933
1318
|
lastCheck = Date.now();
|
|
934
1319
|
cached = build();
|
|
1320
|
+
builtVersion = contentVersion;
|
|
935
1321
|
signature = isProd ? "" : computeSignature();
|
|
936
1322
|
return cached;
|
|
937
1323
|
};
|
|
@@ -1021,8 +1407,11 @@ function comparatorFor(orderBy) {
|
|
|
1021
1407
|
return (a, b) => a.slug.localeCompare(b.slug);
|
|
1022
1408
|
}
|
|
1023
1409
|
}
|
|
1024
|
-
function buildRuntime(config, type, getRuntime) {
|
|
1025
|
-
const load = createContentLoader(config, type
|
|
1410
|
+
function buildRuntime(config, type, getRuntime, options = {}) {
|
|
1411
|
+
const load = createContentLoader(config, type, {
|
|
1412
|
+
resolveAssets: options.resolveAssets,
|
|
1413
|
+
inlineResolver: options.inlineResolver
|
|
1414
|
+
});
|
|
1026
1415
|
const relationFields = new Map(
|
|
1027
1416
|
listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
|
|
1028
1417
|
);
|
|
@@ -1039,12 +1428,12 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1039
1428
|
id: type.id,
|
|
1040
1429
|
config: type,
|
|
1041
1430
|
load,
|
|
1042
|
-
list(locale = config.defaultLocale,
|
|
1431
|
+
list(locale = config.defaultLocale, options2 = {}) {
|
|
1043
1432
|
const idx = load().get(locale);
|
|
1044
1433
|
if (!idx) return [];
|
|
1045
1434
|
const docs = Array.from(idx.bySlug.values());
|
|
1046
|
-
docs.sort(comparatorFor(
|
|
1047
|
-
return
|
|
1435
|
+
docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
|
|
1436
|
+
return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
|
|
1048
1437
|
},
|
|
1049
1438
|
get(slug, locale = config.defaultLocale) {
|
|
1050
1439
|
return load().get(locale)?.bySlug.get(slug) ?? null;
|
|
@@ -1057,7 +1446,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1057
1446
|
load(),
|
|
1058
1447
|
type,
|
|
1059
1448
|
config.localeRouting,
|
|
1060
|
-
config.localeFallbacks[locale] ?? []
|
|
1449
|
+
config.localeFallbacks?.[locale] ?? []
|
|
1061
1450
|
);
|
|
1062
1451
|
if (result.document && type.path) {
|
|
1063
1452
|
return {
|
|
@@ -1071,15 +1460,15 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1071
1460
|
}
|
|
1072
1461
|
return result;
|
|
1073
1462
|
},
|
|
1074
|
-
staticParams(
|
|
1463
|
+
staticParams(options2 = {}) {
|
|
1075
1464
|
assertRoutable("staticParams");
|
|
1076
1465
|
const all = load();
|
|
1077
1466
|
const enIdx = all.get(config.defaultLocale);
|
|
1078
1467
|
if (!enIdx) return [];
|
|
1079
1468
|
const params = [];
|
|
1080
|
-
for (const locale of
|
|
1469
|
+
for (const locale of options2.locales ?? config.locales) {
|
|
1081
1470
|
const localeIdx = all.get(locale);
|
|
1082
|
-
const fallbacks = config.localeFallbacks[locale] ?? [];
|
|
1471
|
+
const fallbacks = config.localeFallbacks?.[locale] ?? [];
|
|
1083
1472
|
for (const doc of enIdx.bySlug.values()) {
|
|
1084
1473
|
let slug;
|
|
1085
1474
|
if (locale === config.defaultLocale) {
|
|
@@ -1158,7 +1547,7 @@ function buildRuntime(config, type, getRuntime) {
|
|
|
1158
1547
|
};
|
|
1159
1548
|
return runtime;
|
|
1160
1549
|
}
|
|
1161
|
-
function createProject(config) {
|
|
1550
|
+
function createProject(config, options = {}) {
|
|
1162
1551
|
const runtimes = /* @__PURE__ */ new Map();
|
|
1163
1552
|
const getRuntime = (id) => {
|
|
1164
1553
|
const runtime = runtimes.get(id);
|
|
@@ -1167,8 +1556,18 @@ function createProject(config) {
|
|
|
1167
1556
|
}
|
|
1168
1557
|
return runtime;
|
|
1169
1558
|
};
|
|
1559
|
+
const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config, {
|
|
1560
|
+
linkStyle: options.inlineLinkStyle,
|
|
1561
|
+
exportExtension: options.exportLinkExtension
|
|
1562
|
+
}) : void 0;
|
|
1170
1563
|
for (const type of config.types) {
|
|
1171
|
-
runtimes.set(
|
|
1564
|
+
runtimes.set(
|
|
1565
|
+
type.id,
|
|
1566
|
+
buildRuntime(config, type, getRuntime, {
|
|
1567
|
+
resolveAssets: options.resolveAssets,
|
|
1568
|
+
inlineResolver
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1172
1571
|
}
|
|
1173
1572
|
return {
|
|
1174
1573
|
config,
|
|
@@ -1261,10 +1660,10 @@ function redirectsFilePath(config, type) {
|
|
|
1261
1660
|
}
|
|
1262
1661
|
function loadTypeRedirectsFile(config, type) {
|
|
1263
1662
|
const filePath = redirectsFilePath(config, type);
|
|
1264
|
-
if (!
|
|
1663
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
1265
1664
|
let raw;
|
|
1266
1665
|
try {
|
|
1267
|
-
raw = JSON.parse(
|
|
1666
|
+
raw = JSON.parse(fs3.readFileSync(filePath, "utf8"));
|
|
1268
1667
|
} catch (error) {
|
|
1269
1668
|
throw new Error(
|
|
1270
1669
|
`${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1420,7 +1819,11 @@ async function generateSitemap(project, options) {
|
|
|
1420
1819
|
// src/create-scribe.ts
|
|
1421
1820
|
function createScribe(input) {
|
|
1422
1821
|
const config = resolveConfig(input);
|
|
1423
|
-
const project = createProject(config
|
|
1822
|
+
const project = createProject(config, {
|
|
1823
|
+
resolveAssets: true,
|
|
1824
|
+
resolveInlineTokens: true,
|
|
1825
|
+
inlineLinkStyle: "app"
|
|
1826
|
+
});
|
|
1424
1827
|
const scribe = {
|
|
1425
1828
|
config,
|
|
1426
1829
|
project,
|
|
@@ -1429,6 +1832,17 @@ function createScribe(input) {
|
|
|
1429
1832
|
listRoutableTypes: project.listRoutableTypes,
|
|
1430
1833
|
sitemap(options) {
|
|
1431
1834
|
return generateSitemap(project, options);
|
|
1835
|
+
},
|
|
1836
|
+
assets: {
|
|
1837
|
+
url(ref, opts) {
|
|
1838
|
+
if (opts && Object.keys(opts).length > 0) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
const assetsConfig = config.assets;
|
|
1844
|
+
return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
|
|
1845
|
+
}
|
|
1432
1846
|
}
|
|
1433
1847
|
};
|
|
1434
1848
|
for (const type of config.types) {
|