@sudajs/cli 0.5.5 → 0.6.0
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 +18 -1
- package/dist/index.js +397 -838
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/templates/theme/AGENTS.md +30 -0
- package/templates/theme/CLAUDE.md +3 -0
- package/templates/theme/README.md +28 -0
- package/templates/theme/package.json +38 -0
- package/templates/theme/pnpm-workspace.yaml +8 -0
- package/templates/theme/postcss.config.mjs +7 -0
- package/templates/theme/src/config.ts +22 -0
- package/templates/theme/src/index.tsx +23 -0
- package/templates/theme/src/layout.tsx +76 -0
- package/templates/theme/src/manifest.ts +18 -0
- package/templates/theme/src/runtime.client.ts +5 -0
- package/templates/theme/src/sections.tsx +161 -0
- package/templates/theme/src/styles.css +102 -0
- package/templates/theme/src/templates.ts +107 -0
- package/templates/theme/src/theme-asset.ts +7 -0
- package/templates/theme/tsconfig.json +22 -0
- package/templates/theme/vite.config.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import fs, { mkdir,
|
|
3
|
+
import fs, { copyFile, mkdir, readdir, writeFile, stat, rm, readFile } from 'fs/promises';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import path2 from 'path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
-
import { createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult,
|
|
9
|
+
import { createThemeAgentManifest, createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
10
10
|
import { themeManifestSchema, FileSystemThemeRegistry } from '@sudajs/theme-engine/server';
|
|
11
11
|
import { Command } from 'commander';
|
|
12
|
-
import { build
|
|
12
|
+
import { build } from 'esbuild';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import os from 'os';
|
|
15
15
|
|
|
@@ -213,6 +213,8 @@ var createProjectOutputSchema = z.object({
|
|
|
213
213
|
defaultDomain: z.string().nullable(),
|
|
214
214
|
dashboardUrl: z.string()
|
|
215
215
|
});
|
|
216
|
+
var packageRoot = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..");
|
|
217
|
+
var themeTemplateRoot = path2.join(packageRoot, "templates", "theme");
|
|
216
218
|
function createHostReactShimPlugin() {
|
|
217
219
|
return {
|
|
218
220
|
name: "suda-host-react-shim",
|
|
@@ -472,29 +474,34 @@ async function validateTheme(root) {
|
|
|
472
474
|
}
|
|
473
475
|
return result;
|
|
474
476
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const packageJson = await readJson(
|
|
491
|
-
path2.join(root, "package.json")
|
|
477
|
+
var VITE_CONFIG_FILES = [
|
|
478
|
+
"vite.config.ts",
|
|
479
|
+
"vite.config.mts",
|
|
480
|
+
"vite.config.js",
|
|
481
|
+
"vite.config.mjs"
|
|
482
|
+
];
|
|
483
|
+
async function findViteConfig(root) {
|
|
484
|
+
for (const file of VITE_CONFIG_FILES) {
|
|
485
|
+
const candidate = path2.join(root, file);
|
|
486
|
+
if (await pathExists(candidate)) {
|
|
487
|
+
return candidate;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Missing vite.config.* in ${root}. Suda theme projects must use the Vite template; run \`suda theme init\` or migrate this theme to Vite.`
|
|
492
492
|
);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
}
|
|
494
|
+
async function loadThemeVite(root) {
|
|
495
|
+
const themeRequire = createRequire(path2.join(root, "package.json"));
|
|
496
|
+
let viteEntry;
|
|
497
|
+
try {
|
|
498
|
+
viteEntry = themeRequire.resolve("vite");
|
|
499
|
+
} catch {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Vite is required for Suda theme projects. Install it in ${root}: \`pnpm add -D vite @vitejs/plugin-react tailwindcss @tailwindcss/postcss postcss\`.`
|
|
502
|
+
);
|
|
496
503
|
}
|
|
497
|
-
await
|
|
504
|
+
return await import(pathToFileURL(viteEntry).href);
|
|
498
505
|
}
|
|
499
506
|
async function findClientEntry(root) {
|
|
500
507
|
const candidates = [
|
|
@@ -517,14 +524,74 @@ async function findServerEntry(root) {
|
|
|
517
524
|
}
|
|
518
525
|
throw new Error(`Missing server entry. Add src/index.ts(x) in ${root}.`);
|
|
519
526
|
}
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
function isThemeExternal(id) {
|
|
528
|
+
return id === "react" || id.startsWith("react/") || id === "react-dom" || id.startsWith("react-dom/") || id === "@puckeditor/core" || id.startsWith("@puckeditor/core/") || id === "@sudajs/theme-engine" || id.startsWith("@sudajs/theme-engine/");
|
|
529
|
+
}
|
|
530
|
+
async function buildViteTheme(root) {
|
|
531
|
+
const viteConfig = await findViteConfig(root);
|
|
532
|
+
const vite = await loadThemeVite(root);
|
|
533
|
+
const serverEntry = await findServerEntry(root);
|
|
534
|
+
const stylesEntry = path2.join(root, "src", "styles.css");
|
|
535
|
+
if (!await pathExists(stylesEntry)) {
|
|
536
|
+
throw new Error(`Missing ${stylesEntry}. Vite Suda themes must define src/styles.css.`);
|
|
537
|
+
}
|
|
538
|
+
await rm(path2.join(root, "dist"), { recursive: true, force: true });
|
|
539
|
+
await rm(path2.join(root, ".suda-build"), { recursive: true, force: true });
|
|
540
|
+
await vite.build({
|
|
541
|
+
root,
|
|
542
|
+
configFile: viteConfig,
|
|
543
|
+
configLoader: "runner",
|
|
544
|
+
cacheDir: path2.join(root, ".suda-build", "vite-cache"),
|
|
545
|
+
publicDir: false,
|
|
546
|
+
build: {
|
|
547
|
+
emptyOutDir: false,
|
|
548
|
+
outDir: path2.join(root, "dist"),
|
|
549
|
+
sourcemap: false,
|
|
550
|
+
ssr: serverEntry,
|
|
551
|
+
rollupOptions: {
|
|
552
|
+
external: isThemeExternal,
|
|
553
|
+
output: {
|
|
554
|
+
entryFileNames: "index.js",
|
|
555
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
556
|
+
assetFileNames: "assets/[name]-[hash][extname]"
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
await vite.build({
|
|
562
|
+
root,
|
|
563
|
+
configFile: viteConfig,
|
|
564
|
+
configLoader: "runner",
|
|
565
|
+
cacheDir: path2.join(root, ".suda-build", "vite-cache"),
|
|
566
|
+
publicDir: false,
|
|
567
|
+
build: {
|
|
568
|
+
emptyOutDir: false,
|
|
569
|
+
outDir: path2.join(root, ".suda-build", "styles"),
|
|
570
|
+
sourcemap: false,
|
|
571
|
+
rollupOptions: {
|
|
572
|
+
input: stylesEntry,
|
|
573
|
+
output: {
|
|
574
|
+
entryFileNames: "style-entry.js",
|
|
575
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
576
|
+
assetFileNames: "assets/[name]-[hash][extname]"
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async function buildClientRuntime(root, minify) {
|
|
583
|
+
const entryPoint = await findClientEntry(root);
|
|
584
|
+
if (!await pathExists(entryPoint)) {
|
|
585
|
+
throw new Error(`Missing client entry: ${entryPoint}`);
|
|
586
|
+
}
|
|
587
|
+
await mkdir(path2.join(root, "dist"), { recursive: true });
|
|
588
|
+
const hostReactShimPlugin = createHostReactShimPlugin();
|
|
522
589
|
await build({
|
|
523
590
|
bundle: true,
|
|
524
591
|
entryPoints: [entryPoint],
|
|
525
592
|
external: [
|
|
526
593
|
"react",
|
|
527
|
-
"react
|
|
594
|
+
"react/*",
|
|
528
595
|
"react-dom",
|
|
529
596
|
"react-dom/*",
|
|
530
597
|
"@puckeditor/core",
|
|
@@ -534,31 +601,7 @@ async function buildServerBundle(root) {
|
|
|
534
601
|
],
|
|
535
602
|
format: "esm",
|
|
536
603
|
jsx: "automatic",
|
|
537
|
-
minify
|
|
538
|
-
outfile: path2.join(root, "dist", "index.js"),
|
|
539
|
-
platform: "node",
|
|
540
|
-
sourcemap: false,
|
|
541
|
-
target: "es2022",
|
|
542
|
-
treeShaking: true
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
async function buildTheme(root, skipThemeBuild) {
|
|
546
|
-
if (!skipThemeBuild) {
|
|
547
|
-
await runThemePackageBuild(root);
|
|
548
|
-
}
|
|
549
|
-
await buildServerBundle(root);
|
|
550
|
-
const entryPoint = await findClientEntry(root);
|
|
551
|
-
if (!await pathExists(entryPoint)) {
|
|
552
|
-
throw new Error(`Missing client entry: ${entryPoint}`);
|
|
553
|
-
}
|
|
554
|
-
await mkdir(path2.join(root, "dist"), { recursive: true });
|
|
555
|
-
const hostReactShimPlugin = createHostReactShimPlugin();
|
|
556
|
-
await build({
|
|
557
|
-
bundle: true,
|
|
558
|
-
entryPoints: [entryPoint],
|
|
559
|
-
format: "esm",
|
|
560
|
-
jsx: "automatic",
|
|
561
|
-
minify: true,
|
|
604
|
+
minify,
|
|
562
605
|
outfile: path2.join(root, "dist", "runtime.client.js"),
|
|
563
606
|
platform: "browser",
|
|
564
607
|
plugins: [hostReactShimPlugin],
|
|
@@ -566,71 +609,219 @@ async function buildTheme(root, skipThemeBuild) {
|
|
|
566
609
|
target: "es2022",
|
|
567
610
|
treeShaking: true
|
|
568
611
|
});
|
|
612
|
+
}
|
|
613
|
+
async function buildTheme(root) {
|
|
614
|
+
await buildViteTheme(root);
|
|
615
|
+
await buildClientRuntime(root, true);
|
|
616
|
+
return finalizeTheme(root);
|
|
617
|
+
}
|
|
618
|
+
async function finalizeTheme(root) {
|
|
619
|
+
await findViteConfig(root);
|
|
620
|
+
await finalizeStylesheet(root);
|
|
569
621
|
const validated = await validateTheme(root);
|
|
570
622
|
await writeThemeArtifacts(validated);
|
|
571
623
|
return validateTheme(root);
|
|
572
624
|
}
|
|
573
|
-
function
|
|
625
|
+
async function finalizeStylesheet(root) {
|
|
626
|
+
const builtStylesDir = path2.join(root, ".suda-build", "styles");
|
|
627
|
+
const cssFiles = [
|
|
628
|
+
...await collectFiles(root, builtStylesDir),
|
|
629
|
+
...await collectFiles(root, path2.join(root, "dist"))
|
|
630
|
+
];
|
|
631
|
+
const cssFile = cssFiles.find((file) => file.relativePath.endsWith(".css"));
|
|
632
|
+
if (!cssFile) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Vite did not emit a stylesheet from src/styles.css. Check ${path2.join(root, "vite.config.ts")} and Tailwind/PostCSS configuration.`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
await copyFile(cssFile.absolutePath, path2.join(root, "styles.css"));
|
|
638
|
+
const assetsDir = path2.join(builtStylesDir, "assets");
|
|
639
|
+
if (await pathExists(assetsDir)) {
|
|
640
|
+
await mkdir(path2.join(root, "assets"), { recursive: true });
|
|
641
|
+
await copyDirectory(assetsDir, path2.join(root, "assets"));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function copyDirectory(source, destination) {
|
|
645
|
+
const children = await readdir(source, { withFileTypes: true });
|
|
646
|
+
await mkdir(destination, { recursive: true });
|
|
647
|
+
for (const child of children) {
|
|
648
|
+
const sourcePath = path2.join(source, child.name);
|
|
649
|
+
const destinationPath = path2.join(destination, child.name);
|
|
650
|
+
if (child.isDirectory()) {
|
|
651
|
+
await copyDirectory(sourcePath, destinationPath);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
await copyFile(sourcePath, destinationPath);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
var THEME_CHECK_WHITE_LABEL = {
|
|
658
|
+
visible: true,
|
|
659
|
+
text: "Powered by <strong>\u901F\u642D\u4E91</strong>",
|
|
660
|
+
href: "https://www.sudayun.cn"
|
|
661
|
+
};
|
|
662
|
+
var THEME_CHECK_ICP = {
|
|
663
|
+
text: "\u4EACICP\u590712345678\u53F7",
|
|
664
|
+
href: "https://beian.miit.gov.cn/"
|
|
665
|
+
};
|
|
666
|
+
function escapeRegExp(value) {
|
|
667
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
668
|
+
}
|
|
669
|
+
function findAnchorTagByHref(html, href) {
|
|
670
|
+
const pattern = new RegExp(
|
|
671
|
+
`<a\\b[^>]*href=(['"])${escapeRegExp(href)}\\1[^>]*>`,
|
|
672
|
+
"i"
|
|
673
|
+
);
|
|
674
|
+
return html.match(pattern)?.[0] ?? null;
|
|
675
|
+
}
|
|
676
|
+
function runFooterContractCheck(theme, renderer) {
|
|
677
|
+
const pageData = theme.module.starterPages[0]?.data ?? { root: { props: {} }, content: [] };
|
|
678
|
+
const metadata = {
|
|
679
|
+
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
|
|
680
|
+
whiteLabel: THEME_CHECK_WHITE_LABEL,
|
|
681
|
+
icp: THEME_CHECK_ICP
|
|
682
|
+
};
|
|
683
|
+
const html = renderer.renderToStaticMarkup(
|
|
684
|
+
renderer.createElement(renderer.ThemeRender, {
|
|
685
|
+
theme: theme.module,
|
|
686
|
+
pageData,
|
|
687
|
+
layoutData: theme.module.defaultLayout,
|
|
688
|
+
metadata
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
const issues = [];
|
|
692
|
+
if (!html.includes("Powered by") || !html.includes("\u901F\u642D\u4E91")) {
|
|
693
|
+
issues.push("Footer must render metadata.whiteLabel.text when whiteLabel.visible is true.");
|
|
694
|
+
}
|
|
695
|
+
if (!html.includes(THEME_CHECK_ICP.text)) {
|
|
696
|
+
issues.push("Footer must render metadata.icp.text when ICP metadata is present.");
|
|
697
|
+
}
|
|
698
|
+
for (const href of [THEME_CHECK_WHITE_LABEL.href, THEME_CHECK_ICP.href]) {
|
|
699
|
+
const anchorTag = findAnchorTagByHref(html, href);
|
|
700
|
+
if (!anchorTag) {
|
|
701
|
+
issues.push(`Footer must render an anchor tag for ${href}.`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (!/\btarget=(['"])_blank\1/i.test(anchorTag)) {
|
|
705
|
+
issues.push(`Footer link ${href} must render with target="_blank".`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return issues;
|
|
709
|
+
}
|
|
710
|
+
var __testUtils = {
|
|
711
|
+
findAnchorTagByHref
|
|
712
|
+
};
|
|
713
|
+
async function runThemeCheck(theme) {
|
|
574
714
|
const result = checkThemeModule(theme.module);
|
|
575
|
-
const
|
|
576
|
-
|
|
715
|
+
const footerIssues = result.ok ? runFooterContractCheck(theme, await loadThemeScopedRenderer(theme.root)) : [];
|
|
716
|
+
const ok = result.ok && footerIssues.length === 0;
|
|
717
|
+
const lines = [formatThemeCheckResult(result)];
|
|
718
|
+
if (footerIssues.length === 0 && result.ok) {
|
|
719
|
+
lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
|
|
720
|
+
} else if (footerIssues.length > 0) {
|
|
721
|
+
for (const issue of footerIssues) {
|
|
722
|
+
lines.push(` error layoutConfig.footerContract: ${issue}`);
|
|
723
|
+
}
|
|
724
|
+
lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
|
|
725
|
+
}
|
|
726
|
+
const text = lines.join("\n");
|
|
727
|
+
if (ok) {
|
|
577
728
|
console.log(text);
|
|
578
729
|
} else {
|
|
579
730
|
console.error(text);
|
|
580
731
|
}
|
|
581
|
-
return
|
|
732
|
+
return ok;
|
|
582
733
|
}
|
|
583
|
-
async function watchTheme(root,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
await
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
entryPoints: [entryPoint],
|
|
596
|
-
format: "esm",
|
|
597
|
-
jsx: "automatic",
|
|
598
|
-
minify: false,
|
|
599
|
-
outfile: path2.join(root, "dist", "runtime.client.js"),
|
|
600
|
-
platform: "browser",
|
|
601
|
-
plugins: [hostReactShimPlugin, watchLogPlugin],
|
|
602
|
-
sourcemap: true,
|
|
603
|
-
target: "es2022",
|
|
604
|
-
treeShaking: true
|
|
734
|
+
async function watchTheme(root, port) {
|
|
735
|
+
const viteConfig = await findViteConfig(root);
|
|
736
|
+
const vite = await loadThemeVite(root);
|
|
737
|
+
const host = "127.0.0.1";
|
|
738
|
+
const server = await vite.createServer({
|
|
739
|
+
root,
|
|
740
|
+
configFile: viteConfig,
|
|
741
|
+
configLoader: "runner",
|
|
742
|
+
cacheDir: path2.join(root, ".suda-build", "vite-cache"),
|
|
743
|
+
appType: "custom",
|
|
744
|
+
server: { host, port },
|
|
745
|
+
plugins: [createSudaPreviewVitePlugin(root)]
|
|
605
746
|
});
|
|
606
|
-
await
|
|
607
|
-
const
|
|
608
|
-
console.log(`
|
|
747
|
+
await server.listen(port);
|
|
748
|
+
const url = server.resolvedUrls?.local.find((candidate) => candidate.includes("127.0.0.1")) ?? server.resolvedUrls?.local[0] ?? `http://${host}:${port}/`;
|
|
749
|
+
console.log(`previewing Vite theme at ${url}`);
|
|
609
750
|
await new Promise(() => void 0);
|
|
610
751
|
}
|
|
611
|
-
function
|
|
752
|
+
function createSudaPreviewVitePlugin(root) {
|
|
612
753
|
return {
|
|
613
|
-
name: "suda-theme-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
754
|
+
name: "suda-theme-preview",
|
|
755
|
+
configureServer(server) {
|
|
756
|
+
server.middlewares.use((request, response, next) => {
|
|
757
|
+
void (async () => {
|
|
758
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
759
|
+
const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
|
|
760
|
+
if (assetPrefixMatch?.[1]) {
|
|
761
|
+
request.url = `/${assetPrefixMatch[1]}${url.search}`;
|
|
762
|
+
next();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (url.pathname !== "/" && !url.pathname.startsWith("/pages/")) {
|
|
766
|
+
next();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const theme = await loadViteDevTheme(root, server);
|
|
771
|
+
const renderer = await loadThemeScopedRenderer(root);
|
|
772
|
+
if (url.pathname === "/") {
|
|
773
|
+
const fallbackSlug = pickStarterSlug(theme);
|
|
774
|
+
if (!fallbackSlug) {
|
|
775
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
776
|
+
response.end("No starter pages declared by this theme.");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const requested = url.searchParams.get("page");
|
|
780
|
+
const activeSlug = requested && findStarterPage(theme, requested) ? requested : fallbackSlug;
|
|
781
|
+
const html2 = await server.transformIndexHtml(
|
|
782
|
+
url.pathname,
|
|
783
|
+
renderPreviewShellHtml(theme, activeSlug)
|
|
784
|
+
);
|
|
785
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
786
|
+
response.end(html2);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const slug = decodeURIComponent(url.pathname.slice("/pages/".length));
|
|
790
|
+
const starter = findStarterPage(theme, slug);
|
|
791
|
+
if (!starter) {
|
|
792
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
793
|
+
response.end(`Unknown starter page: ${slug}`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const html = await server.transformIndexHtml(
|
|
797
|
+
url.pathname,
|
|
798
|
+
renderDevStarterPageHtml(theme, starter, renderer)
|
|
799
|
+
);
|
|
800
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
801
|
+
response.end(html);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
804
|
+
response.end(error instanceof Error ? error.stack : String(error));
|
|
805
|
+
}
|
|
806
|
+
})();
|
|
630
807
|
});
|
|
631
808
|
}
|
|
632
809
|
};
|
|
633
810
|
}
|
|
811
|
+
async function loadViteDevTheme(root, server) {
|
|
812
|
+
const imported = await server.ssrLoadModule("/src/index.tsx");
|
|
813
|
+
if (!imported.default) {
|
|
814
|
+
throw new Error("src/index.tsx must export a default ThemeModule.");
|
|
815
|
+
}
|
|
816
|
+
validateThemeModule(imported.default);
|
|
817
|
+
return {
|
|
818
|
+
root,
|
|
819
|
+
module: imported.default,
|
|
820
|
+
serverEntryPath: path2.join(root, "src", "index.tsx"),
|
|
821
|
+
clientEntryPath: null,
|
|
822
|
+
stylesheetPath: path2.join(root, "src", "styles.css")
|
|
823
|
+
};
|
|
824
|
+
}
|
|
634
825
|
var PREVIEW_PUBLIC_BASE_PATH = "/api/themes";
|
|
635
826
|
function pickStarterSlug(theme) {
|
|
636
827
|
const pages = theme.module.starterPages;
|
|
@@ -718,6 +909,51 @@ function renderStarterPageHtml(theme, resolved, page, renderer) {
|
|
|
718
909
|
"</html>"
|
|
719
910
|
].join("");
|
|
720
911
|
}
|
|
912
|
+
function renderDevStarterPageHtml(theme, page, renderer) {
|
|
913
|
+
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
914
|
+
const metadata = {
|
|
915
|
+
resolveAssetUrl: (value) => {
|
|
916
|
+
if (!value) {
|
|
917
|
+
return value;
|
|
918
|
+
}
|
|
919
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/")) {
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
if (value.startsWith("themes/")) {
|
|
923
|
+
const parts = value.split("/");
|
|
924
|
+
return `/${parts.slice(3).join("/")}`;
|
|
925
|
+
}
|
|
926
|
+
return value;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const body = renderer.renderToStaticMarkup(
|
|
930
|
+
renderer.createElement(renderer.ThemeRender, {
|
|
931
|
+
theme: theme.module,
|
|
932
|
+
pageData: page.data,
|
|
933
|
+
layoutData: theme.module.defaultLayout,
|
|
934
|
+
metadata
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
938
|
+
const customHead = chrome.customHeadCode ?? "";
|
|
939
|
+
const customBody = chrome.customBodyCode ?? "";
|
|
940
|
+
return [
|
|
941
|
+
"<!doctype html>",
|
|
942
|
+
'<html lang="en">',
|
|
943
|
+
"<head>",
|
|
944
|
+
'<meta charset="utf-8" />',
|
|
945
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
946
|
+
`<title>${escapeHtml(`${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
|
|
947
|
+
'<link rel="stylesheet" href="/src/styles.css" />',
|
|
948
|
+
customHead,
|
|
949
|
+
"</head>",
|
|
950
|
+
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
951
|
+
body,
|
|
952
|
+
customBody,
|
|
953
|
+
"</body>",
|
|
954
|
+
"</html>"
|
|
955
|
+
].join("");
|
|
956
|
+
}
|
|
721
957
|
function renderPreviewShellHtml(theme, activeSlug) {
|
|
722
958
|
const items = theme.module.starterPages.map((page) => {
|
|
723
959
|
const slug = page.slug;
|
|
@@ -923,7 +1159,7 @@ async function captureScreenshot(theme, options) {
|
|
|
923
1159
|
}
|
|
924
1160
|
async function screenshotTheme(root, options) {
|
|
925
1161
|
const resolved = resolveScreenshotOptions(root, options);
|
|
926
|
-
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root
|
|
1162
|
+
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root);
|
|
927
1163
|
await captureScreenshot(theme, resolved);
|
|
928
1164
|
}
|
|
929
1165
|
async function loadLocalAgentManifest(root) {
|
|
@@ -1460,14 +1696,11 @@ ${lines.join("\n")}`;
|
|
|
1460
1696
|
}
|
|
1461
1697
|
return message;
|
|
1462
1698
|
}
|
|
1463
|
-
async function publishTheme(root, skipBuild,
|
|
1464
|
-
const theme = skipBuild ? await
|
|
1465
|
-
await
|
|
1466
|
-
if (!
|
|
1467
|
-
|
|
1468
|
-
if (!ok) {
|
|
1469
|
-
throw new Error("AI metadata check failed. Fix the errors above or rerun with --skip-check.");
|
|
1470
|
-
}
|
|
1699
|
+
async function publishTheme(root, skipBuild, force) {
|
|
1700
|
+
const theme = skipBuild ? await finalizeTheme(root) : await buildTheme(root);
|
|
1701
|
+
const ok = await runThemeCheck(theme);
|
|
1702
|
+
if (!ok) {
|
|
1703
|
+
throw new Error("AI metadata check failed. Fix the errors above.");
|
|
1471
1704
|
}
|
|
1472
1705
|
const files = await collectThemeArtifactFiles(theme);
|
|
1473
1706
|
if (!theme.clientEntryPath) {
|
|
@@ -1600,6 +1833,14 @@ async function readCliPackageVersions() {
|
|
|
1600
1833
|
resolved = `^${engineVersion}`;
|
|
1601
1834
|
}
|
|
1602
1835
|
} catch {
|
|
1836
|
+
const monorepoEnginePkgPath = path2.resolve(path2.dirname(here), "..", "..", "theme-engine", "package.json");
|
|
1837
|
+
const engineRaw = await readFile(monorepoEnginePkgPath, "utf8").catch(() => null);
|
|
1838
|
+
if (engineRaw) {
|
|
1839
|
+
const engineVersion = JSON.parse(engineRaw).version;
|
|
1840
|
+
if (typeof engineVersion === "string" && engineVersion.length > 0) {
|
|
1841
|
+
resolved = `^${engineVersion}`;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1603
1844
|
}
|
|
1604
1845
|
if (!resolved) {
|
|
1605
1846
|
throw new Error(
|
|
@@ -1614,720 +1855,43 @@ async function readCliPackageVersions() {
|
|
|
1614
1855
|
};
|
|
1615
1856
|
return cachedCliPackageVersions;
|
|
1616
1857
|
}
|
|
1858
|
+
async function replaceTemplateTokens(filePath, replacements) {
|
|
1859
|
+
let contents = await readFile(filePath, "utf8");
|
|
1860
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
1861
|
+
contents = contents.split(token).join(value);
|
|
1862
|
+
}
|
|
1863
|
+
await writeFile(filePath, contents);
|
|
1864
|
+
}
|
|
1865
|
+
async function loadThemeTemplateFile(relativePath) {
|
|
1866
|
+
return readFile(path2.join(themeTemplateRoot, relativePath), "utf8");
|
|
1867
|
+
}
|
|
1617
1868
|
async function initTheme(target) {
|
|
1618
1869
|
const key = path2.basename(target).replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
|
|
1619
1870
|
const cliPackageVersions = await readCliPackageVersions();
|
|
1620
|
-
await
|
|
1621
|
-
await
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
"@puckeditor/core": "^0.21.2",
|
|
1645
|
-
"@sudajs/cli": cliPackageVersions.cli,
|
|
1646
|
-
"@sudajs/theme-engine": cliPackageVersions.themeEngine,
|
|
1647
|
-
"@types/node": "^22.0.0",
|
|
1648
|
-
"@types/react": "^19.0.0",
|
|
1649
|
-
"@types/react-dom": "^19.0.0",
|
|
1650
|
-
react: "^19.0.0",
|
|
1651
|
-
"react-dom": "^19.0.0",
|
|
1652
|
-
typescript: "^5.6.0"
|
|
1653
|
-
},
|
|
1654
|
-
peerDependencies: {
|
|
1655
|
-
"@puckeditor/core": "^0.21.2",
|
|
1656
|
-
// Themes are consumed by hosts (workspace/site/worker) that already
|
|
1657
|
-
// ship a copy of `@sudajs/theme-engine`. Declaring it as a peer means
|
|
1658
|
-
// hosts can patch/minor-bump the engine without forcing every theme
|
|
1659
|
-
// to re-publish, while CLI/dev installs still get a working version
|
|
1660
|
-
// via devDependencies.
|
|
1661
|
-
"@sudajs/theme-engine": cliPackageVersions.themeEngine,
|
|
1662
|
-
react: "^19.0.0",
|
|
1663
|
-
"react-dom": "^19.0.0"
|
|
1664
|
-
}
|
|
1665
|
-
},
|
|
1666
|
-
null,
|
|
1667
|
-
2
|
|
1668
|
-
)}
|
|
1669
|
-
`
|
|
1670
|
-
);
|
|
1671
|
-
await writeFile(
|
|
1672
|
-
path2.join(target, "pnpm-workspace.yaml"),
|
|
1673
|
-
`# This is NOT a monorepo marker \u2014 it only exists because pnpm 11 moved
|
|
1674
|
-
# project-level settings such as \`allowBuilds\` out of package.json. Without
|
|
1675
|
-
# a \`packages:\` field this workspace contains just the current package, so
|
|
1676
|
-
# the project remains a standalone single-package theme.
|
|
1677
|
-
# See https://pnpm.io/settings and https://pnpm.io/pnpm-workspace_yaml.
|
|
1678
|
-
allowBuilds:
|
|
1679
|
-
esbuild: true
|
|
1680
|
-
`
|
|
1681
|
-
);
|
|
1682
|
-
await writeFile(
|
|
1683
|
-
path2.join(target, "tsconfig.json"),
|
|
1684
|
-
`${JSON.stringify(
|
|
1685
|
-
{
|
|
1686
|
-
compilerOptions: {
|
|
1687
|
-
target: "ES2022",
|
|
1688
|
-
lib: ["DOM", "DOM.Iterable", "ES2022"],
|
|
1689
|
-
module: "NodeNext",
|
|
1690
|
-
moduleResolution: "NodeNext",
|
|
1691
|
-
jsx: "react-jsx",
|
|
1692
|
-
rootDir: "src",
|
|
1693
|
-
outDir: "dist",
|
|
1694
|
-
declaration: true,
|
|
1695
|
-
declarationMap: true,
|
|
1696
|
-
sourceMap: true,
|
|
1697
|
-
strict: true,
|
|
1698
|
-
noUncheckedIndexedAccess: true,
|
|
1699
|
-
esModuleInterop: true,
|
|
1700
|
-
skipLibCheck: true,
|
|
1701
|
-
forceConsistentCasingInFileNames: true,
|
|
1702
|
-
resolveJsonModule: true,
|
|
1703
|
-
types: ["node"]
|
|
1704
|
-
},
|
|
1705
|
-
// `package.json` is included so `src/index.tsx` can read its `version`
|
|
1706
|
-
// via `import pkg from "../package.json"`. tsc tolerates this with
|
|
1707
|
-
// rootDir pinned because the JSON is resolved as input but not emitted.
|
|
1708
|
-
include: ["src", "package.json"]
|
|
1709
|
-
},
|
|
1710
|
-
null,
|
|
1711
|
-
2
|
|
1712
|
-
)}
|
|
1713
|
-
`
|
|
1714
|
-
);
|
|
1715
|
-
await writeFile(
|
|
1716
|
-
path2.join(target, "src", "manifest.ts"),
|
|
1717
|
-
`import type { ThemeSourceManifest } from "@sudajs/theme-engine";
|
|
1718
|
-
|
|
1719
|
-
// Source-side manifest \u2014 version is intentionally omitted because the
|
|
1720
|
-
// authoritative theme version lives in package.json. \`src/index.tsx\` reads
|
|
1721
|
-
// package.json#version and merges it into the published ThemeManifest, so a
|
|
1722
|
-
// single \`pnpm changeset version\` (or \`npm version\`) bumps everything.
|
|
1723
|
-
export const sourceManifest: ThemeSourceManifest = {
|
|
1724
|
-
key: "${key}",
|
|
1725
|
-
name: "${key}",
|
|
1726
|
-
categories: ["other"],
|
|
1727
|
-
minEngineVersion: "0.0.0",
|
|
1728
|
-
entry: "dist/index.js",
|
|
1729
|
-
clientEntry: "dist/runtime.client.js",
|
|
1730
|
-
// Required. SudaCloud currently hosts SSR themes only; CSR/Hybrid postures
|
|
1731
|
-
// are on the roadmap. Authoring \`renderMode\` is the contract a theme uses
|
|
1732
|
-
// to declare which posture it expects from the host.
|
|
1733
|
-
renderMode: "ssr",
|
|
1734
|
-
};
|
|
1735
|
-
`
|
|
1736
|
-
);
|
|
1737
|
-
await writeFile(
|
|
1738
|
-
path2.join(target, "src", "theme-asset.ts"),
|
|
1739
|
-
`import { createThemeAssetResolver } from "@sudajs/theme-engine/runtime";
|
|
1740
|
-
|
|
1741
|
-
import pkg from "../package.json" with { type: "json" };
|
|
1742
|
-
import { sourceManifest } from "./manifest.js";
|
|
1743
|
-
|
|
1744
|
-
export const themeAsset = createThemeAssetResolver(sourceManifest.key, pkg.version);
|
|
1745
|
-
`
|
|
1746
|
-
);
|
|
1747
|
-
await writeFile(
|
|
1748
|
-
path2.join(target, "src", "sections.tsx"),
|
|
1749
|
-
`import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1750
|
-
|
|
1751
|
-
export const Hero: SudaComponentConfig = {
|
|
1752
|
-
label: "Hero",
|
|
1753
|
-
ai: {
|
|
1754
|
-
instructions:
|
|
1755
|
-
"Primary page introduction used to communicate the main value proposition. " +
|
|
1756
|
-
"Place near the beginning of landing, product, service, or campaign pages. " +
|
|
1757
|
-
"Include one concise headline, supporting copy, and one primary call to action. " +
|
|
1758
|
-
"Use at most once per page. Do not use for ordinary section headings or article content.",
|
|
1759
|
-
},
|
|
1760
|
-
fields: {
|
|
1761
|
-
eyebrow: { type: "text", label: "Eyebrow" },
|
|
1762
|
-
title: { type: "text", label: "Title" },
|
|
1763
|
-
description: { type: "textarea", label: "Description" },
|
|
1764
|
-
primaryLabel: { type: "text", label: "Primary button label" },
|
|
1765
|
-
primaryHref: { type: "text", label: "Primary button link" },
|
|
1766
|
-
},
|
|
1767
|
-
defaultProps: {
|
|
1768
|
-
eyebrow: "New theme",
|
|
1769
|
-
title: "Build with SudaCloud",
|
|
1770
|
-
description: "Edit this starter section in the visual editor.",
|
|
1771
|
-
primaryLabel: "Get started",
|
|
1772
|
-
primaryHref: "#contact",
|
|
1773
|
-
},
|
|
1774
|
-
render: ({ eyebrow, title, description, primaryLabel, primaryHref }) => (
|
|
1775
|
-
<section className="${key}-section ${key}-hero">
|
|
1776
|
-
<p className="${key}-eyebrow">{eyebrow}</p>
|
|
1777
|
-
<h1>{title}</h1>
|
|
1778
|
-
<p>{description}</p>
|
|
1779
|
-
<a className="${key}-button" href={primaryHref}>{primaryLabel}</a>
|
|
1780
|
-
</section>
|
|
1781
|
-
),
|
|
1782
|
-
};
|
|
1783
|
-
|
|
1784
|
-
export const FeatureGrid: SudaComponentConfig = {
|
|
1785
|
-
label: "Feature grid",
|
|
1786
|
-
ai: {
|
|
1787
|
-
instructions:
|
|
1788
|
-
"Section that lists 3\u20136 short feature or benefit cards explaining what the product or service offers. " +
|
|
1789
|
-
"Place after the hero / introduction and before the final call-to-action. " +
|
|
1790
|
-
"Use when the page needs to communicate multiple distinct value points; do not use for testimonials, FAQs, or step-by-step processes.",
|
|
1791
|
-
},
|
|
1792
|
-
fields: {
|
|
1793
|
-
title: { type: "text", label: "Title" },
|
|
1794
|
-
description: { type: "textarea", label: "Description" },
|
|
1795
|
-
features: {
|
|
1796
|
-
type: "array",
|
|
1797
|
-
label: "Features",
|
|
1798
|
-
arrayFields: {
|
|
1799
|
-
title: { type: "text", label: "Title" },
|
|
1800
|
-
description: { type: "textarea", label: "Description" },
|
|
1801
|
-
},
|
|
1802
|
-
},
|
|
1803
|
-
},
|
|
1804
|
-
defaultProps: {
|
|
1805
|
-
title: "Everything you need to launch",
|
|
1806
|
-
description: "Use this section to explain the core value of the project.",
|
|
1807
|
-
features: [
|
|
1808
|
-
{ title: "Fast setup", description: "Start from a clean theme contract." },
|
|
1809
|
-
{ title: "Visual editing", description: "Expose content fields through Puck." },
|
|
1810
|
-
{ title: "Publish ready", description: "Build and publish with the Suda CLI." },
|
|
1811
|
-
],
|
|
1812
|
-
},
|
|
1813
|
-
render: ({ title, description, features = [] }) => (
|
|
1814
|
-
<section className="${key}-section">
|
|
1815
|
-
<h2>{title}</h2>
|
|
1816
|
-
<p>{description}</p>
|
|
1817
|
-
<div className="${key}-grid">
|
|
1818
|
-
{features.map((feature: { title?: string; description?: string }, index: number) => (
|
|
1819
|
-
<article className="${key}-card" key={index}>
|
|
1820
|
-
<h3>{feature.title}</h3>
|
|
1821
|
-
<p>{feature.description}</p>
|
|
1822
|
-
</article>
|
|
1823
|
-
))}
|
|
1824
|
-
</div>
|
|
1825
|
-
</section>
|
|
1826
|
-
),
|
|
1827
|
-
};
|
|
1828
|
-
|
|
1829
|
-
export const Testimonial: SudaComponentConfig = {
|
|
1830
|
-
label: "Testimonial",
|
|
1831
|
-
ai: {
|
|
1832
|
-
instructions:
|
|
1833
|
-
"Section that quotes a single customer or expert as social proof. " +
|
|
1834
|
-
"Place mid-page after a feature or value section, or near the end before the final CTA. " +
|
|
1835
|
-
"Do not invent quotes, names, or roles; only use content the user provides.",
|
|
1836
|
-
},
|
|
1837
|
-
fields: {
|
|
1838
|
-
quote: { type: "textarea", label: "Quote" },
|
|
1839
|
-
author: { type: "text", label: "Author" },
|
|
1840
|
-
role: { type: "text", label: "Role" },
|
|
1841
|
-
},
|
|
1842
|
-
defaultProps: {
|
|
1843
|
-
quote: "SudaCloud gives our team a practical editing workflow without giving up theme control.",
|
|
1844
|
-
author: "Alex Chen",
|
|
1845
|
-
role: "Founder",
|
|
1846
|
-
},
|
|
1847
|
-
render: ({ quote, author, role }) => (
|
|
1848
|
-
<section className="${key}-section ${key}-quote">
|
|
1849
|
-
<blockquote>{quote}</blockquote>
|
|
1850
|
-
<p>{author} \xB7 {role}</p>
|
|
1851
|
-
</section>
|
|
1852
|
-
),
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
export const CallToAction: SudaComponentConfig = {
|
|
1856
|
-
label: "Call to action",
|
|
1857
|
-
ai: {
|
|
1858
|
-
instructions:
|
|
1859
|
-
"Closing conversion section that invites the visitor to take a specific action (sign up, contact, buy, etc.). " +
|
|
1860
|
-
"Place near the end of the page, after supporting content. " +
|
|
1861
|
-
"Use at most once per page. Do not use as the page's first introduction \u2014 the Hero component fills that role.",
|
|
1862
|
-
},
|
|
1863
|
-
fields: {
|
|
1864
|
-
title: { type: "text", label: "Title" },
|
|
1865
|
-
description: { type: "textarea", label: "Description" },
|
|
1866
|
-
buttonLabel: { type: "text", label: "Button label" },
|
|
1867
|
-
buttonHref: { type: "text", label: "Button link" },
|
|
1868
|
-
},
|
|
1869
|
-
defaultProps: {
|
|
1870
|
-
title: "Ready to build your next page?",
|
|
1871
|
-
description: "Use this section as the final conversion block.",
|
|
1872
|
-
buttonLabel: "Contact us",
|
|
1873
|
-
buttonHref: "#contact",
|
|
1874
|
-
},
|
|
1875
|
-
render: ({ title, description, buttonLabel, buttonHref }) => (
|
|
1876
|
-
<section className="${key}-section ${key}-cta" id="contact">
|
|
1877
|
-
<h2>{title}</h2>
|
|
1878
|
-
<p>{description}</p>
|
|
1879
|
-
<a className="${key}-button" href={buttonHref}>{buttonLabel}</a>
|
|
1880
|
-
</section>
|
|
1881
|
-
),
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
export const SECTION_COMPONENTS = { Hero, FeatureGrid, Testimonial, CallToAction };
|
|
1885
|
-
`
|
|
1886
|
-
);
|
|
1887
|
-
await writeFile(
|
|
1888
|
-
path2.join(target, "src", "layout.tsx"),
|
|
1889
|
-
`import { getPageSlot } from "@sudajs/theme-engine/runtime";
|
|
1890
|
-
import type { Config } from "@puckeditor/core";
|
|
1891
|
-
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1892
|
-
import type { ReactElement, ReactNode } from "react";
|
|
1893
|
-
|
|
1894
|
-
type PuckExtras = { puck?: { metadata?: Record<string, unknown> } };
|
|
1895
|
-
|
|
1896
|
-
export const rootConfig: NonNullable<Config["root"]> = {
|
|
1897
|
-
fields: {
|
|
1898
|
-
siteName: { type: "text", label: "Site name" },
|
|
1899
|
-
},
|
|
1900
|
-
defaultProps: { siteName: "${key}" },
|
|
1901
|
-
render: ({ children, siteName }: { children?: ReactNode; siteName?: string }) => (
|
|
1902
|
-
<div className="${key}-root" data-site-name={siteName}>{children}</div>
|
|
1903
|
-
),
|
|
1904
|
-
};
|
|
1905
|
-
|
|
1906
|
-
export const Header: SudaComponentConfig = {
|
|
1907
|
-
label: "Header",
|
|
1908
|
-
ai: {
|
|
1909
|
-
instructions:
|
|
1910
|
-
"Site-wide header rendered at the top of every page. " +
|
|
1911
|
-
"Place once at the top of the layout (not inside page content). " +
|
|
1912
|
-
"Used for branding and primary navigation, not for promotional content.",
|
|
1913
|
-
},
|
|
1914
|
-
fields: { siteName: { type: "text", label: "Site name" } },
|
|
1915
|
-
defaultProps: { siteName: "${key}" },
|
|
1916
|
-
render: ({ siteName }) => <header className="${key}-header">{siteName}</header>,
|
|
1917
|
-
};
|
|
1918
|
-
|
|
1919
|
-
export const PageOutlet: SudaComponentConfig = {
|
|
1920
|
-
label: "Page outlet",
|
|
1921
|
-
ai: {
|
|
1922
|
-
exclude: true,
|
|
1923
|
-
instructions:
|
|
1924
|
-
"Structural slot where each page's content is injected. " +
|
|
1925
|
-
"Managed by the layout, never created or placed by AI.",
|
|
1926
|
-
},
|
|
1927
|
-
fields: {},
|
|
1928
|
-
defaultProps: {},
|
|
1929
|
-
render: (props: PuckExtras): ReactElement => <>{getPageSlot(props.puck?.metadata)}</>,
|
|
1930
|
-
};
|
|
1931
|
-
|
|
1932
|
-
export const Footer: SudaComponentConfig = {
|
|
1933
|
-
label: "Footer",
|
|
1934
|
-
ai: {
|
|
1935
|
-
instructions:
|
|
1936
|
-
"Site-wide footer rendered at the bottom of every page. " +
|
|
1937
|
-
"Place once at the end of the layout. " +
|
|
1938
|
-
"Used for legal text, copyright, and secondary links \u2014 not for primary CTAs.",
|
|
1939
|
-
},
|
|
1940
|
-
fields: { text: { type: "text", label: "Text" } },
|
|
1941
|
-
defaultProps: { text: "\xA9 ${key}" },
|
|
1942
|
-
render: ({ text }) => <footer className="${key}-footer">{text}</footer>,
|
|
1943
|
-
};
|
|
1944
|
-
|
|
1945
|
-
export const LAYOUT_COMPONENTS = { Header, PageOutlet, Footer };
|
|
1946
|
-
`
|
|
1947
|
-
);
|
|
1948
|
-
await writeFile(
|
|
1949
|
-
path2.join(target, "src", "config.ts"),
|
|
1950
|
-
`import type { Config, Data } from "@puckeditor/core";
|
|
1951
|
-
|
|
1952
|
-
import { LAYOUT_COMPONENTS, rootConfig } from "./layout.js";
|
|
1953
|
-
import { SECTION_COMPONENTS } from "./sections.js";
|
|
1954
|
-
|
|
1955
|
-
export const pageConfig: Config = {
|
|
1956
|
-
components: SECTION_COMPONENTS,
|
|
1957
|
-
};
|
|
1958
|
-
|
|
1959
|
-
export const layoutConfig: Config = {
|
|
1960
|
-
root: rootConfig,
|
|
1961
|
-
components: LAYOUT_COMPONENTS,
|
|
1962
|
-
};
|
|
1963
|
-
|
|
1964
|
-
export const defaultLayout: Data = {
|
|
1965
|
-
root: { props: { siteName: "${key}" } },
|
|
1966
|
-
content: [
|
|
1967
|
-
{ type: "Header", props: { id: "Header-1", siteName: "${key}" } },
|
|
1968
|
-
{ type: "PageOutlet", props: { id: "PageOutlet-1" } },
|
|
1969
|
-
{ type: "Footer", props: { id: "Footer-1", text: "\xA9 ${key}" } },
|
|
1970
|
-
],
|
|
1971
|
-
};
|
|
1972
|
-
`
|
|
1973
|
-
);
|
|
1974
|
-
await writeFile(
|
|
1975
|
-
path2.join(target, "src", "templates.ts"),
|
|
1976
|
-
`import type { ThemeStarterPage } from "@sudajs/theme-engine";
|
|
1977
|
-
|
|
1978
|
-
export const starterPages: ThemeStarterPage[] = [
|
|
1979
|
-
{
|
|
1980
|
-
slug: "home",
|
|
1981
|
-
title: "Home",
|
|
1982
|
-
isHome: true,
|
|
1983
|
-
data: {
|
|
1984
|
-
root: { props: {} },
|
|
1985
|
-
content: [
|
|
1986
|
-
{
|
|
1987
|
-
type: "Hero",
|
|
1988
|
-
props: {
|
|
1989
|
-
id: "Hero-1",
|
|
1990
|
-
eyebrow: "Starter page",
|
|
1991
|
-
title: "Welcome to ${key}",
|
|
1992
|
-
description: "This page was generated by suda theme init.",
|
|
1993
|
-
primaryLabel: "Explore features",
|
|
1994
|
-
primaryHref: "#features",
|
|
1995
|
-
},
|
|
1996
|
-
},
|
|
1997
|
-
{
|
|
1998
|
-
type: "FeatureGrid",
|
|
1999
|
-
props: {
|
|
2000
|
-
id: "FeatureGrid-1",
|
|
2001
|
-
title: "Designed for editable sites",
|
|
2002
|
-
description: "Starter sections show agents and editors how this theme is structured.",
|
|
2003
|
-
features: [
|
|
2004
|
-
{ title: "Typed fields", description: "Each section exposes a clear field schema." },
|
|
2005
|
-
{ title: "Starter pages", description: "Templates show realistic section composition." },
|
|
2006
|
-
{ title: "CLI workflow", description: "Build, validate, preview, and publish from one tool." },
|
|
2007
|
-
],
|
|
2008
|
-
},
|
|
2009
|
-
},
|
|
2010
|
-
{
|
|
2011
|
-
type: "CallToAction",
|
|
2012
|
-
props: {
|
|
2013
|
-
id: "CallToAction-1",
|
|
2014
|
-
title: "Launch your first page",
|
|
2015
|
-
description: "Customize this starter template or let an agent generate a new draft.",
|
|
2016
|
-
buttonLabel: "Get in touch",
|
|
2017
|
-
buttonHref: "/contact",
|
|
2018
|
-
},
|
|
2019
|
-
},
|
|
2020
|
-
],
|
|
2021
|
-
},
|
|
2022
|
-
},
|
|
2023
|
-
{
|
|
2024
|
-
slug: "about",
|
|
2025
|
-
title: "About",
|
|
2026
|
-
data: {
|
|
2027
|
-
root: { props: {} },
|
|
2028
|
-
content: [
|
|
2029
|
-
{
|
|
2030
|
-
type: "Hero",
|
|
2031
|
-
props: {
|
|
2032
|
-
id: "Hero-About",
|
|
2033
|
-
eyebrow: "About",
|
|
2034
|
-
title: "A clean starting point for your story",
|
|
2035
|
-
description: "Use this page to introduce the project, audience, and promise.",
|
|
2036
|
-
primaryLabel: "Contact us",
|
|
2037
|
-
primaryHref: "/contact",
|
|
2038
|
-
},
|
|
2039
|
-
},
|
|
2040
|
-
{
|
|
2041
|
-
type: "Testimonial",
|
|
2042
|
-
props: {
|
|
2043
|
-
id: "Testimonial-About",
|
|
2044
|
-
quote: "This starter theme keeps the editable surface focused and predictable.",
|
|
2045
|
-
author: "SudaCloud",
|
|
2046
|
-
role: "Theme team",
|
|
2047
|
-
},
|
|
2048
|
-
},
|
|
2049
|
-
],
|
|
2050
|
-
},
|
|
2051
|
-
},
|
|
2052
|
-
{
|
|
2053
|
-
slug: "contact",
|
|
2054
|
-
title: "Contact",
|
|
2055
|
-
data: {
|
|
2056
|
-
root: { props: {} },
|
|
2057
|
-
content: [
|
|
2058
|
-
{
|
|
2059
|
-
type: "Hero",
|
|
2060
|
-
props: {
|
|
2061
|
-
id: "Hero-Contact",
|
|
2062
|
-
eyebrow: "Contact",
|
|
2063
|
-
title: "Let's talk",
|
|
2064
|
-
description: "Tell visitors how to reach you and what happens next.",
|
|
2065
|
-
primaryLabel: "Email us",
|
|
2066
|
-
primaryHref: "mailto:hello@example.com",
|
|
2067
|
-
},
|
|
2068
|
-
},
|
|
2069
|
-
{
|
|
2070
|
-
type: "CallToAction",
|
|
2071
|
-
props: {
|
|
2072
|
-
id: "CallToAction-Contact",
|
|
2073
|
-
title: "Start the conversation",
|
|
2074
|
-
description: "Replace this copy with your preferred contact details or form link.",
|
|
2075
|
-
buttonLabel: "Send an email",
|
|
2076
|
-
buttonHref: "mailto:hello@example.com",
|
|
2077
|
-
},
|
|
2078
|
-
},
|
|
2079
|
-
],
|
|
2080
|
-
},
|
|
2081
|
-
},
|
|
2082
|
-
];
|
|
2083
|
-
`
|
|
2084
|
-
);
|
|
2085
|
-
await writeFile(
|
|
2086
|
-
path2.join(target, "src", "index.tsx"),
|
|
2087
|
-
`import type { ThemeManifest, ThemeModule } from "@sudajs/theme-engine";
|
|
2088
|
-
|
|
2089
|
-
import pkg from "../package.json" with { type: "json" };
|
|
2090
|
-
import { defaultLayout, layoutConfig, pageConfig } from "./config.js";
|
|
2091
|
-
import { sourceManifest } from "./manifest.js";
|
|
2092
|
-
import { starterPages } from "./templates.js";
|
|
2093
|
-
|
|
2094
|
-
// Single source of truth: theme version comes from package.json#version,
|
|
2095
|
-
// merged into the source-side manifest. esbuild inlines the JSON import at
|
|
2096
|
-
// build time, so the published \`dist/index.js\` is fully self-contained.
|
|
2097
|
-
export const manifest: ThemeManifest = { ...sourceManifest, version: pkg.version };
|
|
2098
|
-
|
|
2099
|
-
const theme: ThemeModule = {
|
|
2100
|
-
manifest,
|
|
2101
|
-
pageConfig,
|
|
2102
|
-
layoutConfig,
|
|
2103
|
-
defaultLayout,
|
|
2104
|
-
starterPages,
|
|
2105
|
-
};
|
|
2106
|
-
|
|
2107
|
-
export default theme;
|
|
2108
|
-
export { pageConfig, layoutConfig, defaultLayout, starterPages };
|
|
2109
|
-
`
|
|
2110
|
-
);
|
|
2111
|
-
await writeFile(
|
|
2112
|
-
path2.join(target, "src", "runtime.client.ts"),
|
|
2113
|
-
'"use client";\n\nimport theme from "./index.js";\n\nexport default theme;\n'
|
|
2114
|
-
);
|
|
2115
|
-
await writeFile(
|
|
2116
|
-
path2.join(target, "styles.css"),
|
|
2117
|
-
`.${key}-root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #111827; background: #ffffff; }
|
|
2118
|
-
.${key}-header, .${key}-footer { padding: 20px clamp(20px, 5vw, 64px); border-bottom: 1px solid #e5e7eb; }
|
|
2119
|
-
.${key}-footer { border-top: 1px solid #e5e7eb; border-bottom: 0; color: #6b7280; }
|
|
2120
|
-
.${key}-section { padding: 64px clamp(20px, 5vw, 64px); }
|
|
2121
|
-
.${key}-hero { background: #f8fafc; }
|
|
2122
|
-
.${key}-eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 12px; color: #2563eb; font-weight: 700; }
|
|
2123
|
-
.${key}-section h1 { max-width: 780px; font-size: clamp(40px, 7vw, 72px); line-height: 0.95; margin: 0 0 20px; }
|
|
2124
|
-
.${key}-section h2 { max-width: 720px; font-size: 36px; line-height: 1.05; margin: 0 0 16px; }
|
|
2125
|
-
.${key}-section p { max-width: 680px; line-height: 1.7; color: #4b5563; }
|
|
2126
|
-
.${key}-button { display: inline-flex; align-items: center; min-height: 42px; padding: 0 18px; border-radius: 8px; background: #111827; color: #ffffff; text-decoration: none; font-weight: 700; }
|
|
2127
|
-
.${key}-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 28px; }
|
|
2128
|
-
.${key}-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; }
|
|
2129
|
-
.${key}-quote blockquote { max-width: 780px; font-size: 28px; line-height: 1.25; margin: 0 0 16px; }
|
|
2130
|
-
.${key}-cta { background: #111827; color: #ffffff; }
|
|
2131
|
-
.${key}-cta p { color: #d1d5db; }
|
|
2132
|
-
.${key}-cta .${key}-button { background: #ffffff; color: #111827; }
|
|
2133
|
-
`
|
|
2134
|
-
);
|
|
2135
|
-
await writeFile(path2.join(target, ".gitignore"), "node_modules\ndist\n*.tsbuildinfo\n");
|
|
2136
|
-
await writeFile(
|
|
2137
|
-
path2.join(target, "AGENTS.md"),
|
|
2138
|
-
`# Suda Theme Agent Guide
|
|
2139
|
-
|
|
2140
|
-
This directory is a Suda theme source project generated by \`suda theme init\`.
|
|
2141
|
-
It is a **standalone** npm package \u2014 it does not depend on the SudaCloud monorepo.
|
|
2142
|
-
|
|
2143
|
-
## Render mode
|
|
2144
|
-
|
|
2145
|
-
This theme contract requires \`renderMode: "ssr"\` in the source manifest
|
|
2146
|
-
(\`src/manifest.ts\`). SudaCloud's site runtime currently only supports SSR
|
|
2147
|
-
themes; CSR / Hybrid postures are on the roadmap. **Do not** introduce
|
|
2148
|
-
client-only entry points or assume browser-only globals at module scope \u2014
|
|
2149
|
-
the theme bundle must execute in a Node SSR context.
|
|
2150
|
-
|
|
2151
|
-
## What to edit
|
|
2152
|
-
|
|
2153
|
-
- Add page sections in \`src/sections.tsx\` and register them in \`SECTION_COMPONENTS\`.
|
|
2154
|
-
- Add starter pages in \`src/templates.ts\` to show realistic section combinations.
|
|
2155
|
-
- Keep shared site chrome in \`src/layout.tsx\`; \`PageOutlet\` is where page content renders.
|
|
2156
|
-
- Keep \`src/index.tsx\` exporting the source \`ThemeModule\`. The final artifact is produced by \`suda theme build\`.
|
|
2157
|
-
- All relative imports under \`src/\` MUST include the \`.js\` extension (NodeNext ESM resolution).
|
|
2158
|
-
|
|
2159
|
-
## Page content rules
|
|
2160
|
-
|
|
2161
|
-
- Agent page files passed to \`suda agent page validate/create\` contain \`content\` and optional \`zones\` only.
|
|
2162
|
-
- Every \`content[]\` item must use a section type from \`SECTION_COMPONENTS\`.
|
|
2163
|
-
- Every \`content[]\` item must include \`props.id\`.
|
|
2164
|
-
- Prefer editing section props over changing render code when generating pages.
|
|
2165
|
-
- Do not edit files under \`dist/\`; they are generated.
|
|
2166
|
-
|
|
2167
|
-
## AI metadata (required)
|
|
2168
|
-
|
|
2169
|
-
Every component (and zone-level layout component) **must** declare \`ai.instructions\`.
|
|
2170
|
-
Without it the page-generation agent has no way to know what the component is for,
|
|
2171
|
-
and \`suda theme check\` (run automatically before publish) will fail.
|
|
2172
|
-
|
|
2173
|
-
### Component-level \`ai\`
|
|
2174
|
-
|
|
2175
|
-
Each \`ComponentConfig\` must include:
|
|
2176
|
-
|
|
2177
|
-
\`\`\`ts
|
|
2178
|
-
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
2179
|
-
|
|
2180
|
-
export const Hero: SudaComponentConfig = {
|
|
2181
|
-
label: "Hero",
|
|
2182
|
-
ai: {
|
|
2183
|
-
instructions:
|
|
2184
|
-
"Primary page introduction used to communicate the main value proposition. " +
|
|
2185
|
-
"Place near the beginning of landing or product pages. " +
|
|
2186
|
-
"Include one primary CTA. Use at most once per page. " +
|
|
2187
|
-
"Do not use for ordinary section headings or article content.",
|
|
2188
|
-
},
|
|
2189
|
-
fields: { /* ... */ },
|
|
2190
|
-
defaultProps: { /* ... */ },
|
|
2191
|
-
render: (props) => /* ... */,
|
|
2192
|
-
};
|
|
2193
|
-
\`\`\`
|
|
2194
|
-
|
|
2195
|
-
A good \`instructions\` covers, in plain English:
|
|
2196
|
-
|
|
2197
|
-
1. **Purpose** \u2014 what role this component plays on a page.
|
|
2198
|
-
2. **Use when** \u2014 page types or scenarios where it should be selected.
|
|
2199
|
-
3. **Avoid when** \u2014 situations that look similar but call for a different component.
|
|
2200
|
-
4. **Placement** \u2014 where in the page it normally belongs (top, after features, near end, etc.).
|
|
2201
|
-
5. **Frequency / composition** \u2014 how many times it can appear, what it must contain, and any adjacency rules.
|
|
2202
|
-
|
|
2203
|
-
Write executable, specific sentences (\`Use when...\`, \`Place after...\`, \`Use at most once per page.\`).
|
|
2204
|
-
Do **not** describe styling (colors, fonts, spacing) \u2014 those belong to the design system.
|
|
2205
|
-
Do **not** write vague filler like "modern", "engaging", "beautiful".
|
|
2206
|
-
|
|
2207
|
-
If two components look similar (e.g. Hero vs. PageHeader vs. SectionHeader),
|
|
2208
|
-
their \`instructions\` must explicitly differentiate them, otherwise the agent
|
|
2209
|
-
will mix them up.
|
|
2210
|
-
|
|
2211
|
-
### Hiding a component from AI
|
|
2212
|
-
|
|
2213
|
-
Use \`ai.exclude: true\` for internal/debug components or anything the agent
|
|
2214
|
-
should never auto-create. Such components remain editable by humans:
|
|
2215
|
-
|
|
2216
|
-
\`\`\`ts
|
|
2217
|
-
ai: { exclude: true, instructions: "Internal debug block; AI must not create it." }
|
|
2218
|
-
\`\`\`
|
|
2219
|
-
|
|
2220
|
-
### Field-level \`ai\` (recommended, optional)
|
|
2221
|
-
|
|
2222
|
-
Field-level metadata sharpens the generated content but is **not required**.
|
|
2223
|
-
Use it when a field's name and type alone don't fully convey the constraints:
|
|
2224
|
-
|
|
2225
|
-
\`\`\`ts
|
|
2226
|
-
fields: {
|
|
2227
|
-
title: {
|
|
2228
|
-
type: "text",
|
|
2229
|
-
ai: {
|
|
2230
|
-
instructions:
|
|
2231
|
-
"Main value-proposition headline. 4\u201312 words, focus on the visitor benefit, " +
|
|
2232
|
-
"do not repeat the eyebrow or description.",
|
|
2233
|
-
required: true,
|
|
2234
|
-
},
|
|
2235
|
-
},
|
|
2236
|
-
imageUrl: {
|
|
2237
|
-
type: "text",
|
|
2238
|
-
ai: {
|
|
2239
|
-
// Atomic values that cannot render correctly while half-streamed.
|
|
2240
|
-
stream: false,
|
|
2241
|
-
instructions: "Image that supports the section topic. Avoid logos, screenshots, or unrelated decoration.",
|
|
2242
|
-
},
|
|
2243
|
-
},
|
|
2244
|
-
}
|
|
2245
|
-
\`\`\`
|
|
2246
|
-
|
|
2247
|
-
Supported field-level keys: \`instructions\`, \`required\`, \`exclude\`, \`stream\`,
|
|
2248
|
-
\`bind\` (delegate to a tool), and \`schema\` (only needed for \`custom\` / \`external\` /
|
|
2249
|
-
\`user\` fields whose runtime shape can't be inferred).
|
|
2250
|
-
|
|
2251
|
-
### Verifying
|
|
2252
|
-
|
|
2253
|
-
\`\`\`bash
|
|
2254
|
-
pnpm build # also runs the AI metadata check
|
|
2255
|
-
suda theme check # run the check on its own
|
|
2256
|
-
\`\`\`
|
|
2257
|
-
|
|
2258
|
-
Missing component-level \`ai.instructions\` is reported as an **error** and blocks
|
|
2259
|
-
the build. Very short or label-repeating instructions are reported as
|
|
2260
|
-
**warnings**.
|
|
2261
|
-
|
|
2262
|
-
## Useful commands
|
|
2263
|
-
|
|
2264
|
-
\`\`\`bash
|
|
2265
|
-
pnpm install
|
|
2266
|
-
pnpm typecheck
|
|
2267
|
-
pnpm build # tsc + suda theme build --skip-theme-build (runs AI check)
|
|
2268
|
-
pnpm validate
|
|
2269
|
-
pnpm dev # watch and serve local theme artifact files
|
|
2270
|
-
suda theme check # AI metadata check only
|
|
2271
|
-
suda agent theme describe local --theme-root .
|
|
2272
|
-
suda agent section schema --theme local --section Hero --theme-root .
|
|
2273
|
-
suda agent page validate --theme local --input ./page.json --theme-root .
|
|
2274
|
-
\`\`\`
|
|
2275
|
-
`
|
|
2276
|
-
);
|
|
2277
|
-
await writeFile(
|
|
2278
|
-
path2.join(target, "CLAUDE.md"),
|
|
2279
|
-
`# Claude Code
|
|
2280
|
-
|
|
2281
|
-
Read \`AGENTS.md\` first. It is the canonical guide for this Suda theme project.
|
|
2282
|
-
`
|
|
2283
|
-
);
|
|
2284
|
-
await writeFile(
|
|
2285
|
-
path2.join(target, "README.md"),
|
|
2286
|
-
`# ${key}
|
|
2287
|
-
|
|
2288
|
-
A Suda theme scaffolded with \`suda theme init\`. This is a standalone
|
|
2289
|
-
package; it does not need to live inside the SudaCloud monorepo.
|
|
2290
|
-
|
|
2291
|
-
This theme targets SudaCloud's SSR runtime (\`renderMode: "ssr"\` in
|
|
2292
|
-
\`src/manifest.ts\`); CSR / Hybrid postures are not yet supported by the
|
|
2293
|
-
host.
|
|
2294
|
-
|
|
2295
|
-
## Setup
|
|
2296
|
-
|
|
2297
|
-
\`\`\`bash
|
|
2298
|
-
pnpm install
|
|
2299
|
-
\`\`\`
|
|
2300
|
-
|
|
2301
|
-
## Develop
|
|
2302
|
-
|
|
2303
|
-
\`\`\`bash
|
|
2304
|
-
pnpm typecheck # type-check sources
|
|
2305
|
-
pnpm build # tsc + suda theme build (server bundle + client runtime + manifest)
|
|
2306
|
-
pnpm dev # watch the browser runtime and start preview server
|
|
2307
|
-
pnpm validate # validate the dist artifact
|
|
2308
|
-
\`\`\`
|
|
2309
|
-
|
|
2310
|
-
> \`renderMode\` currently only accepts \`"ssr"\`. \`suda theme validate\` and
|
|
2311
|
-
> \`suda theme check\` will reject manifests that omit it or set a different
|
|
2312
|
-
> value.
|
|
2313
|
-
|
|
2314
|
-
## Agent tooling
|
|
2315
|
-
|
|
2316
|
-
\`\`\`bash
|
|
2317
|
-
suda agent theme describe local --theme-root .
|
|
2318
|
-
suda agent page schema local --theme-root .
|
|
2319
|
-
suda agent section schema --theme local --section Hero --theme-root .
|
|
2320
|
-
\`\`\`
|
|
2321
|
-
|
|
2322
|
-
## Publish
|
|
2323
|
-
|
|
2324
|
-
\`\`\`bash
|
|
2325
|
-
pnpm build
|
|
2326
|
-
suda theme screenshot # optional: capture dist/preview/desktop.png
|
|
2327
|
-
suda theme publish
|
|
2328
|
-
\`\`\`
|
|
2329
|
-
`
|
|
2330
|
-
);
|
|
1871
|
+
await copyDirectory(themeTemplateRoot, target);
|
|
1872
|
+
const packageJson = JSON.parse(await loadThemeTemplateFile("package.json"));
|
|
1873
|
+
packageJson.name = `@suda-themes/${key}`;
|
|
1874
|
+
packageJson.devDependencies["@sudajs/cli"] = cliPackageVersions.cli;
|
|
1875
|
+
packageJson.devDependencies["@sudajs/theme-engine"] = cliPackageVersions.themeEngine;
|
|
1876
|
+
packageJson.peerDependencies["@sudajs/theme-engine"] = cliPackageVersions.themeEngine;
|
|
1877
|
+
await writeFile(path2.join(target, "package.json"), `${JSON.stringify(packageJson, null, 2)}
|
|
1878
|
+
`);
|
|
1879
|
+
const replacements = {
|
|
1880
|
+
"__SUDA_THEME_KEY__": key
|
|
1881
|
+
};
|
|
1882
|
+
const filesToReplace = [
|
|
1883
|
+
"README.md",
|
|
1884
|
+
"src/manifest.ts",
|
|
1885
|
+
"src/theme-asset.ts",
|
|
1886
|
+
"src/sections.tsx",
|
|
1887
|
+
"src/layout.tsx",
|
|
1888
|
+
"src/config.ts",
|
|
1889
|
+
"src/templates.ts",
|
|
1890
|
+
"src/styles.css"
|
|
1891
|
+
];
|
|
1892
|
+
for (const relativePath of filesToReplace) {
|
|
1893
|
+
await replaceTemplateTokens(path2.join(target, relativePath), replacements);
|
|
1894
|
+
}
|
|
2331
1895
|
console.log(`created theme scaffold at ${target}`);
|
|
2332
1896
|
}
|
|
2333
1897
|
function buildProgram() {
|
|
@@ -2346,46 +1910,41 @@ function buildProgram() {
|
|
|
2346
1910
|
);
|
|
2347
1911
|
}
|
|
2348
1912
|
});
|
|
2349
|
-
theme.command("build").description("Build
|
|
2350
|
-
const result = await buildTheme(resolveThemeRoot(options)
|
|
1913
|
+
theme.command("build").description("Build a Vite Suda theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
1914
|
+
const result = await buildTheme(resolveThemeRoot(options));
|
|
2351
1915
|
console.log(`built ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
throw new Error(
|
|
2356
|
-
"AI metadata check failed. Fix the errors above or rerun with --skip-check."
|
|
2357
|
-
);
|
|
2358
|
-
}
|
|
1916
|
+
const ok = await runThemeCheck(result);
|
|
1917
|
+
if (!ok) {
|
|
1918
|
+
throw new Error("AI metadata check failed. Fix the errors above.");
|
|
2359
1919
|
}
|
|
2360
1920
|
});
|
|
1921
|
+
theme.command("finalize").description("Finalize an existing Vite build into the Suda theme artifact contract.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
1922
|
+
const result = await finalizeTheme(resolveThemeRoot(options));
|
|
1923
|
+
console.log(`finalized ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
1924
|
+
});
|
|
2361
1925
|
theme.command("check").description(
|
|
2362
1926
|
"Run AI metadata checks on a built theme without rebuilding. Reports missing component-level ai.instructions as errors and weak content as warnings."
|
|
2363
1927
|
).option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
2364
1928
|
const theme2 = await validateTheme(resolveThemeRoot(options));
|
|
2365
|
-
const ok = runThemeCheck(theme2);
|
|
1929
|
+
const ok = await runThemeCheck(theme2);
|
|
2366
1930
|
if (!ok) {
|
|
2367
1931
|
process.exitCode = 1;
|
|
2368
1932
|
}
|
|
2369
1933
|
});
|
|
2370
|
-
theme.command("dev").description("
|
|
1934
|
+
theme.command("dev").description("Start the Vite-powered local theme preview.").option("--theme-root <path>", "Theme source/artifact root.").option("--port <port>", "Preview server port.", "4177").action(async (options) => {
|
|
2371
1935
|
const port = Number(options.port ?? "4177");
|
|
2372
|
-
await watchTheme(
|
|
2373
|
-
resolveThemeRoot(options),
|
|
2374
|
-
options.skipThemeBuild === true,
|
|
2375
|
-
Number.isFinite(port) ? port : 4177
|
|
2376
|
-
);
|
|
1936
|
+
await watchTheme(resolveThemeRoot(options), Number.isFinite(port) ? port : 4177);
|
|
2377
1937
|
});
|
|
2378
1938
|
theme.command("screenshot").description("Capture a desktop preview screenshot of the home starter page using Playwright.").option("--theme-root <path>", "Theme source/artifact root.").option("--output <path>", "Output PNG path relative to theme root.").option("--width <px>", "Viewport width in pixels.", "1280").option("--height <px>", "Viewport height in pixels.", "800").option("--port <port>", "Preview server port used during capture.", "4178").option("--skip-build", "Skip rebuilding the theme before capturing.").action(async (options) => {
|
|
2379
1939
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
2380
1940
|
});
|
|
2381
|
-
theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").option(
|
|
1941
|
+
theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").option(
|
|
2382
1942
|
"--force",
|
|
2383
1943
|
"Development recovery only: clear the existing themes/<key>/<version>/ prefix and ThemeVersion row before republishing. Re-published clients pinned to this version are unavailable until the new publish completes."
|
|
2384
1944
|
).action(async (options) => {
|
|
2385
1945
|
await publishTheme(
|
|
2386
1946
|
resolveThemeRoot(options),
|
|
2387
1947
|
options.skipBuild === true,
|
|
2388
|
-
options.skipCheck === true,
|
|
2389
1948
|
options.force === true
|
|
2390
1949
|
);
|
|
2391
1950
|
});
|
|
@@ -2469,6 +2028,6 @@ async function main() {
|
|
|
2469
2028
|
await program.parseAsync(argv);
|
|
2470
2029
|
}
|
|
2471
2030
|
|
|
2472
|
-
export { buildProgram, main, parseThemeRef };
|
|
2031
|
+
export { __testUtils, buildProgram, buildTheme, finalizeTheme, initTheme, main, parseThemeRef, validateTheme, watchTheme };
|
|
2473
2032
|
//# sourceMappingURL=index.js.map
|
|
2474
2033
|
//# sourceMappingURL=index.js.map
|