@sudajs/cli 0.8.2 → 0.9.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 +74 -2
- package/dist/index.js +240 -242
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/templates/theme/AGENTS.md +89 -31
- 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,18 @@ function runFooterContractCheck(theme, renderer) {
|
|
|
827
831
|
return issues;
|
|
828
832
|
}
|
|
829
833
|
var __testUtils = {
|
|
834
|
+
createThemeProjectRequire,
|
|
830
835
|
createRemotePageDraft,
|
|
831
836
|
findAnchorTagByHref,
|
|
837
|
+
formatMissingSharpWarning,
|
|
832
838
|
loadThemeScopedRenderer,
|
|
839
|
+
normalizePlaywrightModule,
|
|
840
|
+
normalizeSharpModule,
|
|
833
841
|
initThemeWithKey,
|
|
834
842
|
performActivateTheme,
|
|
835
843
|
performConfirmedPageOperation,
|
|
836
844
|
renderDevStarterPageHtml,
|
|
845
|
+
resolveScreenshotOptions,
|
|
837
846
|
slugifyThemeName,
|
|
838
847
|
validateThemeKey,
|
|
839
848
|
validateThemeName,
|
|
@@ -843,7 +852,8 @@ var __testUtils = {
|
|
|
843
852
|
async function runThemeCheck(theme) {
|
|
844
853
|
const result = checkThemeModule(theme.module);
|
|
845
854
|
const footerIssues = result.ok ? runFooterContractCheck(theme, await loadThemeScopedRenderer(theme.root)) : [];
|
|
846
|
-
const
|
|
855
|
+
const previewIssues = await runPreviewContractCheck(theme);
|
|
856
|
+
const ok = result.ok && footerIssues.length === 0 && previewIssues.length === 0;
|
|
847
857
|
const lines = [formatThemeCheckResult(result)];
|
|
848
858
|
if (footerIssues.length === 0 && result.ok) {
|
|
849
859
|
lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
|
|
@@ -853,6 +863,14 @@ async function runThemeCheck(theme) {
|
|
|
853
863
|
}
|
|
854
864
|
lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
|
|
855
865
|
}
|
|
866
|
+
if (previewIssues.length === 0) {
|
|
867
|
+
lines.push("Preview contract check passed: desktop, tablet, and mobile screenshots exist.");
|
|
868
|
+
} else {
|
|
869
|
+
for (const issue of previewIssues) {
|
|
870
|
+
lines.push(` error assets.preview: ${issue}`);
|
|
871
|
+
}
|
|
872
|
+
lines.push(`Preview contract check: ${previewIssues.length} error(s).`);
|
|
873
|
+
}
|
|
856
874
|
const text = lines.join("\n");
|
|
857
875
|
if (ok) {
|
|
858
876
|
console.log(text);
|
|
@@ -861,7 +879,27 @@ async function runThemeCheck(theme) {
|
|
|
861
879
|
}
|
|
862
880
|
return ok;
|
|
863
881
|
}
|
|
882
|
+
async function runPreviewContractCheck(theme) {
|
|
883
|
+
const issues = [];
|
|
884
|
+
for (const relativePath of REQUIRED_PREVIEW_ARTIFACTS) {
|
|
885
|
+
const filePath = path2.join(theme.root, "dist", relativePath);
|
|
886
|
+
try {
|
|
887
|
+
const info = await stat(filePath);
|
|
888
|
+
if (info.size <= 0) {
|
|
889
|
+
issues.push(`${relativePath} is empty. Run \`suda theme capture\` and rebuild.`);
|
|
890
|
+
}
|
|
891
|
+
} catch {
|
|
892
|
+
issues.push(`Missing ${relativePath}. Run \`suda theme capture\`, then \`suda theme build\`.`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return issues;
|
|
896
|
+
}
|
|
864
897
|
async function watchTheme(root, port) {
|
|
898
|
+
const handle = await startViteDevPreviewServer(root, port);
|
|
899
|
+
console.log(`previewing Vite theme at ${handle.url}`);
|
|
900
|
+
await new Promise(() => void 0);
|
|
901
|
+
}
|
|
902
|
+
async function startViteDevPreviewServer(root, port) {
|
|
865
903
|
const viteConfig = await findViteConfig(root);
|
|
866
904
|
const vite = await loadThemeVite(root);
|
|
867
905
|
const host = "127.0.0.1";
|
|
@@ -876,8 +914,11 @@ async function watchTheme(root, port) {
|
|
|
876
914
|
});
|
|
877
915
|
await server.listen(port);
|
|
878
916
|
const url = server.resolvedUrls?.local.find((candidate) => candidate.includes("127.0.0.1")) ?? server.resolvedUrls?.local[0] ?? `http://${host}:${port}/`;
|
|
879
|
-
|
|
880
|
-
|
|
917
|
+
return {
|
|
918
|
+
url: url.replace(/\/$/, ""),
|
|
919
|
+
port,
|
|
920
|
+
close: () => server.close()
|
|
921
|
+
};
|
|
881
922
|
}
|
|
882
923
|
function createSudaPreviewVitePlugin(root) {
|
|
883
924
|
return {
|
|
@@ -888,7 +929,7 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
888
929
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
889
930
|
const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
|
|
890
931
|
if (assetPrefixMatch?.[1]) {
|
|
891
|
-
request.url = `/
|
|
932
|
+
request.url = `/assets/${assetPrefixMatch[1]}${url.search}`;
|
|
892
933
|
next();
|
|
893
934
|
return;
|
|
894
935
|
}
|
|
@@ -906,11 +947,15 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
906
947
|
response.end("No starter pages declared by this theme.");
|
|
907
948
|
return;
|
|
908
949
|
}
|
|
909
|
-
const
|
|
910
|
-
|
|
950
|
+
const starter2 = findStarterPage(theme, fallbackSlug);
|
|
951
|
+
if (!starter2) {
|
|
952
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
953
|
+
response.end(`Unknown starter page: ${fallbackSlug}`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
911
956
|
const html2 = await server.transformIndexHtml(
|
|
912
957
|
url.pathname,
|
|
913
|
-
|
|
958
|
+
renderDevStarterPageHtml(theme, starter2, renderer)
|
|
914
959
|
);
|
|
915
960
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
916
961
|
response.end(html2);
|
|
@@ -979,10 +1024,6 @@ function createPreviewAssetResolver(resolveAssetPath) {
|
|
|
979
1024
|
});
|
|
980
1025
|
};
|
|
981
1026
|
}
|
|
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
1027
|
async function loadThemeScopedRenderer(themeRoot) {
|
|
987
1028
|
const themeRequire = createRequire(path2.join(themeRoot, "package.json"));
|
|
988
1029
|
const reactPath = themeRequire.resolve("react");
|
|
@@ -1001,45 +1042,6 @@ async function loadThemeScopedRenderer(themeRoot) {
|
|
|
1001
1042
|
renderToString: reactDomServerModule.renderToString
|
|
1002
1043
|
};
|
|
1003
1044
|
}
|
|
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
1045
|
function renderDevStarterPageHtml(theme, page, renderer) {
|
|
1044
1046
|
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
1045
1047
|
const metadata = {
|
|
@@ -1073,152 +1075,49 @@ function renderDevStarterPageHtml(theme, page, renderer) {
|
|
|
1073
1075
|
"</html>"
|
|
1074
1076
|
].join("");
|
|
1075
1077
|
}
|
|
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;
|
|
1078
|
+
var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];
|
|
1079
|
+
var SCREENSHOT_DEVICE_CONFIG = {
|
|
1080
|
+
desktop: {
|
|
1081
|
+
viewport: { width: 1440, height: 900 },
|
|
1082
|
+
outputRelativePath: path2.join("assets", "preview", "desktop.png")
|
|
1083
|
+
},
|
|
1084
|
+
tablet: {
|
|
1085
|
+
viewport: { width: 834, height: 1112 },
|
|
1086
|
+
outputRelativePath: path2.join("assets", "preview", "tablet.png")
|
|
1087
|
+
},
|
|
1088
|
+
mobile: {
|
|
1089
|
+
viewport: { width: 390, height: 844 },
|
|
1090
|
+
outputRelativePath: path2.join("assets", "preview", "mobile.png")
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
var REQUIRED_PREVIEW_ARTIFACTS = SCREENSHOT_DEVICES.map(
|
|
1094
|
+
(device) => `assets/preview/${device}.png`
|
|
1095
|
+
);
|
|
1096
|
+
function isScreenshotDevice(value) {
|
|
1097
|
+
return SCREENSHOT_DEVICES.includes(value);
|
|
1098
|
+
}
|
|
1099
|
+
function parseScreenshotDevices(value) {
|
|
1100
|
+
if (value === void 0) {
|
|
1101
|
+
return [...SCREENSHOT_DEVICES];
|
|
1102
|
+
}
|
|
1103
|
+
const rawValues = Array.isArray(value) ? value : [value];
|
|
1104
|
+
const devices = [];
|
|
1105
|
+
for (const raw of rawValues) {
|
|
1106
|
+
for (const part of raw.split(",")) {
|
|
1107
|
+
const device = part.trim();
|
|
1108
|
+
if (!device) continue;
|
|
1109
|
+
if (!isScreenshotDevice(device)) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Unknown screenshot device "${device}". Use one of: ${SCREENSHOT_DEVICES.join(", ")}.`
|
|
1112
|
+
);
|
|
1191
1113
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
})().catch((error) => {
|
|
1195
|
-
if (!response.headersSent) {
|
|
1196
|
-
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1114
|
+
if (!devices.includes(device)) {
|
|
1115
|
+
devices.push(device);
|
|
1197
1116
|
}
|
|
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
|
-
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return devices.length > 0 ? devices : [...SCREENSHOT_DEVICES];
|
|
1220
1120
|
}
|
|
1221
|
-
var DEFAULT_SCREENSHOT_RELATIVE = path2.join("dist", "preview", "desktop.png");
|
|
1222
1121
|
function parsePositiveInt(value, fallback) {
|
|
1223
1122
|
if (!value) {
|
|
1224
1123
|
return fallback;
|
|
@@ -1227,25 +1126,68 @@ function parsePositiveInt(value, fallback) {
|
|
|
1227
1126
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1228
1127
|
}
|
|
1229
1128
|
function resolveScreenshotOptions(root, options) {
|
|
1129
|
+
const devices = parseScreenshotDevices(options.device);
|
|
1130
|
+
if (options.output && devices.length !== 1) {
|
|
1131
|
+
throw new Error("`--output` can only be used when exactly one `--device` is selected.");
|
|
1132
|
+
}
|
|
1230
1133
|
return {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1134
|
+
targets: devices.map((device) => {
|
|
1135
|
+
const config = SCREENSHOT_DEVICE_CONFIG[device];
|
|
1136
|
+
const viewport = {
|
|
1137
|
+
width: parsePositiveInt(options.width, config.viewport.width),
|
|
1138
|
+
height: parsePositiveInt(options.height, config.viewport.height)
|
|
1139
|
+
};
|
|
1140
|
+
return {
|
|
1141
|
+
device,
|
|
1142
|
+
outputPath: path2.resolve(root, options.output ?? config.outputRelativePath),
|
|
1143
|
+
viewport
|
|
1144
|
+
};
|
|
1145
|
+
}),
|
|
1236
1146
|
port: parsePositiveInt(options.port, 4178),
|
|
1237
|
-
|
|
1147
|
+
fullPage: options.fullPage === true
|
|
1238
1148
|
};
|
|
1239
1149
|
}
|
|
1240
|
-
|
|
1150
|
+
function createThemeProjectRequire(themeRoot) {
|
|
1151
|
+
return createRequire(path2.join(themeRoot, "package.json"));
|
|
1152
|
+
}
|
|
1153
|
+
function normalizePlaywrightModule(moduleValue) {
|
|
1154
|
+
if (typeof moduleValue !== "object" || moduleValue === null) {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
if ("chromium" in moduleValue) {
|
|
1158
|
+
const chromium = moduleValue.chromium;
|
|
1159
|
+
if (typeof chromium === "object" && chromium !== null && "launch" in chromium) {
|
|
1160
|
+
return moduleValue;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if ("default" in moduleValue) {
|
|
1164
|
+
return normalizePlaywrightModule(moduleValue.default);
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
function normalizeSharpModule(moduleValue) {
|
|
1169
|
+
if (typeof moduleValue === "function") {
|
|
1170
|
+
return moduleValue;
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof moduleValue !== "object" || moduleValue === null) {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
if ("default" in moduleValue) {
|
|
1176
|
+
return normalizeSharpModule(moduleValue.default);
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
async function loadPlaywright(themeRoot) {
|
|
1181
|
+
const themeRequire = createThemeProjectRequire(themeRoot);
|
|
1241
1182
|
const candidates = ["playwright", "@playwright/test"];
|
|
1242
1183
|
for (const id of candidates) {
|
|
1243
1184
|
try {
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1185
|
+
const resolvedId = themeRequire.resolve(id);
|
|
1186
|
+
const mod = await import(pathToFileURL(resolvedId).href);
|
|
1187
|
+
const playwright = normalizePlaywrightModule(mod);
|
|
1188
|
+
if (playwright) {
|
|
1189
|
+
return playwright;
|
|
1190
|
+
}
|
|
1249
1191
|
} catch {
|
|
1250
1192
|
}
|
|
1251
1193
|
}
|
|
@@ -1253,33 +1195,82 @@ async function loadPlaywright() {
|
|
|
1253
1195
|
"Playwright is required for screenshot capture. Install it as a dev dependency: `pnpm add -D playwright && pnpm exec playwright install chromium`."
|
|
1254
1196
|
);
|
|
1255
1197
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1198
|
+
function formatMissingSharpWarning() {
|
|
1199
|
+
return "warning: PNG optimization skipped because optional dependency `sharp` is not installed. Install it as a dev dependency in the theme project: `pnpm add -D sharp`.";
|
|
1200
|
+
}
|
|
1201
|
+
var warnedMissingSharp = false;
|
|
1202
|
+
var warnedSharpOptimizeFailure = false;
|
|
1203
|
+
async function loadOptionalSharp(themeRoot) {
|
|
1204
|
+
const resolvers = [
|
|
1205
|
+
createThemeProjectRequire(themeRoot),
|
|
1206
|
+
createRequire(path2.join(packageRoot, "package.json"))
|
|
1207
|
+
];
|
|
1208
|
+
for (const resolver of resolvers) {
|
|
1209
|
+
try {
|
|
1210
|
+
const sharpModulePath = resolver.resolve("sharp");
|
|
1211
|
+
const mod = await import(pathToFileURL(sharpModulePath).href);
|
|
1212
|
+
const sharp = normalizeSharpModule(mod);
|
|
1213
|
+
if (sharp) {
|
|
1214
|
+
return sharp;
|
|
1215
|
+
}
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
async function optimizePngIfAvailable(themeRoot, filePath) {
|
|
1222
|
+
const sharp = await loadOptionalSharp(themeRoot);
|
|
1223
|
+
if (!sharp) {
|
|
1224
|
+
if (!warnedMissingSharp) {
|
|
1225
|
+
console.warn(formatMissingSharpWarning());
|
|
1226
|
+
warnedMissingSharp = true;
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
const optimized = await sharp(filePath).png({ compressionLevel: 9, palette: true }).toBuffer();
|
|
1232
|
+
await writeFile(filePath, optimized);
|
|
1233
|
+
} catch {
|
|
1234
|
+
if (!warnedSharpOptimizeFailure) {
|
|
1235
|
+
console.warn(
|
|
1236
|
+
"warning: PNG optimization failed. The screenshot was saved uncompressed."
|
|
1237
|
+
);
|
|
1238
|
+
warnedSharpOptimizeFailure = true;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function captureScreenshot(root, options) {
|
|
1243
|
+
const playwright = await loadPlaywright(root);
|
|
1244
|
+
const handle = await startViteDevPreviewServer(root, options.port);
|
|
1259
1245
|
try {
|
|
1260
|
-
await mkdir(path2.dirname(options.outputPath), { recursive: true });
|
|
1261
1246
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
1262
1247
|
try {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1248
|
+
for (const target of options.targets) {
|
|
1249
|
+
await mkdir(path2.dirname(target.outputPath), { recursive: true });
|
|
1250
|
+
const context = await browser.newContext({
|
|
1251
|
+
viewport: target.viewport,
|
|
1252
|
+
deviceScaleFactor: 1
|
|
1253
|
+
});
|
|
1254
|
+
try {
|
|
1255
|
+
const page = await context.newPage();
|
|
1256
|
+
await page.goto(handle.url, { waitUntil: "networkidle" });
|
|
1257
|
+
await page.screenshot({ path: target.outputPath, fullPage: options.fullPage });
|
|
1258
|
+
} finally {
|
|
1259
|
+
await context.close();
|
|
1260
|
+
}
|
|
1261
|
+
await optimizePngIfAvailable(root, target.outputPath);
|
|
1262
|
+
console.log(`captured ${target.device} ${path2.relative(root, target.outputPath)}`);
|
|
1263
|
+
}
|
|
1271
1264
|
} finally {
|
|
1272
1265
|
await browser.close();
|
|
1273
1266
|
}
|
|
1274
1267
|
} finally {
|
|
1275
1268
|
await handle.close();
|
|
1276
1269
|
}
|
|
1277
|
-
console.log(`captured ${path2.relative(theme.root, options.outputPath)}`);
|
|
1278
1270
|
}
|
|
1279
1271
|
async function screenshotTheme(root, options) {
|
|
1280
1272
|
const resolved = resolveScreenshotOptions(root, options);
|
|
1281
|
-
|
|
1282
|
-
await captureScreenshot(theme, resolved);
|
|
1273
|
+
await captureScreenshot(root, resolved);
|
|
1283
1274
|
}
|
|
1284
1275
|
async function loadLocalAgentManifest(root) {
|
|
1285
1276
|
const built = await readJsonIfExists(
|
|
@@ -1965,10 +1956,11 @@ async function collectFiles(root, directory) {
|
|
|
1965
1956
|
continue;
|
|
1966
1957
|
}
|
|
1967
1958
|
const relativePath = path2.relative(root, absolutePath).replaceAll(path2.sep, "/");
|
|
1959
|
+
const publishRelativePath = relativePath.startsWith("dist/") ? relativePath.slice("dist/".length) : relativePath;
|
|
1968
1960
|
files.push({
|
|
1969
1961
|
absolutePath,
|
|
1970
|
-
relativePath,
|
|
1971
|
-
contentType: contentTypeFor(
|
|
1962
|
+
relativePath: publishRelativePath,
|
|
1963
|
+
contentType: contentTypeFor(publishRelativePath)
|
|
1972
1964
|
});
|
|
1973
1965
|
}
|
|
1974
1966
|
return files;
|
|
@@ -1978,10 +1970,12 @@ async function collectThemeArtifactFiles(theme) {
|
|
|
1978
1970
|
return files;
|
|
1979
1971
|
}
|
|
1980
1972
|
var SSR_REQUIRED_ARTIFACTS = [
|
|
1981
|
-
"
|
|
1982
|
-
"
|
|
1983
|
-
"
|
|
1984
|
-
"
|
|
1973
|
+
"index.js",
|
|
1974
|
+
"runtime.client.js",
|
|
1975
|
+
"manifest.json",
|
|
1976
|
+
"styles.css",
|
|
1977
|
+
"agent-manifest.json",
|
|
1978
|
+
...REQUIRED_PREVIEW_ARTIFACTS
|
|
1985
1979
|
];
|
|
1986
1980
|
async function checksum(files) {
|
|
1987
1981
|
const hash = createHash("sha256");
|
|
@@ -2017,13 +2011,13 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2017
2011
|
}
|
|
2018
2012
|
const files = await collectThemeArtifactFiles(theme);
|
|
2019
2013
|
if (!theme.clientEntryPath) {
|
|
2020
|
-
throw new Error("Missing
|
|
2014
|
+
throw new Error("Missing runtime.client.js. Run `suda theme build` first.");
|
|
2021
2015
|
}
|
|
2022
2016
|
const collectedPaths = new Set(files.map((f) => f.relativePath));
|
|
2023
2017
|
for (const required of SSR_REQUIRED_ARTIFACTS) {
|
|
2024
2018
|
if (!collectedPaths.has(required)) {
|
|
2025
2019
|
throw new Error(
|
|
2026
|
-
`Missing required
|
|
2020
|
+
`Missing required theme artifact: ${required}. Run \`suda theme build\` and ensure \`${required}\` is generated.`
|
|
2027
2021
|
);
|
|
2028
2022
|
}
|
|
2029
2023
|
}
|
|
@@ -2090,9 +2084,8 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2090
2084
|
}
|
|
2091
2085
|
console.log(`uploaded ${createThemeObjectKey(key, version, file.relativePath)}`);
|
|
2092
2086
|
}
|
|
2093
|
-
const bundleArtifactRelative = "
|
|
2094
|
-
const previewArtifactRelative = "
|
|
2095
|
-
const previewArtifactExists = await pathExists(path2.join(theme.root, previewArtifactRelative));
|
|
2087
|
+
const bundleArtifactRelative = "index.js";
|
|
2088
|
+
const previewArtifactRelative = "assets/preview/desktop.png";
|
|
2096
2089
|
const agentManifest = createThemeAgentManifest(theme.module);
|
|
2097
2090
|
const completeRes = await fetch(`${baseUrl}/api/cli/themes/publish-complete`, {
|
|
2098
2091
|
method: "POST",
|
|
@@ -2107,7 +2100,7 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2107
2100
|
manifest: theme.module.manifest,
|
|
2108
2101
|
agentManifest,
|
|
2109
2102
|
bundleArtifactRelative,
|
|
2110
|
-
previewArtifactRelative
|
|
2103
|
+
previewArtifactRelative
|
|
2111
2104
|
})
|
|
2112
2105
|
});
|
|
2113
2106
|
if (!completeRes.ok) {
|
|
@@ -2304,7 +2297,7 @@ function buildProgram() {
|
|
|
2304
2297
|
console.log(`valid ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
2305
2298
|
if (!result.clientEntryPath) {
|
|
2306
2299
|
console.warn(
|
|
2307
|
-
"warning:
|
|
2300
|
+
"warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
|
|
2308
2301
|
);
|
|
2309
2302
|
}
|
|
2310
2303
|
});
|
|
@@ -2333,7 +2326,12 @@ function buildProgram() {
|
|
|
2333
2326
|
const port = Number(options.port ?? "4177");
|
|
2334
2327
|
await watchTheme(resolveThemeRoot(options), Number.isFinite(port) ? port : 4177);
|
|
2335
2328
|
});
|
|
2336
|
-
theme.command("screenshot").description("Capture
|
|
2329
|
+
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(
|
|
2330
|
+
"--device <device>",
|
|
2331
|
+
"Device to capture: desktop, tablet, mobile. Repeat or use comma-separated values.",
|
|
2332
|
+
collectOption,
|
|
2333
|
+
[]
|
|
2334
|
+
).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
2335
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
2338
2336
|
});
|
|
2339
2337
|
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(
|