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/cli/index.cjs
CHANGED
|
@@ -13,6 +13,7 @@ var dotenv = require('dotenv');
|
|
|
13
13
|
var nodeServer = require('@hono/node-server');
|
|
14
14
|
var hono = require('hono');
|
|
15
15
|
var prompts = require('@inquirer/prompts');
|
|
16
|
+
var url = require('url');
|
|
16
17
|
|
|
17
18
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
19
|
|
|
@@ -70,12 +71,19 @@ function addColumnIfMissing(db, table, column, ddlType) {
|
|
|
70
71
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddlType}`);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
function readSchemaVersion(db) {
|
|
75
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
76
|
+
return row ? Number.parseInt(row.value, 10) || 0 : 0;
|
|
77
|
+
}
|
|
73
78
|
function migrate(db) {
|
|
79
|
+
const previousVersion = readSchemaVersion(db);
|
|
74
80
|
for (const sql of MIGRATIONS) {
|
|
75
81
|
db.exec(sql);
|
|
76
82
|
}
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
if (previousVersion < 4) {
|
|
84
|
+
db.exec(`DROP TABLE IF EXISTS revisions`);
|
|
85
|
+
}
|
|
86
|
+
addColumnIfMissing(db, "translations", "snapshot_id", "INTEGER");
|
|
79
87
|
db.prepare(
|
|
80
88
|
`INSERT INTO meta(key, value) VALUES('schema_version', ?)
|
|
81
89
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -88,7 +96,7 @@ var SCHEMA_VERSION, MIGRATIONS;
|
|
|
88
96
|
var init_sqlite = __esm({
|
|
89
97
|
"src/storage/sqlite.ts"() {
|
|
90
98
|
init_cjs_shims();
|
|
91
|
-
SCHEMA_VERSION =
|
|
99
|
+
SCHEMA_VERSION = 4;
|
|
92
100
|
MIGRATIONS = [
|
|
93
101
|
`CREATE TABLE IF NOT EXISTS meta (
|
|
94
102
|
key TEXT PRIMARY KEY,
|
|
@@ -108,20 +116,6 @@ var init_sqlite = __esm({
|
|
|
108
116
|
)`,
|
|
109
117
|
`CREATE INDEX IF NOT EXISTS idx_translations_type_locale
|
|
110
118
|
ON translations(content_type, locale)`,
|
|
111
|
-
`CREATE TABLE IF NOT EXISTS revisions (
|
|
112
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
-
content_type TEXT NOT NULL,
|
|
114
|
-
en_slug TEXT NOT NULL,
|
|
115
|
-
locale TEXT,
|
|
116
|
-
revision_kind TEXT NOT NULL,
|
|
117
|
-
en_hash TEXT NOT NULL,
|
|
118
|
-
body_hash TEXT NOT NULL,
|
|
119
|
-
created_at TEXT NOT NULL,
|
|
120
|
-
model TEXT,
|
|
121
|
-
body_preview TEXT
|
|
122
|
-
)`,
|
|
123
|
-
`CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
|
124
|
-
ON revisions(content_type, en_slug, locale, created_at DESC)`,
|
|
125
119
|
`CREATE TABLE IF NOT EXISTS slug_aliases (
|
|
126
120
|
content_type TEXT NOT NULL,
|
|
127
121
|
canonical_en_slug TEXT NOT NULL,
|
|
@@ -138,7 +132,19 @@ var init_sqlite = __esm({
|
|
|
138
132
|
locale_slug TEXT NOT NULL,
|
|
139
133
|
captured_at TEXT NOT NULL,
|
|
140
134
|
PRIMARY KEY (content_type, alias_en_slug, locale)
|
|
141
|
-
)
|
|
135
|
+
)`,
|
|
136
|
+
`CREATE TABLE IF NOT EXISTS en_snapshots (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
content_type TEXT NOT NULL,
|
|
139
|
+
en_slug TEXT NOT NULL,
|
|
140
|
+
en_hash TEXT NOT NULL,
|
|
141
|
+
frontmatter_json TEXT NOT NULL,
|
|
142
|
+
body TEXT NOT NULL,
|
|
143
|
+
created_at TEXT NOT NULL,
|
|
144
|
+
UNIQUE (content_type, en_slug, en_hash)
|
|
145
|
+
)`,
|
|
146
|
+
`CREATE INDEX IF NOT EXISTS idx_en_snapshots_lookup
|
|
147
|
+
ON en_snapshots(content_type, en_slug, created_at DESC)`
|
|
142
148
|
];
|
|
143
149
|
}
|
|
144
150
|
});
|
|
@@ -232,14 +238,14 @@ var SLUG_PLACEHOLDER = "{slug}";
|
|
|
232
238
|
function isRoutableType(type) {
|
|
233
239
|
return typeof type.path === "string" && type.path.length > 0;
|
|
234
240
|
}
|
|
235
|
-
function assertValidPathTemplate(
|
|
236
|
-
if (!
|
|
237
|
-
throw new Error(`Content type "${typeId}": path must start with / (got "${
|
|
241
|
+
function assertValidPathTemplate(path13, typeId) {
|
|
242
|
+
if (!path13.startsWith("/")) {
|
|
243
|
+
throw new Error(`Content type "${typeId}": path must start with / (got "${path13}")`);
|
|
238
244
|
}
|
|
239
|
-
const count = (
|
|
245
|
+
const count = (path13.match(/\{slug\}/g) ?? []).length;
|
|
240
246
|
if (count !== 1) {
|
|
241
247
|
throw new Error(
|
|
242
|
-
`Content type "${typeId}": path must contain exactly one {slug} (got "${
|
|
248
|
+
`Content type "${typeId}": path must contain exactly one {slug} (got "${path13}")`
|
|
243
249
|
);
|
|
244
250
|
}
|
|
245
251
|
}
|
|
@@ -291,6 +297,7 @@ function resolveConfig(input, baseDir) {
|
|
|
291
297
|
const projectRoot = path3__default.default.resolve(baseDir ?? process.cwd(), raw.rootDir);
|
|
292
298
|
const contentRoot = path3__default.default.resolve(projectRoot, raw.contentDir ?? "content");
|
|
293
299
|
const storePath = path3__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
|
|
300
|
+
const assetsPath = raw.assetsDir ? path3__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
|
|
294
301
|
const seenIds = /* @__PURE__ */ new Set();
|
|
295
302
|
const types = raw.types.map((type) => {
|
|
296
303
|
if (seenIds.has(type.id)) {
|
|
@@ -311,6 +318,7 @@ function resolveConfig(input, baseDir) {
|
|
|
311
318
|
const config = {
|
|
312
319
|
rootDir: contentRoot,
|
|
313
320
|
storePath,
|
|
321
|
+
assetsPath,
|
|
314
322
|
locales: [...raw.locales],
|
|
315
323
|
defaultLocale,
|
|
316
324
|
localePresets: raw.localePresets,
|
|
@@ -367,10 +375,10 @@ function introspectSchema(schema, prefix = []) {
|
|
|
367
375
|
}
|
|
368
376
|
function extractByPaths(data, paths) {
|
|
369
377
|
const out = {};
|
|
370
|
-
for (const
|
|
371
|
-
const value = getAtPath(data,
|
|
378
|
+
for (const path13 of paths) {
|
|
379
|
+
const value = getAtPath(data, path13);
|
|
372
380
|
if (value !== void 0) {
|
|
373
|
-
setAtPath(out,
|
|
381
|
+
setAtPath(out, path13.filter((p) => p !== "*"), value);
|
|
374
382
|
}
|
|
375
383
|
}
|
|
376
384
|
return out;
|
|
@@ -388,13 +396,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
|
|
|
388
396
|
const translatable = pickTranslatable(localeData, schema);
|
|
389
397
|
return deepMerge(structural, translatable);
|
|
390
398
|
}
|
|
391
|
-
function getAtPath(obj,
|
|
399
|
+
function getAtPath(obj, path13) {
|
|
392
400
|
let current = obj;
|
|
393
|
-
for (const segment of
|
|
401
|
+
for (const segment of path13) {
|
|
394
402
|
if (segment === "*") {
|
|
395
403
|
if (!Array.isArray(current)) return void 0;
|
|
396
404
|
return current.map(
|
|
397
|
-
(item) => typeof item === "object" && item !== null ? getAtPath(item,
|
|
405
|
+
(item) => typeof item === "object" && item !== null ? getAtPath(item, path13.slice(path13.indexOf("*") + 1)) : void 0
|
|
398
406
|
);
|
|
399
407
|
}
|
|
400
408
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
@@ -402,13 +410,13 @@ function getAtPath(obj, path11) {
|
|
|
402
410
|
}
|
|
403
411
|
return current;
|
|
404
412
|
}
|
|
405
|
-
function setAtPath(obj,
|
|
406
|
-
if (
|
|
407
|
-
if (
|
|
408
|
-
obj[
|
|
413
|
+
function setAtPath(obj, path13, value) {
|
|
414
|
+
if (path13.length === 0) return;
|
|
415
|
+
if (path13.length === 1) {
|
|
416
|
+
obj[path13[0]] = value;
|
|
409
417
|
return;
|
|
410
418
|
}
|
|
411
|
-
const [head, ...rest] =
|
|
419
|
+
const [head, ...rest] = path13;
|
|
412
420
|
if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
|
|
413
421
|
obj[head] = {};
|
|
414
422
|
}
|
|
@@ -610,18 +618,51 @@ init_sqlite();
|
|
|
610
618
|
|
|
611
619
|
// src/storage/translations.ts
|
|
612
620
|
init_cjs_shims();
|
|
621
|
+
function getOrCreateEnSnapshot(db, input) {
|
|
622
|
+
db.prepare(
|
|
623
|
+
`INSERT OR IGNORE INTO en_snapshots (
|
|
624
|
+
content_type, en_slug, en_hash, frontmatter_json, body, created_at
|
|
625
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
626
|
+
).run(
|
|
627
|
+
input.contentType,
|
|
628
|
+
input.enSlug,
|
|
629
|
+
input.enHash,
|
|
630
|
+
JSON.stringify(input.frontmatter),
|
|
631
|
+
input.body,
|
|
632
|
+
input.createdAt
|
|
633
|
+
);
|
|
634
|
+
const row = db.prepare(
|
|
635
|
+
`SELECT id FROM en_snapshots
|
|
636
|
+
WHERE content_type = ? AND en_slug = ? AND en_hash = ?`
|
|
637
|
+
).get(input.contentType, input.enSlug, input.enHash);
|
|
638
|
+
return row.id;
|
|
639
|
+
}
|
|
640
|
+
function getEnSnapshot(db, snapshotId) {
|
|
641
|
+
return db.prepare(`SELECT * FROM en_snapshots WHERE id = ?`).get(snapshotId);
|
|
642
|
+
}
|
|
643
|
+
function listEnSnapshotsForEnSlug(db, contentType, enSlug) {
|
|
644
|
+
return db.prepare(
|
|
645
|
+
`SELECT s.*, GROUP_CONCAT(t.locale) AS locales
|
|
646
|
+
FROM en_snapshots s
|
|
647
|
+
LEFT JOIN translations t ON t.snapshot_id = s.id
|
|
648
|
+
WHERE s.content_type = ? AND s.en_slug = ?
|
|
649
|
+
GROUP BY s.id
|
|
650
|
+
ORDER BY s.created_at DESC, s.id DESC`
|
|
651
|
+
).all(contentType, enSlug);
|
|
652
|
+
}
|
|
613
653
|
function upsertTranslation(db, input) {
|
|
614
654
|
db.prepare(
|
|
615
655
|
`INSERT INTO translations (
|
|
616
|
-
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model
|
|
617
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
656
|
+
content_type, en_slug, locale, slug, frontmatter_json, body, en_hash, translated_at, model, snapshot_id
|
|
657
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
618
658
|
ON CONFLICT(content_type, en_slug, locale) DO UPDATE SET
|
|
619
659
|
slug = excluded.slug,
|
|
620
660
|
frontmatter_json = excluded.frontmatter_json,
|
|
621
661
|
body = excluded.body,
|
|
622
662
|
en_hash = excluded.en_hash,
|
|
623
663
|
translated_at = excluded.translated_at,
|
|
624
|
-
model = excluded.model
|
|
664
|
+
model = excluded.model,
|
|
665
|
+
snapshot_id = excluded.snapshot_id`
|
|
625
666
|
).run(
|
|
626
667
|
input.contentType,
|
|
627
668
|
input.enSlug,
|
|
@@ -631,7 +672,8 @@ function upsertTranslation(db, input) {
|
|
|
631
672
|
input.body,
|
|
632
673
|
input.enHash,
|
|
633
674
|
input.translatedAt,
|
|
634
|
-
input.model
|
|
675
|
+
input.model,
|
|
676
|
+
input.snapshotId
|
|
635
677
|
);
|
|
636
678
|
}
|
|
637
679
|
function getTranslation(db, contentType, enSlug, locale) {
|
|
@@ -653,41 +695,6 @@ function listTranslationsForLocale(db, contentType, locale) {
|
|
|
653
695
|
function bulkLoadTranslations(db) {
|
|
654
696
|
return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
|
|
655
697
|
}
|
|
656
|
-
function appendRevision(db, input) {
|
|
657
|
-
const result = db.prepare(
|
|
658
|
-
`INSERT INTO revisions (
|
|
659
|
-
content_type, en_slug, locale, revision_kind, en_hash, body_hash, created_at,
|
|
660
|
-
model, body_preview, frontmatter_json, body
|
|
661
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
662
|
-
).run(
|
|
663
|
-
input.contentType,
|
|
664
|
-
input.enSlug,
|
|
665
|
-
input.locale,
|
|
666
|
-
input.revisionKind,
|
|
667
|
-
input.enHash,
|
|
668
|
-
input.bodyHash,
|
|
669
|
-
input.createdAt,
|
|
670
|
-
input.model ?? null,
|
|
671
|
-
input.bodyPreview ?? null,
|
|
672
|
-
input.frontmatterJson ?? null,
|
|
673
|
-
input.body ?? null
|
|
674
|
-
);
|
|
675
|
-
return Number(result.lastInsertRowid);
|
|
676
|
-
}
|
|
677
|
-
function listRevisions(db, contentType, enSlug, locale) {
|
|
678
|
-
if (locale) {
|
|
679
|
-
return db.prepare(
|
|
680
|
-
`SELECT * FROM revisions
|
|
681
|
-
WHERE content_type = ? AND en_slug = ? AND locale = ?
|
|
682
|
-
ORDER BY created_at DESC, id DESC`
|
|
683
|
-
).all(contentType, enSlug, locale);
|
|
684
|
-
}
|
|
685
|
-
return db.prepare(
|
|
686
|
-
`SELECT * FROM revisions
|
|
687
|
-
WHERE content_type = ? AND en_slug = ?
|
|
688
|
-
ORDER BY created_at DESC, id DESC`
|
|
689
|
-
).all(contentType, enSlug);
|
|
690
|
-
}
|
|
691
698
|
|
|
692
699
|
// src/loader/normalize-en.ts
|
|
693
700
|
init_cjs_shims();
|
|
@@ -1480,43 +1487,6 @@ function loadConfigSync(options = {}) {
|
|
|
1480
1487
|
|
|
1481
1488
|
// src/validate/validate-project.ts
|
|
1482
1489
|
init_cjs_shims();
|
|
1483
|
-
|
|
1484
|
-
// src/hash/page-hash.ts
|
|
1485
|
-
init_cjs_shims();
|
|
1486
|
-
function sha256(input) {
|
|
1487
|
-
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
1488
|
-
}
|
|
1489
|
-
function computePageEnHash(translatableFrontmatter, body) {
|
|
1490
|
-
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1491
|
-
return sha256(payload);
|
|
1492
|
-
}
|
|
1493
|
-
function computeBodyHash(body) {
|
|
1494
|
-
return sha256(body);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// src/history/record-revision.ts
|
|
1498
|
-
init_cjs_shims();
|
|
1499
|
-
init_sqlite();
|
|
1500
|
-
function recordRevision(config, input) {
|
|
1501
|
-
const db = openStore(config, "readwrite");
|
|
1502
|
-
const id = appendRevision(db, {
|
|
1503
|
-
contentType: input.contentType,
|
|
1504
|
-
enSlug: input.enSlug,
|
|
1505
|
-
locale: input.locale,
|
|
1506
|
-
revisionKind: input.revisionKind,
|
|
1507
|
-
enHash: input.enHash,
|
|
1508
|
-
bodyHash: computeBodyHash(input.body),
|
|
1509
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1510
|
-
model: input.model,
|
|
1511
|
-
bodyPreview: input.body.slice(0, 200),
|
|
1512
|
-
frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
|
|
1513
|
-
body: input.body
|
|
1514
|
-
});
|
|
1515
|
-
db.close();
|
|
1516
|
-
return id;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// src/validate/validate-project.ts
|
|
1520
1490
|
init_sqlite();
|
|
1521
1491
|
|
|
1522
1492
|
// src/validate/validate-relations.ts
|
|
@@ -1601,6 +1571,75 @@ function validateRelations(project) {
|
|
|
1601
1571
|
return issues;
|
|
1602
1572
|
}
|
|
1603
1573
|
|
|
1574
|
+
// src/validate/validate-assets.ts
|
|
1575
|
+
init_cjs_shims();
|
|
1576
|
+
var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
|
|
1577
|
+
var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
|
|
1578
|
+
var MARKDOWN_IMAGE_RE = new RegExp(
|
|
1579
|
+
String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
|
|
1580
|
+
"gi"
|
|
1581
|
+
);
|
|
1582
|
+
var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
|
|
1583
|
+
function isImageWebPath(value) {
|
|
1584
|
+
return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
|
|
1585
|
+
}
|
|
1586
|
+
function isSiteAssetPath(webPath) {
|
|
1587
|
+
const segment = webPath.split("/")[1];
|
|
1588
|
+
return Boolean(segment && !segment.includes("."));
|
|
1589
|
+
}
|
|
1590
|
+
function addImagePath(webPath, out) {
|
|
1591
|
+
if (isSiteAssetPath(webPath)) out.add(webPath);
|
|
1592
|
+
}
|
|
1593
|
+
function collectBodyImagePaths(body, out) {
|
|
1594
|
+
for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
|
|
1595
|
+
for (const match of body.matchAll(re)) {
|
|
1596
|
+
addImagePath(match[1], out);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function collectFrontmatterImagePaths(value, out) {
|
|
1601
|
+
if (typeof value === "string") {
|
|
1602
|
+
if (isImageWebPath(value)) addImagePath(value, out);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (Array.isArray(value)) {
|
|
1606
|
+
for (const item of value) collectFrontmatterImagePaths(item, out);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (value && typeof value === "object") {
|
|
1610
|
+
for (const nested of Object.values(value)) {
|
|
1611
|
+
collectFrontmatterImagePaths(nested, out);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function collectImagePaths(frontmatter, body) {
|
|
1616
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1617
|
+
collectFrontmatterImagePaths(frontmatter, paths);
|
|
1618
|
+
collectBodyImagePaths(body, paths);
|
|
1619
|
+
return [...paths].sort();
|
|
1620
|
+
}
|
|
1621
|
+
function assetFilePath(assetsPath, webPath) {
|
|
1622
|
+
return path3__default.default.join(assetsPath, webPath.replace(/^\//, ""));
|
|
1623
|
+
}
|
|
1624
|
+
function validateDocumentAssets(config, input) {
|
|
1625
|
+
const assetsPath = config.assetsPath;
|
|
1626
|
+
if (!assetsPath) return [];
|
|
1627
|
+
const issues = [];
|
|
1628
|
+
for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
|
|
1629
|
+
const filePath = assetFilePath(assetsPath, webPath);
|
|
1630
|
+
if (fs2__default.default.existsSync(filePath)) continue;
|
|
1631
|
+
issues.push({
|
|
1632
|
+
level: "warning",
|
|
1633
|
+
contentType: input.contentType,
|
|
1634
|
+
enSlug: input.enSlug,
|
|
1635
|
+
locale: input.locale,
|
|
1636
|
+
field: "asset",
|
|
1637
|
+
message: `Missing image asset ${webPath} (expected ${filePath})`
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
return issues;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1604
1643
|
// src/validate/validate-slug-suffix.ts
|
|
1605
1644
|
init_cjs_shims();
|
|
1606
1645
|
function validateTranslationSlugSuffixes(config, db) {
|
|
@@ -1663,8 +1702,6 @@ function validateProject(config) {
|
|
|
1663
1702
|
});
|
|
1664
1703
|
continue;
|
|
1665
1704
|
}
|
|
1666
|
-
const payload = getTranslatablePayload(enDoc, type);
|
|
1667
|
-
const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
|
|
1668
1705
|
const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
|
|
1669
1706
|
locale: config.defaultLocale,
|
|
1670
1707
|
defaultLocale: config.defaultLocale,
|
|
@@ -1682,29 +1719,18 @@ function validateProject(config) {
|
|
|
1682
1719
|
message: issue.message
|
|
1683
1720
|
});
|
|
1684
1721
|
}
|
|
1722
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1723
|
+
contentType: type.id,
|
|
1724
|
+
enSlug,
|
|
1725
|
+
frontmatter: enDoc.frontmatter,
|
|
1726
|
+
body: enDoc.content
|
|
1727
|
+
})) {
|
|
1728
|
+
issues.push(issue);
|
|
1729
|
+
}
|
|
1685
1730
|
for (const locale of config.locales) {
|
|
1686
1731
|
if (locale === config.defaultLocale) continue;
|
|
1687
1732
|
const row = getTranslation(db, type.id, enSlug, locale);
|
|
1688
1733
|
if (!row) continue;
|
|
1689
|
-
if (row.en_hash !== currentEnHash) {
|
|
1690
|
-
issues.push({
|
|
1691
|
-
level: "warning",
|
|
1692
|
-
contentType: type.id,
|
|
1693
|
-
enSlug,
|
|
1694
|
-
locale,
|
|
1695
|
-
field: "en_hash",
|
|
1696
|
-
message: "Translation is stale (en_hash mismatch)"
|
|
1697
|
-
});
|
|
1698
|
-
recordRevision(config, {
|
|
1699
|
-
contentType: type.id,
|
|
1700
|
-
enSlug,
|
|
1701
|
-
locale,
|
|
1702
|
-
revisionKind: "en_edit_detected",
|
|
1703
|
-
enHash: currentEnHash,
|
|
1704
|
-
body: row.body,
|
|
1705
|
-
frontmatter: JSON.parse(row.frontmatter_json)
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
1734
|
const localeFm = JSON.parse(row.frontmatter_json);
|
|
1709
1735
|
for (const issue of validateLocaleBuiltinFields(localeFm)) {
|
|
1710
1736
|
issues.push({
|
|
@@ -1716,6 +1742,15 @@ function validateProject(config) {
|
|
|
1716
1742
|
message: issue.message
|
|
1717
1743
|
});
|
|
1718
1744
|
}
|
|
1745
|
+
for (const issue of validateDocumentAssets(config, {
|
|
1746
|
+
contentType: type.id,
|
|
1747
|
+
enSlug,
|
|
1748
|
+
locale,
|
|
1749
|
+
frontmatter: localeFm,
|
|
1750
|
+
body: row.body
|
|
1751
|
+
})) {
|
|
1752
|
+
issues.push(issue);
|
|
1753
|
+
}
|
|
1719
1754
|
}
|
|
1720
1755
|
}
|
|
1721
1756
|
try {
|
|
@@ -1786,6 +1821,18 @@ function validateProject(config) {
|
|
|
1786
1821
|
|
|
1787
1822
|
// src/translate/worklist.ts
|
|
1788
1823
|
init_cjs_shims();
|
|
1824
|
+
|
|
1825
|
+
// src/hash/page-hash.ts
|
|
1826
|
+
init_cjs_shims();
|
|
1827
|
+
function sha256(input) {
|
|
1828
|
+
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
1829
|
+
}
|
|
1830
|
+
function computePageEnHash(translatableFrontmatter, body) {
|
|
1831
|
+
const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
|
|
1832
|
+
return sha256(payload);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/translate/worklist.ts
|
|
1789
1836
|
init_sqlite();
|
|
1790
1837
|
function listEnSlugs3(rootDir, contentDir) {
|
|
1791
1838
|
const dir = path3__default.default.join(rootDir, contentDir);
|
|
@@ -1847,6 +1894,25 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
|
|
|
1847
1894
|
|
|
1848
1895
|
// src/translate/page-translator.ts
|
|
1849
1896
|
init_cjs_shims();
|
|
1897
|
+
|
|
1898
|
+
// src/history/record-snapshot.ts
|
|
1899
|
+
init_cjs_shims();
|
|
1900
|
+
init_sqlite();
|
|
1901
|
+
function recordEnSnapshot(config, input, db) {
|
|
1902
|
+
const ownDb = db ?? openStore(config, "readwrite");
|
|
1903
|
+
const id = getOrCreateEnSnapshot(ownDb, {
|
|
1904
|
+
contentType: input.contentType,
|
|
1905
|
+
enSlug: input.enSlug,
|
|
1906
|
+
enHash: input.enHash,
|
|
1907
|
+
frontmatter: input.frontmatter,
|
|
1908
|
+
body: input.body,
|
|
1909
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1910
|
+
});
|
|
1911
|
+
if (!db) ownDb.close();
|
|
1912
|
+
return id;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/translate/page-translator.ts
|
|
1850
1916
|
init_sqlite();
|
|
1851
1917
|
|
|
1852
1918
|
// src/translate/gemini-client.ts
|
|
@@ -2244,6 +2310,17 @@ async function translatePage(config, item, options = {}) {
|
|
|
2244
2310
|
throw new Error(`Translation validation failed: ${validated.error}`);
|
|
2245
2311
|
}
|
|
2246
2312
|
const writeDb = openStore(config, "readwrite");
|
|
2313
|
+
const snapshotId = recordEnSnapshot(
|
|
2314
|
+
config,
|
|
2315
|
+
{
|
|
2316
|
+
contentType: type.id,
|
|
2317
|
+
enSlug: item.enSlug,
|
|
2318
|
+
enHash: currentEnHash,
|
|
2319
|
+
frontmatter: payload.frontmatter,
|
|
2320
|
+
body: payload.body
|
|
2321
|
+
},
|
|
2322
|
+
writeDb
|
|
2323
|
+
);
|
|
2247
2324
|
upsertTranslation(writeDb, {
|
|
2248
2325
|
contentType: type.id,
|
|
2249
2326
|
enSlug: item.enSlug,
|
|
@@ -2253,19 +2330,10 @@ async function translatePage(config, item, options = {}) {
|
|
|
2253
2330
|
body: result.parsed.body,
|
|
2254
2331
|
enHash: currentEnHash,
|
|
2255
2332
|
translatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2256
|
-
model: result.model
|
|
2333
|
+
model: result.model,
|
|
2334
|
+
snapshotId
|
|
2257
2335
|
});
|
|
2258
2336
|
writeDb.close();
|
|
2259
|
-
recordRevision(config, {
|
|
2260
|
-
contentType: type.id,
|
|
2261
|
-
enSlug: item.enSlug,
|
|
2262
|
-
locale: item.locale,
|
|
2263
|
-
revisionKind: "translation",
|
|
2264
|
-
enHash: currentEnHash,
|
|
2265
|
-
body: result.parsed.body,
|
|
2266
|
-
frontmatter: validated.frontmatter,
|
|
2267
|
-
model: result.model
|
|
2268
|
-
});
|
|
2269
2337
|
const estimatedCostUsd = estimateTranslationCostUsd(
|
|
2270
2338
|
normalizeGeminiDisplayName(result.model),
|
|
2271
2339
|
result.usage.inputTokens,
|
|
@@ -2504,38 +2572,31 @@ function renderFrontmatterTable(frontmatter, schema) {
|
|
|
2504
2572
|
<tbody>${rows || `<tr><td colspan="2" class="dim">\u2014</td></tr>`}</tbody>
|
|
2505
2573
|
</table>`;
|
|
2506
2574
|
}
|
|
2507
|
-
function
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2517
|
-
).join("");
|
|
2518
|
-
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2519
|
-
<pre class="code">${escapeHtml(revision.body)}</pre>`;
|
|
2520
|
-
}
|
|
2521
|
-
return `<p class="dim">${escapeHtml(revision.body_preview ?? "(no snapshot)")}</p>`;
|
|
2522
|
-
}
|
|
2523
|
-
function renderRevisionTimeline(revisions) {
|
|
2524
|
-
if (revisions.length === 0) {
|
|
2525
|
-
return `<p class="dim">No history.</p>`;
|
|
2526
|
-
}
|
|
2527
|
-
const items = revisions.map(
|
|
2528
|
-
(row) => `<tr>
|
|
2529
|
-
<td class="dim">${escapeHtml(row.created_at.slice(0, 19))}</td>
|
|
2530
|
-
<td>${escapeHtml(row.revision_kind)}</td>
|
|
2531
|
-
<td class="dim">${row.model ? escapeHtml(row.model) : "\u2014"}</td>
|
|
2532
|
-
<td class="dim mono">${escapeHtml(row.en_hash.slice(0, 8))}</td>
|
|
2533
|
-
<td><details><summary>view</summary>${renderRevisionSnapshot(row)}</details></td>
|
|
2534
|
-
</tr>`
|
|
2575
|
+
function renderEnSnapshotPreview(snapshot) {
|
|
2576
|
+
let frontmatter = {};
|
|
2577
|
+
try {
|
|
2578
|
+
frontmatter = JSON.parse(snapshot.frontmatter_json);
|
|
2579
|
+
} catch {
|
|
2580
|
+
frontmatter = {};
|
|
2581
|
+
}
|
|
2582
|
+
const fmRows = flattenFrontmatter(frontmatter).map(
|
|
2583
|
+
(row) => `<tr><td class="k">${escapeHtml(row.key)}</td><td class="v">${escapeHtml(row.value)}</td></tr>`
|
|
2535
2584
|
).join("");
|
|
2536
|
-
return `<table class="
|
|
2537
|
-
<
|
|
2538
|
-
|
|
2585
|
+
return `<table class="kv"><tbody>${fmRows}</tbody></table>
|
|
2586
|
+
<pre class="code">${escapeHtml(snapshot.body)}</pre>`;
|
|
2587
|
+
}
|
|
2588
|
+
function renderTranslationSnapshotPanel(snapshot, currentEnHash) {
|
|
2589
|
+
if (!snapshot) {
|
|
2590
|
+
return `<p class="dim">No EN snapshot linked to this translation.</p>`;
|
|
2591
|
+
}
|
|
2592
|
+
const staleNote = currentEnHash && currentEnHash !== snapshot.en_hash ? `<p class="dim">Current EN hash differs from snapshot (${escapeHtml(currentEnHash.slice(0, 12))} vs ${escapeHtml(snapshot.en_hash.slice(0, 12))}).</p>` : "";
|
|
2593
|
+
return `<dl class="meta">
|
|
2594
|
+
<dt>snapshot</dt><dd>#${snapshot.id}</dd>
|
|
2595
|
+
<dt>captured</dt><dd>${escapeHtml(snapshot.created_at.slice(0, 19))}</dd>
|
|
2596
|
+
<dt>en_hash</dt><dd class="mono">${escapeHtml(snapshot.en_hash.slice(0, 12))}</dd>
|
|
2597
|
+
</dl>
|
|
2598
|
+
${staleNote}
|
|
2599
|
+
<details><summary>EN source at translation time</summary>${renderEnSnapshotPreview(snapshot)}</details>`;
|
|
2539
2600
|
}
|
|
2540
2601
|
function renderLayout(title, body, project, options = {}) {
|
|
2541
2602
|
const typeLinks = project.listTypes().map((type) => {
|
|
@@ -2846,10 +2907,10 @@ async function startStudio(project, options = {}) {
|
|
|
2846
2907
|
<dt>slug</dt><dd>${escapeHtml(translation.slug)}</dd>
|
|
2847
2908
|
<dt>en_hash</dt><dd>${escapeHtml(currentEnHash?.slice(0, 12) ?? "\u2014")} / ${escapeHtml(storedEnHash?.slice(0, 12) ?? "\u2014")}</dd>
|
|
2848
2909
|
</dl>`;
|
|
2849
|
-
const
|
|
2910
|
+
const snapshot = translation.snapshot_id != null ? getEnSnapshot(db, translation.snapshot_id) : void 0;
|
|
2850
2911
|
historyPanel = `<div class="section">
|
|
2851
|
-
<div class="section-head">
|
|
2852
|
-
<div class="section-body">${
|
|
2912
|
+
<div class="section-head">EN snapshot</div>
|
|
2913
|
+
<div class="section-body">${renderTranslationSnapshotPanel(snapshot, currentEnHash)}</div>
|
|
2853
2914
|
</div>`;
|
|
2854
2915
|
} else {
|
|
2855
2916
|
contentPanel = `<p class="dim" style="padding:12px">No translation for ${escapeHtml(locale)}.</p>`;
|
|
@@ -3178,6 +3239,24 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
3178
3239
|
};
|
|
3179
3240
|
}
|
|
3180
3241
|
|
|
3242
|
+
// src/version.ts
|
|
3243
|
+
init_cjs_shims();
|
|
3244
|
+
function readScribeVersion() {
|
|
3245
|
+
let dir = path3__default.default.dirname(url.fileURLToPath(importMetaUrl));
|
|
3246
|
+
for (; ; ) {
|
|
3247
|
+
const candidate = path3__default.default.join(dir, "package.json");
|
|
3248
|
+
if (fs2__default.default.existsSync(candidate)) {
|
|
3249
|
+
const pkg = JSON.parse(fs2__default.default.readFileSync(candidate, "utf8"));
|
|
3250
|
+
if (pkg.name === "scribe-cms" && pkg.version) return pkg.version;
|
|
3251
|
+
}
|
|
3252
|
+
const parent = path3__default.default.dirname(dir);
|
|
3253
|
+
if (parent === dir) break;
|
|
3254
|
+
dir = parent;
|
|
3255
|
+
}
|
|
3256
|
+
return "unknown";
|
|
3257
|
+
}
|
|
3258
|
+
var SCRIBE_VERSION = readScribeVersion();
|
|
3259
|
+
|
|
3181
3260
|
// cli/index.ts
|
|
3182
3261
|
function parseArgs(argv) {
|
|
3183
3262
|
const options = { cwd: process.cwd() };
|
|
@@ -3280,7 +3359,13 @@ function loadProject(options) {
|
|
|
3280
3359
|
return createProject(config);
|
|
3281
3360
|
}
|
|
3282
3361
|
async function main() {
|
|
3283
|
-
const
|
|
3362
|
+
const argv = process.argv.slice(2);
|
|
3363
|
+
const first = argv[0];
|
|
3364
|
+
if (first === "--version" || first === "-V" || first === "version") {
|
|
3365
|
+
console.log(SCRIBE_VERSION);
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
const { command, options, rest } = parseArgs(argv);
|
|
3284
3369
|
loadEnvFromCwd(options.cwd);
|
|
3285
3370
|
if (command === "help" || command === "--help") {
|
|
3286
3371
|
console.log(`Usage: scribe <command>
|
|
@@ -3290,8 +3375,9 @@ Commands:
|
|
|
3290
3375
|
validate Validate EN files and sqlite consistency
|
|
3291
3376
|
export-static Write raw MDX files for static hosting
|
|
3292
3377
|
translate Translate stale/missing locale pages
|
|
3293
|
-
history <type> <slug> Show
|
|
3378
|
+
history <type> <slug> Show EN snapshot timeline
|
|
3294
3379
|
studio Start read-only local studio
|
|
3380
|
+
version Print scribe-cms version
|
|
3295
3381
|
|
|
3296
3382
|
Export-static flags:
|
|
3297
3383
|
--out <dir> Output directory (default: public)
|
|
@@ -3313,6 +3399,10 @@ Translate flags:
|
|
|
3313
3399
|
`);
|
|
3314
3400
|
return;
|
|
3315
3401
|
}
|
|
3402
|
+
if (command === "version") {
|
|
3403
|
+
console.log(SCRIBE_VERSION);
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3316
3406
|
const project = loadProject(options);
|
|
3317
3407
|
const config = project.config;
|
|
3318
3408
|
switch (command) {
|
|
@@ -3391,11 +3481,13 @@ Translate flags:
|
|
|
3391
3481
|
}
|
|
3392
3482
|
const { openStore: openStore2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3393
3483
|
const db = openStore2(config, "readonly");
|
|
3394
|
-
const rows =
|
|
3484
|
+
const rows = listEnSnapshotsForEnSlug(db, typeId, enSlug);
|
|
3395
3485
|
db.close();
|
|
3396
3486
|
for (const row of rows) {
|
|
3487
|
+
const locales = row.locales ?? "";
|
|
3488
|
+
if (locale && !locales.split(",").includes(locale)) continue;
|
|
3397
3489
|
console.log(
|
|
3398
|
-
`${row.created_at}
|
|
3490
|
+
`${row.created_at} snapshot=#${row.id} en_hash=${row.en_hash.slice(0, 8)} locales=${locales || "\u2014"}`
|
|
3399
3491
|
);
|
|
3400
3492
|
}
|
|
3401
3493
|
break;
|