@sudajs/cli 0.5.5 → 0.6.1
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 +413 -840
- 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, readFile, rm } 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,233 @@ async function buildTheme(root, skipThemeBuild) {
|
|
|
566
609
|
target: "es2022",
|
|
567
610
|
treeShaking: true
|
|
568
611
|
});
|
|
612
|
+
}
|
|
613
|
+
async function withPreservedNodeEnv(run) {
|
|
614
|
+
const previous = process.env.NODE_ENV;
|
|
615
|
+
try {
|
|
616
|
+
return await run();
|
|
617
|
+
} finally {
|
|
618
|
+
if (previous === void 0) {
|
|
619
|
+
delete process.env.NODE_ENV;
|
|
620
|
+
} else {
|
|
621
|
+
process.env.NODE_ENV = previous;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function buildTheme(root) {
|
|
626
|
+
return withPreservedNodeEnv(async () => {
|
|
627
|
+
await buildViteTheme(root);
|
|
628
|
+
await buildClientRuntime(root, true);
|
|
629
|
+
return finalizeTheme(root);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
async function finalizeTheme(root) {
|
|
633
|
+
await findViteConfig(root);
|
|
634
|
+
await finalizeStylesheet(root);
|
|
569
635
|
const validated = await validateTheme(root);
|
|
570
636
|
await writeThemeArtifacts(validated);
|
|
571
637
|
return validateTheme(root);
|
|
572
638
|
}
|
|
573
|
-
function
|
|
639
|
+
async function finalizeStylesheet(root) {
|
|
640
|
+
const builtStylesDir = path2.join(root, ".suda-build", "styles");
|
|
641
|
+
const cssFiles = [
|
|
642
|
+
...await collectFiles(root, builtStylesDir),
|
|
643
|
+
...await collectFiles(root, path2.join(root, "dist"))
|
|
644
|
+
];
|
|
645
|
+
const cssFile = cssFiles.find((file) => file.relativePath.endsWith(".css"));
|
|
646
|
+
if (!cssFile) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Vite did not emit a stylesheet from src/styles.css. Check ${path2.join(root, "vite.config.ts")} and Tailwind/PostCSS configuration.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
await copyFile(cssFile.absolutePath, path2.join(root, "styles.css"));
|
|
652
|
+
const assetsDir = path2.join(builtStylesDir, "assets");
|
|
653
|
+
if (await pathExists(assetsDir)) {
|
|
654
|
+
await mkdir(path2.join(root, "assets"), { recursive: true });
|
|
655
|
+
await copyDirectory(assetsDir, path2.join(root, "assets"));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function copyDirectory(source, destination) {
|
|
659
|
+
const children = await readdir(source, { withFileTypes: true });
|
|
660
|
+
await mkdir(destination, { recursive: true });
|
|
661
|
+
for (const child of children) {
|
|
662
|
+
const sourcePath = path2.join(source, child.name);
|
|
663
|
+
const destinationPath = path2.join(destination, child.name);
|
|
664
|
+
if (child.isDirectory()) {
|
|
665
|
+
await copyDirectory(sourcePath, destinationPath);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
await copyFile(sourcePath, destinationPath);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
var THEME_CHECK_WHITE_LABEL = {
|
|
672
|
+
visible: true,
|
|
673
|
+
text: "Powered by <strong>\u901F\u642D\u4E91</strong>",
|
|
674
|
+
href: "https://www.sudayun.cn"
|
|
675
|
+
};
|
|
676
|
+
var THEME_CHECK_ICP = {
|
|
677
|
+
text: "\u4EACICP\u590712345678\u53F7",
|
|
678
|
+
href: "https://beian.miit.gov.cn/"
|
|
679
|
+
};
|
|
680
|
+
function escapeRegExp(value) {
|
|
681
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
682
|
+
}
|
|
683
|
+
function findAnchorTagByHref(html, href) {
|
|
684
|
+
const pattern = new RegExp(
|
|
685
|
+
`<a\\b[^>]*href=(['"])${escapeRegExp(href)}\\1[^>]*>`,
|
|
686
|
+
"i"
|
|
687
|
+
);
|
|
688
|
+
return html.match(pattern)?.[0] ?? null;
|
|
689
|
+
}
|
|
690
|
+
function runFooterContractCheck(theme, renderer) {
|
|
691
|
+
const pageData = theme.module.starterPages[0]?.data ?? { root: { props: {} }, content: [] };
|
|
692
|
+
const metadata = {
|
|
693
|
+
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
|
|
694
|
+
whiteLabel: THEME_CHECK_WHITE_LABEL,
|
|
695
|
+
icp: THEME_CHECK_ICP
|
|
696
|
+
};
|
|
697
|
+
const html = renderer.renderToString(
|
|
698
|
+
renderer.createElement(renderer.ThemeRender, {
|
|
699
|
+
theme: theme.module,
|
|
700
|
+
pageData,
|
|
701
|
+
layoutData: theme.module.defaultLayout,
|
|
702
|
+
metadata
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
const issues = [];
|
|
706
|
+
if (!html.includes("Powered by") || !html.includes("\u901F\u642D\u4E91")) {
|
|
707
|
+
issues.push("Footer must render metadata.whiteLabel.text when whiteLabel.visible is true.");
|
|
708
|
+
}
|
|
709
|
+
if (!html.includes(THEME_CHECK_ICP.text)) {
|
|
710
|
+
issues.push("Footer must render metadata.icp.text when ICP metadata is present.");
|
|
711
|
+
}
|
|
712
|
+
for (const href of [THEME_CHECK_WHITE_LABEL.href, THEME_CHECK_ICP.href]) {
|
|
713
|
+
const anchorTag = findAnchorTagByHref(html, href);
|
|
714
|
+
if (!anchorTag) {
|
|
715
|
+
issues.push(`Footer must render an anchor tag for ${href}.`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (!/\btarget=(['"])_blank\1/i.test(anchorTag)) {
|
|
719
|
+
issues.push(`Footer link ${href} must render with target="_blank".`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return issues;
|
|
723
|
+
}
|
|
724
|
+
var __testUtils = {
|
|
725
|
+
findAnchorTagByHref
|
|
726
|
+
};
|
|
727
|
+
async function runThemeCheck(theme) {
|
|
574
728
|
const result = checkThemeModule(theme.module);
|
|
575
|
-
const
|
|
576
|
-
|
|
729
|
+
const footerIssues = result.ok ? runFooterContractCheck(theme, await loadThemeScopedRenderer(theme.root)) : [];
|
|
730
|
+
const ok = result.ok && footerIssues.length === 0;
|
|
731
|
+
const lines = [formatThemeCheckResult(result)];
|
|
732
|
+
if (footerIssues.length === 0 && result.ok) {
|
|
733
|
+
lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
|
|
734
|
+
} else if (footerIssues.length > 0) {
|
|
735
|
+
for (const issue of footerIssues) {
|
|
736
|
+
lines.push(` error layoutConfig.footerContract: ${issue}`);
|
|
737
|
+
}
|
|
738
|
+
lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
|
|
739
|
+
}
|
|
740
|
+
const text = lines.join("\n");
|
|
741
|
+
if (ok) {
|
|
577
742
|
console.log(text);
|
|
578
743
|
} else {
|
|
579
744
|
console.error(text);
|
|
580
745
|
}
|
|
581
|
-
return
|
|
746
|
+
return ok;
|
|
582
747
|
}
|
|
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
|
|
748
|
+
async function watchTheme(root, port) {
|
|
749
|
+
const viteConfig = await findViteConfig(root);
|
|
750
|
+
const vite = await loadThemeVite(root);
|
|
751
|
+
const host = "127.0.0.1";
|
|
752
|
+
const server = await vite.createServer({
|
|
753
|
+
root,
|
|
754
|
+
configFile: viteConfig,
|
|
755
|
+
configLoader: "runner",
|
|
756
|
+
cacheDir: path2.join(root, ".suda-build", "vite-cache"),
|
|
757
|
+
appType: "custom",
|
|
758
|
+
server: { host, port },
|
|
759
|
+
plugins: [createSudaPreviewVitePlugin(root)]
|
|
605
760
|
});
|
|
606
|
-
await
|
|
607
|
-
const
|
|
608
|
-
console.log(`
|
|
761
|
+
await server.listen(port);
|
|
762
|
+
const url = server.resolvedUrls?.local.find((candidate) => candidate.includes("127.0.0.1")) ?? server.resolvedUrls?.local[0] ?? `http://${host}:${port}/`;
|
|
763
|
+
console.log(`previewing Vite theme at ${url}`);
|
|
609
764
|
await new Promise(() => void 0);
|
|
610
765
|
}
|
|
611
|
-
function
|
|
766
|
+
function createSudaPreviewVitePlugin(root) {
|
|
612
767
|
return {
|
|
613
|
-
name: "suda-theme-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
768
|
+
name: "suda-theme-preview",
|
|
769
|
+
configureServer(server) {
|
|
770
|
+
server.middlewares.use((request, response, next) => {
|
|
771
|
+
void (async () => {
|
|
772
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
773
|
+
const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
|
|
774
|
+
if (assetPrefixMatch?.[1]) {
|
|
775
|
+
request.url = `/${assetPrefixMatch[1]}${url.search}`;
|
|
776
|
+
next();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (url.pathname !== "/" && !url.pathname.startsWith("/pages/")) {
|
|
780
|
+
next();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const theme = await loadViteDevTheme(root, server);
|
|
785
|
+
const renderer = await loadThemeScopedRenderer(root);
|
|
786
|
+
if (url.pathname === "/") {
|
|
787
|
+
const fallbackSlug = pickStarterSlug(theme);
|
|
788
|
+
if (!fallbackSlug) {
|
|
789
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
790
|
+
response.end("No starter pages declared by this theme.");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const requested = url.searchParams.get("page");
|
|
794
|
+
const activeSlug = requested && findStarterPage(theme, requested) ? requested : fallbackSlug;
|
|
795
|
+
const html2 = await server.transformIndexHtml(
|
|
796
|
+
url.pathname,
|
|
797
|
+
renderPreviewShellHtml(theme, activeSlug)
|
|
798
|
+
);
|
|
799
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
800
|
+
response.end(html2);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const slug = decodeURIComponent(url.pathname.slice("/pages/".length));
|
|
804
|
+
const starter = findStarterPage(theme, slug);
|
|
805
|
+
if (!starter) {
|
|
806
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
807
|
+
response.end(`Unknown starter page: ${slug}`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const html = await server.transformIndexHtml(
|
|
811
|
+
url.pathname,
|
|
812
|
+
renderDevStarterPageHtml(theme, starter, renderer)
|
|
813
|
+
);
|
|
814
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
815
|
+
response.end(html);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
818
|
+
response.end(error instanceof Error ? error.stack : String(error));
|
|
819
|
+
}
|
|
820
|
+
})();
|
|
630
821
|
});
|
|
631
822
|
}
|
|
632
823
|
};
|
|
633
824
|
}
|
|
825
|
+
async function loadViteDevTheme(root, server) {
|
|
826
|
+
const imported = await server.ssrLoadModule("/src/index.tsx");
|
|
827
|
+
if (!imported.default) {
|
|
828
|
+
throw new Error("src/index.tsx must export a default ThemeModule.");
|
|
829
|
+
}
|
|
830
|
+
validateThemeModule(imported.default);
|
|
831
|
+
return {
|
|
832
|
+
root,
|
|
833
|
+
module: imported.default,
|
|
834
|
+
serverEntryPath: path2.join(root, "src", "index.tsx"),
|
|
835
|
+
clientEntryPath: null,
|
|
836
|
+
stylesheetPath: path2.join(root, "src", "styles.css")
|
|
837
|
+
};
|
|
838
|
+
}
|
|
634
839
|
var PREVIEW_PUBLIC_BASE_PATH = "/api/themes";
|
|
635
840
|
function pickStarterSlug(theme) {
|
|
636
841
|
const pages = theme.module.starterPages;
|
|
@@ -676,7 +881,7 @@ async function loadThemeScopedRenderer(themeRoot) {
|
|
|
676
881
|
extractLayoutChrome: themeEngineServerModule.extractLayoutChrome,
|
|
677
882
|
resolveAssetPath: themeEngineServerModule.resolveAssetPath,
|
|
678
883
|
createElement: reactModule.createElement,
|
|
679
|
-
|
|
884
|
+
renderToString: reactDomServerModule.renderToString
|
|
680
885
|
};
|
|
681
886
|
}
|
|
682
887
|
function renderStarterPageHtml(theme, resolved, page, renderer) {
|
|
@@ -684,7 +889,7 @@ function renderStarterPageHtml(theme, resolved, page, renderer) {
|
|
|
684
889
|
const metadata = {
|
|
685
890
|
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath)
|
|
686
891
|
};
|
|
687
|
-
const body = renderer.
|
|
892
|
+
const body = renderer.renderToString(
|
|
688
893
|
renderer.createElement(renderer.ThemeRender, {
|
|
689
894
|
theme: theme.module,
|
|
690
895
|
pageData: page.data,
|
|
@@ -718,6 +923,51 @@ function renderStarterPageHtml(theme, resolved, page, renderer) {
|
|
|
718
923
|
"</html>"
|
|
719
924
|
].join("");
|
|
720
925
|
}
|
|
926
|
+
function renderDevStarterPageHtml(theme, page, renderer) {
|
|
927
|
+
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
928
|
+
const metadata = {
|
|
929
|
+
resolveAssetUrl: (value) => {
|
|
930
|
+
if (!value) {
|
|
931
|
+
return value;
|
|
932
|
+
}
|
|
933
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/")) {
|
|
934
|
+
return value;
|
|
935
|
+
}
|
|
936
|
+
if (value.startsWith("themes/")) {
|
|
937
|
+
const parts = value.split("/");
|
|
938
|
+
return `/${parts.slice(3).join("/")}`;
|
|
939
|
+
}
|
|
940
|
+
return value;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const body = renderer.renderToString(
|
|
944
|
+
renderer.createElement(renderer.ThemeRender, {
|
|
945
|
+
theme: theme.module,
|
|
946
|
+
pageData: page.data,
|
|
947
|
+
layoutData: theme.module.defaultLayout,
|
|
948
|
+
metadata
|
|
949
|
+
})
|
|
950
|
+
);
|
|
951
|
+
const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
952
|
+
const customHead = chrome.customHeadCode ?? "";
|
|
953
|
+
const customBody = chrome.customBodyCode ?? "";
|
|
954
|
+
return [
|
|
955
|
+
"<!doctype html>",
|
|
956
|
+
'<html lang="en">',
|
|
957
|
+
"<head>",
|
|
958
|
+
'<meta charset="utf-8" />',
|
|
959
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
960
|
+
`<title>${escapeHtml(`${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
|
|
961
|
+
'<link rel="stylesheet" href="/src/styles.css" />',
|
|
962
|
+
customHead,
|
|
963
|
+
"</head>",
|
|
964
|
+
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
965
|
+
body,
|
|
966
|
+
customBody,
|
|
967
|
+
"</body>",
|
|
968
|
+
"</html>"
|
|
969
|
+
].join("");
|
|
970
|
+
}
|
|
721
971
|
function renderPreviewShellHtml(theme, activeSlug) {
|
|
722
972
|
const items = theme.module.starterPages.map((page) => {
|
|
723
973
|
const slug = page.slug;
|
|
@@ -923,7 +1173,7 @@ async function captureScreenshot(theme, options) {
|
|
|
923
1173
|
}
|
|
924
1174
|
async function screenshotTheme(root, options) {
|
|
925
1175
|
const resolved = resolveScreenshotOptions(root, options);
|
|
926
|
-
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root
|
|
1176
|
+
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root);
|
|
927
1177
|
await captureScreenshot(theme, resolved);
|
|
928
1178
|
}
|
|
929
1179
|
async function loadLocalAgentManifest(root) {
|
|
@@ -1460,14 +1710,11 @@ ${lines.join("\n")}`;
|
|
|
1460
1710
|
}
|
|
1461
1711
|
return message;
|
|
1462
1712
|
}
|
|
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
|
-
}
|
|
1713
|
+
async function publishTheme(root, skipBuild, force) {
|
|
1714
|
+
const theme = skipBuild ? await finalizeTheme(root) : await buildTheme(root);
|
|
1715
|
+
const ok = await runThemeCheck(theme);
|
|
1716
|
+
if (!ok) {
|
|
1717
|
+
throw new Error("AI metadata check failed. Fix the errors above.");
|
|
1471
1718
|
}
|
|
1472
1719
|
const files = await collectThemeArtifactFiles(theme);
|
|
1473
1720
|
if (!theme.clientEntryPath) {
|
|
@@ -1600,6 +1847,14 @@ async function readCliPackageVersions() {
|
|
|
1600
1847
|
resolved = `^${engineVersion}`;
|
|
1601
1848
|
}
|
|
1602
1849
|
} catch {
|
|
1850
|
+
const monorepoEnginePkgPath = path2.resolve(path2.dirname(here), "..", "..", "theme-engine", "package.json");
|
|
1851
|
+
const engineRaw = await readFile(monorepoEnginePkgPath, "utf8").catch(() => null);
|
|
1852
|
+
if (engineRaw) {
|
|
1853
|
+
const engineVersion = JSON.parse(engineRaw).version;
|
|
1854
|
+
if (typeof engineVersion === "string" && engineVersion.length > 0) {
|
|
1855
|
+
resolved = `^${engineVersion}`;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1603
1858
|
}
|
|
1604
1859
|
if (!resolved) {
|
|
1605
1860
|
throw new Error(
|
|
@@ -1614,720 +1869,43 @@ async function readCliPackageVersions() {
|
|
|
1614
1869
|
};
|
|
1615
1870
|
return cachedCliPackageVersions;
|
|
1616
1871
|
}
|
|
1872
|
+
async function replaceTemplateTokens(filePath, replacements) {
|
|
1873
|
+
let contents = await readFile(filePath, "utf8");
|
|
1874
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
1875
|
+
contents = contents.split(token).join(value);
|
|
1876
|
+
}
|
|
1877
|
+
await writeFile(filePath, contents);
|
|
1878
|
+
}
|
|
1879
|
+
async function loadThemeTemplateFile(relativePath) {
|
|
1880
|
+
return readFile(path2.join(themeTemplateRoot, relativePath), "utf8");
|
|
1881
|
+
}
|
|
1617
1882
|
async function initTheme(target) {
|
|
1618
1883
|
const key = path2.basename(target).replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
|
|
1619
1884
|
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
|
-
);
|
|
1885
|
+
await copyDirectory(themeTemplateRoot, target);
|
|
1886
|
+
const packageJson = JSON.parse(await loadThemeTemplateFile("package.json"));
|
|
1887
|
+
packageJson.name = `@suda-themes/${key}`;
|
|
1888
|
+
packageJson.devDependencies["@sudajs/cli"] = cliPackageVersions.cli;
|
|
1889
|
+
packageJson.devDependencies["@sudajs/theme-engine"] = cliPackageVersions.themeEngine;
|
|
1890
|
+
packageJson.peerDependencies["@sudajs/theme-engine"] = cliPackageVersions.themeEngine;
|
|
1891
|
+
await writeFile(path2.join(target, "package.json"), `${JSON.stringify(packageJson, null, 2)}
|
|
1892
|
+
`);
|
|
1893
|
+
const replacements = {
|
|
1894
|
+
"__SUDA_THEME_KEY__": key
|
|
1895
|
+
};
|
|
1896
|
+
const filesToReplace = [
|
|
1897
|
+
"README.md",
|
|
1898
|
+
"src/manifest.ts",
|
|
1899
|
+
"src/theme-asset.ts",
|
|
1900
|
+
"src/sections.tsx",
|
|
1901
|
+
"src/layout.tsx",
|
|
1902
|
+
"src/config.ts",
|
|
1903
|
+
"src/templates.ts",
|
|
1904
|
+
"src/styles.css"
|
|
1905
|
+
];
|
|
1906
|
+
for (const relativePath of filesToReplace) {
|
|
1907
|
+
await replaceTemplateTokens(path2.join(target, relativePath), replacements);
|
|
1908
|
+
}
|
|
2331
1909
|
console.log(`created theme scaffold at ${target}`);
|
|
2332
1910
|
}
|
|
2333
1911
|
function buildProgram() {
|
|
@@ -2346,46 +1924,41 @@ function buildProgram() {
|
|
|
2346
1924
|
);
|
|
2347
1925
|
}
|
|
2348
1926
|
});
|
|
2349
|
-
theme.command("build").description("Build
|
|
2350
|
-
const result = await buildTheme(resolveThemeRoot(options)
|
|
1927
|
+
theme.command("build").description("Build a Vite Suda theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
1928
|
+
const result = await buildTheme(resolveThemeRoot(options));
|
|
2351
1929
|
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
|
-
}
|
|
1930
|
+
const ok = await runThemeCheck(result);
|
|
1931
|
+
if (!ok) {
|
|
1932
|
+
throw new Error("AI metadata check failed. Fix the errors above.");
|
|
2359
1933
|
}
|
|
2360
1934
|
});
|
|
1935
|
+
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) => {
|
|
1936
|
+
const result = await finalizeTheme(resolveThemeRoot(options));
|
|
1937
|
+
console.log(`finalized ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
1938
|
+
});
|
|
2361
1939
|
theme.command("check").description(
|
|
2362
1940
|
"Run AI metadata checks on a built theme without rebuilding. Reports missing component-level ai.instructions as errors and weak content as warnings."
|
|
2363
1941
|
).option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
2364
1942
|
const theme2 = await validateTheme(resolveThemeRoot(options));
|
|
2365
|
-
const ok = runThemeCheck(theme2);
|
|
1943
|
+
const ok = await runThemeCheck(theme2);
|
|
2366
1944
|
if (!ok) {
|
|
2367
1945
|
process.exitCode = 1;
|
|
2368
1946
|
}
|
|
2369
1947
|
});
|
|
2370
|
-
theme.command("dev").description("
|
|
1948
|
+
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
1949
|
const port = Number(options.port ?? "4177");
|
|
2372
|
-
await watchTheme(
|
|
2373
|
-
resolveThemeRoot(options),
|
|
2374
|
-
options.skipThemeBuild === true,
|
|
2375
|
-
Number.isFinite(port) ? port : 4177
|
|
2376
|
-
);
|
|
1950
|
+
await watchTheme(resolveThemeRoot(options), Number.isFinite(port) ? port : 4177);
|
|
2377
1951
|
});
|
|
2378
1952
|
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
1953
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
2380
1954
|
});
|
|
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(
|
|
1955
|
+
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
1956
|
"--force",
|
|
2383
1957
|
"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
1958
|
).action(async (options) => {
|
|
2385
1959
|
await publishTheme(
|
|
2386
1960
|
resolveThemeRoot(options),
|
|
2387
1961
|
options.skipBuild === true,
|
|
2388
|
-
options.skipCheck === true,
|
|
2389
1962
|
options.force === true
|
|
2390
1963
|
);
|
|
2391
1964
|
});
|
|
@@ -2469,6 +2042,6 @@ async function main() {
|
|
|
2469
2042
|
await program.parseAsync(argv);
|
|
2470
2043
|
}
|
|
2471
2044
|
|
|
2472
|
-
export { buildProgram, main, parseThemeRef };
|
|
2045
|
+
export { __testUtils, buildProgram, buildTheme, finalizeTheme, initTheme, main, parseThemeRef, validateTheme, watchTheme };
|
|
2473
2046
|
//# sourceMappingURL=index.js.map
|
|
2474
2047
|
//# sourceMappingURL=index.js.map
|