@sudajs/cli 0.18.3 → 0.18.5

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 CHANGED
@@ -1800,6 +1800,7 @@ declare function fetchJson<T>(url: string, options?: RequestInit & {
1800
1800
  token?: string;
1801
1801
  }): Promise<T>;
1802
1802
  declare function validateThemeTailwindContract(root: string): Promise<void>;
1803
+ declare function validateThemeCssTokenContract(root: string): Promise<void>;
1803
1804
  declare function validateTheme(root: string): Promise<ValidatedTheme>;
1804
1805
  declare function validateThemeLocales(root: string): Promise<void>;
1805
1806
  declare function validateThemePreviewLocales(root: string, previewLocales: string[] | undefined): Promise<void>;
@@ -1827,6 +1828,7 @@ declare const __testUtils: {
1827
1828
  slugifyThemeName: typeof slugifyThemeName;
1828
1829
  validateThemeKey: typeof validateThemeKey;
1829
1830
  validateThemeName: typeof validateThemeName;
1831
+ validateThemeCssTokenContract: typeof validateThemeCssTokenContract;
1830
1832
  validateThemeTailwindContract: typeof validateThemeTailwindContract;
1831
1833
  validateThemeLocales: typeof validateThemeLocales;
1832
1834
  validateThemePreviewLocales: typeof validateThemePreviewLocales;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { createInterface } from 'readline/promises';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import { themeDesignSystemSchema, createThemeAgentManifest, createSudaPageAgentRuntime, checkThemeModule, formatThemeCheckResult, agentValidationResultSchema, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
11
+ import { themeDesignSystemSchema, createThemeDesignCssVariables, createThemeAgentManifest, createSudaPageAgentRuntime, checkThemeModule, formatThemeCheckResult, agentValidationResultSchema, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
12
12
  import { themeManifestSchema, runWithThemePreviewLocale } from '@sudajs/theme-engine/server';
13
13
  import { Command } from 'commander';
14
14
  import { build } from 'esbuild';
@@ -1123,6 +1123,108 @@ async function validateThemeTailwindContract(root) {
1123
1123
  if (!styles.includes('@import "tailwindcss";') && !styles.includes("@import 'tailwindcss';")) {
1124
1124
  throw new Error('src/styles.css must include `@import "tailwindcss";`.');
1125
1125
  }
1126
+ await validateThemeCssTokenContract(root);
1127
+ }
1128
+ async function collectThemeSourceCssFiles(directory) {
1129
+ const entries = await readdir(directory, { withFileTypes: true });
1130
+ const files = [];
1131
+ for (const entry of entries) {
1132
+ const absolutePath = path2.join(directory, entry.name);
1133
+ if (entry.isDirectory()) {
1134
+ files.push(...await collectThemeSourceCssFiles(absolutePath));
1135
+ } else if (entry.isFile() && entry.name.endsWith(".css")) {
1136
+ files.push(absolutePath);
1137
+ }
1138
+ }
1139
+ return files;
1140
+ }
1141
+ function findThemeInlineBlocks(styles, filePath) {
1142
+ const blocks = [];
1143
+ const pattern = /@theme\s+inline\s*\{/g;
1144
+ for (const match of styles.matchAll(pattern)) {
1145
+ const start = match.index;
1146
+ let depth = 1;
1147
+ let cursor = start + match[0].length;
1148
+ while (cursor < styles.length && depth > 0) {
1149
+ if (styles[cursor] === "{") depth += 1;
1150
+ if (styles[cursor] === "}") depth -= 1;
1151
+ cursor += 1;
1152
+ }
1153
+ if (depth !== 0) {
1154
+ throw new Error(`${filePath} contains an unclosed \`@theme inline\` block.`);
1155
+ }
1156
+ blocks.push({
1157
+ body: styles.slice(start + match[0].length, cursor - 1),
1158
+ end: cursor,
1159
+ start
1160
+ });
1161
+ }
1162
+ return blocks;
1163
+ }
1164
+ async function validateThemeCssTokenContract(root) {
1165
+ const sourceRoot = path2.join(root, "src");
1166
+ const files = await collectThemeSourceCssFiles(sourceRoot);
1167
+ const sources = await Promise.all(
1168
+ files.map(async (filePath) => {
1169
+ const styles = await readFile(filePath, "utf8");
1170
+ return { blocks: findThemeInlineBlocks(styles, filePath), filePath, styles };
1171
+ })
1172
+ );
1173
+ const declarations = /* @__PURE__ */ new Map();
1174
+ for (const source of sources) {
1175
+ for (const block of source.blocks) {
1176
+ for (const match of block.body.matchAll(/(--[a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g)) {
1177
+ const name = match[1];
1178
+ const value = match[2];
1179
+ if (name !== void 0 && value !== void 0) {
1180
+ declarations.set(name, value);
1181
+ }
1182
+ }
1183
+ }
1184
+ }
1185
+ const sudaAliases = /* @__PURE__ */ new Set();
1186
+ let changed = true;
1187
+ while (changed) {
1188
+ changed = false;
1189
+ for (const [name, value] of declarations) {
1190
+ const dependsOnSuda = /var\(\s*--suda-/.test(value);
1191
+ const dependsOnAlias = [...sudaAliases].some(
1192
+ (alias) => new RegExp(`var\\(\\s*${escapeRegExp(alias)}(?:\\s*[,\\)])`).test(value)
1193
+ );
1194
+ if ((dependsOnSuda || dependsOnAlias) && !sudaAliases.has(name)) {
1195
+ sudaAliases.add(name);
1196
+ changed = true;
1197
+ }
1198
+ }
1199
+ }
1200
+ const issues = [];
1201
+ for (const source of sources) {
1202
+ const handwrittenStyles = source.styles.split("");
1203
+ for (const block of source.blocks) {
1204
+ for (let index = block.start; index < block.end; index += 1) {
1205
+ if (handwrittenStyles[index] !== "\n") {
1206
+ handwrittenStyles[index] = " ";
1207
+ }
1208
+ }
1209
+ }
1210
+ const searchable = handwrittenStyles.join("");
1211
+ for (const alias of sudaAliases) {
1212
+ const reference = new RegExp(`var\\(\\s*${escapeRegExp(alias)}(?:\\s*[,\\)])`, "g");
1213
+ for (const match of searchable.matchAll(reference)) {
1214
+ const line = searchable.slice(0, match.index ?? 0).split("\n").length;
1215
+ issues.push(`${path2.relative(root, source.filePath)}:${line} uses ${alias}`);
1216
+ }
1217
+ }
1218
+ }
1219
+ if (issues.length > 0) {
1220
+ throw new Error(
1221
+ [
1222
+ "Hand-written theme CSS must not consume Suda-backed Tailwind aliases from `@theme inline`.",
1223
+ ...issues.map((issue) => ` - ${issue}`),
1224
+ "Use `var(--suda-*)` directly, or define and consume a theme-prefixed variable on the theme root. Tailwind utilities such as `bg-primary` remain supported."
1225
+ ].join("\n")
1226
+ );
1227
+ }
1126
1228
  }
1127
1229
  async function validateTheme(root) {
1128
1230
  const packageJsonPath = path2.join(root, "package.json");
@@ -1657,6 +1759,7 @@ var __testUtils = {
1657
1759
  slugifyThemeName,
1658
1760
  validateThemeKey,
1659
1761
  validateThemeName,
1762
+ validateThemeCssTokenContract,
1660
1763
  validateThemeTailwindContract,
1661
1764
  validateThemeLocales,
1662
1765
  validateThemePreviewLocales,
@@ -2018,6 +2121,7 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2018
2121
  const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
2019
2122
  const customHead = chrome.customHeadCode ?? "";
2020
2123
  const customBody = chrome.customBodyCode ?? "";
2124
+ const designPalette = renderDevDesignPalette(theme.module.manifest.designSystem);
2021
2125
  return [
2022
2126
  "<!doctype html>",
2023
2127
  `<html lang="${escapeHtml(locale)}">`,
@@ -2026,10 +2130,12 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2026
2130
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
2027
2131
  `<title>${escapeHtml(`${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
2028
2132
  '<link rel="stylesheet" href="/src/styles.css" />',
2133
+ designPalette.style,
2029
2134
  customHead,
2030
2135
  "</head>",
2031
2136
  `<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
2032
2137
  body,
2138
+ designPalette.html,
2033
2139
  customBody,
2034
2140
  ...preserveLang ? [
2035
2141
  '<script>for(const a of document.querySelectorAll("a[href]")){try{const u=new URL(a.href,location.href);if(u.origin===location.origin){u.searchParams.set("lang",new URL(location.href).searchParams.get("lang"));a.href=u.pathname+u.search+u.hash}}catch{}}</script>'
@@ -2038,6 +2144,33 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2038
2144
  "</html>"
2039
2145
  ].join("");
2040
2146
  }
2147
+ function serializeInlineScriptValue(value) {
2148
+ return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
2149
+ }
2150
+ function renderDevDesignPalette(designSystem) {
2151
+ if (designSystem.presets.length < 2) {
2152
+ return { style: "", html: "" };
2153
+ }
2154
+ const presets = designSystem.presets.map((preset) => ({
2155
+ id: preset.id,
2156
+ variables: createThemeDesignCssVariables(preset.tokens)
2157
+ }));
2158
+ const buttons = designSystem.presets.map(
2159
+ (preset) => `<button type="button" data-suda-dev-preset="${escapeHtml(preset.id)}" title="${escapeHtml(
2160
+ preset.description ? `${preset.label} \u2014 ${preset.description}` : preset.label
2161
+ )}" aria-label="${escapeHtml(preset.label)}"><span style="background:${escapeHtml(
2162
+ preset.tokens.colors.primary
2163
+ )}"></span></button>`
2164
+ ).join("");
2165
+ const config = serializeInlineScriptValue({
2166
+ defaultPresetId: designSystem.defaultPresetId,
2167
+ presets
2168
+ });
2169
+ return {
2170
+ style: '<style data-suda-dev-palette-style>\n#suda-dev-palette{all:initial;position:fixed;z-index:2147483000;top:50%;right:16px;width:64px;max-height:calc(100svh - 32px);padding:8px 9px;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#fff;border:1px solid #d1d5db;border-radius:999px;box-shadow:0 8px 24px rgb(15 23 42 / 12%);transform:translateY(-50%);color-scheme:light;scrollbar-width:none;-ms-overflow-style:none}\n#suda-dev-palette::-webkit-scrollbar{display:none}\n#suda-dev-palette button{all:initial;position:relative;display:grid;width:44px;height:44px;box-sizing:border-box;cursor:pointer;border-radius:999px;place-items:center}\n#suda-dev-palette button span{display:block;width:32px;height:32px;border-radius:999px;transition:transform 120ms ease}\n#suda-dev-palette button:hover span{transform:scale(1.08)}\n#suda-dev-palette button[aria-pressed="true"] span{visibility:hidden}\n#suda-dev-palette button[aria-pressed="true"]::after{position:absolute;display:grid;width:32px;height:32px;color:var(--suda-dev-preset-primary);font:700 22px/1 ui-sans-serif,system-ui,sans-serif;content:"\u2713";place-items:center}\n#suda-dev-palette button:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n@media(max-width:640px){#suda-dev-palette{right:8px;max-height:calc(100svh - 16px)}}\n@media(prefers-reduced-motion:reduce){#suda-dev-palette button span{transition:none}}\n</style>',
2171
+ html: `<aside id="suda-dev-palette" aria-label="Design presets">${buttons}</aside><script data-suda-dev-palette-script>(function(){const config=${config};const palette=document.getElementById("suda-dev-palette");if(!palette)return;const byId=new Map(config.presets.map((preset)=>[preset.id,preset]));const params=new URL(location.href).searchParams;const requested=params.get("designPreset");const selected=byId.has(requested)?requested:config.defaultPresetId;function apply(id,updateUrl){const preset=byId.get(id);if(!preset)return;const properties=Object.keys(preset.variables);const targets=[document.documentElement,document.body,...document.querySelectorAll("[style]")].filter((element,index,items)=>items.indexOf(element)===index&&(element===document.documentElement||element===document.body||properties.some((property)=>element.style.getPropertyValue(property)!=="")));for(const target of targets){for(const [property,value] of Object.entries(preset.variables))target.style.setProperty(property,value)}for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){const active=button.dataset.sudaDevPreset===id;button.setAttribute("aria-pressed",String(active));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}if(updateUrl){const url=new URL(location.href);url.searchParams.set("designPreset",id);history.replaceState(history.state,"",url)}}if(requested&&byId.has(requested))apply(selected,false);else for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){button.setAttribute("aria-pressed",String(button.dataset.sudaDevPreset===selected));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>`
2172
+ };
2173
+ }
2041
2174
  var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];
2042
2175
  var SCREENSHOT_DEVICE_CONFIG = {
2043
2176
  desktop: {