scribe-cms 0.0.4 → 0.0.5

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/index.js CHANGED
@@ -128,14 +128,14 @@ var SLUG_PLACEHOLDER = "{slug}";
128
128
  function isRoutableType(type) {
129
129
  return typeof type.path === "string" && type.path.length > 0;
130
130
  }
131
- function assertValidPathTemplate(path10, typeId) {
132
- if (!path10.startsWith("/")) {
133
- throw new Error(`Content type "${typeId}": path must start with / (got "${path10}")`);
131
+ function assertValidPathTemplate(path11, typeId) {
132
+ if (!path11.startsWith("/")) {
133
+ throw new Error(`Content type "${typeId}": path must start with / (got "${path11}")`);
134
134
  }
135
- const count = (path10.match(/\{slug\}/g) ?? []).length;
135
+ const count = (path11.match(/\{slug\}/g) ?? []).length;
136
136
  if (count !== 1) {
137
137
  throw new Error(
138
- `Content type "${typeId}": path must contain exactly one {slug} (got "${path10}")`
138
+ `Content type "${typeId}": path must contain exactly one {slug} (got "${path11}")`
139
139
  );
140
140
  }
141
141
  }
@@ -187,6 +187,7 @@ function resolveConfig(input, baseDir) {
187
187
  const projectRoot = path3.resolve(baseDir ?? process.cwd(), raw.rootDir);
188
188
  const contentRoot = path3.resolve(projectRoot, raw.contentDir ?? "content");
189
189
  const storePath = path3.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
190
+ const assetsPath = raw.assetsDir ? path3.resolve(projectRoot, raw.assetsDir) : void 0;
190
191
  const seenIds = /* @__PURE__ */ new Set();
191
192
  const types = raw.types.map((type) => {
192
193
  if (seenIds.has(type.id)) {
@@ -207,6 +208,7 @@ function resolveConfig(input, baseDir) {
207
208
  const config = {
208
209
  rootDir: contentRoot,
209
210
  storePath,
211
+ assetsPath,
210
212
  locales: [...raw.locales],
211
213
  defaultLocale,
212
214
  localePresets: raw.localePresets,
@@ -259,10 +261,10 @@ function introspectSchema(schema, prefix = []) {
259
261
  }
260
262
  function extractByPaths(data, paths) {
261
263
  const out = {};
262
- for (const path10 of paths) {
263
- const value = getAtPath(data, path10);
264
+ for (const path11 of paths) {
265
+ const value = getAtPath(data, path11);
264
266
  if (value !== void 0) {
265
- setAtPath(out, path10.filter((p) => p !== "*"), value);
267
+ setAtPath(out, path11.filter((p) => p !== "*"), value);
266
268
  }
267
269
  }
268
270
  return out;
@@ -280,13 +282,13 @@ function mergeStructuralOntoLocale(localeData, enData, schema) {
280
282
  const translatable = pickTranslatable(localeData, schema);
281
283
  return deepMerge(structural, translatable);
282
284
  }
283
- function getAtPath(obj, path10) {
285
+ function getAtPath(obj, path11) {
284
286
  let current = obj;
285
- for (const segment of path10) {
287
+ for (const segment of path11) {
286
288
  if (segment === "*") {
287
289
  if (!Array.isArray(current)) return void 0;
288
290
  return current.map(
289
- (item) => typeof item === "object" && item !== null ? getAtPath(item, path10.slice(path10.indexOf("*") + 1)) : void 0
291
+ (item) => typeof item === "object" && item !== null ? getAtPath(item, path11.slice(path11.indexOf("*") + 1)) : void 0
290
292
  );
291
293
  }
292
294
  if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
@@ -294,13 +296,13 @@ function getAtPath(obj, path10) {
294
296
  }
295
297
  return current;
296
298
  }
297
- function setAtPath(obj, path10, value) {
298
- if (path10.length === 0) return;
299
- if (path10.length === 1) {
300
- obj[path10[0]] = value;
299
+ function setAtPath(obj, path11, value) {
300
+ if (path11.length === 0) return;
301
+ if (path11.length === 1) {
302
+ obj[path11[0]] = value;
301
303
  return;
302
304
  }
303
- const [head, ...rest] = path10;
305
+ const [head, ...rest] = path11;
304
306
  if (!(head in obj) || typeof obj[head] !== "object" || obj[head] === null) {
305
307
  obj[head] = {};
306
308
  }
@@ -1571,8 +1573,8 @@ function getRedirectSourceSlugs(project) {
1571
1573
  // src/sitemap/join-base-url.ts
1572
1574
  function joinBaseUrl(baseUrl, pathname) {
1573
1575
  const origin = baseUrl.replace(/\/$/, "");
1574
- const path10 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1575
- return `${origin}${path10}`;
1576
+ const path11 = pathname.startsWith("/") ? pathname : `/${pathname}`;
1577
+ return `${origin}${path11}`;
1576
1578
  }
1577
1579
 
1578
1580
  // src/sitemap/generate-sitemap.ts
@@ -1716,36 +1718,6 @@ function loadConfigSync(options = {}) {
1716
1718
  }
1717
1719
  return resolveConfig(config, path3.dirname(configPath));
1718
1720
  }
1719
- function sha256(input) {
1720
- return createHash("sha256").update(input, "utf8").digest("hex");
1721
- }
1722
- function computePageEnHash(translatableFrontmatter, body) {
1723
- const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
1724
- return sha256(payload);
1725
- }
1726
- function computeBodyHash(body) {
1727
- return sha256(body);
1728
- }
1729
-
1730
- // src/history/record-revision.ts
1731
- function recordRevision(config, input) {
1732
- const db = openStore(config, "readwrite");
1733
- const id = appendRevision(db, {
1734
- contentType: input.contentType,
1735
- enSlug: input.enSlug,
1736
- locale: input.locale,
1737
- revisionKind: input.revisionKind,
1738
- enHash: input.enHash,
1739
- bodyHash: computeBodyHash(input.body),
1740
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1741
- model: input.model,
1742
- bodyPreview: input.body.slice(0, 200),
1743
- frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
1744
- body: input.body
1745
- });
1746
- db.close();
1747
- return id;
1748
- }
1749
1721
  function getAtPath2(obj, fieldPath) {
1750
1722
  let current = obj;
1751
1723
  for (const segment of fieldPath) {
@@ -1825,6 +1797,72 @@ function validateRelations(project) {
1825
1797
  }
1826
1798
  return issues;
1827
1799
  }
1800
+ var IMAGE_EXT = String.raw`(?:png|jpe?g|webp|gif|svg)`;
1801
+ var IMAGE_WEB_PATH = String.raw`\/[\w\-./]+\.${IMAGE_EXT}`;
1802
+ var MARKDOWN_IMAGE_RE = new RegExp(
1803
+ String.raw`!\[[^\]]*\]\((${IMAGE_WEB_PATH})\)`,
1804
+ "gi"
1805
+ );
1806
+ var HTML_SRC_RE = new RegExp(String.raw`src=["'](${IMAGE_WEB_PATH})["']`, "gi");
1807
+ function isImageWebPath(value) {
1808
+ return new RegExp(String.raw`^${IMAGE_WEB_PATH}$`, "i").test(value);
1809
+ }
1810
+ function isSiteAssetPath(webPath) {
1811
+ const segment = webPath.split("/")[1];
1812
+ return Boolean(segment && !segment.includes("."));
1813
+ }
1814
+ function addImagePath(webPath, out) {
1815
+ if (isSiteAssetPath(webPath)) out.add(webPath);
1816
+ }
1817
+ function collectBodyImagePaths(body, out) {
1818
+ for (const re of [MARKDOWN_IMAGE_RE, HTML_SRC_RE]) {
1819
+ for (const match of body.matchAll(re)) {
1820
+ addImagePath(match[1], out);
1821
+ }
1822
+ }
1823
+ }
1824
+ function collectFrontmatterImagePaths(value, out) {
1825
+ if (typeof value === "string") {
1826
+ if (isImageWebPath(value)) addImagePath(value, out);
1827
+ return;
1828
+ }
1829
+ if (Array.isArray(value)) {
1830
+ for (const item of value) collectFrontmatterImagePaths(item, out);
1831
+ return;
1832
+ }
1833
+ if (value && typeof value === "object") {
1834
+ for (const nested of Object.values(value)) {
1835
+ collectFrontmatterImagePaths(nested, out);
1836
+ }
1837
+ }
1838
+ }
1839
+ function collectImagePaths(frontmatter, body) {
1840
+ const paths = /* @__PURE__ */ new Set();
1841
+ collectFrontmatterImagePaths(frontmatter, paths);
1842
+ collectBodyImagePaths(body, paths);
1843
+ return [...paths].sort();
1844
+ }
1845
+ function assetFilePath(assetsPath, webPath) {
1846
+ return path3.join(assetsPath, webPath.replace(/^\//, ""));
1847
+ }
1848
+ function validateDocumentAssets(config, input) {
1849
+ const assetsPath = config.assetsPath;
1850
+ if (!assetsPath) return [];
1851
+ const issues = [];
1852
+ for (const webPath of collectImagePaths(input.frontmatter, input.body)) {
1853
+ const filePath = assetFilePath(assetsPath, webPath);
1854
+ if (fs2.existsSync(filePath)) continue;
1855
+ issues.push({
1856
+ level: "warning",
1857
+ contentType: input.contentType,
1858
+ enSlug: input.enSlug,
1859
+ locale: input.locale,
1860
+ field: "asset",
1861
+ message: `Missing image asset ${webPath} (expected ${filePath})`
1862
+ });
1863
+ }
1864
+ return issues;
1865
+ }
1828
1866
 
1829
1867
  // src/validate/validate-slug-suffix.ts
1830
1868
  function validateTranslationSlugSuffixes(config, db) {
@@ -1887,8 +1925,6 @@ function validateProject(config) {
1887
1925
  });
1888
1926
  continue;
1889
1927
  }
1890
- const payload = getTranslatablePayload(enDoc, type);
1891
- const currentEnHash = computePageEnHash(payload.frontmatter, payload.body);
1892
1928
  const crossIssues = type.crossValidate?.(enDoc.frontmatter, {
1893
1929
  locale: config.defaultLocale,
1894
1930
  defaultLocale: config.defaultLocale,
@@ -1906,29 +1942,18 @@ function validateProject(config) {
1906
1942
  message: issue.message
1907
1943
  });
1908
1944
  }
1945
+ for (const issue of validateDocumentAssets(config, {
1946
+ contentType: type.id,
1947
+ enSlug,
1948
+ frontmatter: enDoc.frontmatter,
1949
+ body: enDoc.content
1950
+ })) {
1951
+ issues.push(issue);
1952
+ }
1909
1953
  for (const locale of config.locales) {
1910
1954
  if (locale === config.defaultLocale) continue;
1911
1955
  const row = getTranslation(db, type.id, enSlug, locale);
1912
1956
  if (!row) continue;
1913
- if (row.en_hash !== currentEnHash) {
1914
- issues.push({
1915
- level: "warning",
1916
- contentType: type.id,
1917
- enSlug,
1918
- locale,
1919
- field: "en_hash",
1920
- message: "Translation is stale (en_hash mismatch)"
1921
- });
1922
- recordRevision(config, {
1923
- contentType: type.id,
1924
- enSlug,
1925
- locale,
1926
- revisionKind: "en_edit_detected",
1927
- enHash: currentEnHash,
1928
- body: row.body,
1929
- frontmatter: JSON.parse(row.frontmatter_json)
1930
- });
1931
- }
1932
1957
  const localeFm = JSON.parse(row.frontmatter_json);
1933
1958
  for (const issue of validateLocaleBuiltinFields(localeFm)) {
1934
1959
  issues.push({
@@ -1940,6 +1965,15 @@ function validateProject(config) {
1940
1965
  message: issue.message
1941
1966
  });
1942
1967
  }
1968
+ for (const issue of validateDocumentAssets(config, {
1969
+ contentType: type.id,
1970
+ enSlug,
1971
+ locale,
1972
+ frontmatter: localeFm,
1973
+ body: row.body
1974
+ })) {
1975
+ issues.push(issue);
1976
+ }
1943
1977
  }
1944
1978
  }
1945
1979
  try {
@@ -2007,6 +2041,18 @@ function validateProject(config) {
2007
2041
  }
2008
2042
  return { ok: issues.every((i) => i.level !== "error"), issues };
2009
2043
  }
2044
+ function sha256(input) {
2045
+ return createHash("sha256").update(input, "utf8").digest("hex");
2046
+ }
2047
+ function computePageEnHash(translatableFrontmatter, body) {
2048
+ const payload = JSON.stringify({ frontmatter: translatableFrontmatter, body });
2049
+ return sha256(payload);
2050
+ }
2051
+ function computeBodyHash(body) {
2052
+ return sha256(body);
2053
+ }
2054
+
2055
+ // src/translate/worklist.ts
2010
2056
  function listEnSlugs3(rootDir, contentDir) {
2011
2057
  const dir = path3.join(rootDir, contentDir);
2012
2058
  if (!fs2.existsSync(dir)) return [];
@@ -2065,6 +2111,26 @@ function resolveLocalesFromPreset(config, preset, explicitLocales) {
2065
2111
  return config.locales.filter((l) => l !== config.defaultLocale);
2066
2112
  }
2067
2113
 
2114
+ // src/history/record-revision.ts
2115
+ function recordRevision(config, input) {
2116
+ const db = openStore(config, "readwrite");
2117
+ const id = appendRevision(db, {
2118
+ contentType: input.contentType,
2119
+ enSlug: input.enSlug,
2120
+ locale: input.locale,
2121
+ revisionKind: input.revisionKind,
2122
+ enHash: input.enHash,
2123
+ bodyHash: computeBodyHash(input.body),
2124
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2125
+ model: input.model,
2126
+ bodyPreview: input.body.slice(0, 200),
2127
+ frontmatterJson: input.frontmatter ? JSON.stringify(input.frontmatter) : null,
2128
+ body: input.body
2129
+ });
2130
+ db.close();
2131
+ return id;
2132
+ }
2133
+
2068
2134
  // src/translate/gemini-models.ts
2069
2135
  var GEMINI_MODEL_IDS = {
2070
2136
  "gemini-2.5-pro": "gemini-2.5-pro",