dommief 0.1.1 → 0.1.3

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.
Files changed (45) hide show
  1. package/dist/cli.js +182 -147
  2. package/dist/index.js +5 -1
  3. package/package.json +1 -1
  4. package/src/build.ts +290 -0
  5. package/src/cache.ts +172 -0
  6. package/src/cli.ts +87 -0
  7. package/src/config.ts +258 -0
  8. package/src/exclude.ts +38 -0
  9. package/src/index.ts +17 -0
  10. package/src/link-check.ts +101 -0
  11. package/src/log.ts +10 -0
  12. package/src/markdown/errors.ts +28 -0
  13. package/src/markdown/index.ts +96 -0
  14. package/src/markdown/rehype-api.ts +131 -0
  15. package/src/markdown/rehype-code.ts +147 -0
  16. package/src/markdown/rehype-demo.ts +76 -0
  17. package/src/markdown/rehype-desc.ts +30 -0
  18. package/src/markdown/rehype-img.ts +33 -0
  19. package/src/markdown/rehype-link.ts +50 -0
  20. package/src/markdown/rehype-slug.ts +41 -0
  21. package/src/markdown/rehype-text.ts +53 -0
  22. package/src/markdown/remark-container.ts +76 -0
  23. package/src/markdown/remark-meta.ts +73 -0
  24. package/src/markdown/to-module.ts +35 -0
  25. package/src/paths.ts +38 -0
  26. package/src/plugins.ts +131 -0
  27. package/src/prefers-color.ts +28 -0
  28. package/src/preview.ts +127 -0
  29. package/src/routes.ts +232 -0
  30. package/src/search.ts +88 -0
  31. package/src/siteFeatures.ts +123 -0
  32. package/src/siteModule.ts +47 -0
  33. package/src/sitemap.ts +59 -0
  34. package/src/ssr-guard.ts +72 -0
  35. package/src/theme/index.ts +5 -0
  36. package/src/theme/loader.ts +151 -0
  37. package/src/theme/resolver.ts +115 -0
  38. package/src/theme/types.ts +25 -0
  39. package/src/theme-modules.d.ts +52 -0
  40. package/src/utils/path.ts +8 -0
  41. package/src/vite/plugin.ts +193 -0
  42. package/src/vite/routesPlugin.ts +39 -0
  43. package/src/vite/searchIndexPlugin.ts +66 -0
  44. package/src/vite/sitePlugin.ts +97 -0
  45. package/src/vite/state.ts +27 -0
package/dist/cli.js CHANGED
@@ -547,7 +547,7 @@ function buildApiTable(src, filePath = "") {
547
547
  try {
548
548
  docs = parser.parse(abs) ?? [];
549
549
  } catch {
550
- docs = [];
550
+ return null;
551
551
  }
552
552
  const entries = (docs || []).map((d) => ({
553
553
  displayName: d.displayName,
@@ -569,6 +569,7 @@ function rehypeApi() {
569
569
  return (tree, file) => {
570
570
  const apis = Array.isArray(file?.data?.apis) ? file.data.apis : [];
571
571
  const watched = Array.isArray(file?.data?.apiSrcFiles) ? file.data.apiSrcFiles : [];
572
+ const warnings = Array.isArray(file?.data?.warnings) ? file.data.warnings : [];
572
573
  const filePath = String(file?.path ?? "");
573
574
  visit7(tree, "element", (node, index, parent) => {
574
575
  if (!parent || typeof index !== "number") return;
@@ -578,6 +579,7 @@ function rehypeApi() {
578
579
  const abs = resolveApiSrc(src, filePath);
579
580
  const data = buildApiTable(src, filePath);
580
581
  watched.push(abs);
582
+ if (!data?.length) warnings.push(`<API src="${src}">: ${data ? "no component found (syntax error or no exported component)" : "file missing or failed to parse"}: ${abs}`);
581
583
  apis.push({ id: hashFileContent(src + filePath).slice(0, 8), src: abs, data });
582
584
  parent.children[index] = {
583
585
  type: "element",
@@ -592,6 +594,7 @@ function rehypeApi() {
592
594
  if (file?.data) {
593
595
  file.data.apis = apis;
594
596
  file.data.apiSrcFiles = watched;
597
+ file.data.warnings = warnings;
595
598
  }
596
599
  };
597
600
  }
@@ -1010,8 +1013,9 @@ import { cac } from "cac";
1010
1013
  import { createServer } from "vite";
1011
1014
 
1012
1015
  // src/vite/plugin.ts
1013
- import fs9 from "fs";
1014
- import path9 from "path";
1016
+ import { transformWithOxc } from "vite";
1017
+ import fs10 from "fs";
1018
+ import path10 from "path";
1015
1019
 
1016
1020
  // src/config.ts
1017
1021
  init_errors();
@@ -1149,6 +1153,7 @@ function resolveConfig(userConfig, opts) {
1149
1153
  srcDir: c.srcDir,
1150
1154
  docDirs: (c.docDirs ?? [c.srcDir]).map((d) => typeof d === "string" ? { dir: d } : { dir: d.dir }),
1151
1155
  forceKebabCase: c.forceKebabCase,
1156
+ preset: c.preset,
1152
1157
  locales,
1153
1158
  themeConfig: c.themeConfig,
1154
1159
  theme: c.theme,
@@ -1767,15 +1772,89 @@ function createState(cwd, get) {
1767
1772
  // src/vite/plugin.ts
1768
1773
  init_path();
1769
1774
 
1775
+ // src/sitemap.ts
1776
+ import fs6 from "fs";
1777
+ import path6 from "path";
1778
+
1779
+ // src/exclude.ts
1780
+ var cache = /* @__PURE__ */ new Map();
1781
+ function globToRegExp(pattern) {
1782
+ let src = "";
1783
+ for (let i = 0; i < pattern.length; i++) {
1784
+ const c = pattern.charAt(i);
1785
+ if (c === "*" && pattern.charAt(i + 1) === "*") {
1786
+ const slash = pattern.charAt(i + 2) === "/";
1787
+ src += slash ? "(?:.*/)?" : ".*";
1788
+ i += slash ? 2 : 1;
1789
+ } else if (c === "*") {
1790
+ src += "[^/]*";
1791
+ } else if (c === "?") {
1792
+ src += "[^/]";
1793
+ } else {
1794
+ src += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1795
+ }
1796
+ }
1797
+ return new RegExp(`^${src}$`);
1798
+ }
1799
+ function matcher(pattern) {
1800
+ const hit = cache.get(pattern);
1801
+ if (hit) return hit;
1802
+ const re = globToRegExp(pattern);
1803
+ cache.set(pattern, re);
1804
+ return re;
1805
+ }
1806
+ function matchExclude(path15, patterns) {
1807
+ return patterns.some((p) => matcher(p).test(path15));
1808
+ }
1809
+
1810
+ // src/sitemap.ts
1811
+ function canonicalUrl(hostname, base, routePath) {
1812
+ const host = hostname.replace(/\/$/, "");
1813
+ const b = base.replace(/\/$/, "");
1814
+ const p = routePath === "/" ? "/" : routePath;
1815
+ return host + (b + p).replace(/\/{2,}/g, "/").replace(/^(https?:\/)([^/])/, "$1/$2");
1816
+ }
1817
+ function buildSitemap(routes, config, base) {
1818
+ const urls = routes.filter((r) => r.path !== "/404" && !/[:*]/.test(r.path) && !matchExclude(r.path, config.exclude)).map((r) => ` <url><loc>${escapeXml(canonicalUrl(config.hostname, base, r.path))}</loc></url>`).join("\n");
1819
+ return `<?xml version="1.0" encoding="UTF-8"?>
1820
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1821
+ ${urls}
1822
+ </urlset>
1823
+ `;
1824
+ }
1825
+ function buildRobots(sitemapUrl) {
1826
+ return `User-agent: *
1827
+ Allow: /
1828
+
1829
+ Sitemap: ${sitemapUrl}
1830
+ `;
1831
+ }
1832
+ function escapeXml(s) {
1833
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
1834
+ }
1835
+ function writeSitemapAndRobots(opts) {
1836
+ if (opts.sitemap === false) return;
1837
+ if (!opts.sitemap.hostname) {
1838
+ opts.log.warn("sitemap.hostname thi\u1EBFu \u2014 b\u1ECF qua sitemap.xml/robots.txt");
1839
+ return;
1840
+ }
1841
+ const xml = buildSitemap(opts.routes, opts.sitemap, opts.base);
1842
+ fs6.writeFileSync(path6.join(opts.outDir, "sitemap.xml"), xml);
1843
+ if (opts.robots) {
1844
+ const sitemapUrl = canonicalUrl(opts.sitemap.hostname, opts.base, "/").replace(/\/$/, "") + "/sitemap.xml";
1845
+ fs6.writeFileSync(path6.join(opts.outDir, "robots.txt"), buildRobots(sitemapUrl));
1846
+ }
1847
+ }
1848
+
1770
1849
  // src/theme/resolver.ts
1771
1850
  init_errors();
1772
- import fs7 from "fs";
1773
- import path7 from "path";
1851
+ import fs8 from "fs";
1852
+ import path8 from "path";
1774
1853
 
1775
1854
  // src/theme/loader.ts
1776
1855
  init_errors();
1777
- import fs6 from "fs";
1778
- import path6 from "path";
1856
+ import fs7 from "fs";
1857
+ import path7 from "path";
1779
1858
  import { fileURLToPath as fileURLToPath2 } from "url";
1780
1859
  var EXTS = [".tsx", ".ts", ".jsx", ".js"];
1781
1860
  var DEFAULT_THEME = "dommief-theme-default";
@@ -1783,17 +1862,17 @@ function isComponentName(name) {
1783
1862
  return /^[A-Z0-9]/.test(name);
1784
1863
  }
1785
1864
  function repoPackagesDir() {
1786
- let cur = path6.dirname(fileURLToPath2(import.meta.url));
1787
- while (cur !== path6.parse(cur).root) {
1788
- if (fs6.existsSync(path6.join(cur, "pnpm-workspace.yaml"))) return path6.join(cur, "packages");
1789
- cur = path6.dirname(cur);
1865
+ let cur = path7.dirname(fileURLToPath2(import.meta.url));
1866
+ while (cur !== path7.parse(cur).root) {
1867
+ if (fs7.existsSync(path7.join(cur, "pnpm-workspace.yaml"))) return path7.join(cur, "packages");
1868
+ cur = path7.dirname(cur);
1790
1869
  }
1791
1870
  return null;
1792
1871
  }
1793
1872
  function assertInside(cwd, target, theme) {
1794
- const rel = path6.relative(cwd, target);
1795
- const insideCwd = rel === "" || !rel.startsWith("..") && !path6.isAbsolute(rel);
1796
- const insideNodeModules = target.split(path6.sep).includes("node_modules");
1873
+ const rel = path7.relative(cwd, target);
1874
+ const insideCwd = rel === "" || !rel.startsWith("..") && !path7.isAbsolute(rel);
1875
+ const insideNodeModules = target.split(path7.sep).includes("node_modules");
1797
1876
  if (!insideCwd && !insideNodeModules) {
1798
1877
  throw new DommiefError(`\u0111\u01B0\u1EDDng d\u1EABn theme "${theme}" tho\xE1t ra ngo\xE0i th\u01B0 m\u1EE5c d\u1EF1 \xE1n: ${target}`, { code: "theme_path_escape", file: target });
1799
1878
  }
@@ -1802,44 +1881,44 @@ function resolveThemeRoot(cwd, theme) {
1802
1881
  const isPathLike = theme.startsWith(".") || theme.startsWith("/");
1803
1882
  let root = null;
1804
1883
  if (isPathLike) {
1805
- const candidate = path6.resolve(cwd, theme);
1806
- if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) root = candidate;
1884
+ const candidate = path7.resolve(cwd, theme);
1885
+ if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) root = candidate;
1807
1886
  if (!root) throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y theme t\u1EA1i \u0111\u01B0\u1EDDng d\u1EABn "${theme}"`, { code: "theme_package_not_found" });
1808
1887
  assertInside(cwd, root, theme);
1809
1888
  } else {
1810
- const nmCandidate = path6.resolve(cwd, "node_modules", theme);
1811
- if (fs6.existsSync(nmCandidate)) root = nmCandidate;
1889
+ const nmCandidate = path7.resolve(cwd, "node_modules", theme);
1890
+ if (fs7.existsSync(nmCandidate)) root = nmCandidate;
1812
1891
  else {
1813
1892
  const packages = repoPackagesDir();
1814
1893
  const pkgDirName = theme === DEFAULT_THEME ? "theme-default" : theme;
1815
- const srcCandidate = packages ? path6.join(packages, pkgDirName) : null;
1816
- if (srcCandidate && fs6.existsSync(srcCandidate)) root = srcCandidate;
1894
+ const srcCandidate = packages ? path7.join(packages, pkgDirName) : null;
1895
+ if (srcCandidate && fs7.existsSync(srcCandidate)) root = srcCandidate;
1817
1896
  }
1818
1897
  if (!root) {
1819
1898
  throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y theme package "${theme}" (\u0111\xE3 th\u1EED node_modules/${theme})`, { code: "theme_package_not_found" });
1820
1899
  }
1821
1900
  }
1822
- if (fs6.existsSync(path6.join(root, "src", "layouts"))) root = path6.join(root, "src");
1901
+ if (fs7.existsSync(path7.join(root, "src", "layouts"))) root = path7.join(root, "src");
1823
1902
  return root;
1824
1903
  }
1825
1904
  function pickEntry(dir, name) {
1826
1905
  for (const ext of EXTS) {
1827
- const f = path6.join(dir, name + ext);
1828
- if (fs6.existsSync(f) && fs6.statSync(f).isFile()) return f;
1906
+ const f = path7.join(dir, name + ext);
1907
+ if (fs7.existsSync(f) && fs7.statSync(f).isFile()) return f;
1829
1908
  }
1830
- const subIndexDir = path6.join(dir, name);
1831
- if (fs6.existsSync(subIndexDir) && fs6.statSync(subIndexDir).isDirectory()) {
1909
+ const subIndexDir = path7.join(dir, name);
1910
+ if (fs7.existsSync(subIndexDir) && fs7.statSync(subIndexDir).isDirectory()) {
1832
1911
  for (const ext of EXTS) {
1833
- const f = path6.join(subIndexDir, "index" + ext);
1834
- if (fs6.existsSync(f) && fs6.statSync(f).isFile()) return f;
1912
+ const f = path7.join(subIndexDir, "index" + ext);
1913
+ if (fs7.existsSync(f) && fs7.statSync(f).isFile()) return f;
1835
1914
  }
1836
1915
  }
1837
1916
  return null;
1838
1917
  }
1839
1918
  function scanComponents(dir) {
1840
1919
  const out = {};
1841
- if (!fs6.existsSync(dir)) return out;
1842
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1920
+ if (!fs7.existsSync(dir)) return out;
1921
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1843
1922
  const name = entry.name;
1844
1923
  if (!isComponentName(name)) continue;
1845
1924
  if (entry.isFile()) {
@@ -1847,26 +1926,26 @@ function scanComponents(dir) {
1847
1926
  if (!ext) continue;
1848
1927
  const specifier = name.slice(0, -ext.length);
1849
1928
  if (!isComponentName(specifier)) continue;
1850
- out[specifier] ??= path6.resolve(dir, name);
1929
+ out[specifier] ??= path7.resolve(dir, name);
1851
1930
  } else if (entry.isDirectory()) {
1852
1931
  const found = pickEntry(dir, name);
1853
- if (found) out[name] = path6.resolve(found);
1932
+ if (found) out[name] = path7.resolve(found);
1854
1933
  }
1855
1934
  }
1856
1935
  return out;
1857
1936
  }
1858
1937
  function scanLocales(dir) {
1859
1938
  const out = {};
1860
- if (!fs6.existsSync(dir)) return out;
1861
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1939
+ if (!fs7.existsSync(dir)) return out;
1940
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1862
1941
  if (entry.isFile() && entry.name.endsWith(".json")) {
1863
- out[entry.name.slice(0, -".json".length)] = path6.resolve(dir, entry.name);
1942
+ out[entry.name.slice(0, -".json".length)] = path7.resolve(dir, entry.name);
1864
1943
  }
1865
1944
  }
1866
1945
  return out;
1867
1946
  }
1868
1947
  function loadThemeManifest(root, name) {
1869
- const layoutsDir = path6.join(root, "layouts");
1948
+ const layoutsDir = path7.join(root, "layouts");
1870
1949
  const layouts = {};
1871
1950
  const missing = [];
1872
1951
  for (const layoutName of ["GlobalLayout", "DocLayout", "DemoLayout"]) {
@@ -1877,19 +1956,19 @@ function loadThemeManifest(root, name) {
1877
1956
  if (missing.length) {
1878
1957
  throw new DommiefError(`theme "${name}" thi\u1EBFu layout: ${missing.join(", ")}`, { code: "theme_layout_missing", file: layoutsDir, layout: missing.join(",") });
1879
1958
  }
1880
- const pluginJs = ["plugin.ts", "plugin.js", path6.join("plugin", "index.ts"), path6.join("plugin", "index.js")].map((f) => path6.join(root, f)).find((f) => fs6.existsSync(f));
1959
+ const pluginJs = ["plugin.ts", "plugin.js", path7.join("plugin", "index.ts"), path7.join("plugin", "index.js")].map((f) => path7.join(root, f)).find((f) => fs7.existsSync(f));
1881
1960
  return {
1882
1961
  name,
1883
1962
  root,
1884
- builtins: scanComponents(path6.join(root, "builtins")),
1885
- slots: scanComponents(path6.join(root, "slots")),
1963
+ builtins: scanComponents(path7.join(root, "builtins")),
1964
+ slots: scanComponents(path7.join(root, "slots")),
1886
1965
  layouts,
1887
- locales: scanLocales(path6.join(root, "locales")),
1966
+ locales: scanLocales(path7.join(root, "locales")),
1888
1967
  plugin: pluginJs ?? null
1889
1968
  };
1890
1969
  }
1891
1970
  function loadTheme(opts) {
1892
- const cwd = path6.resolve(opts.cwd);
1971
+ const cwd = path7.resolve(opts.cwd);
1893
1972
  const theme = opts.theme ?? DEFAULT_THEME;
1894
1973
  const root = resolveThemeRoot(cwd, theme);
1895
1974
  return loadThemeManifest(root, theme);
@@ -1897,9 +1976,9 @@ function loadTheme(opts) {
1897
1976
 
1898
1977
  // src/theme/resolver.ts
1899
1978
  function tryLoadLocal(cwd) {
1900
- const localRoot = path7.resolve(cwd, ".dommief", "theme");
1901
- if (!fs7.existsSync(localRoot)) return null;
1902
- const layoutsDir = path7.join(localRoot, "layouts");
1979
+ const localRoot = path8.resolve(cwd, ".dommief", "theme");
1980
+ if (!fs8.existsSync(localRoot)) return null;
1981
+ const layoutsDir = path8.join(localRoot, "layouts");
1903
1982
  const layouts = {};
1904
1983
  for (const name of ["GlobalLayout", "DocLayout", "DemoLayout"]) {
1905
1984
  const found = scanComponents(layoutsDir)[name];
@@ -1908,8 +1987,8 @@ function tryLoadLocal(cwd) {
1908
1987
  return {
1909
1988
  name: ".dommief/theme",
1910
1989
  root: localRoot,
1911
- builtins: scanComponents(path7.join(localRoot, "builtins")),
1912
- slots: scanComponents(path7.join(localRoot, "slots")),
1990
+ builtins: scanComponents(path8.join(localRoot, "builtins")),
1991
+ slots: scanComponents(path8.join(localRoot, "slots")),
1913
1992
  layouts,
1914
1993
  locales: {},
1915
1994
  plugin: null
@@ -1955,8 +2034,8 @@ function buildAliasChain(cwd, config) {
1955
2034
  // src/cache.ts
1956
2035
  init_rehype_code();
1957
2036
  import { createHash as createHash5 } from "crypto";
1958
- import fs8 from "fs";
1959
- import path8 from "path";
2037
+ import fs9 from "fs";
2038
+ import path9 from "path";
1960
2039
  var CACHE_VERSION = 3;
1961
2040
  function stableStringify(value) {
1962
2041
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
@@ -1983,7 +2062,7 @@ var TransformCache = class {
1983
2062
  this.enabled = enabled;
1984
2063
  if (!enabled) return;
1985
2064
  try {
1986
- this.manifest = JSON.parse(fs8.readFileSync(path8.join(dir, "manifest.json"), "utf8"));
2065
+ this.manifest = JSON.parse(fs9.readFileSync(path9.join(dir, "manifest.json"), "utf8"));
1987
2066
  } catch {
1988
2067
  this.manifest = {};
1989
2068
  }
@@ -1996,12 +2075,12 @@ var TransformCache = class {
1996
2075
  manifest = {};
1997
2076
  nextManifest = {};
1998
2077
  fileOf(key) {
1999
- return path8.join(this.dir, key.slice(0, 2), `${key}.json`);
2078
+ return path9.join(this.dir, key.slice(0, 2), `${key}.json`);
2000
2079
  }
2001
2080
  read(key) {
2002
2081
  if (!this.enabled) return null;
2003
2082
  try {
2004
- const v = JSON.parse(fs8.readFileSync(this.fileOf(key), "utf8"));
2083
+ const v = JSON.parse(fs9.readFileSync(this.fileOf(key), "utf8"));
2005
2084
  return typeof v?.code === "string" && v.meta ? v : null;
2006
2085
  } catch {
2007
2086
  return null;
@@ -2011,19 +2090,19 @@ var TransformCache = class {
2011
2090
  write(key, payload) {
2012
2091
  if (!this.enabled) return;
2013
2092
  const file = this.fileOf(key);
2014
- fs8.mkdirSync(path8.dirname(file), { recursive: true });
2093
+ fs9.mkdirSync(path9.dirname(file), { recursive: true });
2015
2094
  const tmp = `${file}.${process.pid}.tmp`;
2016
- fs8.writeFileSync(tmp, JSON.stringify(payload));
2017
- fs8.renameSync(tmp, file);
2095
+ fs9.writeFileSync(tmp, JSON.stringify(payload));
2096
+ fs9.renameSync(tmp, file);
2018
2097
  }
2019
2098
  /** Xoá file cache không còn dùng (trang bị xoá/đổi tên/đổi nội dung). Best-effort, không throw. */
2020
2099
  gc(keepKeys) {
2021
2100
  if (!this.enabled) return;
2022
2101
  try {
2023
- for (const shard of fs8.readdirSync(this.dir, { withFileTypes: true })) {
2102
+ for (const shard of fs9.readdirSync(this.dir, { withFileTypes: true })) {
2024
2103
  if (!shard.isDirectory()) continue;
2025
- for (const f of fs8.readdirSync(path8.join(this.dir, shard.name))) {
2026
- if (!keepKeys.has(f.replace(/\.json$/, "")) || f.endsWith(".tmp")) fs8.rmSync(path8.join(this.dir, shard.name, f), { force: true });
2104
+ for (const f of fs9.readdirSync(path9.join(this.dir, shard.name))) {
2105
+ if (!keepKeys.has(f.replace(/\.json$/, "")) || f.endsWith(".tmp")) fs9.rmSync(path9.join(this.dir, shard.name, f), { force: true });
2027
2106
  }
2028
2107
  }
2029
2108
  } catch {
@@ -2073,8 +2152,8 @@ var TransformCache = class {
2073
2152
  if (!this.enabled) return;
2074
2153
  this.gc(this.used);
2075
2154
  try {
2076
- fs8.mkdirSync(this.dir, { recursive: true });
2077
- fs8.writeFileSync(path8.join(this.dir, "manifest.json"), JSON.stringify(this.nextManifest));
2155
+ fs9.mkdirSync(this.dir, { recursive: true });
2156
+ fs9.writeFileSync(path9.join(this.dir, "manifest.json"), JSON.stringify(this.nextManifest));
2078
2157
  } catch {
2079
2158
  }
2080
2159
  }
@@ -2095,16 +2174,23 @@ function resolveConfiguredColor(defaultValue) {
2095
2174
 
2096
2175
  // src/vite/plugin.ts
2097
2176
  var demoRegistry = /* @__PURE__ */ new Map();
2177
+ var demoDirs = /* @__PURE__ */ new Map();
2098
2178
  function dommiefPlugin(opts) {
2099
2179
  let loaded = opts.config ? { config: opts.config, host: opts.host ?? createPluginHost(opts.config.plugins ?? []) } : void 0;
2100
2180
  const state = createState(opts.root, () => {
2101
2181
  if (!loaded) throw new Error("dommief: config ch\u01B0a \u0111\u01B0\u1EE3c n\u1EA1p");
2102
2182
  return loaded;
2103
2183
  });
2184
+ let isServe = false;
2185
+ const jsx2 = async (code, name) => {
2186
+ const r = await transformWithOxc(code, name + ".jsx", { lang: "jsx", jsx: { runtime: "automatic", development: isServe } });
2187
+ return { code: r.code, map: r.map ?? null, moduleType: "js" };
2188
+ };
2104
2189
  const markdownPlugin = {
2105
2190
  name: "dommief:markdown",
2106
2191
  enforce: "pre",
2107
- async config() {
2192
+ async config(_, env) {
2193
+ isServe = env.command === "serve";
2108
2194
  if (!loaded) {
2109
2195
  const config = await loadConfig(opts.root);
2110
2196
  loaded = { config, host: createPluginHost(config.plugins ?? []) };
@@ -2114,7 +2200,10 @@ function dommiefPlugin(opts) {
2114
2200
  return {
2115
2201
  base: loaded.config.base,
2116
2202
  resolve: { dedupe: ["react", "react-dom", "react-router"], alias: aliases },
2117
- ssr: { noExternal: ["dommief"] }
2203
+ ssr: { noExternal: ["dommief"] },
2204
+ // Client dommief nằm trong node_modules nên Vite không quét dep → React CJS bị serve thô
2205
+ // ("does not provide an export named 'jsxDEV'").
2206
+ optimizeDeps: { include: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime", "react-router"] }
2118
2207
  };
2119
2208
  },
2120
2209
  resolveId(id, importer) {
@@ -2122,12 +2211,16 @@ function dommiefPlugin(opts) {
2122
2211
  const bare = id.replace(/^\//, "");
2123
2212
  if (bare === "virtual:dommief/entry") return "\0virtual:dommief/entry";
2124
2213
  if (bare.startsWith("virtual:dommief-demo/")) return "\0" + bare;
2214
+ if (importer?.startsWith("\0virtual:dommief-demo/") && id.startsWith(".")) {
2215
+ const dir = demoDirs.get(importer.slice("\0virtual:dommief-demo/".length));
2216
+ if (dir) return this.resolve(path10.resolve(dir, id), path10.join(dir, "_demo.tsx"), { skipSelf: true });
2217
+ }
2125
2218
  if (bare === "virtual:dommief/routes" || bare === "virtual:dommief/site") return null;
2126
2219
  if (id.endsWith(".md") || id.endsWith(".md?meta")) {
2127
- const base = importer ? path9.dirname(importer.replace(/\?meta$/, "")) : process.cwd();
2220
+ const base = importer ? path10.dirname(importer.replace(/\?meta$/, "")) : process.cwd();
2128
2221
  const q = id.endsWith("?meta") ? "?meta" : "";
2129
2222
  const clean = id.replace(/\?meta$/, "");
2130
- return winPath(path9.isAbsolute(clean) ? clean : path9.resolve(base, clean)) + q;
2223
+ return winPath(path10.isAbsolute(clean) ? clean : path10.resolve(base, clean)) + q;
2131
2224
  }
2132
2225
  return null;
2133
2226
  },
@@ -2135,7 +2228,7 @@ function dommiefPlugin(opts) {
2135
2228
  if (id.startsWith("\0virtual:dommief-demo/")) {
2136
2229
  const demoId2 = id.slice("\0virtual:dommief-demo/".length);
2137
2230
  const source = demoRegistry.get(demoId2) ?? "export default function Demo(){ return null; }";
2138
- return { code: source, map: null, moduleType: "jsx" };
2231
+ return jsx2(source, "demo-" + demoId2);
2139
2232
  }
2140
2233
  if (id === "\0virtual:dommief/entry") {
2141
2234
  return { code: `import ${JSON.stringify(resolveClientFile("entry-client.tsx"))};`, map: null, moduleType: "js" };
@@ -2143,7 +2236,7 @@ function dommiefPlugin(opts) {
2143
2236
  if (id.endsWith(".md") || id.endsWith(".md?meta")) {
2144
2237
  const isMeta = id.endsWith("?meta");
2145
2238
  const file = isMeta ? id.slice(0, -"?meta".length) : id;
2146
- const code = await fs9.promises.readFile(file, "utf8");
2239
+ const code = await fs10.promises.readFile(file, "utf8");
2147
2240
  const transformOpts = { hooks: state.host(), byFile: state.getTable().byFile, base: state.config().base, code: state.config().markdown };
2148
2241
  let out;
2149
2242
  let meta;
@@ -2168,7 +2261,8 @@ function dommiefPlugin(opts) {
2168
2261
  hitDemos = hit.demos ?? [];
2169
2262
  for (const d of hitDemos) {
2170
2263
  demoRegistry.set(d.id, d.source);
2171
- if (d.src) this.addWatchFile(path9.resolve(path9.dirname(file), d.src));
2264
+ demoDirs.set(d.id, path10.dirname(file));
2265
+ if (d.src) this.addWatchFile(path10.resolve(path10.dirname(file), d.src));
2172
2266
  }
2173
2267
  } else {
2174
2268
  const r = await transformMarkdown(code, file, transformOpts);
@@ -2176,7 +2270,8 @@ function dommiefPlugin(opts) {
2176
2270
  const nonCacheDemos = r.demos ?? [];
2177
2271
  for (const d of nonCacheDemos) {
2178
2272
  demoRegistry.set(d.id, d.source);
2179
- if (d.src) this.addWatchFile(path9.resolve(path9.dirname(file), d.src));
2273
+ demoDirs.set(d.id, path10.dirname(file));
2274
+ if (d.src) this.addWatchFile(path10.resolve(path10.dirname(file), d.src));
2180
2275
  }
2181
2276
  }
2182
2277
  for (const w of warnings) this.warn(w);
@@ -2185,7 +2280,7 @@ function dommiefPlugin(opts) {
2185
2280
  if (isMeta) {
2186
2281
  return { code: `export default ${JSON.stringify({ frontmatter: meta.frontmatter, toc: meta.toc })};`, map: null, moduleType: "js" };
2187
2282
  }
2188
- return { code: out, map: null, moduleType: "jsx" };
2283
+ return jsx2(out, file);
2189
2284
  }
2190
2285
  return null;
2191
2286
  },
@@ -2193,7 +2288,7 @@ function dommiefPlugin(opts) {
2193
2288
  transformIndexHtml: { order: "pre", handler(html) {
2194
2289
  const title = state.config().title;
2195
2290
  let out = html;
2196
- if (!/<title>/.test(out)) out = out.replace("<head>", `<head><title>${title}</title>`);
2291
+ if (!/<title>/.test(out)) out = out.replace("<head>", `<head><title>${escapeXml(title)}</title>`);
2197
2292
  const tags = state.host().collectHtmlHeadTags();
2198
2293
  if (tags.length) out = out.replace("</head>", tags.join("\n") + "</head>");
2199
2294
  const themeConfig = state.config().themeConfig;
@@ -2210,7 +2305,7 @@ function dommiefPlugin(opts) {
2210
2305
  return out;
2211
2306
  } },
2212
2307
  configureServer(server) {
2213
- const localThemeDir = winPath(path9.resolve(opts.root, ".dommief", "theme")) + "/";
2308
+ const localThemeDir = winPath(path10.resolve(opts.root, ".dommief", "theme")) + "/";
2214
2309
  const update = () => {
2215
2310
  const aliases = buildAliasChain(opts.root, loaded.config);
2216
2311
  server.config.resolve.alias = { ...server.config.resolve.alias, ...aliases };
@@ -2251,8 +2346,8 @@ import { build } from "vite";
2251
2346
  init_errors();
2252
2347
 
2253
2348
  // src/link-check.ts
2254
- import fs10 from "fs";
2255
- import path10 from "path";
2349
+ import fs11 from "fs";
2350
+ import path11 from "path";
2256
2351
  var HREF_RE = /<a\b[^>]*\shref="([^"]*)"[^>]*>/g;
2257
2352
  var ASSET_RE = /<(?:img|link)\b[^>]*\s(?:src|href)="([^"]*)"[^>]*>/g;
2258
2353
  var LINE_RE = /\sdata-src-line="(\d+)"/;
@@ -2287,8 +2382,8 @@ function check(refs, opts) {
2287
2382
  const cleanPath = (rawPath ?? "").split("?")[0];
2288
2383
  if (isAsset) {
2289
2384
  const relPath = stripBase(cleanPath, opts.base) || "/";
2290
- const onDisk = path10.join(opts.outDir, decodeURIComponent(relPath));
2291
- if (!fs10.existsSync(onDisk)) issues.push({ code: "link_asset_missing", ref });
2385
+ const onDisk = path11.join(opts.outDir, decodeURIComponent(relPath));
2386
+ if (!fs11.existsSync(onDisk)) issues.push({ code: "link_asset_missing", ref });
2292
2387
  continue;
2293
2388
  }
2294
2389
  const routePath = stripBase(cleanPath, opts.base) || "/";
@@ -2314,78 +2409,6 @@ function formatIssues(issues) {
2314
2409
  return lines.join("\n");
2315
2410
  }
2316
2411
 
2317
- // src/exclude.ts
2318
- var cache = /* @__PURE__ */ new Map();
2319
- function globToRegExp(pattern) {
2320
- let src = "";
2321
- for (let i = 0; i < pattern.length; i++) {
2322
- const c = pattern.charAt(i);
2323
- if (c === "*" && pattern.charAt(i + 1) === "*") {
2324
- const slash = pattern.charAt(i + 2) === "/";
2325
- src += slash ? "(?:.*/)?" : ".*";
2326
- i += slash ? 2 : 1;
2327
- } else if (c === "*") {
2328
- src += "[^/]*";
2329
- } else if (c === "?") {
2330
- src += "[^/]";
2331
- } else {
2332
- src += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2333
- }
2334
- }
2335
- return new RegExp(`^${src}$`);
2336
- }
2337
- function matcher(pattern) {
2338
- const hit = cache.get(pattern);
2339
- if (hit) return hit;
2340
- const re = globToRegExp(pattern);
2341
- cache.set(pattern, re);
2342
- return re;
2343
- }
2344
- function matchExclude(path15, patterns) {
2345
- return patterns.some((p) => matcher(p).test(path15));
2346
- }
2347
-
2348
- // src/sitemap.ts
2349
- import fs11 from "fs";
2350
- import path11 from "path";
2351
- function canonicalUrl(hostname, base, routePath) {
2352
- const host = hostname.replace(/\/$/, "");
2353
- const b = base.replace(/\/$/, "");
2354
- const p = routePath === "/" ? "/" : routePath;
2355
- return host + (b + p).replace(/\/{2,}/g, "/").replace(/^(https?:\/)([^/])/, "$1/$2");
2356
- }
2357
- function buildSitemap(routes, config, base) {
2358
- const urls = routes.filter((r) => r.path !== "/404" && !/[:*]/.test(r.path) && !matchExclude(r.path, config.exclude)).map((r) => ` <url><loc>${escapeXml(canonicalUrl(config.hostname, base, r.path))}</loc></url>`).join("\n");
2359
- return `<?xml version="1.0" encoding="UTF-8"?>
2360
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
2361
- ${urls}
2362
- </urlset>
2363
- `;
2364
- }
2365
- function buildRobots(sitemapUrl) {
2366
- return `User-agent: *
2367
- Allow: /
2368
-
2369
- Sitemap: ${sitemapUrl}
2370
- `;
2371
- }
2372
- function escapeXml(s) {
2373
- return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
2374
- }
2375
- function writeSitemapAndRobots(opts) {
2376
- if (opts.sitemap === false) return;
2377
- if (!opts.sitemap.hostname) {
2378
- opts.log.warn("sitemap.hostname thi\u1EBFu \u2014 b\u1ECF qua sitemap.xml/robots.txt");
2379
- return;
2380
- }
2381
- const xml = buildSitemap(opts.routes, opts.sitemap, opts.base);
2382
- fs11.writeFileSync(path11.join(opts.outDir, "sitemap.xml"), xml);
2383
- if (opts.robots) {
2384
- const sitemapUrl = canonicalUrl(opts.sitemap.hostname, opts.base, "/").replace(/\/$/, "") + "/sitemap.xml";
2385
- fs11.writeFileSync(path11.join(opts.outDir, "robots.txt"), buildRobots(sitemapUrl));
2386
- }
2387
- }
2388
-
2389
2412
  // src/build.ts
2390
2413
  var ROOT_PLACEHOLDER = /<div id="root"><\/div>/;
2391
2414
  async function mapLimit(items, limit, fn) {
@@ -2501,7 +2524,12 @@ async function runBuild({ root, config, host, log }) {
2501
2524
  const linkRefs = [];
2502
2525
  await timed("prerender", async () => {
2503
2526
  await mapLimit(plan.prerender, config.ssg.concurrency, async (r) => {
2504
- const html = await render(r.path);
2527
+ let html;
2528
+ try {
2529
+ html = await render(r.path);
2530
+ } catch (e) {
2531
+ throw new DommiefError(`prerender ${r.path}: ${formatError(e)}`, { code: "ssg_render_failed" });
2532
+ }
2505
2533
  if (linkCheckEnabled) linkRefs.push(...collect(r.id, html, fileOf(r.id)));
2506
2534
  writePage(r, wrap(html, r.path));
2507
2535
  });
@@ -2604,7 +2632,14 @@ function spaFallbackPlugin(opts) {
2604
2632
  name: "dommief:preview-spa-fallback",
2605
2633
  configurePreviewServer(s) {
2606
2634
  s.middlewares.use((req, res, next) => {
2607
- const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
2635
+ let urlPath;
2636
+ try {
2637
+ urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
2638
+ } catch {
2639
+ res.statusCode = 400;
2640
+ res.end("Invalid URL");
2641
+ return;
2642
+ }
2608
2643
  if (req.method !== "GET") return next();
2609
2644
  if (base && urlPath !== base && !urlPath.startsWith(base + "/")) {
2610
2645
  res.statusCode = 404;
package/dist/index.js CHANGED
@@ -272,6 +272,7 @@ function resolveConfig(userConfig, opts) {
272
272
  srcDir: c.srcDir,
273
273
  docDirs: (c.docDirs ?? [c.srcDir]).map((d) => typeof d === "string" ? { dir: d } : { dir: d.dir }),
274
274
  forceKebabCase: c.forceKebabCase,
275
+ preset: c.preset,
275
276
  locales,
276
277
  themeConfig: c.themeConfig,
277
278
  theme: c.theme,
@@ -961,7 +962,7 @@ function buildApiTable(src, filePath = "") {
961
962
  try {
962
963
  docs = parser.parse(abs) ?? [];
963
964
  } catch {
964
- docs = [];
965
+ return null;
965
966
  }
966
967
  const entries = (docs || []).map((d) => ({
967
968
  displayName: d.displayName,
@@ -983,6 +984,7 @@ function rehypeApi() {
983
984
  return (tree, file) => {
984
985
  const apis = Array.isArray(file?.data?.apis) ? file.data.apis : [];
985
986
  const watched = Array.isArray(file?.data?.apiSrcFiles) ? file.data.apiSrcFiles : [];
987
+ const warnings = Array.isArray(file?.data?.warnings) ? file.data.warnings : [];
986
988
  const filePath = String(file?.path ?? "");
987
989
  visit7(tree, "element", (node, index, parent) => {
988
990
  if (!parent || typeof index !== "number") return;
@@ -992,6 +994,7 @@ function rehypeApi() {
992
994
  const abs = resolveApiSrc(src, filePath);
993
995
  const data = buildApiTable(src, filePath);
994
996
  watched.push(abs);
997
+ if (!data?.length) warnings.push(`<API src="${src}">: ${data ? "no component found (syntax error or no exported component)" : "file missing or failed to parse"}: ${abs}`);
995
998
  apis.push({ id: hashFileContent(src + filePath).slice(0, 8), src: abs, data });
996
999
  parent.children[index] = {
997
1000
  type: "element",
@@ -1006,6 +1009,7 @@ function rehypeApi() {
1006
1009
  if (file?.data) {
1007
1010
  file.data.apis = apis;
1008
1011
  file.data.apiSrcFiles = watched;
1012
+ file.data.warnings = warnings;
1009
1013
  }
1010
1014
  };
1011
1015
  }