@sudajs/cli 0.8.2 → 0.9.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 +72 -2
- package/dist/index.js +238 -242
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/templates/theme/AGENTS.md +2 -1
- package/templates/theme/README.md +11 -2
- package/templates/theme/src/manifest.ts +2 -2
- package/templates/theme/src/sections.tsx +1 -1
- package/templates/theme/src/templates.ts +1 -1
- /package/templates/theme/{src/assets → assets/brand}/suda-logo.svg +0 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
10
|
import { createThemeAgentManifest, createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, validateAgentPageContentWithManifest, agentValidationResultSchema, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
11
|
-
import { themeManifestSchema
|
|
11
|
+
import { themeManifestSchema } from '@sudajs/theme-engine/server';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import { build } from 'esbuild';
|
|
14
14
|
import { z } from 'zod';
|
|
@@ -409,6 +409,10 @@ function createThemeObjectKey(themeKey, version, relativePath) {
|
|
|
409
409
|
function resolveThemeRoot(options) {
|
|
410
410
|
return path2.resolve(options.themeRoot ?? process.cwd());
|
|
411
411
|
}
|
|
412
|
+
function collectOption(value, previous) {
|
|
413
|
+
previous.push(value);
|
|
414
|
+
return previous;
|
|
415
|
+
}
|
|
412
416
|
async function pathExists(filePath) {
|
|
413
417
|
try {
|
|
414
418
|
await stat(filePath);
|
|
@@ -491,12 +495,12 @@ ${issues}`);
|
|
|
491
495
|
if (!module.manifest.key || !module.manifest.version || !module.manifest.name) {
|
|
492
496
|
throw new Error("manifest.key, manifest.version and manifest.name are required.");
|
|
493
497
|
}
|
|
494
|
-
if (module.manifest.entry !== "
|
|
495
|
-
throw new Error('manifest.entry must be artifact-local: "
|
|
498
|
+
if (module.manifest.entry !== "index.js") {
|
|
499
|
+
throw new Error('manifest.entry must be artifact-local: "index.js".');
|
|
496
500
|
}
|
|
497
|
-
if (module.manifest.clientEntry !== void 0 && module.manifest.clientEntry !== "
|
|
501
|
+
if (module.manifest.clientEntry !== void 0 && module.manifest.clientEntry !== "runtime.client.js") {
|
|
498
502
|
throw new Error(
|
|
499
|
-
'manifest.clientEntry must be artifact-local: "
|
|
503
|
+
'manifest.clientEntry must be artifact-local: "runtime.client.js" when provided.'
|
|
500
504
|
);
|
|
501
505
|
}
|
|
502
506
|
assertRecord(module.pageConfig?.components, "pageConfig.components");
|
|
@@ -631,7 +635,7 @@ async function buildViteTheme(root) {
|
|
|
631
635
|
}
|
|
632
636
|
}
|
|
633
637
|
});
|
|
634
|
-
await
|
|
638
|
+
await copyThemeAssets(root);
|
|
635
639
|
const stylesheetEntry = path2.join(root, ".suda-build", "styles-entry.ts");
|
|
636
640
|
await mkdir(path2.dirname(stylesheetEntry), { recursive: true });
|
|
637
641
|
await writeFile(stylesheetEntry, 'import "../src/styles.css";\n', "utf8");
|
|
@@ -659,8 +663,8 @@ async function buildViteTheme(root) {
|
|
|
659
663
|
});
|
|
660
664
|
await rm(path2.join(root, "dist", "chunks", "theme-styles.js"), { force: true });
|
|
661
665
|
}
|
|
662
|
-
async function
|
|
663
|
-
const sourceAssets = path2.join(root, "
|
|
666
|
+
async function copyThemeAssets(root) {
|
|
667
|
+
const sourceAssets = path2.join(root, "assets");
|
|
664
668
|
if (!await pathExists(sourceAssets)) {
|
|
665
669
|
return;
|
|
666
670
|
}
|
|
@@ -827,13 +831,17 @@ function runFooterContractCheck(theme, renderer) {
|
|
|
827
831
|
return issues;
|
|
828
832
|
}
|
|
829
833
|
var __testUtils = {
|
|
834
|
+
createThemeProjectRequire,
|
|
830
835
|
createRemotePageDraft,
|
|
831
836
|
findAnchorTagByHref,
|
|
832
837
|
loadThemeScopedRenderer,
|
|
838
|
+
normalizePlaywrightModule,
|
|
839
|
+
normalizeSharpModule,
|
|
833
840
|
initThemeWithKey,
|
|
834
841
|
performActivateTheme,
|
|
835
842
|
performConfirmedPageOperation,
|
|
836
843
|
renderDevStarterPageHtml,
|
|
844
|
+
resolveScreenshotOptions,
|
|
837
845
|
slugifyThemeName,
|
|
838
846
|
validateThemeKey,
|
|
839
847
|
validateThemeName,
|
|
@@ -843,7 +851,8 @@ var __testUtils = {
|
|
|
843
851
|
async function runThemeCheck(theme) {
|
|
844
852
|
const result = checkThemeModule(theme.module);
|
|
845
853
|
const footerIssues = result.ok ? runFooterContractCheck(theme, await loadThemeScopedRenderer(theme.root)) : [];
|
|
846
|
-
const
|
|
854
|
+
const previewIssues = await runPreviewContractCheck(theme);
|
|
855
|
+
const ok = result.ok && footerIssues.length === 0 && previewIssues.length === 0;
|
|
847
856
|
const lines = [formatThemeCheckResult(result)];
|
|
848
857
|
if (footerIssues.length === 0 && result.ok) {
|
|
849
858
|
lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
|
|
@@ -853,6 +862,14 @@ async function runThemeCheck(theme) {
|
|
|
853
862
|
}
|
|
854
863
|
lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
|
|
855
864
|
}
|
|
865
|
+
if (previewIssues.length === 0) {
|
|
866
|
+
lines.push("Preview contract check passed: desktop, tablet, and mobile screenshots exist.");
|
|
867
|
+
} else {
|
|
868
|
+
for (const issue of previewIssues) {
|
|
869
|
+
lines.push(` error assets.preview: ${issue}`);
|
|
870
|
+
}
|
|
871
|
+
lines.push(`Preview contract check: ${previewIssues.length} error(s).`);
|
|
872
|
+
}
|
|
856
873
|
const text = lines.join("\n");
|
|
857
874
|
if (ok) {
|
|
858
875
|
console.log(text);
|
|
@@ -861,7 +878,27 @@ async function runThemeCheck(theme) {
|
|
|
861
878
|
}
|
|
862
879
|
return ok;
|
|
863
880
|
}
|
|
881
|
+
async function runPreviewContractCheck(theme) {
|
|
882
|
+
const issues = [];
|
|
883
|
+
for (const relativePath of REQUIRED_PREVIEW_ARTIFACTS) {
|
|
884
|
+
const filePath = path2.join(theme.root, "dist", relativePath);
|
|
885
|
+
try {
|
|
886
|
+
const info = await stat(filePath);
|
|
887
|
+
if (info.size <= 0) {
|
|
888
|
+
issues.push(`${relativePath} is empty. Run \`suda theme capture\` and rebuild.`);
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
issues.push(`Missing ${relativePath}. Run \`suda theme capture\`, then \`suda theme build\`.`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return issues;
|
|
895
|
+
}
|
|
864
896
|
async function watchTheme(root, port) {
|
|
897
|
+
const handle = await startViteDevPreviewServer(root, port);
|
|
898
|
+
console.log(`previewing Vite theme at ${handle.url}`);
|
|
899
|
+
await new Promise(() => void 0);
|
|
900
|
+
}
|
|
901
|
+
async function startViteDevPreviewServer(root, port) {
|
|
865
902
|
const viteConfig = await findViteConfig(root);
|
|
866
903
|
const vite = await loadThemeVite(root);
|
|
867
904
|
const host = "127.0.0.1";
|
|
@@ -876,8 +913,11 @@ async function watchTheme(root, port) {
|
|
|
876
913
|
});
|
|
877
914
|
await server.listen(port);
|
|
878
915
|
const url = server.resolvedUrls?.local.find((candidate) => candidate.includes("127.0.0.1")) ?? server.resolvedUrls?.local[0] ?? `http://${host}:${port}/`;
|
|
879
|
-
|
|
880
|
-
|
|
916
|
+
return {
|
|
917
|
+
url: url.replace(/\/$/, ""),
|
|
918
|
+
port,
|
|
919
|
+
close: () => server.close()
|
|
920
|
+
};
|
|
881
921
|
}
|
|
882
922
|
function createSudaPreviewVitePlugin(root) {
|
|
883
923
|
return {
|
|
@@ -888,7 +928,7 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
888
928
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
889
929
|
const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
|
|
890
930
|
if (assetPrefixMatch?.[1]) {
|
|
891
|
-
request.url = `/
|
|
931
|
+
request.url = `/assets/${assetPrefixMatch[1]}${url.search}`;
|
|
892
932
|
next();
|
|
893
933
|
return;
|
|
894
934
|
}
|
|
@@ -906,11 +946,15 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
906
946
|
response.end("No starter pages declared by this theme.");
|
|
907
947
|
return;
|
|
908
948
|
}
|
|
909
|
-
const
|
|
910
|
-
|
|
949
|
+
const starter2 = findStarterPage(theme, fallbackSlug);
|
|
950
|
+
if (!starter2) {
|
|
951
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
952
|
+
response.end(`Unknown starter page: ${fallbackSlug}`);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
911
955
|
const html2 = await server.transformIndexHtml(
|
|
912
956
|
url.pathname,
|
|
913
|
-
|
|
957
|
+
renderDevStarterPageHtml(theme, starter2, renderer)
|
|
914
958
|
);
|
|
915
959
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
916
960
|
response.end(html2);
|
|
@@ -979,10 +1023,6 @@ function createPreviewAssetResolver(resolveAssetPath) {
|
|
|
979
1023
|
});
|
|
980
1024
|
};
|
|
981
1025
|
}
|
|
982
|
-
function createPreviewThemeAssetUrl(themeKey, themeVersion, relativePath) {
|
|
983
|
-
const normalizedRelativePath = relativePath.replace(/^\/+/, "");
|
|
984
|
-
return `${PREVIEW_PUBLIC_BASE_PATH}/${themeKey}/${themeVersion}/assets/${normalizedRelativePath}?v=${encodeURIComponent(themeVersion)}`;
|
|
985
|
-
}
|
|
986
1026
|
async function loadThemeScopedRenderer(themeRoot) {
|
|
987
1027
|
const themeRequire = createRequire(path2.join(themeRoot, "package.json"));
|
|
988
1028
|
const reactPath = themeRequire.resolve("react");
|
|
@@ -1001,45 +1041,6 @@ async function loadThemeScopedRenderer(themeRoot) {
|
|
|
1001
1041
|
renderToString: reactDomServerModule.renderToString
|
|
1002
1042
|
};
|
|
1003
1043
|
}
|
|
1004
|
-
function renderStarterPageHtml(theme, resolved, page, renderer) {
|
|
1005
|
-
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
1006
|
-
const metadata = {
|
|
1007
|
-
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath)
|
|
1008
|
-
};
|
|
1009
|
-
const body = renderer.renderToString(
|
|
1010
|
-
renderer.createElement(renderer.ThemeRender, {
|
|
1011
|
-
theme: theme.module,
|
|
1012
|
-
pageData: page.data,
|
|
1013
|
-
layoutData: theme.module.defaultLayout,
|
|
1014
|
-
metadata
|
|
1015
|
-
})
|
|
1016
|
-
);
|
|
1017
|
-
const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
1018
|
-
const stylesheetUrl = resolved.runtime?.stylesheet ? createPreviewThemeAssetUrl(
|
|
1019
|
-
theme.module.manifest.key,
|
|
1020
|
-
theme.module.manifest.version,
|
|
1021
|
-
resolved.runtime.stylesheet
|
|
1022
|
-
) : null;
|
|
1023
|
-
const stylesheetTag = stylesheetUrl ? `<link rel="stylesheet" href="${escapeHtml(stylesheetUrl)}" />` : "";
|
|
1024
|
-
const customHead = chrome.customHeadCode ?? "";
|
|
1025
|
-
const customBody = chrome.customBodyCode ?? "";
|
|
1026
|
-
return [
|
|
1027
|
-
"<!doctype html>",
|
|
1028
|
-
'<html lang="en">',
|
|
1029
|
-
"<head>",
|
|
1030
|
-
'<meta charset="utf-8" />',
|
|
1031
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
1032
|
-
`<title>${escapeHtml(`${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
|
|
1033
|
-
stylesheetTag,
|
|
1034
|
-
customHead,
|
|
1035
|
-
"</head>",
|
|
1036
|
-
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
1037
|
-
body,
|
|
1038
|
-
customBody,
|
|
1039
|
-
"</body>",
|
|
1040
|
-
"</html>"
|
|
1041
|
-
].join("");
|
|
1042
|
-
}
|
|
1043
1044
|
function renderDevStarterPageHtml(theme, page, renderer) {
|
|
1044
1045
|
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
1045
1046
|
const metadata = {
|
|
@@ -1073,152 +1074,49 @@ function renderDevStarterPageHtml(theme, page, renderer) {
|
|
|
1073
1074
|
"</html>"
|
|
1074
1075
|
].join("");
|
|
1075
1076
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const renderer = await loadThemeScopedRenderer(theme.root);
|
|
1112
|
-
const registry = new FileSystemThemeRegistry({
|
|
1113
|
-
artifacts: [
|
|
1114
|
-
{
|
|
1115
|
-
rootPath: theme.root,
|
|
1116
|
-
serverEntryPath: theme.serverEntryPath,
|
|
1117
|
-
module: theme.module
|
|
1118
|
-
}
|
|
1119
|
-
]
|
|
1120
|
-
});
|
|
1121
|
-
const themeKey = theme.module.manifest.key;
|
|
1122
|
-
const themeVersion = theme.module.manifest.version;
|
|
1123
|
-
const assetPathPrefix = `${PREVIEW_PUBLIC_BASE_PATH}/${themeKey}/${themeVersion}/assets/`;
|
|
1124
|
-
const clientRuntimePrefix = `${PREVIEW_PUBLIC_BASE_PATH}/${themeKey}/${themeVersion}/runtime/client`;
|
|
1125
|
-
const server = createServer((request, response) => {
|
|
1126
|
-
void (async () => {
|
|
1127
|
-
const url = new URL(request.url ?? "/", `http://localhost:${port}`);
|
|
1128
|
-
const pathname = url.pathname;
|
|
1129
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
1130
|
-
const fallbackSlug = pickStarterSlug(theme);
|
|
1131
|
-
if (!fallbackSlug) {
|
|
1132
|
-
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1133
|
-
response.end("No starter pages declared by this theme.");
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
const requested = url.searchParams.get("page");
|
|
1137
|
-
const activeSlug = requested && findStarterPage(theme, requested) ? requested : fallbackSlug;
|
|
1138
|
-
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1139
|
-
response.end(renderPreviewShellHtml(theme, activeSlug));
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
if (pathname.startsWith("/pages/")) {
|
|
1143
|
-
const slug = decodeURIComponent(pathname.slice("/pages/".length));
|
|
1144
|
-
const starter = findStarterPage(theme, slug);
|
|
1145
|
-
if (!starter) {
|
|
1146
|
-
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1147
|
-
response.end(`Unknown starter page: ${slug}`);
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
const resolved = await registry.resolve(themeKey, themeVersion);
|
|
1151
|
-
if (!resolved) {
|
|
1152
|
-
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1153
|
-
response.end("Failed to resolve theme via FileSystemThemeRegistry.");
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const html = renderStarterPageHtml(theme, resolved, starter, renderer);
|
|
1157
|
-
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1158
|
-
response.end(html);
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
if (pathname.startsWith(assetPathPrefix)) {
|
|
1162
|
-
const relativePath = decodeURIComponent(pathname.slice(assetPathPrefix.length));
|
|
1163
|
-
const filePath = await registry.resolveAssetPath(themeKey, themeVersion, relativePath);
|
|
1164
|
-
if (!filePath) {
|
|
1165
|
-
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1166
|
-
response.end("Not found");
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
const body = await readFile(filePath);
|
|
1170
|
-
response.writeHead(200, {
|
|
1171
|
-
"Content-Type": contentTypeFor(relativePath),
|
|
1172
|
-
"Cache-Control": "no-cache"
|
|
1173
|
-
});
|
|
1174
|
-
response.end(body);
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
if (pathname === clientRuntimePrefix) {
|
|
1178
|
-
const filePath = await registry.resolveClientEntryPath(themeKey, themeVersion);
|
|
1179
|
-
if (!filePath) {
|
|
1180
|
-
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1181
|
-
response.end("Theme has no client runtime bundle.");
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
const body = await readFile(filePath);
|
|
1185
|
-
response.writeHead(200, {
|
|
1186
|
-
"Content-Type": "application/javascript; charset=utf-8",
|
|
1187
|
-
"Cache-Control": "no-cache"
|
|
1188
|
-
});
|
|
1189
|
-
response.end(body);
|
|
1190
|
-
return;
|
|
1077
|
+
var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];
|
|
1078
|
+
var SCREENSHOT_DEVICE_CONFIG = {
|
|
1079
|
+
desktop: {
|
|
1080
|
+
viewport: { width: 1440, height: 900 },
|
|
1081
|
+
outputRelativePath: path2.join("assets", "preview", "desktop.png")
|
|
1082
|
+
},
|
|
1083
|
+
tablet: {
|
|
1084
|
+
viewport: { width: 834, height: 1112 },
|
|
1085
|
+
outputRelativePath: path2.join("assets", "preview", "tablet.png")
|
|
1086
|
+
},
|
|
1087
|
+
mobile: {
|
|
1088
|
+
viewport: { width: 390, height: 844 },
|
|
1089
|
+
outputRelativePath: path2.join("assets", "preview", "mobile.png")
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
var REQUIRED_PREVIEW_ARTIFACTS = SCREENSHOT_DEVICES.map(
|
|
1093
|
+
(device) => `assets/preview/${device}.png`
|
|
1094
|
+
);
|
|
1095
|
+
function isScreenshotDevice(value) {
|
|
1096
|
+
return SCREENSHOT_DEVICES.includes(value);
|
|
1097
|
+
}
|
|
1098
|
+
function parseScreenshotDevices(value) {
|
|
1099
|
+
if (value === void 0) {
|
|
1100
|
+
return [...SCREENSHOT_DEVICES];
|
|
1101
|
+
}
|
|
1102
|
+
const rawValues = Array.isArray(value) ? value : [value];
|
|
1103
|
+
const devices = [];
|
|
1104
|
+
for (const raw of rawValues) {
|
|
1105
|
+
for (const part of raw.split(",")) {
|
|
1106
|
+
const device = part.trim();
|
|
1107
|
+
if (!device) continue;
|
|
1108
|
+
if (!isScreenshotDevice(device)) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`Unknown screenshot device "${device}". Use one of: ${SCREENSHOT_DEVICES.join(", ")}.`
|
|
1111
|
+
);
|
|
1191
1112
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
})().catch((error) => {
|
|
1195
|
-
if (!response.headersSent) {
|
|
1196
|
-
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1113
|
+
if (!devices.includes(device)) {
|
|
1114
|
+
devices.push(device);
|
|
1197
1115
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
await new Promise((resolve, reject) => {
|
|
1202
|
-
const onError = (err) => {
|
|
1203
|
-
reject(err);
|
|
1204
|
-
};
|
|
1205
|
-
server.once("error", onError);
|
|
1206
|
-
server.listen(port, () => {
|
|
1207
|
-
server.off("error", onError);
|
|
1208
|
-
resolve();
|
|
1209
|
-
});
|
|
1210
|
-
});
|
|
1211
|
-
return {
|
|
1212
|
-
url: `http://localhost:${port}`,
|
|
1213
|
-
port,
|
|
1214
|
-
close: () => new Promise((resolve) => {
|
|
1215
|
-
server.close(() => {
|
|
1216
|
-
resolve();
|
|
1217
|
-
});
|
|
1218
|
-
})
|
|
1219
|
-
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return devices.length > 0 ? devices : [...SCREENSHOT_DEVICES];
|
|
1220
1119
|
}
|
|
1221
|
-
var DEFAULT_SCREENSHOT_RELATIVE = path2.join("dist", "preview", "desktop.png");
|
|
1222
1120
|
function parsePositiveInt(value, fallback) {
|
|
1223
1121
|
if (!value) {
|
|
1224
1122
|
return fallback;
|
|
@@ -1227,25 +1125,68 @@ function parsePositiveInt(value, fallback) {
|
|
|
1227
1125
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1228
1126
|
}
|
|
1229
1127
|
function resolveScreenshotOptions(root, options) {
|
|
1128
|
+
const devices = parseScreenshotDevices(options.device);
|
|
1129
|
+
if (options.output && devices.length !== 1) {
|
|
1130
|
+
throw new Error("`--output` can only be used when exactly one `--device` is selected.");
|
|
1131
|
+
}
|
|
1230
1132
|
return {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1133
|
+
targets: devices.map((device) => {
|
|
1134
|
+
const config = SCREENSHOT_DEVICE_CONFIG[device];
|
|
1135
|
+
const viewport = {
|
|
1136
|
+
width: parsePositiveInt(options.width, config.viewport.width),
|
|
1137
|
+
height: parsePositiveInt(options.height, config.viewport.height)
|
|
1138
|
+
};
|
|
1139
|
+
return {
|
|
1140
|
+
device,
|
|
1141
|
+
outputPath: path2.resolve(root, options.output ?? config.outputRelativePath),
|
|
1142
|
+
viewport
|
|
1143
|
+
};
|
|
1144
|
+
}),
|
|
1236
1145
|
port: parsePositiveInt(options.port, 4178),
|
|
1237
|
-
|
|
1146
|
+
fullPage: options.fullPage === true
|
|
1238
1147
|
};
|
|
1239
1148
|
}
|
|
1240
|
-
|
|
1149
|
+
function createThemeProjectRequire(themeRoot) {
|
|
1150
|
+
return createRequire(path2.join(themeRoot, "package.json"));
|
|
1151
|
+
}
|
|
1152
|
+
function normalizePlaywrightModule(moduleValue) {
|
|
1153
|
+
if (typeof moduleValue !== "object" || moduleValue === null) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
if ("chromium" in moduleValue) {
|
|
1157
|
+
const chromium = moduleValue.chromium;
|
|
1158
|
+
if (typeof chromium === "object" && chromium !== null && "launch" in chromium) {
|
|
1159
|
+
return moduleValue;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if ("default" in moduleValue) {
|
|
1163
|
+
return normalizePlaywrightModule(moduleValue.default);
|
|
1164
|
+
}
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
function normalizeSharpModule(moduleValue) {
|
|
1168
|
+
if (typeof moduleValue === "function") {
|
|
1169
|
+
return moduleValue;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof moduleValue !== "object" || moduleValue === null) {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
if ("default" in moduleValue) {
|
|
1175
|
+
return normalizeSharpModule(moduleValue.default);
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
async function loadPlaywright(themeRoot) {
|
|
1180
|
+
const themeRequire = createThemeProjectRequire(themeRoot);
|
|
1241
1181
|
const candidates = ["playwright", "@playwright/test"];
|
|
1242
1182
|
for (const id of candidates) {
|
|
1243
1183
|
try {
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1184
|
+
const resolvedId = themeRequire.resolve(id);
|
|
1185
|
+
const mod = await import(pathToFileURL(resolvedId).href);
|
|
1186
|
+
const playwright = normalizePlaywrightModule(mod);
|
|
1187
|
+
if (playwright) {
|
|
1188
|
+
return playwright;
|
|
1189
|
+
}
|
|
1249
1190
|
} catch {
|
|
1250
1191
|
}
|
|
1251
1192
|
}
|
|
@@ -1253,33 +1194,81 @@ async function loadPlaywright() {
|
|
|
1253
1194
|
"Playwright is required for screenshot capture. Install it as a dev dependency: `pnpm add -D playwright && pnpm exec playwright install chromium`."
|
|
1254
1195
|
);
|
|
1255
1196
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1197
|
+
var warnedMissingSharp = false;
|
|
1198
|
+
var warnedSharpOptimizeFailure = false;
|
|
1199
|
+
async function loadOptionalSharp(themeRoot) {
|
|
1200
|
+
const resolvers = [
|
|
1201
|
+
createThemeProjectRequire(themeRoot),
|
|
1202
|
+
createRequire(path2.join(packageRoot, "package.json"))
|
|
1203
|
+
];
|
|
1204
|
+
for (const resolver of resolvers) {
|
|
1205
|
+
try {
|
|
1206
|
+
const sharpModulePath = resolver.resolve("sharp");
|
|
1207
|
+
const mod = await import(pathToFileURL(sharpModulePath).href);
|
|
1208
|
+
const sharp = normalizeSharpModule(mod);
|
|
1209
|
+
if (sharp) {
|
|
1210
|
+
return sharp;
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
async function optimizePngIfAvailable(themeRoot, filePath) {
|
|
1218
|
+
const sharp = await loadOptionalSharp(themeRoot);
|
|
1219
|
+
if (!sharp) {
|
|
1220
|
+
if (!warnedMissingSharp) {
|
|
1221
|
+
console.warn(
|
|
1222
|
+
"warning: PNG optimization skipped because optional dependency `sharp` is not installed. Install it in the theme project to enable preview compression."
|
|
1223
|
+
);
|
|
1224
|
+
warnedMissingSharp = true;
|
|
1225
|
+
}
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
const optimized = await sharp(filePath).png({ compressionLevel: 9, palette: true }).toBuffer();
|
|
1230
|
+
await writeFile(filePath, optimized);
|
|
1231
|
+
} catch {
|
|
1232
|
+
if (!warnedSharpOptimizeFailure) {
|
|
1233
|
+
console.warn(
|
|
1234
|
+
"warning: PNG optimization failed. The screenshot was saved uncompressed."
|
|
1235
|
+
);
|
|
1236
|
+
warnedSharpOptimizeFailure = true;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async function captureScreenshot(root, options) {
|
|
1241
|
+
const playwright = await loadPlaywright(root);
|
|
1242
|
+
const handle = await startViteDevPreviewServer(root, options.port);
|
|
1259
1243
|
try {
|
|
1260
|
-
await mkdir(path2.dirname(options.outputPath), { recursive: true });
|
|
1261
1244
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
1262
1245
|
try {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1246
|
+
for (const target of options.targets) {
|
|
1247
|
+
await mkdir(path2.dirname(target.outputPath), { recursive: true });
|
|
1248
|
+
const context = await browser.newContext({
|
|
1249
|
+
viewport: target.viewport,
|
|
1250
|
+
deviceScaleFactor: 1
|
|
1251
|
+
});
|
|
1252
|
+
try {
|
|
1253
|
+
const page = await context.newPage();
|
|
1254
|
+
await page.goto(handle.url, { waitUntil: "networkidle" });
|
|
1255
|
+
await page.screenshot({ path: target.outputPath, fullPage: options.fullPage });
|
|
1256
|
+
} finally {
|
|
1257
|
+
await context.close();
|
|
1258
|
+
}
|
|
1259
|
+
await optimizePngIfAvailable(root, target.outputPath);
|
|
1260
|
+
console.log(`captured ${target.device} ${path2.relative(root, target.outputPath)}`);
|
|
1261
|
+
}
|
|
1271
1262
|
} finally {
|
|
1272
1263
|
await browser.close();
|
|
1273
1264
|
}
|
|
1274
1265
|
} finally {
|
|
1275
1266
|
await handle.close();
|
|
1276
1267
|
}
|
|
1277
|
-
console.log(`captured ${path2.relative(theme.root, options.outputPath)}`);
|
|
1278
1268
|
}
|
|
1279
1269
|
async function screenshotTheme(root, options) {
|
|
1280
1270
|
const resolved = resolveScreenshotOptions(root, options);
|
|
1281
|
-
|
|
1282
|
-
await captureScreenshot(theme, resolved);
|
|
1271
|
+
await captureScreenshot(root, resolved);
|
|
1283
1272
|
}
|
|
1284
1273
|
async function loadLocalAgentManifest(root) {
|
|
1285
1274
|
const built = await readJsonIfExists(
|
|
@@ -1965,10 +1954,11 @@ async function collectFiles(root, directory) {
|
|
|
1965
1954
|
continue;
|
|
1966
1955
|
}
|
|
1967
1956
|
const relativePath = path2.relative(root, absolutePath).replaceAll(path2.sep, "/");
|
|
1957
|
+
const publishRelativePath = relativePath.startsWith("dist/") ? relativePath.slice("dist/".length) : relativePath;
|
|
1968
1958
|
files.push({
|
|
1969
1959
|
absolutePath,
|
|
1970
|
-
relativePath,
|
|
1971
|
-
contentType: contentTypeFor(
|
|
1960
|
+
relativePath: publishRelativePath,
|
|
1961
|
+
contentType: contentTypeFor(publishRelativePath)
|
|
1972
1962
|
});
|
|
1973
1963
|
}
|
|
1974
1964
|
return files;
|
|
@@ -1978,10 +1968,12 @@ async function collectThemeArtifactFiles(theme) {
|
|
|
1978
1968
|
return files;
|
|
1979
1969
|
}
|
|
1980
1970
|
var SSR_REQUIRED_ARTIFACTS = [
|
|
1981
|
-
"
|
|
1982
|
-
"
|
|
1983
|
-
"
|
|
1984
|
-
"
|
|
1971
|
+
"index.js",
|
|
1972
|
+
"runtime.client.js",
|
|
1973
|
+
"manifest.json",
|
|
1974
|
+
"styles.css",
|
|
1975
|
+
"agent-manifest.json",
|
|
1976
|
+
...REQUIRED_PREVIEW_ARTIFACTS
|
|
1985
1977
|
];
|
|
1986
1978
|
async function checksum(files) {
|
|
1987
1979
|
const hash = createHash("sha256");
|
|
@@ -2017,13 +2009,13 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2017
2009
|
}
|
|
2018
2010
|
const files = await collectThemeArtifactFiles(theme);
|
|
2019
2011
|
if (!theme.clientEntryPath) {
|
|
2020
|
-
throw new Error("Missing
|
|
2012
|
+
throw new Error("Missing runtime.client.js. Run `suda theme build` first.");
|
|
2021
2013
|
}
|
|
2022
2014
|
const collectedPaths = new Set(files.map((f) => f.relativePath));
|
|
2023
2015
|
for (const required of SSR_REQUIRED_ARTIFACTS) {
|
|
2024
2016
|
if (!collectedPaths.has(required)) {
|
|
2025
2017
|
throw new Error(
|
|
2026
|
-
`Missing required
|
|
2018
|
+
`Missing required theme artifact: ${required}. Run \`suda theme build\` and ensure \`${required}\` is generated.`
|
|
2027
2019
|
);
|
|
2028
2020
|
}
|
|
2029
2021
|
}
|
|
@@ -2090,9 +2082,8 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2090
2082
|
}
|
|
2091
2083
|
console.log(`uploaded ${createThemeObjectKey(key, version, file.relativePath)}`);
|
|
2092
2084
|
}
|
|
2093
|
-
const bundleArtifactRelative = "
|
|
2094
|
-
const previewArtifactRelative = "
|
|
2095
|
-
const previewArtifactExists = await pathExists(path2.join(theme.root, previewArtifactRelative));
|
|
2085
|
+
const bundleArtifactRelative = "index.js";
|
|
2086
|
+
const previewArtifactRelative = "assets/preview/desktop.png";
|
|
2096
2087
|
const agentManifest = createThemeAgentManifest(theme.module);
|
|
2097
2088
|
const completeRes = await fetch(`${baseUrl}/api/cli/themes/publish-complete`, {
|
|
2098
2089
|
method: "POST",
|
|
@@ -2107,7 +2098,7 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2107
2098
|
manifest: theme.module.manifest,
|
|
2108
2099
|
agentManifest,
|
|
2109
2100
|
bundleArtifactRelative,
|
|
2110
|
-
previewArtifactRelative
|
|
2101
|
+
previewArtifactRelative
|
|
2111
2102
|
})
|
|
2112
2103
|
});
|
|
2113
2104
|
if (!completeRes.ok) {
|
|
@@ -2304,7 +2295,7 @@ function buildProgram() {
|
|
|
2304
2295
|
console.log(`valid ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
2305
2296
|
if (!result.clientEntryPath) {
|
|
2306
2297
|
console.warn(
|
|
2307
|
-
"warning:
|
|
2298
|
+
"warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
|
|
2308
2299
|
);
|
|
2309
2300
|
}
|
|
2310
2301
|
});
|
|
@@ -2333,7 +2324,12 @@ function buildProgram() {
|
|
|
2333
2324
|
const port = Number(options.port ?? "4177");
|
|
2334
2325
|
await watchTheme(resolveThemeRoot(options), Number.isFinite(port) ? port : 4177);
|
|
2335
2326
|
});
|
|
2336
|
-
theme.command("screenshot").description("Capture
|
|
2327
|
+
theme.command("screenshot").alias("capture").description("Capture preview screenshots of the home starter page using Playwright.").option("--theme-root <path>", "Theme source/artifact root.").option(
|
|
2328
|
+
"--device <device>",
|
|
2329
|
+
"Device to capture: desktop, tablet, mobile. Repeat or use comma-separated values.",
|
|
2330
|
+
collectOption,
|
|
2331
|
+
[]
|
|
2332
|
+
).option("--output <path>", "Output PNG path relative to theme root. Requires exactly one device.").option("--width <px>", "Override viewport width in pixels.").option("--height <px>", "Override viewport height in pixels.").option("--full-page", "Capture the full page instead of the viewport.").option("--port <port>", "Preview server port used during capture.", "4178").action(async (options) => {
|
|
2337
2333
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
2338
2334
|
});
|
|
2339
2335
|
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(
|