@sudajs/cli 0.18.9 → 0.18.10

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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { ThemeModule, ThemeStarterPage } from '@sudajs/theme-engine';
3
- import { ThemeRender, extractLayoutChrome, resolveAssetPath } from '@sudajs/theme-engine/server';
3
+ import { ThemeRender, extractLayoutChrome, resolveAssetPath, ThemePreviewLocaleMessages } from '@sudajs/theme-engine/server';
4
4
  import { Command } from 'commander';
5
5
  import { createElement } from 'react';
6
6
  import { renderToString } from 'react-dom/server';
@@ -1816,6 +1816,7 @@ declare const __testUtils: {
1816
1816
  performUpdateThemeSettings: typeof performUpdateThemeSettings;
1817
1817
  getRemoteThemeSettings: typeof getRemoteThemeSettings;
1818
1818
  renderDevStarterPageHtml: typeof renderDevStarterPageHtml;
1819
+ writeVitePreviewEntries: typeof writeVitePreviewEntries;
1819
1820
  resolveDevStarterSlug: typeof resolveDevStarterSlug;
1820
1821
  resolveScreenshotOptions: typeof resolveScreenshotOptions;
1821
1822
  slugifyThemeName: typeof slugifyThemeName;
@@ -1831,6 +1832,7 @@ declare const __testUtils: {
1831
1832
  updateRemotePageDraft: typeof updateRemotePageDraft;
1832
1833
  };
1833
1834
  declare function watchTheme(root: string, port: number): Promise<void>;
1835
+ declare function writeVitePreviewEntries(root: string): Promise<void>;
1834
1836
  declare function withSerializedVitePreviewLocaleLoad<Result>(server: object, load: () => Promise<Result>): Promise<Result>;
1835
1837
  declare function resolveDevStarterSlug(pathname: string): string | null | undefined;
1836
1838
  declare function resolveThemePreviewLocale(params: {
@@ -1856,7 +1858,7 @@ interface ThemeRenderer {
1856
1858
  renderToString: typeof renderToString;
1857
1859
  }
1858
1860
  declare function loadThemeScopedRenderer(themeRoot: string): Promise<ThemeRenderer>;
1859
- declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean): string;
1861
+ declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean, previewLocaleMessages?: ThemePreviewLocaleMessages): string;
1860
1862
  interface ScreenshotOptions extends ThemeRootOptions {
1861
1863
  device?: string[] | string;
1862
1864
  output?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'crypto';
3
3
  import { existsSync } from 'fs';
4
- import fs, { readFile, writeFile, stat, readdir, mkdir, copyFile, rm } from 'fs/promises';
4
+ import fs, { readFile, mkdir, writeFile, stat, readdir, copyFile, rm } from 'fs/promises';
5
5
  import { createRequire } from 'module';
6
6
  import path2 from 'path';
7
7
  import { createInterface } from 'readline/promises';
@@ -12,6 +12,9 @@ import { themeDesignSystemSchema, createThemeDesignCssVariables, createThemeAgen
12
12
  import { themeManifestSchema, runWithThemePreviewLocale } from '@sudajs/theme-engine/server';
13
13
  import { Command } from 'commander';
14
14
  import { build } from 'esbuild';
15
+ import { CircleCheck } from 'lucide-react';
16
+ import { createElement } from 'react';
17
+ import { renderToStaticMarkup } from 'react-dom/server';
15
18
  import { z } from 'zod';
16
19
  import os from 'os';
17
20
  import pc from 'picocolors';
@@ -1520,26 +1523,7 @@ async function buildClientRuntime(root, minify) {
1520
1523
  await mkdir(path2.dirname(generatedEntry), { recursive: true });
1521
1524
  await writeFile(
1522
1525
  generatedEntry,
1523
- [
1524
- '"use client";',
1525
- 'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
1526
- 'import * as themeConfig from "../src/config.js";',
1527
- clientHooksImport,
1528
- "const theme: ThemeRenderModule = {",
1529
- " pageConfig: themeConfig.pageConfig,",
1530
- " layoutConfig: themeConfig.layoutConfig,",
1531
- "};",
1532
- 'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
1533
- " theme.rendering = themeConfig.rendering;",
1534
- "}",
1535
- "const hooks: ThemeClientHooks = clientHooks;",
1536
- "const runtime: ThemeClientRuntime = { theme };",
1537
- "if (hooks.initialize !== undefined) {",
1538
- " runtime.initialize = hooks.initialize;",
1539
- "}",
1540
- "export default runtime;",
1541
- ""
1542
- ].join("\n"),
1526
+ createClientRuntimeSource(clientHooksImport, true),
1543
1527
  "utf8"
1544
1528
  );
1545
1529
  await mkdir(path2.join(root, "dist"), { recursive: true });
@@ -1754,6 +1738,7 @@ var __testUtils = {
1754
1738
  performUpdateThemeSettings,
1755
1739
  getRemoteThemeSettings,
1756
1740
  renderDevStarterPageHtml,
1741
+ writeVitePreviewEntries,
1757
1742
  resolveDevStarterSlug,
1758
1743
  resolveScreenshotOptions,
1759
1744
  slugifyThemeName,
@@ -1825,13 +1810,17 @@ async function watchTheme(root, port) {
1825
1810
  async function startViteDevPreviewServer(root, port) {
1826
1811
  const viteConfig = await findViteConfig(root);
1827
1812
  const vite = await loadThemeVite(root);
1813
+ await writeVitePreviewEntries(root);
1828
1814
  const host = "127.0.0.1";
1829
1815
  const server = await vite.createServer({
1830
1816
  root,
1831
1817
  configFile: viteConfig,
1832
1818
  configLoader: "runner",
1833
- cacheDir: path2.join(root, ".suda-build", "vite-cache"),
1819
+ cacheDir: path2.join(root, ".suda-build", "node_modules", ".vite"),
1834
1820
  appType: "custom",
1821
+ optimizeDeps: {
1822
+ entries: [".suda-build/preview-client.ts"]
1823
+ },
1835
1824
  server: { host, port },
1836
1825
  plugins: [createSudaPreviewVitePlugin(root)]
1837
1826
  });
@@ -1843,6 +1832,55 @@ async function startViteDevPreviewServer(root, port) {
1843
1832
  close: () => server.close()
1844
1833
  };
1845
1834
  }
1835
+ async function writeVitePreviewEntries(root) {
1836
+ const clientHooksPath = path2.join(root, "src", "client.ts");
1837
+ const clientHooksImport = existsSync(clientHooksPath) ? 'import { clientHooks } from "../src/client.js";' : "const clientHooks = {};";
1838
+ const buildDir = path2.join(root, ".suda-build");
1839
+ await mkdir(buildDir, { recursive: true });
1840
+ await writeFile(
1841
+ path2.join(buildDir, "runtime.dev.ts"),
1842
+ createClientRuntimeSource(clientHooksImport, false),
1843
+ "utf8"
1844
+ );
1845
+ await writeFile(
1846
+ path2.join(buildDir, "preview-client.ts"),
1847
+ [
1848
+ 'import { hydrateThemePage, loadThemeRuntime } from "@sudajs/theme-engine/client";',
1849
+ 'import { installThemePreviewLocale, resolveAssetPath } from "@sudajs/theme-engine/site-runtime";',
1850
+ 'const payloadElement=document.querySelector("#suda-theme-hydration-data");',
1851
+ 'if(!payloadElement){throw new Error("Missing Suda theme hydration payload.")}',
1852
+ "const payload=JSON.parse(payloadElement.textContent ?? '{}');",
1853
+ "if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
1854
+ 'const runtime=(await loadThemeRuntime("/.suda-build/runtime.dev.ts")).default;',
1855
+ 'const metadata={contactForm:payload.contactForm,resolveAssetUrl:(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined})};',
1856
+ 'await hydrateThemePage({container:document.querySelector("[data-theme-hydration-root]")!,runtime,pageData:payload.pageData,layoutData:payload.layoutData,metadata,onRecoverableError:(error)=>console.error("[theme-dev] theme hydration recovered from an error",error)});',
1857
+ ""
1858
+ ].join("\n"),
1859
+ "utf8"
1860
+ );
1861
+ }
1862
+ function createClientRuntimeSource(clientHooksImport, useClientDirective) {
1863
+ return [
1864
+ ...useClientDirective ? ['"use client";'] : [],
1865
+ 'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
1866
+ 'import * as themeConfig from "../src/config.js";',
1867
+ clientHooksImport,
1868
+ "const theme: ThemeRenderModule = {",
1869
+ " pageConfig: themeConfig.pageConfig,",
1870
+ " layoutConfig: themeConfig.layoutConfig,",
1871
+ "};",
1872
+ 'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
1873
+ " theme.rendering = themeConfig.rendering;",
1874
+ "}",
1875
+ "const hooks: ThemeClientHooks = clientHooks;",
1876
+ "const runtime: ThemeClientRuntime = { theme };",
1877
+ "if (hooks.initialize !== undefined) {",
1878
+ " runtime.initialize = hooks.initialize;",
1879
+ "}",
1880
+ "export default runtime;",
1881
+ ""
1882
+ ].join("\n");
1883
+ }
1846
1884
  function createSudaPreviewVitePlugin(root) {
1847
1885
  return {
1848
1886
  name: "suda-theme-preview",
@@ -1865,54 +1903,31 @@ function createSudaPreviewVitePlugin(root) {
1865
1903
  const acceptLanguageHeader = request.headers["accept-language"];
1866
1904
  const acceptLanguage = Array.isArray(acceptLanguageHeader) ? acceptLanguageHeader.join(",") : acceptLanguageHeader;
1867
1905
  const explicitLang = url.searchParams.get("lang");
1868
- const { theme, locale } = await loadLocalizedViteDevTheme(
1906
+ const localized = await loadLocalizedViteDevTheme(
1869
1907
  root,
1870
1908
  server,
1871
1909
  explicitLang,
1872
1910
  acceptLanguage
1873
1911
  );
1874
1912
  const renderer = await loadThemeScopedRenderer(root);
1875
- if (slug === null) {
1876
- const homeSlug = pickStarterSlug(theme);
1877
- if (!homeSlug) {
1878
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1879
- response.end("No starter pages declared by this theme.");
1880
- return;
1881
- }
1882
- const starter2 = findStarterPage(theme, homeSlug);
1883
- if (!starter2) {
1884
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1885
- response.end(`Unknown starter page: ${homeSlug}`);
1886
- return;
1887
- }
1888
- const html2 = await server.transformIndexHtml(
1889
- url.pathname,
1890
- renderDevStarterPageHtml(
1891
- theme,
1892
- starter2,
1893
- renderer,
1894
- locale,
1895
- canonicalLocale(explicitLang) !== null
1896
- )
1897
- );
1898
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1899
- response.end(html2);
1900
- return;
1901
- }
1902
- const starter = findStarterPage(theme, slug);
1913
+ const starterSlug = slug ?? pickStarterSlug(localized.theme);
1914
+ const starter = starterSlug ? findStarterPage(localized.theme, starterSlug) : null;
1903
1915
  if (!starter) {
1904
1916
  response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1905
- response.end(`Unknown starter page: ${slug}`);
1917
+ response.end(
1918
+ starterSlug ? `Unknown starter page: ${starterSlug}` : "No starter pages declared by this theme."
1919
+ );
1906
1920
  return;
1907
1921
  }
1908
1922
  const html = await server.transformIndexHtml(
1909
1923
  url.pathname,
1910
1924
  renderDevStarterPageHtml(
1911
- theme,
1925
+ localized.theme,
1912
1926
  starter,
1913
1927
  renderer,
1914
- locale,
1915
- canonicalLocale(explicitLang) !== null
1928
+ localized.locale,
1929
+ canonicalLocale(explicitLang) !== null,
1930
+ localized.previewLocaleMessages
1916
1931
  )
1917
1932
  );
1918
1933
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -1940,13 +1955,13 @@ async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLangu
1940
1955
  if (!baseTheme.module.manifest.previewLocales) {
1941
1956
  return { theme: baseTheme, locale };
1942
1957
  }
1943
- const messages = await readDevPreviewLocaleMessages(root, locale);
1958
+ const previewLocaleMessages = await readDevPreviewLocaleMessages(root, locale);
1944
1959
  server.moduleGraph.invalidateAll();
1945
1960
  const theme = await runWithThemePreviewLocale(
1946
- messages,
1961
+ previewLocaleMessages,
1947
1962
  () => loadViteDevTheme(root, server)
1948
1963
  );
1949
- return { theme, locale };
1964
+ return { theme, locale, previewLocaleMessages };
1950
1965
  });
1951
1966
  }
1952
1967
  var vitePreviewLocaleLoadTails = /* @__PURE__ */ new WeakMap();
@@ -1998,6 +2013,9 @@ function resolveDevStarterSlug(pathname) {
1998
2013
  if (pathname === "/") {
1999
2014
  return null;
2000
2015
  }
2016
+ if (pathname.startsWith("/@")) {
2017
+ return void 0;
2018
+ }
2001
2019
  const normalized = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
2002
2020
  if (!normalized || normalized.includes("/")) {
2003
2021
  return void 0;
@@ -2104,7 +2122,7 @@ async function loadThemeScopedRenderer(themeRoot) {
2104
2122
  renderToString: reactDomServerModule.renderToString
2105
2123
  };
2106
2124
  }
2107
- function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false) {
2125
+ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false, previewLocaleMessages) {
2108
2126
  const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
2109
2127
  const metadata = {
2110
2128
  resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
@@ -2122,6 +2140,16 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2122
2140
  const customHead = chrome.customHeadCode ?? "";
2123
2141
  const customBody = chrome.customBodyCode ?? "";
2124
2142
  const designPalette = renderDevDesignPalette(theme.module.manifest.designSystem);
2143
+ const hydrationPayload = serializeInlineScriptValue({
2144
+ pageData: page.data,
2145
+ layoutData: theme.module.defaultLayout,
2146
+ contactForm: PREVIEW_CONTACT_FORM,
2147
+ previewLocaleMessages
2148
+ });
2149
+ const hydrationScript = [
2150
+ `<script id="suda-theme-hydration-data" type="application/json">${hydrationPayload}</script>`,
2151
+ '<script type="module">void import("/.suda-build/preview-client.ts").catch((error)=>console.error("[theme-dev] theme client failed to load",error))</script>'
2152
+ ].join("");
2125
2153
  return [
2126
2154
  "<!doctype html>",
2127
2155
  `<html lang="${escapeHtml(locale)}">`,
@@ -2134,9 +2162,10 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2134
2162
  customHead,
2135
2163
  "</head>",
2136
2164
  `<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
2137
- body,
2165
+ `<div data-theme-hydration-root="" style="display:contents">${body}</div>`,
2138
2166
  designPalette.html,
2139
2167
  customBody,
2168
+ hydrationScript,
2140
2169
  ...preserveLang ? [
2141
2170
  '<script>for(const a of document.querySelectorAll("a[href]")){try{const u=new URL(a.href,location.href);if(u.origin===location.origin){u.searchParams.set("lang",new URL(location.href).searchParams.get("lang"));a.href=u.pathname+u.search+u.hash}}catch{}}</script>'
2142
2171
  ] : [],
@@ -2155,19 +2184,28 @@ function renderDevDesignPalette(designSystem) {
2155
2184
  id: preset.id,
2156
2185
  variables: createThemeDesignCssVariables(preset.tokens)
2157
2186
  }));
2187
+ const selectedIcon = renderToStaticMarkup(
2188
+ createElement(CircleCheck, {
2189
+ className: "suda-dev-preset-selected-icon",
2190
+ width: 32,
2191
+ height: 32,
2192
+ "aria-hidden": true,
2193
+ focusable: false
2194
+ })
2195
+ );
2158
2196
  const buttons = designSystem.presets.map(
2159
2197
  (preset) => `<button type="button" data-suda-dev-preset="${escapeHtml(preset.id)}" title="${escapeHtml(
2160
2198
  preset.description ? `${preset.label} \u2014 ${preset.description}` : preset.label
2161
2199
  )}" aria-label="${escapeHtml(preset.label)}"><span style="background:${escapeHtml(
2162
2200
  preset.tokens.colors.primary
2163
- )}"></span></button>`
2201
+ )}"></span>${selectedIcon}</button>`
2164
2202
  ).join("");
2165
2203
  const config = serializeInlineScriptValue({
2166
2204
  defaultPresetId: designSystem.defaultPresetId,
2167
2205
  presets
2168
2206
  });
2169
2207
  return {
2170
- style: '<style data-suda-dev-palette-style>\n#suda-dev-palette{all:initial;position:fixed;z-index:2147483000;top:50%;right:16px;width:64px;max-height:calc(100svh - 32px);padding:8px 9px;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#fff;border:1px solid #d1d5db;border-radius:999px;box-shadow:0 8px 24px rgb(15 23 42 / 12%);transform:translateY(-50%);color-scheme:light;scrollbar-width:none;-ms-overflow-style:none}\n#suda-dev-palette::-webkit-scrollbar{display:none}\n#suda-dev-palette button{all:initial;position:relative;display:grid;width:44px;height:44px;box-sizing:border-box;cursor:pointer;border-radius:999px;place-items:center}\n#suda-dev-palette button span{display:block;width:32px;height:32px;border-radius:999px;transition:transform 120ms ease}\n#suda-dev-palette button:hover span{transform:scale(1.08)}\n#suda-dev-palette button[aria-pressed="true"] span{visibility:hidden}\n#suda-dev-palette button[aria-pressed="true"]::after{position:absolute;display:grid;width:32px;height:32px;color:var(--suda-dev-preset-primary);font:700 22px/1 ui-sans-serif,system-ui,sans-serif;content:"\u2713";place-items:center}\n#suda-dev-palette button:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n@media(max-width:640px){#suda-dev-palette{right:8px;max-height:calc(100svh - 16px)}}\n@media(prefers-reduced-motion:reduce){#suda-dev-palette button span{transition:none}}\n</style>',
2208
+ style: '<style data-suda-dev-palette-style>\n#suda-dev-palette{all:initial;position:fixed;z-index:2147483000;top:50%;right:16px;width:64px;max-height:calc(100svh - 32px);padding:8px 9px;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#fff;border:1px solid #d1d5db;border-radius:999px;box-shadow:0 8px 24px rgb(15 23 42 / 12%);transform:translateY(-50%);color-scheme:light;scrollbar-width:none;-ms-overflow-style:none}\n#suda-dev-palette::-webkit-scrollbar{display:none}\n#suda-dev-palette button{all:initial;position:relative;display:grid;width:44px;height:44px;box-sizing:border-box;cursor:pointer;border-radius:999px;place-items:center}\n#suda-dev-palette button span{display:block;width:32px;height:32px;border-radius:999px;transition:transform 120ms ease}\n#suda-dev-palette button:hover span{transform:scale(1.08)}\n#suda-dev-palette button[aria-pressed="true"] span{visibility:hidden}\n#suda-dev-palette .suda-dev-preset-selected-icon{position:absolute;display:block;width:32px;height:32px;color:var(--suda-dev-preset-primary);visibility:hidden;opacity:0;transform:scale(.85);transition:opacity 120ms ease,transform 120ms ease}\n#suda-dev-palette button[aria-pressed="true"] .suda-dev-preset-selected-icon{visibility:visible;opacity:1;transform:scale(1)}\n#suda-dev-palette button:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n@media(max-width:640px){#suda-dev-palette{right:8px;max-height:calc(100svh - 16px)}}\n@media(prefers-reduced-motion:reduce){#suda-dev-palette button span,#suda-dev-palette .suda-dev-preset-selected-icon{transition:none}}\n</style>',
2171
2209
  html: `<aside id="suda-dev-palette" aria-label="Design presets">${buttons}</aside><script data-suda-dev-palette-script>(function(){const config=${config};const palette=document.getElementById("suda-dev-palette");if(!palette)return;const byId=new Map(config.presets.map((preset)=>[preset.id,preset]));const params=new URL(location.href).searchParams;const requested=params.get("designPreset");const selected=byId.has(requested)?requested:config.defaultPresetId;function apply(id,updateUrl){const preset=byId.get(id);if(!preset)return;const properties=Object.keys(preset.variables);const targets=[document.documentElement,document.body,...document.querySelectorAll("[style]")].filter((element,index,items)=>items.indexOf(element)===index&&(element===document.documentElement||element===document.body||properties.some((property)=>element.style.getPropertyValue(property)!=="")));for(const target of targets){for(const [property,value] of Object.entries(preset.variables))target.style.setProperty(property,value)}for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){const active=button.dataset.sudaDevPreset===id;button.setAttribute("aria-pressed",String(active));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}if(updateUrl){const url=new URL(location.href);url.searchParams.set("designPreset",id);history.replaceState(history.state,"",url)}}if(requested&&byId.has(requested))apply(selected,false);else for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){button.setAttribute("aria-pressed",String(button.dataset.sudaDevPreset===selected));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>`
2172
2210
  };
2173
2211
  }