@sudajs/cli 0.13.3 → 0.14.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 +11 -1
- package/dist/index.js +193 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/theme/AGENTS.md +23 -4
- package/templates/theme/README.md +1 -1
- package/templates/theme/src/locales/en.content.json +94 -0
- package/templates/theme/src/locales/zh-CN.content.json +94 -0
- package/templates/theme/src/manifest.ts +1 -0
- package/templates/theme/src/preview-i18n.ts +13 -0
- package/templates/theme/src/templates.ts +55 -45
package/dist/index.d.ts
CHANGED
|
@@ -1702,6 +1702,7 @@ declare function fetchJson<T>(url: string, options?: RequestInit & {
|
|
|
1702
1702
|
declare function validateThemeTailwindContract(root: string): Promise<void>;
|
|
1703
1703
|
declare function validateTheme(root: string): Promise<ValidatedTheme>;
|
|
1704
1704
|
declare function validateThemeLocales(root: string): Promise<void>;
|
|
1705
|
+
declare function validateThemePreviewLocales(root: string, previewLocales: string[] | undefined): Promise<void>;
|
|
1705
1706
|
declare function toViteModuleUrl(root: string, absolutePath: string): string;
|
|
1706
1707
|
declare function buildTheme(root: string): Promise<ValidatedTheme>;
|
|
1707
1708
|
declare function finalizeTheme(root: string): Promise<ValidatedTheme>;
|
|
@@ -1728,11 +1729,20 @@ declare const __testUtils: {
|
|
|
1728
1729
|
validateThemeName: typeof validateThemeName;
|
|
1729
1730
|
validateThemeTailwindContract: typeof validateThemeTailwindContract;
|
|
1730
1731
|
validateThemeLocales: typeof validateThemeLocales;
|
|
1732
|
+
validateThemePreviewLocales: typeof validateThemePreviewLocales;
|
|
1733
|
+
resolveThemePreviewLocale: typeof resolveThemePreviewLocale;
|
|
1734
|
+
withSerializedVitePreviewLocaleLoad: typeof withSerializedVitePreviewLocaleLoad;
|
|
1731
1735
|
toViteModuleUrl: typeof toViteModuleUrl;
|
|
1732
1736
|
updateRemotePageDraft: typeof updateRemotePageDraft;
|
|
1733
1737
|
};
|
|
1734
1738
|
declare function watchTheme(root: string, port: number): Promise<void>;
|
|
1739
|
+
declare function withSerializedVitePreviewLocaleLoad<Result>(server: object, load: () => Promise<Result>): Promise<Result>;
|
|
1735
1740
|
declare function resolveDevStarterSlug(pathname: string): string | null | undefined;
|
|
1741
|
+
declare function resolveThemePreviewLocale(params: {
|
|
1742
|
+
previewLocales: string[] | undefined;
|
|
1743
|
+
explicitLang?: string | null;
|
|
1744
|
+
acceptLanguage?: string;
|
|
1745
|
+
}): string;
|
|
1736
1746
|
/**
|
|
1737
1747
|
* Theme-project-scoped renderer bundle. We deliberately resolve `react`,
|
|
1738
1748
|
* `react-dom/server` and `@sudajs/theme-engine/server` through the theme
|
|
@@ -1751,7 +1761,7 @@ interface ThemeRenderer {
|
|
|
1751
1761
|
renderToString: typeof renderToString;
|
|
1752
1762
|
}
|
|
1753
1763
|
declare function loadThemeScopedRenderer(themeRoot: string): Promise<ThemeRenderer>;
|
|
1754
|
-
declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer): string;
|
|
1764
|
+
declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean): string;
|
|
1755
1765
|
interface ScreenshotOptions extends ThemeRootOptions {
|
|
1756
1766
|
device?: string[] | string;
|
|
1757
1767
|
output?: string;
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ 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
11
|
import { themeDesignSystemSchema, createThemeAgentManifest, createSudaPageAgentRuntime, checkThemeModule, formatThemeCheckResult, agentValidationResultSchema, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
12
|
-
import { themeManifestSchema } from '@sudajs/theme-engine/server';
|
|
12
|
+
import { themeManifestSchema, runWithThemePreviewLocale } from '@sudajs/theme-engine/server';
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import { build } from 'esbuild';
|
|
15
15
|
import { z } from 'zod';
|
|
@@ -839,6 +839,7 @@ async function validateTheme(root) {
|
|
|
839
839
|
validateThemeModule(module);
|
|
840
840
|
await validateThemeTailwindContract(root);
|
|
841
841
|
await validateThemeLocales(root);
|
|
842
|
+
await validateThemePreviewLocales(root, module.manifest.previewLocales);
|
|
842
843
|
const result = {
|
|
843
844
|
root,
|
|
844
845
|
module,
|
|
@@ -885,7 +886,16 @@ async function listLocaleFiles(localesDir) {
|
|
|
885
886
|
return [];
|
|
886
887
|
}
|
|
887
888
|
const entries = await readdir(localesDir, { withFileTypes: true });
|
|
888
|
-
return entries.filter(
|
|
889
|
+
return entries.filter(
|
|
890
|
+
(entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".content.json")
|
|
891
|
+
).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
892
|
+
}
|
|
893
|
+
async function listPreviewLocaleFiles(localesDir) {
|
|
894
|
+
if (!await pathExists(localesDir)) {
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
const entries = await readdir(localesDir, { withFileTypes: true });
|
|
898
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".content.json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
889
899
|
}
|
|
890
900
|
async function validateThemeLocales(root) {
|
|
891
901
|
const localesDir = path2.join(root, "dist", "locales");
|
|
@@ -921,6 +931,47 @@ ${details}`
|
|
|
921
931
|
}
|
|
922
932
|
}
|
|
923
933
|
}
|
|
934
|
+
async function validateThemePreviewLocales(root, previewLocales) {
|
|
935
|
+
const localesDir = path2.join(root, "dist", "locales");
|
|
936
|
+
const files = await listPreviewLocaleFiles(localesDir);
|
|
937
|
+
const declared = previewLocales ?? [];
|
|
938
|
+
const expectedFiles = new Set(declared.map((locale) => `${locale}.content.json`));
|
|
939
|
+
const undeclared = files.filter((file) => !expectedFiles.has(file));
|
|
940
|
+
const missing = [...expectedFiles].filter((file) => !files.includes(file));
|
|
941
|
+
if (missing.length > 0 || undeclared.length > 0) {
|
|
942
|
+
const details = [
|
|
943
|
+
...missing.map((file) => ` - missing locales/${file}`),
|
|
944
|
+
...undeclared.map((file) => ` - undeclared locales/${file}`)
|
|
945
|
+
].join("\n");
|
|
946
|
+
throw new Error(`Theme preview locale files do not match manifest.previewLocales:
|
|
947
|
+
${details}`);
|
|
948
|
+
}
|
|
949
|
+
if (declared.length === 0) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const referenceFile = "en.content.json";
|
|
953
|
+
const referenceMessages = await readLocaleJson(path2.join(localesDir, referenceFile));
|
|
954
|
+
const referenceKeys = new Set(collectLocaleKeys(referenceMessages).sort());
|
|
955
|
+
for (const file of files) {
|
|
956
|
+
if (file === referenceFile) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const messages = await readLocaleJson(path2.join(localesDir, file));
|
|
960
|
+
const keys = new Set(collectLocaleKeys(messages).sort());
|
|
961
|
+
const missingKeys = [...referenceKeys].filter((key) => !keys.has(key));
|
|
962
|
+
const extraKeys = [...keys].filter((key) => !referenceKeys.has(key));
|
|
963
|
+
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
|
964
|
+
const details = [
|
|
965
|
+
...missingKeys.map((key) => ` - missing ${key}`),
|
|
966
|
+
...extraKeys.map((key) => ` - extra ${key}`)
|
|
967
|
+
].join("\n");
|
|
968
|
+
throw new Error(
|
|
969
|
+
`Theme preview locale ${file} does not match ${referenceFile} key set:
|
|
970
|
+
${details}`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
924
975
|
var VITE_CONFIG_FILES = [
|
|
925
976
|
"vite.config.ts",
|
|
926
977
|
"vite.config.mts",
|
|
@@ -1284,6 +1335,9 @@ var __testUtils = {
|
|
|
1284
1335
|
validateThemeName,
|
|
1285
1336
|
validateThemeTailwindContract,
|
|
1286
1337
|
validateThemeLocales,
|
|
1338
|
+
validateThemePreviewLocales,
|
|
1339
|
+
resolveThemePreviewLocale,
|
|
1340
|
+
withSerializedVitePreviewLocaleLoad,
|
|
1287
1341
|
toViteModuleUrl,
|
|
1288
1342
|
updateRemotePageDraft
|
|
1289
1343
|
};
|
|
@@ -1381,7 +1435,15 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
1381
1435
|
return;
|
|
1382
1436
|
}
|
|
1383
1437
|
try {
|
|
1384
|
-
const
|
|
1438
|
+
const acceptLanguageHeader = request.headers["accept-language"];
|
|
1439
|
+
const acceptLanguage = Array.isArray(acceptLanguageHeader) ? acceptLanguageHeader.join(",") : acceptLanguageHeader;
|
|
1440
|
+
const explicitLang = url.searchParams.get("lang");
|
|
1441
|
+
const { theme, locale } = await loadLocalizedViteDevTheme(
|
|
1442
|
+
root,
|
|
1443
|
+
server,
|
|
1444
|
+
explicitLang,
|
|
1445
|
+
acceptLanguage
|
|
1446
|
+
);
|
|
1385
1447
|
const renderer = await loadThemeScopedRenderer(root);
|
|
1386
1448
|
if (slug === null) {
|
|
1387
1449
|
const homeSlug = pickStarterSlug(theme);
|
|
@@ -1398,7 +1460,13 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
1398
1460
|
}
|
|
1399
1461
|
const html2 = await server.transformIndexHtml(
|
|
1400
1462
|
url.pathname,
|
|
1401
|
-
renderDevStarterPageHtml(
|
|
1463
|
+
renderDevStarterPageHtml(
|
|
1464
|
+
theme,
|
|
1465
|
+
starter2,
|
|
1466
|
+
renderer,
|
|
1467
|
+
locale,
|
|
1468
|
+
canonicalLocale(explicitLang) !== null
|
|
1469
|
+
)
|
|
1402
1470
|
);
|
|
1403
1471
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1404
1472
|
response.end(html2);
|
|
@@ -1412,7 +1480,13 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
1412
1480
|
}
|
|
1413
1481
|
const html = await server.transformIndexHtml(
|
|
1414
1482
|
url.pathname,
|
|
1415
|
-
renderDevStarterPageHtml(
|
|
1483
|
+
renderDevStarterPageHtml(
|
|
1484
|
+
theme,
|
|
1485
|
+
starter,
|
|
1486
|
+
renderer,
|
|
1487
|
+
locale,
|
|
1488
|
+
canonicalLocale(explicitLang) !== null
|
|
1489
|
+
)
|
|
1416
1490
|
);
|
|
1417
1491
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1418
1492
|
response.end(html);
|
|
@@ -1425,6 +1499,48 @@ function createSudaPreviewVitePlugin(root) {
|
|
|
1425
1499
|
}
|
|
1426
1500
|
};
|
|
1427
1501
|
}
|
|
1502
|
+
async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLanguage) {
|
|
1503
|
+
return withSerializedVitePreviewLocaleLoad(server, async () => {
|
|
1504
|
+
const baseTheme = await loadViteDevTheme(root, server);
|
|
1505
|
+
const localeParams = {
|
|
1506
|
+
previewLocales: baseTheme.module.manifest.previewLocales,
|
|
1507
|
+
explicitLang
|
|
1508
|
+
};
|
|
1509
|
+
if (acceptLanguage !== void 0) {
|
|
1510
|
+
localeParams.acceptLanguage = acceptLanguage;
|
|
1511
|
+
}
|
|
1512
|
+
const locale = resolveThemePreviewLocale(localeParams);
|
|
1513
|
+
if (!baseTheme.module.manifest.previewLocales) {
|
|
1514
|
+
return { theme: baseTheme, locale };
|
|
1515
|
+
}
|
|
1516
|
+
const messages = await readDevPreviewLocaleMessages(root, locale);
|
|
1517
|
+
server.moduleGraph.invalidateAll();
|
|
1518
|
+
const theme = await runWithThemePreviewLocale(
|
|
1519
|
+
messages,
|
|
1520
|
+
() => loadViteDevTheme(root, server)
|
|
1521
|
+
);
|
|
1522
|
+
return { theme, locale };
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
var vitePreviewLocaleLoadTails = /* @__PURE__ */ new WeakMap();
|
|
1526
|
+
async function withSerializedVitePreviewLocaleLoad(server, load) {
|
|
1527
|
+
const previous = vitePreviewLocaleLoadTails.get(server) ?? Promise.resolve();
|
|
1528
|
+
let release;
|
|
1529
|
+
const gate = new Promise((resolve) => {
|
|
1530
|
+
release = resolve;
|
|
1531
|
+
});
|
|
1532
|
+
const tail = previous.then(() => gate);
|
|
1533
|
+
vitePreviewLocaleLoadTails.set(server, tail);
|
|
1534
|
+
await previous;
|
|
1535
|
+
try {
|
|
1536
|
+
return await load();
|
|
1537
|
+
} finally {
|
|
1538
|
+
release();
|
|
1539
|
+
if (vitePreviewLocaleLoadTails.get(server) === tail) {
|
|
1540
|
+
vitePreviewLocaleLoadTails.delete(server);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1428
1544
|
async function loadViteDevTheme(root, server) {
|
|
1429
1545
|
const serverEntryPath = await findServerEntry(root);
|
|
1430
1546
|
const imported = await server.ssrLoadModule(toViteModuleUrl(root, serverEntryPath));
|
|
@@ -1461,6 +1577,73 @@ function resolveDevStarterSlug(pathname) {
|
|
|
1461
1577
|
}
|
|
1462
1578
|
return decodeURIComponent(normalized);
|
|
1463
1579
|
}
|
|
1580
|
+
function canonicalLocale(value) {
|
|
1581
|
+
const candidate = value?.trim();
|
|
1582
|
+
if (!candidate || candidate === "*") {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
try {
|
|
1586
|
+
return Intl.getCanonicalLocales(candidate)[0] ?? null;
|
|
1587
|
+
} catch {
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function acceptLanguageLocales(value) {
|
|
1592
|
+
if (!value) {
|
|
1593
|
+
return [];
|
|
1594
|
+
}
|
|
1595
|
+
return value.split(",").map((part, index) => {
|
|
1596
|
+
const [rawLocale, ...parameters] = part.trim().split(";");
|
|
1597
|
+
const locale = canonicalLocale(rawLocale);
|
|
1598
|
+
const qualityParameter = parameters.find((parameter) => parameter.trim().startsWith("q="));
|
|
1599
|
+
const parsedQuality = qualityParameter ? Number.parseFloat(qualityParameter.trim().slice(2)) : 1;
|
|
1600
|
+
const quality = Number.isFinite(parsedQuality) ? parsedQuality : 0;
|
|
1601
|
+
return { locale, quality, index };
|
|
1602
|
+
}).filter(
|
|
1603
|
+
(entry) => Boolean(entry.locale) && entry.quality > 0
|
|
1604
|
+
).sort((left, right) => right.quality - left.quality || left.index - right.index).map((entry) => entry.locale);
|
|
1605
|
+
}
|
|
1606
|
+
function localeLookupCandidates(locale) {
|
|
1607
|
+
const candidates = [];
|
|
1608
|
+
let current = locale;
|
|
1609
|
+
while (current) {
|
|
1610
|
+
candidates.push(current);
|
|
1611
|
+
const separator = current.lastIndexOf("-");
|
|
1612
|
+
if (separator < 0) {
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
current = current.slice(0, separator);
|
|
1616
|
+
}
|
|
1617
|
+
return candidates;
|
|
1618
|
+
}
|
|
1619
|
+
function resolveThemePreviewLocale(params) {
|
|
1620
|
+
const supported = params.previewLocales ?? ["en"];
|
|
1621
|
+
const supportedByNormalized = new Map(
|
|
1622
|
+
supported.map((locale) => [locale.toLowerCase(), locale])
|
|
1623
|
+
);
|
|
1624
|
+
const explicitLocale = canonicalLocale(params.explicitLang);
|
|
1625
|
+
const requestedLocales = explicitLocale ? [explicitLocale] : acceptLanguageLocales(params.acceptLanguage);
|
|
1626
|
+
for (const requested of requestedLocales) {
|
|
1627
|
+
for (const candidate of localeLookupCandidates(requested)) {
|
|
1628
|
+
const supportedLocale = supportedByNormalized.get(candidate.toLowerCase());
|
|
1629
|
+
if (supportedLocale) {
|
|
1630
|
+
return supportedLocale;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (explicitLocale) {
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return supportedByNormalized.get("en") ?? "en";
|
|
1638
|
+
}
|
|
1639
|
+
async function readDevPreviewLocaleMessages(root, locale) {
|
|
1640
|
+
const filePath = path2.join(root, "src", "locales", `${locale}.content.json`);
|
|
1641
|
+
const messages = await readLocaleJson(filePath);
|
|
1642
|
+
if (!isRecord(messages)) {
|
|
1643
|
+
throw new Error(`Theme preview locale ${locale} must be a JSON object.`);
|
|
1644
|
+
}
|
|
1645
|
+
return messages;
|
|
1646
|
+
}
|
|
1464
1647
|
function escapeHtml(value) {
|
|
1465
1648
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1466
1649
|
}
|
|
@@ -1494,7 +1677,7 @@ async function loadThemeScopedRenderer(themeRoot) {
|
|
|
1494
1677
|
renderToString: reactDomServerModule.renderToString
|
|
1495
1678
|
};
|
|
1496
1679
|
}
|
|
1497
|
-
function renderDevStarterPageHtml(theme, page, renderer) {
|
|
1680
|
+
function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false) {
|
|
1498
1681
|
const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
|
|
1499
1682
|
const metadata = {
|
|
1500
1683
|
resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
|
|
@@ -1513,7 +1696,7 @@ function renderDevStarterPageHtml(theme, page, renderer) {
|
|
|
1513
1696
|
const customBody = chrome.customBodyCode ?? "";
|
|
1514
1697
|
return [
|
|
1515
1698
|
"<!doctype html>",
|
|
1516
|
-
|
|
1699
|
+
`<html lang="${escapeHtml(locale)}">`,
|
|
1517
1700
|
"<head>",
|
|
1518
1701
|
'<meta charset="utf-8" />',
|
|
1519
1702
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
@@ -1524,6 +1707,9 @@ function renderDevStarterPageHtml(theme, page, renderer) {
|
|
|
1524
1707
|
`<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
|
|
1525
1708
|
body,
|
|
1526
1709
|
customBody,
|
|
1710
|
+
...preserveLang ? [
|
|
1711
|
+
'<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>'
|
|
1712
|
+
] : [],
|
|
1527
1713
|
"</body>",
|
|
1528
1714
|
"</html>"
|
|
1529
1715
|
].join("");
|