scribe-cms 0.0.4 → 0.0.6
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/dist/cli/index.cjs +272 -180
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +272 -181
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +188 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +188 -123
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +25 -18
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +25 -18
- package/dist/runtime.js.map +1 -1
- package/dist/src/config/resolve-config.d.ts.map +1 -1
- package/dist/src/core/types.d.ts +4 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/history/record-snapshot.d.ts +11 -0
- package/dist/src/history/record-snapshot.d.ts.map +1 -0
- package/dist/src/storage/sqlite.d.ts.map +1 -1
- package/dist/src/storage/translations.d.ts +14 -21
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/validate/validate-assets.d.ts +20 -0
- package/dist/src/validate/validate-assets.d.ts.map +1 -0
- package/dist/src/validate/validate-project.d.ts +1 -1
- package/dist/src/validate/validate-project.d.ts.map +1 -1
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/studio/server.cjs +51 -64
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +51 -64
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/history/record-revision.d.ts +0 -14
- package/dist/src/history/record-revision.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -139,14 +139,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
139
139
|
function isRoutableType(type) {
|
|
140
140
|
return typeof type.path === "string" && type.path.length > 0;
|
|
141
141
|
}
|
|
142
|
-
function assertValidPathTemplate(
|
|
143
|
-
if (!
|
|
144
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
142
|
+
function assertValidPathTemplate(path11, typeId) {
|
|
143
|
+
if (!path11.startsWith("/")) {
|
|
144
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
|
|
145
145
|
}
|
|
146
|
-
const count = (
|
|
146
|
+
const count = (path11.match(/\{slug\}/g) ?? []).length;
|
|
147
147
|
if (count !== 1) {
|
|
148
148
|
throw new Error(
|
|
149
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
149
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -198,6 +198,7 @@ function resolveConfig(input, baseDir) {
|
|
|
198
198
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
199
199
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
200
200
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
201
|
+
const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
|
|
201
202
|
const seenIds = /* @__PURE__ */ new Set();
|
|
202
203
|
const types = raw.types.map((type) => {
|
|
203
204
|
if (seenIds.has(type.id)) {
|
|
@@ -218,6 +219,7 @@ function resolveConfig(input, baseDir) {
|
|
|
218
219
|
const config = {
|
|
219
220
|
rootDir: contentRoot,
|
|
220
221
|
storePath,
|
|
222
|
+
assetsPath,
|
|
221
223
|
locales: [...raw.locales],
|
|
222
224
|
defaultLocale,
|
|
223
225
|
localePresets: raw.localePresets,
|
|
@@ -270,10 +272,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
270
272
|
}
|
|
271
273
|
function extractByPaths(data, paths) {
|
|
272
274
|
const out = {};
|
|
273
|
-
for (const
|
|
274
|
-
const value = getAtPath(data,
|
|
275
|
+
for (const path11 of paths) {
|
|
276
|
+
const value = getAtPath(data, path11);
|
|
275
277
|
if (value !== void 0) {
|
|
276
|
-
setAtPath(out,
|
|
278
|
+
setAtPath(out, path11.filter((p) => p !== "*"), value);
|
|
277
279
|
}
|
|
278
280
|
}
|
|
279
281
|
return out;
|
|
@@ -291,13 +293,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
291
293
|
const translatable = pickTranslatable(localeData, schema);
|
|
292
294
|
return deepMerge(structural, translatable);
|
|
293
295
|
}
|
|
294
|
-
function getAtPath(obj,
|
|
296
|
+
function getAtPath(obj, path11) {
|
|
295
297
|
let current = obj;
|
|
296
|
-
for (const segment of
|
|
298
|
+
for (const segment of path11) {
|
|
297
299
|
if (segment === "*") {
|
|
298
300
|
if (!Array.isArray(current)) return void 0;
|
|
299
301
|
return current.map(
|
|
300
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
302
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
|
|
301
303
|
);
|
|
302
304
|
}
|
|
303
305
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -305,13 +307,13 @@ function getAtPath(obj, path10) {
|
|
|
305
307
|
}
|
|
306
308
|
return current;
|
|
307
309
|
}
|
|
308
|
-
function setAtPath(obj,
|
|
309
|
-
if (
|
|
310
|
-
if (
|
|
311
|
-
obj[
|
|
310
|
+
function setAtPath(obj, path11, value) {
|
|
311
|
+
if (path11.length === 0) return;
|
|
312
|
+
if (path11.length === 1) {
|
|
313
|
+
obj[path11[0]] = value;
|
|
312
314
|
return;
|
|
313
315
|
}
|
|
314
|
-
const [head, ...rest] =
|
|
316
|
+
const [head, ...rest] = path11;
|
|
315
317
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
316
318
|
obj[head] = {};
|
|
317
319
|
}
|
|
@@ -507,7 +509,7 @@ function seoFieldsFromEn(enDoc) {
|
|
|
507
509
|
redirectTo: enDoc.redirectTo
|
|
508
510
|
};
|
|
509
511
|
}
|
|
510
|
-
var SCHEMA_VERSION =
|
|
512
|
+
var SCHEMA_VERSION = 4;
|
|
511
513
|
var MIGRATIONS = [
|
|
512
514
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
513
515
|
key TEXT PRIMARY KEY,
|
|
@@ -527,20 +529,6 @@ var MIGRATIONS = [
|
|
|
527
529
|
)`,
|
|
528
530
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
529
531
|
ON translations(content_type, locale)`,
|
|
530
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
531
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
532
|
-
content_type TEXT NOT NULL,
|
|
533
|
-
en_slug TEXT NOT NULL,
|
|
534
|
-
locale TEXT,
|
|
535
|
-
revision_kind TEXT NOT NULL,
|
|
536
|
-
en_hash TEXT NOT NULL,
|
|
537
|
-
body_hash TEXT NOT NULL,
|
|
538
|
-
created_at TEXT NOT NULL,
|
|
539
|
-
model TEXT,
|
|
540
|
-
body_preview TEXT
|
|
541
|
-
)`,
|
|
542
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
543
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
544
532
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
545
533
|
content_type TEXT NOT NULL,
|
|
546
534
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -557,7 +545,19 @@ var MIGRATIONS = [
|
|
|
557
545
|
locale_slug TEXT NOT NULL,
|
|
558
546
|
captured_at TEXT NOT NULL,
|
|
559
547
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
560
|
-
)
|
|
548
|
+
)`,
|
|
549
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
550
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
551
|
+
content_type TEXT NOT NULL,
|
|
552
|
+
en_slug TEXT NOT NULL,
|
|
553
|
+
en_hash TEXT NOT NULL,
|
|
554
|
+
frontmatter_json TEXT NOT NULL,
|
|
555
|
+
body TEXT NOT NULL,
|
|
556
|
+
created_at TEXT NOT NULL,
|
|
557
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
558
|
+
)`,
|
|
559
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
560
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
561
561
|
];
|
|
562
562
|
function resolveStorePath(config) {
|
|
563
563
|
return config.storePath;
|
|
@@ -579,12 +579,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
579
579
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
|
+
function readSchemaVersion(db) {
|
|
583
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
584
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
585
|
+
}
|
|
582
586
|
function migrate(db) {
|
|
587
|
+
const previousVersion = readSchemaVersion(db);
|
|
583
588
|
for (const sql of MIGRATIONS) {
|
|
584
589
|
db.exec(sql);
|
|
585
590
|
}
|
|
586
|
-
|
|
587
|
-
|
|
591
|
+
if (previousVersion < 4) {
|
|
592
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
593
|
+
}
|
|
594
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
588
595
|
db.prepare(
|
|
589
596
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
590
597
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -592,18 +599,38 @@ function migrate(db) {
|
|
|
592
599
|
}
|
|
593
600
|
|
|
594
601
|
// src/storage/translations.ts
|
|
602
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
603
|
+
db.prepare(
|
|
604
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
605
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
606
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
607
|
+
).run(
|
|
608
|
+
input.contentType,
|
|
609
|
+
input.enSlug,
|
|
610
|
+
input.enHash,
|
|
611
|
+
JSON.stringify(input.frontmatter),
|
|
612
|
+
input.body,
|
|
613
|
+
input.createdAt
|
|
614
|
+
);
|
|
615
|
+
const row = db.prepare(
|
|
616
|
+
`SELECT id FROM en_snapshots
|
|
617
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
618
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
619
|
+
return row.id;
|
|
620
|
+
}
|
|
595
621
|
function upsertTranslation(db, input) {
|
|
596
622
|
db.prepare(
|
|
597
623
|
`INSERT INTO translations (
|
|
598
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
599
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
624
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
625
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
600
626
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
601
627
|
slug = excluded.slug,
|
|
602
628
|
frontmatter_json = excluded.frontmatter_json,
|
|
603
629
|
body = excluded.body,
|
|
604
630
|
en_hash = excluded.en_hash,
|
|
605
631
|
translated_at = excluded.translated_at,
|
|
606
|
-
model = excluded.model
|
|
632
|
+
model = excluded.model,
|
|
633
|
+
snapshot_id = excluded.snapshot_id`
|
|
607
634
|
).run(
|
|
608
635
|
input.contentType,
|
|
609
636
|
input.enSlug,
|
|
@@ -613,7 +640,8 @@ function upsertTranslation(db, input) {
|
|
|
613
640
|
input.body,
|
|
614
641
|
input.enHash,
|
|
615
642
|
input.translatedAt,
|
|
616
|
-
input.model
|
|
643
|
+
input.model,
|
|
644
|
+
input.snapshotId
|
|
617
645
|
);
|
|
618
646
|
}
|
|
619
647
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -632,27 +660,6 @@ function listTranslationsForEnSlug(db, contentType, enSlug) {
|
|
|
632
660
|
function bulkLoadTranslations(db) {
|
|
633
661
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
634
662
|
}
|
|
635
|
-
function appendRevision(db, input) {
|
|
636
|
-
const result = db.prepare(
|
|
637
|
-
`INSERT INTO revisions (
|
|
638
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
639
|
-
model, body_preview, frontmatter_json, body
|
|
640
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
641
|
-
).run(
|
|
642
|
-
input.contentType,
|
|
643
|
-
input.enSlug,
|
|
644
|
-
input.locale,
|
|
645
|
-
input.revisionKind,
|
|
646
|
-
input.enHash,
|
|
647
|
-
input.bodyHash,
|
|
648
|
-
input.createdAt,
|
|
649
|
-
input.model ?? null,
|
|
650
|
-
input.bodyPreview ?? null,
|
|
651
|
-
input.frontmatterJson ?? null,
|
|
652
|
-
input.body ?? null
|
|
653
|
-
);
|
|
654
|
-
return Number(result.lastInsertRowid);
|
|
655
|
-
}
|
|
656
663
|
|
|
657
664
|
// src/loader/normalize-en.ts
|
|
658
665
|
function normalizeEnFrontmatter(data) {
|
|
@@ -1582,8 +1589,8 @@ function getRedirectSourceSlugs(project) {
|
|
|
1582
1589
|
// src/sitemap/join-base-url.ts
|
|
1583
1590
|
function joinBaseUrl(baseUrl, pathname) {
|
|
1584
1591
|
const origin = baseUrl.replace(/\/$/, "");
|
|
1585
|
-
const
|
|
1586
|
-
return `${origin}${
|
|
1592
|
+
const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1593
|
+
return `${origin}${path11}`;
|
|
1587
1594
|
}
|
|
1588
1595
|
|
|
1589
1596
|
// src/sitemap/generate-sitemap.ts
|
|
@@ -1727,36 +1734,6 @@ function loadConfigSync(options = {}) {
|
|
|
1727
1734
|
}
|
|
1728
1735
|
return resolveConfig(config, path3__default.default.dirname(configPath));
|
|
1729
1736
|
}
|
|
1730
|
-
function sha256(input) {
|
|
1731
|
-
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
1732
|
-
}
|
|
1733
|
-
function computePageEnHash(translatableFrontmatter, body) {
|
|
1734
|
-
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1735
|
-
return sha256(payload);
|
|
1736
|
-
}
|
|
1737
|
-
function computeBodyHash(body) {
|
|
1738
|
-
return sha256(body);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
// src/history/record-revision.ts
|
|
1742
|
-
function recordRevision(config, input) {
|
|
1743
|
-
const db = openStore(config, "readwrite");
|
|
1744
|
-
const id = appendRevision(db, {
|
|
1745
|
-
contentType: input.contentType,
|
|
1746
|
-
enSlug: input.enSlug,
|
|
1747
|
-
locale: input.locale,
|
|
1748
|
-
revisionKind: input.revisionKind,
|
|
1749
|
-
enHash: input.enHash,
|
|
1750
|
-
bodyHash: computeBodyHash(input.body),
|
|
1751
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1752
|
-
model: input.model,
|
|
1753
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1754
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1755
|
-
body: input.body
|
|
1756
|
-
});
|
|
1757
|
-
db.close();
|
|
1758
|
-
return id;
|
|
1759
|
-
}
|
|
1760
1737
|
function getAtPath2(obj, fieldPath) {
|
|
1761
1738
|
let current = obj;
|
|
1762
1739
|
for (const segment of fieldPath) {
|
|
@@ -1836,6 +1813,72 @@ function validateRelations(project) {
|
|
|
1836
1813
|
}
|
|
1837
1814
|
return issues;
|
|
1838
1815
|
}
|
|
1816
|
+
var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
|
|
1817
|
+
var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
|
|
1818
|
+
var MARKDOWN_IMAGE_RE = new RegExp(
|
|
1819
|
+
String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
|
|
1820
|
+
"gi"
|
|
1821
|
+
);
|
|
1822
|
+
var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
|
|
1823
|
+
function isImageWebPath(value) {
|
|
1824
|
+
return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
|
|
1825
|
+
}
|
|
1826
|
+
function isSiteAssetPath(webPath) {
|
|
1827
|
+
const segment = webPath.split("/")[1];
|
|
1828
|
+
return Boolean(segment && !segment.includes("."));
|
|
1829
|
+
}
|
|
1830
|
+
function addImagePath(webPath, out) {
|
|
1831
|
+
if (isSiteAssetPath(webPath)) out.add(webPath);
|
|
1832
|
+
}
|
|
1833
|
+
function collectBodyImagePaths(body, out) {
|
|
1834
|
+
for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
|
|
1835
|
+
for (const match of body.matchAll(re)) {
|
|
1836
|
+
addImagePath(match[1], out);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function collectFrontmatterImagePaths(value, out) {
|
|
1841
|
+
if (typeof value === "string") {
|
|
1842
|
+
if (isImageWebPath(value)) addImagePath(value, out);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (Array.isArray(value)) {
|
|
1846
|
+
for (const item of value) collectFrontmatterImagePaths(item, out);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (value && typeof value === "object") {
|
|
1850
|
+
for (const nested of Object.values(value)) {
|
|
1851
|
+
collectFrontmatterImagePaths(nested, out);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function collectImagePaths(frontmatter, body) {
|
|
1856
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1857
|
+
collectFrontmatterImagePaths(frontmatter, paths);
|
|
1858
|
+
collectBodyImagePaths(body, paths);
|
|
1859
|
+
return [...paths].sort();
|
|
1860
|
+
}
|
|
1861
|
+
function assetFilePath(assetsPath, webPath) {
|
|
1862
|
+
return path3__default.default.join(assetsPath, webPath.replace(/^\//, ""));
|
|
1863
|
+
}
|
|
1864
|
+
function validateDocumentAssets(config, input) {
|
|
1865
|
+
const assetsPath = config.assetsPath;
|
|
1866
|
+
if (!assetsPath) return [];
|
|
1867
|
+
const issues = [];
|
|
1868
|
+
for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
|
|
1869
|
+
const filePath = assetFilePath(assetsPath, webPath);
|
|
1870
|
+
if (fs2__default.default.existsSync(filePath)) continue;
|
|
1871
|
+
issues.push({
|
|
1872
|
+
level: "warning",
|
|
1873
|
+
contentType: input.contentType,
|
|
1874
|
+
enSlug: input.enSlug,
|
|
1875
|
+
locale: input.locale,
|
|
1876
|
+
field: "asset",
|
|
1877
|
+
message: `Missing image asset ${webPath} (expected ${filePath})`
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
return issues;
|
|
1881
|
+
}
|
|
1839
1882
|
|
|
1840
1883
|
// src/validate/validate-slug-suffix.ts
|
|
1841
1884
|
function validateTranslationSlugSuffixes(config, db) {
|
|
@@ -1898,8 +1941,6 @@ function validateProject(config) {
|
|
|
1898
1941
|
});
|
|
1899
1942
|
continue;
|
|
1900
1943
|
}
|
|
1901
|
-
const payload = getTranslatablePayload(enDoc, type);
|
|
1902
|
-
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
1903
1944
|
const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
|
|
1904
1945
|
locale: config.defaultLocale,
|
|
1905
1946
|
defaultLocale: config.defaultLocale,
|
|
@@ -1917,29 +1958,18 @@ function validateProject(config) {
|
|
|
1917
1958
|
message: issue.message
|
|
1918
1959
|
});
|
|
1919
1960
|
}
|
|
1961
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1962
|
+
contentType: type.id,
|
|
1963
|
+
enSlug,
|
|
1964
|
+
frontmatter: enDoc.frontmatter,
|
|
1965
|
+
body: enDoc.content
|
|
1966
|
+
})) {
|
|
1967
|
+
issues.push(issue);
|
|
1968
|
+
}
|
|
1920
1969
|
for (const locale of config.locales) {
|
|
1921
1970
|
if (locale === config.defaultLocale) continue;
|
|
1922
1971
|
const row = getTranslation(db, type.id, enSlug, locale);
|
|
1923
1972
|
if (!row) continue;
|
|
1924
|
-
if (row.en_hash !== currentEnHash) {
|
|
1925
|
-
issues.push({
|
|
1926
|
-
level: "warning",
|
|
1927
|
-
contentType: type.id,
|
|
1928
|
-
enSlug,
|
|
1929
|
-
locale,
|
|
1930
|
-
field: "en_hash",
|
|
1931
|
-
message: "Translation is stale (en_hash mismatch)"
|
|
1932
|
-
});
|
|
1933
|
-
recordRevision(config, {
|
|
1934
|
-
contentType: type.id,
|
|
1935
|
-
enSlug,
|
|
1936
|
-
locale,
|
|
1937
|
-
revisionKind: "en_edit_detected",
|
|
1938
|
-
enHash: currentEnHash,
|
|
1939
|
-
body: row.body,
|
|
1940
|
-
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1941
|
-
});
|
|
1942
|
-
}
|
|
1943
1973
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
1944
1974
|
for (const issue of validateLocaleBuiltinFields(localeFm)) {
|
|
1945
1975
|
issues.push({
|
|
@@ -1951,6 +1981,15 @@ function validateProject(config) {
|
|
|
1951
1981
|
message: issue.message
|
|
1952
1982
|
});
|
|
1953
1983
|
}
|
|
1984
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1985
|
+
contentType: type.id,
|
|
1986
|
+
enSlug,
|
|
1987
|
+
locale,
|
|
1988
|
+
frontmatter: localeFm,
|
|
1989
|
+
body: row.body
|
|
1990
|
+
})) {
|
|
1991
|
+
issues.push(issue);
|
|
1992
|
+
}
|
|
1954
1993
|
}
|
|
1955
1994
|
}
|
|
1956
1995
|
try {
|
|
@@ -2018,6 +2057,15 @@ function validateProject(config) {
|
|
|
2018
2057
|
}
|
|
2019
2058
|
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
2020
2059
|
}
|
|
2060
|
+
function sha256(input) {
|
|
2061
|
+
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
2062
|
+
}
|
|
2063
|
+
function computePageEnHash(translatableFrontmatter, body) {
|
|
2064
|
+
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
2065
|
+
return sha256(payload);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/translate/worklist.ts
|
|
2021
2069
|
function listEnSlugs3(rootDir, contentDir) {
|
|
2022
2070
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
2023
2071
|
if (!fs2__default.default.existsSync(dir)) return [];
|
|
@@ -2076,6 +2124,21 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
2076
2124
|
return config.locales.filter((l) => l !== config.defaultLocale);
|
|
2077
2125
|
}
|
|
2078
2126
|
|
|
2127
|
+
// src/history/record-snapshot.ts
|
|
2128
|
+
function recordEnSnapshot(config, input, db) {
|
|
2129
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
2130
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
2131
|
+
contentType: input.contentType,
|
|
2132
|
+
enSlug: input.enSlug,
|
|
2133
|
+
enHash: input.enHash,
|
|
2134
|
+
frontmatter: input.frontmatter,
|
|
2135
|
+
body: input.body,
|
|
2136
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2137
|
+
});
|
|
2138
|
+
if (!db) ownDb.close();
|
|
2139
|
+
return id;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2079
2142
|
// src/translate/gemini-models.ts
|
|
2080
2143
|
var GEMINI_MODEL_IDS = {
|
|
2081
2144
|
"gemini-2.5-pro": "gemini-2.5-pro",
|
|
@@ -2449,6 +2512,17 @@ async function translatePage(config, item, options = {}) {
|
|
|
2449
2512
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2450
2513
|
}
|
|
2451
2514
|
const writeDb = openStore(config, "readwrite");
|
|
2515
|
+
const snapshotId = recordEnSnapshot(
|
|
2516
|
+
config,
|
|
2517
|
+
{
|
|
2518
|
+
contentType: type.id,
|
|
2519
|
+
enSlug: item.enSlug,
|
|
2520
|
+
enHash: currentEnHash,
|
|
2521
|
+
frontmatter: payload.frontmatter,
|
|
2522
|
+
body: payload.body
|
|
2523
|
+
},
|
|
2524
|
+
writeDb
|
|
2525
|
+
);
|
|
2452
2526
|
upsertTranslation(writeDb, {
|
|
2453
2527
|
contentType: type.id,
|
|
2454
2528
|
enSlug: item.enSlug,
|
|
@@ -2458,19 +2532,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2458
2532
|
body: result.parsed.body,
|
|
2459
2533
|
enHash: currentEnHash,
|
|
2460
2534
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2461
|
-
model: result.model
|
|
2535
|
+
model: result.model,
|
|
2536
|
+
snapshotId
|
|
2462
2537
|
});
|
|
2463
2538
|
writeDb.close();
|
|
2464
|
-
recordRevision(config, {
|
|
2465
|
-
contentType: type.id,
|
|
2466
|
-
enSlug: item.enSlug,
|
|
2467
|
-
locale: item.locale,
|
|
2468
|
-
revisionKind: "translation",
|
|
2469
|
-
enHash: currentEnHash,
|
|
2470
|
-
body: result.parsed.body,
|
|
2471
|
-
frontmatter: validated.frontmatter,
|
|
2472
|
-
model: result.model
|
|
2473
|
-
});
|
|
2474
2539
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2475
2540
|
normalizeGeminiDisplayName(result.model),
|
|
2476
2541
|
result.usage.inputTokens,
|