@sudajs/cli 0.18.9 → 0.18.11
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 +8 -2
- package/dist/index.js +259 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/theme/AGENTS.md +183 -1182
- package/templates/theme/docs/agent-guides/component-authoring.md +270 -0
- package/templates/theme/docs/agent-guides/design-and-runtime.md +170 -0
- package/templates/theme/docs/agent-guides/editor-compatibility.md +231 -0
- package/templates/theme/docs/agent-guides/templates-and-locales.md +206 -0
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,9 @@ declare const __testUtils: {
|
|
|
1816
1816
|
performUpdateThemeSettings: typeof performUpdateThemeSettings;
|
|
1817
1817
|
getRemoteThemeSettings: typeof getRemoteThemeSettings;
|
|
1818
1818
|
renderDevStarterPageHtml: typeof renderDevStarterPageHtml;
|
|
1819
|
+
renderDevEditorPageHtml: typeof renderDevEditorPageHtml;
|
|
1820
|
+
writeVitePreviewEntries: typeof writeVitePreviewEntries;
|
|
1821
|
+
resolveDevEditorSlug: typeof resolveDevEditorSlug;
|
|
1819
1822
|
resolveDevStarterSlug: typeof resolveDevStarterSlug;
|
|
1820
1823
|
resolveScreenshotOptions: typeof resolveScreenshotOptions;
|
|
1821
1824
|
slugifyThemeName: typeof slugifyThemeName;
|
|
@@ -1831,7 +1834,9 @@ declare const __testUtils: {
|
|
|
1831
1834
|
updateRemotePageDraft: typeof updateRemotePageDraft;
|
|
1832
1835
|
};
|
|
1833
1836
|
declare function watchTheme(root: string, port: number): Promise<void>;
|
|
1837
|
+
declare function writeVitePreviewEntries(root: string): Promise<void>;
|
|
1834
1838
|
declare function withSerializedVitePreviewLocaleLoad<Result>(server: object, load: () => Promise<Result>): Promise<Result>;
|
|
1839
|
+
declare function resolveDevEditorSlug(pathname: string): string | null | undefined;
|
|
1835
1840
|
declare function resolveDevStarterSlug(pathname: string): string | null | undefined;
|
|
1836
1841
|
declare function resolveThemePreviewLocale(params: {
|
|
1837
1842
|
previewLocales: string[] | undefined;
|
|
@@ -1856,7 +1861,8 @@ interface ThemeRenderer {
|
|
|
1856
1861
|
renderToString: typeof renderToString;
|
|
1857
1862
|
}
|
|
1858
1863
|
declare function loadThemeScopedRenderer(themeRoot: string): Promise<ThemeRenderer>;
|
|
1859
|
-
declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean): string;
|
|
1864
|
+
declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean, previewLocaleMessages?: ThemePreviewLocaleMessages, searchParams?: URLSearchParams): string;
|
|
1865
|
+
declare function renderDevEditorPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, searchParams?: URLSearchParams, previewLocaleMessages?: ThemePreviewLocaleMessages): string;
|
|
1860
1866
|
interface ScreenshotOptions extends ThemeRootOptions {
|
|
1861
1867
|
device?: string[] | string;
|
|
1862
1868
|
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,
|
|
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, Pencil } 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,9 @@ var __testUtils = {
|
|
|
1754
1738
|
performUpdateThemeSettings,
|
|
1755
1739
|
getRemoteThemeSettings,
|
|
1756
1740
|
renderDevStarterPageHtml,
|
|
1741
|
+
renderDevEditorPageHtml,
|
|
1742
|
+
writeVitePreviewEntries,
|
|
1743
|
+
resolveDevEditorSlug,
|
|
1757
1744
|
resolveDevStarterSlug,
|
|
1758
1745
|
resolveScreenshotOptions,
|
|
1759
1746
|
slugifyThemeName,
|
|
@@ -1825,13 +1812,17 @@ async function watchTheme(root, port) {
|
|
|
1825
1812
|
async function startViteDevPreviewServer(root, port) {
|
|
1826
1813
|
const viteConfig = await findViteConfig(root);
|
|
1827
1814
|
const vite = await loadThemeVite(root);
|
|
1815
|
+
await writeVitePreviewEntries(root);
|
|
1828
1816
|
const host = "127.0.0.1";
|
|
1829
1817
|
const server = await vite.createServer({
|
|
1830
1818
|
root,
|
|
1831
1819
|
configFile: viteConfig,
|
|
1832
1820
|
configLoader: "runner",
|
|
1833
|
-
cacheDir: path2.join(root, ".suda-build", "vite
|
|
1821
|
+
cacheDir: path2.join(root, ".suda-build", "node_modules", ".vite"),
|
|
1834
1822
|
appType: "custom",
|
|
1823
|
+
optimizeDeps: {
|
|
1824
|
+
entries: [".suda-build/preview-client.ts", ".suda-build/editor-client.ts"]
|
|
1825
|
+
},
|
|
1835
1826
|
server: { host, port },
|
|
1836
1827
|
plugins: [createSudaPreviewVitePlugin(root)]
|
|
1837
1828
|
});
|
|
@@ -1843,6 +1834,77 @@ async function startViteDevPreviewServer(root, port) {
|
|
|
1843
1834
|
close: () => server.close()
|
|
1844
1835
|
};
|
|
1845
1836
|
}
|
|
1837
|
+
async function writeVitePreviewEntries(root) {
|
|
1838
|
+
const clientHooksPath = path2.join(root, "src", "client.ts");
|
|
1839
|
+
const clientHooksImport = existsSync(clientHooksPath) ? 'import { clientHooks } from "../src/client.js";' : "const clientHooks = {};";
|
|
1840
|
+
const buildDir = path2.join(root, ".suda-build");
|
|
1841
|
+
await mkdir(buildDir, { recursive: true });
|
|
1842
|
+
await writeFile(
|
|
1843
|
+
path2.join(buildDir, "runtime.dev.ts"),
|
|
1844
|
+
createClientRuntimeSource(clientHooksImport, false),
|
|
1845
|
+
"utf8"
|
|
1846
|
+
);
|
|
1847
|
+
await writeFile(
|
|
1848
|
+
path2.join(buildDir, "preview-client.ts"),
|
|
1849
|
+
[
|
|
1850
|
+
'import { hydrateThemePage, loadThemeRuntime } from "@sudajs/theme-engine/client";',
|
|
1851
|
+
'import { installThemePreviewLocale, resolveAssetPath } from "@sudajs/theme-engine/site-runtime";',
|
|
1852
|
+
'const payloadElement=document.querySelector("#suda-theme-hydration-data");',
|
|
1853
|
+
'if(!payloadElement){throw new Error("Missing Suda theme hydration payload.")}',
|
|
1854
|
+
"const payload=JSON.parse(payloadElement.textContent ?? '{}');",
|
|
1855
|
+
"if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
|
|
1856
|
+
'const runtime=(await loadThemeRuntime("/.suda-build/runtime.dev.ts")).default;',
|
|
1857
|
+
'const metadata={contactForm:payload.contactForm,resolveAssetUrl:(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined})};',
|
|
1858
|
+
'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)});',
|
|
1859
|
+
""
|
|
1860
|
+
].join("\n"),
|
|
1861
|
+
"utf8"
|
|
1862
|
+
);
|
|
1863
|
+
await writeFile(
|
|
1864
|
+
path2.join(buildDir, "editor-client.ts"),
|
|
1865
|
+
[
|
|
1866
|
+
'import "@puckeditor/core/puck.css";',
|
|
1867
|
+
'import { composeUnifiedThemeEditorData, createUnifiedThemeEditorConfig, installThemePreviewLocale, resolveAssetPath, ThemeEditor } from "@sudajs/theme-engine";',
|
|
1868
|
+
'import { createElement } from "react";',
|
|
1869
|
+
'import { createRoot } from "react-dom/client";',
|
|
1870
|
+
'import runtime from "./runtime.dev";',
|
|
1871
|
+
'const payloadElement=document.querySelector("#suda-theme-editor-data");',
|
|
1872
|
+
'if(!payloadElement){throw new Error("Missing Suda theme editor payload.")}',
|
|
1873
|
+
"const payload=JSON.parse(payloadElement.textContent ?? '{}');",
|
|
1874
|
+
"if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
|
|
1875
|
+
'const resolveAssetUrl=(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined});',
|
|
1876
|
+
'const config=createUnifiedThemeEditorConfig(runtime.theme,undefined,{labels:{pageContent:"Page content",themeSettings:"Theme settings"}});',
|
|
1877
|
+
"const data=composeUnifiedThemeEditorData(payload.layoutData,payload.pageData,{layoutConfig:runtime.theme.layoutConfig,pageConfig:runtime.theme.pageConfig});",
|
|
1878
|
+
'const editorContext={assets:[],resolveAssetUrl,currentPageId:payload.currentPageId,backHref:payload.backHref,editorPages:payload.editorPages,editorLocale:payload.locale,chromeMessages:{backToPages:"Back to preview",pageMenuLabel:"Switch starter page",themeSettings:"Theme settings",outline:"Outline",blocks:"Blocks",switchViewport:"Switch viewport"}};',
|
|
1879
|
+
"const metadata={contactForm:payload.contactForm,isEditor:true,resolveAssetUrl};",
|
|
1880
|
+
'createRoot(document.querySelector("#suda-theme-dev-editor-root")!).render(createElement(ThemeEditor,{config,data,editorContext,metadata,iframe:false,height:"100%",publishActionVisible:false}));',
|
|
1881
|
+
""
|
|
1882
|
+
].join("\n"),
|
|
1883
|
+
"utf8"
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
function createClientRuntimeSource(clientHooksImport, useClientDirective) {
|
|
1887
|
+
return [
|
|
1888
|
+
...useClientDirective ? ['"use client";'] : [],
|
|
1889
|
+
'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
|
|
1890
|
+
'import * as themeConfig from "../src/config.js";',
|
|
1891
|
+
clientHooksImport,
|
|
1892
|
+
"const theme: ThemeRenderModule = {",
|
|
1893
|
+
" pageConfig: themeConfig.pageConfig,",
|
|
1894
|
+
" layoutConfig: themeConfig.layoutConfig,",
|
|
1895
|
+
"};",
|
|
1896
|
+
'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
|
|
1897
|
+
" theme.rendering = themeConfig.rendering;",
|
|
1898
|
+
"}",
|
|
1899
|
+
"const hooks: ThemeClientHooks = clientHooks;",
|
|
1900
|
+
"const runtime: ThemeClientRuntime = { theme };",
|
|
1901
|
+
"if (hooks.initialize !== undefined) {",
|
|
1902
|
+
" runtime.initialize = hooks.initialize;",
|
|
1903
|
+
"}",
|
|
1904
|
+
"export default runtime;",
|
|
1905
|
+
""
|
|
1906
|
+
].join("\n");
|
|
1907
|
+
}
|
|
1846
1908
|
function createSudaPreviewVitePlugin(root) {
|
|
1847
1909
|
return {
|
|
1848
1910
|
name: "suda-theme-preview",
|
|
@@ -1856,65 +1918,51 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
1856
1918
|
next();
|
|
1857
1919
|
return;
|
|
1858
1920
|
}
|
|
1859
|
-
const
|
|
1860
|
-
|
|
1921
|
+
const editorSlug = resolveDevEditorSlug(url.pathname);
|
|
1922
|
+
const previewSlug = resolveDevStarterSlug(url.pathname);
|
|
1923
|
+
if (editorSlug === void 0 && previewSlug === void 0) {
|
|
1861
1924
|
next();
|
|
1862
1925
|
return;
|
|
1863
1926
|
}
|
|
1927
|
+
const isEditor = editorSlug !== void 0;
|
|
1928
|
+
const slug = isEditor ? editorSlug : previewSlug;
|
|
1864
1929
|
try {
|
|
1865
1930
|
const acceptLanguageHeader = request.headers["accept-language"];
|
|
1866
1931
|
const acceptLanguage = Array.isArray(acceptLanguageHeader) ? acceptLanguageHeader.join(",") : acceptLanguageHeader;
|
|
1867
1932
|
const explicitLang = url.searchParams.get("lang");
|
|
1868
|
-
const
|
|
1933
|
+
const localized = await loadLocalizedViteDevTheme(
|
|
1869
1934
|
root,
|
|
1870
1935
|
server,
|
|
1871
1936
|
explicitLang,
|
|
1872
1937
|
acceptLanguage
|
|
1873
1938
|
);
|
|
1874
1939
|
const renderer = await loadThemeScopedRenderer(root);
|
|
1875
|
-
|
|
1876
|
-
|
|
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);
|
|
1940
|
+
const starterSlug = slug ?? pickStarterSlug(localized.theme);
|
|
1941
|
+
const starter = starterSlug ? findStarterPage(localized.theme, starterSlug) : null;
|
|
1903
1942
|
if (!starter) {
|
|
1904
1943
|
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1905
|
-
response.end(
|
|
1944
|
+
response.end(
|
|
1945
|
+
starterSlug ? `Unknown starter page: ${starterSlug}` : "No starter pages declared by this theme."
|
|
1946
|
+
);
|
|
1906
1947
|
return;
|
|
1907
1948
|
}
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1949
|
+
const htmlSource = isEditor ? renderDevEditorPageHtml(
|
|
1950
|
+
localized.theme,
|
|
1951
|
+
starter,
|
|
1952
|
+
renderer,
|
|
1953
|
+
localized.locale,
|
|
1954
|
+
url.searchParams,
|
|
1955
|
+
localized.previewLocaleMessages
|
|
1956
|
+
) : renderDevStarterPageHtml(
|
|
1957
|
+
localized.theme,
|
|
1958
|
+
starter,
|
|
1959
|
+
renderer,
|
|
1960
|
+
localized.locale,
|
|
1961
|
+
canonicalLocale(explicitLang) !== null,
|
|
1962
|
+
localized.previewLocaleMessages,
|
|
1963
|
+
url.searchParams
|
|
1917
1964
|
);
|
|
1965
|
+
const html = await server.transformIndexHtml(url.pathname, htmlSource);
|
|
1918
1966
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1919
1967
|
response.end(html);
|
|
1920
1968
|
} catch (error2) {
|
|
@@ -1940,13 +1988,13 @@ async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLangu
|
|
|
1940
1988
|
if (!baseTheme.module.manifest.previewLocales) {
|
|
1941
1989
|
return { theme: baseTheme, locale };
|
|
1942
1990
|
}
|
|
1943
|
-
const
|
|
1991
|
+
const previewLocaleMessages = await readDevPreviewLocaleMessages(root, locale);
|
|
1944
1992
|
server.moduleGraph.invalidateAll();
|
|
1945
1993
|
const theme = await runWithThemePreviewLocale(
|
|
1946
|
-
|
|
1994
|
+
previewLocaleMessages,
|
|
1947
1995
|
() => loadViteDevTheme(root, server)
|
|
1948
1996
|
);
|
|
1949
|
-
return { theme, locale };
|
|
1997
|
+
return { theme, locale, previewLocaleMessages };
|
|
1950
1998
|
});
|
|
1951
1999
|
}
|
|
1952
2000
|
var vitePreviewLocaleLoadTails = /* @__PURE__ */ new WeakMap();
|
|
@@ -1994,10 +2042,27 @@ function pickStarterSlug(theme) {
|
|
|
1994
2042
|
function findStarterPage(theme, slug) {
|
|
1995
2043
|
return theme.module.starterPages.find((page) => page.slug === slug) ?? null;
|
|
1996
2044
|
}
|
|
2045
|
+
var DEV_EDITOR_PATH = "/__suda/editor";
|
|
2046
|
+
function resolveDevEditorSlug(pathname) {
|
|
2047
|
+
if (pathname === DEV_EDITOR_PATH || pathname === `${DEV_EDITOR_PATH}/`) {
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
if (!pathname.startsWith(`${DEV_EDITOR_PATH}/`)) {
|
|
2051
|
+
return void 0;
|
|
2052
|
+
}
|
|
2053
|
+
const normalized = pathname.slice(DEV_EDITOR_PATH.length + 1).replace(/\/+$/, "");
|
|
2054
|
+
if (!normalized || normalized.includes("/")) {
|
|
2055
|
+
return void 0;
|
|
2056
|
+
}
|
|
2057
|
+
return decodeURIComponent(normalized);
|
|
2058
|
+
}
|
|
1997
2059
|
function resolveDevStarterSlug(pathname) {
|
|
1998
2060
|
if (pathname === "/") {
|
|
1999
2061
|
return null;
|
|
2000
2062
|
}
|
|
2063
|
+
if (pathname.startsWith("/@")) {
|
|
2064
|
+
return void 0;
|
|
2065
|
+
}
|
|
2001
2066
|
const normalized = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2002
2067
|
if (!normalized || normalized.includes("/")) {
|
|
2003
2068
|
return void 0;
|
|
@@ -2104,8 +2169,9 @@ async function loadThemeScopedRenderer(themeRoot) {
|
|
|
2104
2169
|
renderToString: reactDomServerModule.renderToString
|
|
2105
2170
|
};
|
|
2106
2171
|
}
|
|
2107
|
-
function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false) {
|
|
2108
|
-
const
|
|
2172
|
+
function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false, previewLocaleMessages, searchParams = new URLSearchParams()) {
|
|
2173
|
+
const layoutData = createDevPreviewLayout(theme, searchParams);
|
|
2174
|
+
const chrome = renderer.extractLayoutChrome(layoutData);
|
|
2109
2175
|
const metadata = {
|
|
2110
2176
|
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
|
|
2111
2177
|
contactForm: PREVIEW_CONTACT_FORM
|
|
@@ -2114,14 +2180,27 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
|
|
|
2114
2180
|
renderer.createElement(renderer.ThemeRender, {
|
|
2115
2181
|
theme: theme.module,
|
|
2116
2182
|
pageData: page.data,
|
|
2117
|
-
layoutData
|
|
2183
|
+
layoutData,
|
|
2118
2184
|
metadata
|
|
2119
2185
|
})
|
|
2120
2186
|
);
|
|
2121
2187
|
const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
2122
2188
|
const customHead = chrome.customHeadCode ?? "";
|
|
2123
2189
|
const customBody = chrome.customBodyCode ?? "";
|
|
2124
|
-
const designPalette = renderDevDesignPalette(
|
|
2190
|
+
const designPalette = renderDevDesignPalette(
|
|
2191
|
+
theme.module.manifest.designSystem,
|
|
2192
|
+
createDevEditorHref(page.slug, searchParams)
|
|
2193
|
+
);
|
|
2194
|
+
const hydrationPayload = serializeInlineScriptValue({
|
|
2195
|
+
pageData: page.data,
|
|
2196
|
+
layoutData,
|
|
2197
|
+
contactForm: PREVIEW_CONTACT_FORM,
|
|
2198
|
+
previewLocaleMessages
|
|
2199
|
+
});
|
|
2200
|
+
const hydrationScript = [
|
|
2201
|
+
`<script id="suda-theme-hydration-data" type="application/json">${hydrationPayload}</script>`,
|
|
2202
|
+
'<script type="module">void import("/.suda-build/preview-client.ts").catch((error)=>console.error("[theme-dev] theme client failed to load",error))</script>'
|
|
2203
|
+
].join("");
|
|
2125
2204
|
return [
|
|
2126
2205
|
"<!doctype html>",
|
|
2127
2206
|
`<html lang="${escapeHtml(locale)}">`,
|
|
@@ -2134,9 +2213,10 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
|
|
|
2134
2213
|
customHead,
|
|
2135
2214
|
"</head>",
|
|
2136
2215
|
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
2137
|
-
body
|
|
2216
|
+
`<div data-theme-hydration-root="" style="display:contents">${body}</div>`,
|
|
2138
2217
|
designPalette.html,
|
|
2139
2218
|
customBody,
|
|
2219
|
+
hydrationScript,
|
|
2140
2220
|
...preserveLang ? [
|
|
2141
2221
|
'<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
2222
|
] : [],
|
|
@@ -2144,31 +2224,130 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
|
|
|
2144
2224
|
"</html>"
|
|
2145
2225
|
].join("");
|
|
2146
2226
|
}
|
|
2227
|
+
function copyDevPreviewSearchParams(searchParams) {
|
|
2228
|
+
const copied = new URLSearchParams();
|
|
2229
|
+
for (const key of ["lang", "designPreset"]) {
|
|
2230
|
+
const value = searchParams.get(key);
|
|
2231
|
+
if (value) {
|
|
2232
|
+
copied.set(key, value);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return copied;
|
|
2236
|
+
}
|
|
2237
|
+
function createDevEditorHref(slug, searchParams) {
|
|
2238
|
+
const query = copyDevPreviewSearchParams(searchParams).toString();
|
|
2239
|
+
return `${DEV_EDITOR_PATH}/${encodeURIComponent(slug)}${query ? `?${query}` : ""}`;
|
|
2240
|
+
}
|
|
2241
|
+
function createDevPreviewHref(page, searchParams) {
|
|
2242
|
+
const pathname = page.isHome ? "/" : `/${encodeURIComponent(page.slug)}`;
|
|
2243
|
+
const query = copyDevPreviewSearchParams(searchParams).toString();
|
|
2244
|
+
return `${pathname}${query ? `?${query}` : ""}`;
|
|
2245
|
+
}
|
|
2246
|
+
function createDevPreviewLayout(theme, searchParams) {
|
|
2247
|
+
const requestedPresetId = searchParams.get("designPreset");
|
|
2248
|
+
const requestedPreset = requestedPresetId ? theme.module.manifest.designSystem.presets.find(
|
|
2249
|
+
(preset) => preset.id === requestedPresetId
|
|
2250
|
+
) : void 0;
|
|
2251
|
+
if (!requestedPreset) {
|
|
2252
|
+
return theme.module.defaultLayout;
|
|
2253
|
+
}
|
|
2254
|
+
const rootFields = theme.module.layoutConfig.root?.fields ?? {};
|
|
2255
|
+
const designFieldName = Object.entries(rootFields).find(
|
|
2256
|
+
([, field]) => field.metadata?.sudaField === "designSystem"
|
|
2257
|
+
)?.[0];
|
|
2258
|
+
if (!designFieldName) {
|
|
2259
|
+
return theme.module.defaultLayout;
|
|
2260
|
+
}
|
|
2261
|
+
const layoutData = structuredClone(theme.module.defaultLayout);
|
|
2262
|
+
layoutData.root = {
|
|
2263
|
+
...layoutData.root ?? {},
|
|
2264
|
+
props: {
|
|
2265
|
+
...layoutData.root?.props ?? {},
|
|
2266
|
+
[designFieldName]: { presetId: requestedPreset.id }
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
return layoutData;
|
|
2270
|
+
}
|
|
2271
|
+
function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchParams = new URLSearchParams(), previewLocaleMessages) {
|
|
2272
|
+
const layoutData = createDevPreviewLayout(theme, searchParams);
|
|
2273
|
+
const chrome = renderer.extractLayoutChrome(layoutData);
|
|
2274
|
+
const cssVariables = { ...chrome.cssVariables };
|
|
2275
|
+
const cssVarStyle = Object.entries(cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
2276
|
+
const editorPages = theme.module.starterPages.map((starterPage) => ({
|
|
2277
|
+
id: starterPage.slug,
|
|
2278
|
+
handle: starterPage.slug,
|
|
2279
|
+
label: starterPage.title,
|
|
2280
|
+
group: starterPage.isHome ? "home" : "static",
|
|
2281
|
+
editorHref: createDevEditorHref(starterPage.slug, searchParams)
|
|
2282
|
+
}));
|
|
2283
|
+
const payload = serializeInlineScriptValue({
|
|
2284
|
+
pageData: page.data,
|
|
2285
|
+
layoutData,
|
|
2286
|
+
contactForm: PREVIEW_CONTACT_FORM,
|
|
2287
|
+
previewLocaleMessages,
|
|
2288
|
+
locale,
|
|
2289
|
+
currentPageId: page.slug,
|
|
2290
|
+
backHref: createDevPreviewHref(page, searchParams),
|
|
2291
|
+
editorPages
|
|
2292
|
+
});
|
|
2293
|
+
return [
|
|
2294
|
+
"<!doctype html>",
|
|
2295
|
+
`<html lang="${escapeHtml(locale)}">`,
|
|
2296
|
+
"<head>",
|
|
2297
|
+
'<meta charset="utf-8" />',
|
|
2298
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
2299
|
+
`<title>${escapeHtml(`Edit ${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
|
|
2300
|
+
'<link rel="stylesheet" href="/src/styles.css" />',
|
|
2301
|
+
"<style>html,body,#suda-theme-dev-editor-root{height:100%;margin:0}body{overflow:hidden}</style>",
|
|
2302
|
+
"</head>",
|
|
2303
|
+
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
2304
|
+
'<div id="suda-theme-dev-editor-root"></div>',
|
|
2305
|
+
`<script id="suda-theme-editor-data" type="application/json">${payload}</script>`,
|
|
2306
|
+
'<script type="module">void import("/.suda-build/editor-client.ts").catch((error)=>console.error("[theme-dev] theme editor failed to load",error))</script>',
|
|
2307
|
+
"</body>",
|
|
2308
|
+
"</html>"
|
|
2309
|
+
].join("");
|
|
2310
|
+
}
|
|
2147
2311
|
function serializeInlineScriptValue(value) {
|
|
2148
2312
|
return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
2149
2313
|
}
|
|
2150
|
-
function renderDevDesignPalette(designSystem) {
|
|
2151
|
-
|
|
2152
|
-
return { style: "", html: "" };
|
|
2153
|
-
}
|
|
2314
|
+
function renderDevDesignPalette(designSystem, editorHref) {
|
|
2315
|
+
const hasPresetSwitcher = designSystem.presets.length >= 2;
|
|
2154
2316
|
const presets = designSystem.presets.map((preset) => ({
|
|
2155
2317
|
id: preset.id,
|
|
2156
2318
|
variables: createThemeDesignCssVariables(preset.tokens)
|
|
2157
2319
|
}));
|
|
2158
|
-
const
|
|
2320
|
+
const selectedIcon = renderToStaticMarkup(
|
|
2321
|
+
createElement(CircleCheck, {
|
|
2322
|
+
className: "suda-dev-preset-selected-icon",
|
|
2323
|
+
width: 32,
|
|
2324
|
+
height: 32,
|
|
2325
|
+
"aria-hidden": true,
|
|
2326
|
+
focusable: false
|
|
2327
|
+
})
|
|
2328
|
+
);
|
|
2329
|
+
const editIcon = renderToStaticMarkup(
|
|
2330
|
+
createElement(Pencil, {
|
|
2331
|
+
width: 20,
|
|
2332
|
+
height: 20,
|
|
2333
|
+
"aria-hidden": true,
|
|
2334
|
+
focusable: false
|
|
2335
|
+
})
|
|
2336
|
+
);
|
|
2337
|
+
const buttons = hasPresetSwitcher ? designSystem.presets.map(
|
|
2159
2338
|
(preset) => `<button type="button" data-suda-dev-preset="${escapeHtml(preset.id)}" title="${escapeHtml(
|
|
2160
2339
|
preset.description ? `${preset.label} \u2014 ${preset.description}` : preset.label
|
|
2161
2340
|
)}" aria-label="${escapeHtml(preset.label)}"><span style="background:${escapeHtml(
|
|
2162
2341
|
preset.tokens.colors.primary
|
|
2163
|
-
)}"></span
|
|
2164
|
-
).join("");
|
|
2342
|
+
)}"></span>${selectedIcon}</button>`
|
|
2343
|
+
).join("") : "";
|
|
2165
2344
|
const config = serializeInlineScriptValue({
|
|
2166
2345
|
defaultPresetId: designSystem.defaultPresetId,
|
|
2167
2346
|
presets
|
|
2168
2347
|
});
|
|
2169
2348
|
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
|
|
2171
|
-
html: `<aside id="suda-dev-palette" aria-label="
|
|
2349
|
+
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#suda-dev-palette .suda-dev-edit-divider{display:block;width:28px;height:1px;margin:4px auto;background:#e2e8f0}\n#suda-dev-palette .suda-dev-edit-link{all:initial;display:grid;width:44px;height:44px;box-sizing:border-box;color:#334155;cursor:pointer;border-radius:999px;place-items:center;transition:background-color 120ms ease,color 120ms ease}\n#suda-dev-palette .suda-dev-edit-link:hover{color:#0f172a;background:#f1f5f9}\n#suda-dev-palette .suda-dev-edit-link: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,#suda-dev-palette .suda-dev-edit-link{transition:none}}\n</style>',
|
|
2350
|
+
html: `<aside id="suda-dev-palette" aria-label="Theme development tools">${buttons}${hasPresetSwitcher ? '<span class="suda-dev-edit-divider" aria-hidden="true"></span>' : ""}<a class="suda-dev-edit-link" data-suda-dev-editor-link="" href="${escapeHtml(editorHref)}" title="Edit starter page" aria-label="Edit starter page">${editIcon}</a></aside>` + (hasPresetSwitcher ? `<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"])}const editorLink=palette.querySelector("[data-suda-dev-editor-link]");if(editorLink){const editorUrl=new URL(editorLink.href,location.href);editorUrl.searchParams.set("designPreset",id);editorLink.href=editorUrl.pathname+editorUrl.search+editorUrl.hash}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
2351
|
};
|
|
2173
2352
|
}
|
|
2174
2353
|
var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];
|