@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.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, FileSystemThemeRegistry } from '@sudajs/theme-engine/server';
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 !== "dist/index.js") {
495
- throw new Error('manifest.entry must be artifact-local: "dist/index.js".');
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 !== "dist/runtime.client.js") {
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: "dist/runtime.client.js" when provided.'
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 copyThemeSourceAssets(root);
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 copyThemeSourceAssets(root) {
663
- const sourceAssets = path2.join(root, "src", "assets");
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 ok = result.ok && footerIssues.length === 0;
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
- console.log(`previewing Vite theme at ${url}`);
880
- await new Promise(() => void 0);
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 = `/src/assets/${assetPrefixMatch[1]}${url.search}`;
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 requested = url.searchParams.get("page");
910
- const activeSlug = requested && findStarterPage(theme, requested) ? requested : fallbackSlug;
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
- renderPreviewShellHtml(theme, activeSlug)
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
- function renderPreviewShellHtml(theme, activeSlug) {
1077
- const items = theme.module.starterPages.map((page) => {
1078
- const slug = page.slug;
1079
- const label = `${page.title}${page.isHome ? " (home)" : ""}`;
1080
- const active = slug === activeSlug;
1081
- const href = `/?page=${encodeURIComponent(slug)}`;
1082
- const className = active ? "suda-preview-tab suda-preview-tab--active" : "suda-preview-tab";
1083
- return `<a class="${className}" href="${escapeHtml(href)}" target="_self">${escapeHtml(label)}</a>`;
1084
- }).join("");
1085
- const frameSrc = `/pages/${encodeURIComponent(activeSlug)}`;
1086
- return [
1087
- "<!doctype html>",
1088
- '<html lang="en">',
1089
- "<head>",
1090
- '<meta charset="utf-8" />',
1091
- '<meta name="viewport" content="width=device-width, initial-scale=1" />',
1092
- `<title>${escapeHtml(theme.module.manifest.name)} \u2014 preview</title>`,
1093
- "<style>",
1094
- "html,body{margin:0;height:100%;font-family:system-ui,sans-serif;background:#0b0d12;color:#e6e8ee;}",
1095
- ".suda-preview-bar{display:flex;flex-wrap:wrap;gap:.25rem;padding:.5rem .75rem;background:#0b0d12;border-bottom:1px solid #1f2330;position:sticky;top:0;z-index:10;}",
1096
- ".suda-preview-tab{display:inline-flex;align-items:center;padding:.35rem .75rem;border-radius:.5rem;font-size:.875rem;color:#c9cdd6;text-decoration:none;border:1px solid transparent;}",
1097
- ".suda-preview-tab:hover{background:#1a1d27;}",
1098
- ".suda-preview-tab--active{background:#222633;color:#fff;border-color:#2c3142;}",
1099
- ".suda-preview-frame{display:block;border:0;width:100%;height:calc(100vh - 2.5rem);background:#fff;}",
1100
- "</style>",
1101
- "</head>",
1102
- "<body>",
1103
- `<nav class="suda-preview-bar">${items}</nav>`,
1104
- `<iframe class="suda-preview-frame" src="${escapeHtml(frameSrc)}" title="theme preview"></iframe>`,
1105
- "</body>",
1106
- "</html>"
1107
- ].join("");
1108
- }
1109
- async function startPreviewServer(theme, port) {
1110
- const { createServer } = await import('http');
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
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1193
- response.end("Not found");
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
- response.end(error instanceof Error ? error.message : "Internal error");
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
- outputPath: path2.resolve(root, options.output ?? DEFAULT_SCREENSHOT_RELATIVE),
1232
- viewport: {
1233
- width: parsePositiveInt(options.width, 1280),
1234
- height: parsePositiveInt(options.height, 800)
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
- skipBuild: options.skipBuild === true
1147
+ fullPage: options.fullPage === true
1238
1148
  };
1239
1149
  }
1240
- async function loadPlaywright() {
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 mod = await import(
1245
- /* @vite-ignore */
1246
- id
1247
- );
1248
- return mod;
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
- async function captureScreenshot(theme, options) {
1257
- const playwright = await loadPlaywright();
1258
- const handle = await startPreviewServer(theme, options.port);
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 context = await browser.newContext({
1264
- viewport: options.viewport,
1265
- deviceScaleFactor: 1
1266
- });
1267
- const page = await context.newPage();
1268
- await page.goto(handle.url, { waitUntil: "networkidle" });
1269
- await page.screenshot({ path: options.outputPath, fullPage: false });
1270
- await context.close();
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
- const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root);
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(relativePath)
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
- "dist/index.js",
1982
- "dist/runtime.client.js",
1983
- "dist/manifest.json",
1984
- "dist/styles.css"
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 dist/runtime.client.js. Run `suda theme build` first.");
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 SSR artifact: ${required}. Run \`suda theme build\` and ensure ${required === "dist/styles.css" ? "`dist/styles.css` is generated" : `\`${required}\` is generated`}.`
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 = "dist/index.js";
2094
- const previewArtifactRelative = "dist/preview/desktop.png";
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: previewArtifactExists ? previewArtifactRelative : void 0
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: dist/runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
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 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) => {
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(