@numueg/theme-cli 0.1.0 → 0.2.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.js +1111 -81
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -591,13 +591,154 @@ var init_manifest_required_fields = __esm({
|
|
|
591
591
|
}
|
|
592
592
|
});
|
|
593
593
|
|
|
594
|
+
// src/lint/rules/contrast-hint.ts
|
|
595
|
+
var contrast_hint_exports = {};
|
|
596
|
+
__export(contrast_hint_exports, {
|
|
597
|
+
default: () => contrast_hint_default
|
|
598
|
+
});
|
|
599
|
+
function hexToRgb(hex) {
|
|
600
|
+
let h = hex.replace("#", "").trim();
|
|
601
|
+
if (h.length === 3) {
|
|
602
|
+
h = h.split("").map((c) => c + c).join("");
|
|
603
|
+
}
|
|
604
|
+
if (h.length !== 6 || /[^0-9a-fA-F]/.test(h)) return null;
|
|
605
|
+
const n = parseInt(h, 16);
|
|
606
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
607
|
+
}
|
|
608
|
+
function relativeLuminance([r, g, b]) {
|
|
609
|
+
const a = [r, g, b].map((v) => {
|
|
610
|
+
const s = v / 255;
|
|
611
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
612
|
+
});
|
|
613
|
+
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
|
614
|
+
}
|
|
615
|
+
function contrastRatio(c1, c2) {
|
|
616
|
+
const l1 = relativeLuminance(c1);
|
|
617
|
+
const l2 = relativeLuminance(c2);
|
|
618
|
+
const hi = Math.max(l1, l2);
|
|
619
|
+
const lo = Math.min(l1, l2);
|
|
620
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
621
|
+
}
|
|
622
|
+
var rule11, contrast_hint_default;
|
|
623
|
+
var init_contrast_hint = __esm({
|
|
624
|
+
"src/lint/rules/contrast-hint.ts"() {
|
|
625
|
+
"use strict";
|
|
626
|
+
rule11 = {
|
|
627
|
+
id: "contrast-hint",
|
|
628
|
+
description: "Co-located color/background hex pairs should meet WCAG 4.5:1 contrast",
|
|
629
|
+
check(ctx) {
|
|
630
|
+
const issues = [];
|
|
631
|
+
const css = ctx.styles;
|
|
632
|
+
if (!css) return issues;
|
|
633
|
+
const blockRe = /([^{}]*)\{([^{}]*)\}/g;
|
|
634
|
+
let m;
|
|
635
|
+
while ((m = blockRe.exec(css)) !== null) {
|
|
636
|
+
const rawSelector = m[1].trim().split("\n").pop()?.trim() || m[1].trim();
|
|
637
|
+
const body = m[2];
|
|
638
|
+
const colorM = body.match(/(?:^|[;{\s])color:\s*(#[0-9a-fA-F]{3,6})\b/);
|
|
639
|
+
const bgM = body.match(
|
|
640
|
+
/background(?:-color)?:\s*(#[0-9a-fA-F]{3,6})\b/
|
|
641
|
+
);
|
|
642
|
+
if (!colorM || !bgM) continue;
|
|
643
|
+
const fg = hexToRgb(colorM[1]);
|
|
644
|
+
const bg = hexToRgb(bgM[1]);
|
|
645
|
+
if (!fg || !bg) continue;
|
|
646
|
+
const ratio = contrastRatio(fg, bg);
|
|
647
|
+
if (ratio < 4.5) {
|
|
648
|
+
const line = css.slice(0, m.index).split("\n").length;
|
|
649
|
+
issues.push({
|
|
650
|
+
rule: rule11.id,
|
|
651
|
+
severity: "warning",
|
|
652
|
+
file: "styles.css",
|
|
653
|
+
line,
|
|
654
|
+
message: `Low contrast ${ratio.toFixed(2)}:1 for "${rawSelector}" (${colorM[1]} on ${bgM[1]}).`,
|
|
655
|
+
suggestion: ratio >= 3 ? `Meets 3:1 (OK for large text \u226524px/19px-bold) but not the 4.5:1 body-text minimum.` : `Below WCAG 4.5:1 \u2014 darken/lighten one color, or drive both from a color_scheme so merchants control contrast.`
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return issues;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
contrast_hint_default = rule11;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// src/lint/rules/touch-target.ts
|
|
667
|
+
var touch_target_exports = {};
|
|
668
|
+
__export(touch_target_exports, {
|
|
669
|
+
default: () => touch_target_default
|
|
670
|
+
});
|
|
671
|
+
var INTERACTIVE, DIM_PROPS, rule12, touch_target_default;
|
|
672
|
+
var init_touch_target = __esm({
|
|
673
|
+
"src/lint/rules/touch-target.ts"() {
|
|
674
|
+
"use strict";
|
|
675
|
+
INTERACTIVE = /(^|[\s,>+~])(button|a|\.btn\b|\[role=["']?button["']?\]|input\[type=["']?(?:button|submit|reset)["']?\])/i;
|
|
676
|
+
DIM_PROPS = ["width", "height", "min-width", "min-height"];
|
|
677
|
+
rule12 = {
|
|
678
|
+
id: "touch-target",
|
|
679
|
+
description: "Interactive targets should be \u2265 24\xD724px (WCAG 2.5.8)",
|
|
680
|
+
check(ctx) {
|
|
681
|
+
const issues = [];
|
|
682
|
+
const css = ctx.styles;
|
|
683
|
+
if (!css) return issues;
|
|
684
|
+
const blockRe = /([^{}]*)\{([^{}]*)\}/g;
|
|
685
|
+
let m;
|
|
686
|
+
while ((m = blockRe.exec(css)) !== null) {
|
|
687
|
+
const rawSelector = m[1].trim().split("\n").pop()?.trim() || m[1].trim();
|
|
688
|
+
if (rawSelector.includes("::")) continue;
|
|
689
|
+
if (!INTERACTIVE.test(rawSelector)) continue;
|
|
690
|
+
const body = m[2];
|
|
691
|
+
for (const prop of DIM_PROPS) {
|
|
692
|
+
const pm = body.match(
|
|
693
|
+
new RegExp(`(?:^|[;{\\s])${prop}:\\s*([0-9.]+)px\\b`)
|
|
694
|
+
);
|
|
695
|
+
if (!pm) continue;
|
|
696
|
+
const px = parseFloat(pm[1]);
|
|
697
|
+
if (px > 0 && px < 24) {
|
|
698
|
+
const line = css.slice(0, m.index).split("\n").length;
|
|
699
|
+
issues.push({
|
|
700
|
+
rule: rule12.id,
|
|
701
|
+
severity: "warning",
|
|
702
|
+
file: "styles.css",
|
|
703
|
+
line,
|
|
704
|
+
message: `Interactive "${rawSelector}" sets ${prop}: ${px}px (< 24px touch target).`,
|
|
705
|
+
suggestion: `Give clickable elements a \u226524\xD724px hit area (min-width/min-height: 24px or adequate padding).`
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return issues;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
touch_target_default = rule12;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
594
718
|
// src/index.ts
|
|
595
|
-
var
|
|
719
|
+
var import_commander17 = require("commander");
|
|
596
720
|
|
|
597
721
|
// src/commands/init.ts
|
|
598
722
|
var import_commander = require("commander");
|
|
723
|
+
var import_child_process = require("child_process");
|
|
599
724
|
var fs = __toESM(require("fs"));
|
|
600
725
|
var path = __toESM(require("path"));
|
|
726
|
+
function detectAuthor() {
|
|
727
|
+
const git = (args) => {
|
|
728
|
+
try {
|
|
729
|
+
return (0, import_child_process.execSync)(`git config ${args}`, {
|
|
730
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
731
|
+
}).toString().trim();
|
|
732
|
+
} catch {
|
|
733
|
+
return "";
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const name = git("user.name");
|
|
737
|
+
const email = git("user.email");
|
|
738
|
+
if (name && email) return `${name} <${email}>`;
|
|
739
|
+
if (name) return name;
|
|
740
|
+
return "Theme Author";
|
|
741
|
+
}
|
|
601
742
|
var initCommand = new import_commander.Command("init").description("Scaffold a new NUMU theme project").argument("<name>", "Theme name").option("--template <template>", "Starter template", "basic").action(async (name, _options) => {
|
|
602
743
|
const dir = path.resolve(process.cwd(), name);
|
|
603
744
|
if (fs.existsSync(dir)) {
|
|
@@ -620,7 +761,7 @@ var initCommand = new import_commander.Command("init").description("Scaffold a n
|
|
|
620
761
|
{
|
|
621
762
|
id: name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
622
763
|
name,
|
|
623
|
-
author:
|
|
764
|
+
author: detectAuthor(),
|
|
624
765
|
version: "0.1.0",
|
|
625
766
|
layout: "single-column",
|
|
626
767
|
description: `${name} NUMU theme`,
|
|
@@ -1020,10 +1161,10 @@ export default defineConfig({
|
|
|
1020
1161
|
build: "numu-theme build",
|
|
1021
1162
|
check: "numu-theme check"
|
|
1022
1163
|
},
|
|
1023
|
-
dependencies: { "@numueg/theme-sdk": "^0.
|
|
1164
|
+
dependencies: { "@numueg/theme-sdk": "^0.2.0" },
|
|
1024
1165
|
devDependencies: {
|
|
1025
|
-
"@numueg/theme-cli": "^0.
|
|
1026
|
-
"@numueg/theme-plugin": "^0.
|
|
1166
|
+
"@numueg/theme-cli": "^0.2.0",
|
|
1167
|
+
"@numueg/theme-plugin": "^0.2.0",
|
|
1027
1168
|
"@vitejs/plugin-react": "^4.3.0",
|
|
1028
1169
|
vite: "^6.0.0",
|
|
1029
1170
|
typescript: "^5.8.0",
|
|
@@ -1056,20 +1197,27 @@ Next steps:
|
|
|
1056
1197
|
|
|
1057
1198
|
// src/commands/dev.ts
|
|
1058
1199
|
var import_commander2 = require("commander");
|
|
1059
|
-
var
|
|
1200
|
+
var import_child_process2 = require("child_process");
|
|
1060
1201
|
|
|
1061
1202
|
// src/utils/config.ts
|
|
1062
1203
|
var fs2 = __toESM(require("fs"));
|
|
1063
1204
|
var path2 = __toESM(require("path"));
|
|
1064
1205
|
var os = __toESM(require("os"));
|
|
1065
|
-
|
|
1206
|
+
function configHome() {
|
|
1207
|
+
const override = process.env.NUMU_HOME;
|
|
1208
|
+
if (override && override.length > 0) return override;
|
|
1209
|
+
return os.homedir();
|
|
1210
|
+
}
|
|
1211
|
+
function rcFile() {
|
|
1212
|
+
return path2.join(configHome(), ".numurc");
|
|
1213
|
+
}
|
|
1066
1214
|
function loadConfig() {
|
|
1067
1215
|
const config = {
|
|
1068
1216
|
api_url: process.env.NUMU_API_URL || "https://api.numu.io/api/v1"
|
|
1069
1217
|
};
|
|
1070
|
-
if (fs2.existsSync(
|
|
1218
|
+
if (fs2.existsSync(rcFile())) {
|
|
1071
1219
|
try {
|
|
1072
|
-
const rc = JSON.parse(fs2.readFileSync(
|
|
1220
|
+
const rc = JSON.parse(fs2.readFileSync(rcFile(), "utf-8"));
|
|
1073
1221
|
if (rc.token) config.token = rc.token;
|
|
1074
1222
|
if (rc.api_url) config.api_url = rc.api_url;
|
|
1075
1223
|
if (rc.store_id) config.store_id = rc.store_id;
|
|
@@ -1082,16 +1230,16 @@ function loadConfig() {
|
|
|
1082
1230
|
}
|
|
1083
1231
|
function saveConfig(updates) {
|
|
1084
1232
|
let existing = {};
|
|
1085
|
-
if (fs2.existsSync(
|
|
1233
|
+
if (fs2.existsSync(rcFile())) {
|
|
1086
1234
|
try {
|
|
1087
|
-
existing = JSON.parse(fs2.readFileSync(
|
|
1235
|
+
existing = JSON.parse(fs2.readFileSync(rcFile(), "utf-8"));
|
|
1088
1236
|
} catch {
|
|
1089
1237
|
}
|
|
1090
1238
|
}
|
|
1091
1239
|
const merged = { ...existing, ...updates };
|
|
1092
|
-
fs2.writeFileSync(
|
|
1240
|
+
fs2.writeFileSync(rcFile(), JSON.stringify(merged, null, 2), { mode: 384 });
|
|
1093
1241
|
try {
|
|
1094
|
-
fs2.chmodSync(
|
|
1242
|
+
fs2.chmodSync(rcFile(), 384);
|
|
1095
1243
|
} catch {
|
|
1096
1244
|
}
|
|
1097
1245
|
}
|
|
@@ -1126,12 +1274,12 @@ function assertHttpsOrLocalhost(urlStr) {
|
|
|
1126
1274
|
}
|
|
1127
1275
|
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
1128
1276
|
}
|
|
1129
|
-
async function apiRequest(method,
|
|
1277
|
+
async function apiRequest(method, path15, body) {
|
|
1130
1278
|
const config = loadConfig();
|
|
1131
|
-
const url = assertHttpsOrLocalhost(`${config.api_url}${
|
|
1279
|
+
const url = assertHttpsOrLocalhost(`${config.api_url}${path15}`);
|
|
1132
1280
|
const isHttps = url.protocol === "https:";
|
|
1133
1281
|
const transport = isHttps ? https : http;
|
|
1134
|
-
return new Promise((
|
|
1282
|
+
return new Promise((resolve9, reject) => {
|
|
1135
1283
|
const headers = {};
|
|
1136
1284
|
if (config.token) headers["Authorization"] = `Bearer ${config.token}`;
|
|
1137
1285
|
let postData;
|
|
@@ -1153,7 +1301,7 @@ async function apiRequest(method, path14, body) {
|
|
|
1153
1301
|
res.on("data", (chunk) => data += chunk);
|
|
1154
1302
|
res.on("end", () => {
|
|
1155
1303
|
const { unwrapped, raw } = parseBody(data);
|
|
1156
|
-
|
|
1304
|
+
resolve9({
|
|
1157
1305
|
status: res.statusCode ?? 0,
|
|
1158
1306
|
data: unwrapped,
|
|
1159
1307
|
raw
|
|
@@ -1186,7 +1334,7 @@ Content-Type: application/zip\r
|
|
|
1186
1334
|
const fullBody = Buffer.concat([head, fileBuffer, tail]);
|
|
1187
1335
|
const isHttps = url.protocol === "https:";
|
|
1188
1336
|
const transport = isHttps ? https : http;
|
|
1189
|
-
return new Promise((
|
|
1337
|
+
return new Promise((resolve9, reject) => {
|
|
1190
1338
|
const headers = {
|
|
1191
1339
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
1192
1340
|
"Content-Length": String(fullBody.byteLength)
|
|
@@ -1209,7 +1357,7 @@ Content-Type: application/zip\r
|
|
|
1209
1357
|
res.on("data", (chunk) => data += chunk);
|
|
1210
1358
|
res.on("end", () => {
|
|
1211
1359
|
const { unwrapped, raw } = parseBody(data);
|
|
1212
|
-
|
|
1360
|
+
resolve9({
|
|
1213
1361
|
status: res.statusCode ?? 0,
|
|
1214
1362
|
data: unwrapped,
|
|
1215
1363
|
raw
|
|
@@ -1265,11 +1413,11 @@ var devCommand = new import_commander2.Command("dev").description("Start local d
|
|
|
1265
1413
|
console.log(
|
|
1266
1414
|
`Starting Vite on ${options.expose ? "0.0.0.0" : "localhost"}:${options.port}...`
|
|
1267
1415
|
);
|
|
1268
|
-
const child = (0,
|
|
1416
|
+
const child = (0, import_child_process2.spawn)("npx", args, { stdio: "inherit", shell: true });
|
|
1269
1417
|
let watcher = null;
|
|
1270
1418
|
if (options.watch) {
|
|
1271
1419
|
console.log("Starting bundle watcher (vite build --watch)...");
|
|
1272
|
-
watcher = (0,
|
|
1420
|
+
watcher = (0, import_child_process2.spawn)(
|
|
1273
1421
|
"npx",
|
|
1274
1422
|
["vite", "build", "--watch", "--mode", "development"],
|
|
1275
1423
|
{ stdio: "inherit", shell: true }
|
|
@@ -1295,6 +1443,7 @@ var devCommand = new import_commander2.Command("dev").description("Start local d
|
|
|
1295
1443
|
|
|
1296
1444
|
// src/commands/check.ts
|
|
1297
1445
|
var import_commander3 = require("commander");
|
|
1446
|
+
var import_child_process3 = require("child_process");
|
|
1298
1447
|
|
|
1299
1448
|
// src/utils/validator.ts
|
|
1300
1449
|
var fs4 = __toESM(require("fs"));
|
|
@@ -1387,23 +1536,88 @@ function validateTheme(themeDir) {
|
|
|
1387
1536
|
}
|
|
1388
1537
|
|
|
1389
1538
|
// src/commands/check.ts
|
|
1390
|
-
var checkCommand = new import_commander3.Command("check").description("Validate theme schemas and structure").option("-d, --dir <directory>", "Theme directory", ".").
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1539
|
+
var checkCommand = new import_commander3.Command("check").description("Validate theme schemas and structure").option("-d, --dir <directory>", "Theme directory", ".").option(
|
|
1540
|
+
"--lighthouse",
|
|
1541
|
+
"Run a Lighthouse audit (performance + accessibility) \u2014 needs --url"
|
|
1542
|
+
).option(
|
|
1543
|
+
"--url <url>",
|
|
1544
|
+
"URL of the built theme on a seeded store to audit (e.g. http://localhost:3100/<store>)"
|
|
1545
|
+
).option("--perf <score>", "Minimum Lighthouse performance score (0-100)", "60").option(
|
|
1546
|
+
"--a11y <score>",
|
|
1547
|
+
"Minimum Lighthouse accessibility score (0-100)",
|
|
1548
|
+
"90"
|
|
1549
|
+
).option(
|
|
1550
|
+
"--strict",
|
|
1551
|
+
"Exit non-zero when below thresholds (default: soft gate \u2014 report only)"
|
|
1552
|
+
).action(
|
|
1553
|
+
async (options) => {
|
|
1554
|
+
console.log("Validating theme...");
|
|
1555
|
+
const result = validateTheme(options.dir);
|
|
1556
|
+
if (result.warnings.length > 0) {
|
|
1557
|
+
console.log("\nWarnings:");
|
|
1558
|
+
result.warnings.forEach((w) => console.log(` \u26A0 ${w}`));
|
|
1559
|
+
}
|
|
1560
|
+
if (result.errors.length > 0) {
|
|
1561
|
+
console.log("\nErrors:");
|
|
1562
|
+
result.errors.forEach((e) => console.log(` \u2717 ${e}`));
|
|
1563
|
+
console.log(`
|
|
1401
1564
|
Validation failed with ${result.errors.length} error(s)`);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
console.log(`
|
|
1568
|
+
\u2713 Theme is valid (${result.warnings.length} warning(s))`);
|
|
1569
|
+
if (options.lighthouse) {
|
|
1570
|
+
runLighthouse(options);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
function runLighthouse(options) {
|
|
1575
|
+
if (!options.url) {
|
|
1576
|
+
console.error(
|
|
1577
|
+
"\n\u2718 --lighthouse requires --url <url> (a built theme on a seeded store)."
|
|
1578
|
+
);
|
|
1402
1579
|
process.exit(1);
|
|
1403
1580
|
}
|
|
1581
|
+
const perfMin = parseFloat(options.perf);
|
|
1582
|
+
const a11yMin = parseFloat(options.a11y);
|
|
1404
1583
|
console.log(`
|
|
1405
|
-
|
|
1406
|
-
|
|
1584
|
+
Running Lighthouse against ${options.url} ...`);
|
|
1585
|
+
let raw;
|
|
1586
|
+
try {
|
|
1587
|
+
raw = (0, import_child_process3.execSync)(
|
|
1588
|
+
`npx --yes lighthouse "${options.url}" --output=json --output-path=stdout --only-categories=performance,accessibility --chrome-flags="--headless=new --no-sandbox --disable-gpu" --quiet --no-enable-error-reporting`,
|
|
1589
|
+
{ encoding: "utf-8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }
|
|
1590
|
+
);
|
|
1591
|
+
} catch (e) {
|
|
1592
|
+
console.warn(
|
|
1593
|
+
"\n\u26A0 Could not run Lighthouse (install it: `npm i -g lighthouse`, and ensure Chrome is available). Soft-skipping."
|
|
1594
|
+
);
|
|
1595
|
+
console.warn(` ${String(e.message).split("\n")[0]}`);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
let report;
|
|
1599
|
+
try {
|
|
1600
|
+
report = JSON.parse(raw);
|
|
1601
|
+
} catch {
|
|
1602
|
+
console.warn("\n\u26A0 Lighthouse output was not parseable JSON; skipping gate.");
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const perf = Math.round((report.categories?.performance?.score ?? 0) * 100);
|
|
1606
|
+
const a11y = Math.round((report.categories?.accessibility?.score ?? 0) * 100);
|
|
1607
|
+
console.log(` Performance: ${perf} (min ${perfMin})`);
|
|
1608
|
+
console.log(` Accessibility: ${a11y} (min ${a11yMin})`);
|
|
1609
|
+
const failed = perf < perfMin || a11y < a11yMin;
|
|
1610
|
+
if (!failed) {
|
|
1611
|
+
console.log("\u2713 Lighthouse thresholds met.");
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const msg = `Lighthouse below threshold (Perf ${perf}/${perfMin}, A11y ${a11y}/${a11yMin}).`;
|
|
1615
|
+
if (options.strict) {
|
|
1616
|
+
console.error(`\u2718 ${msg}`);
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
console.warn(`\u26A0 ${msg} (soft gate \u2014 pass --strict to enforce.)`);
|
|
1620
|
+
}
|
|
1407
1621
|
|
|
1408
1622
|
// src/commands/lint.ts
|
|
1409
1623
|
var import_commander4 = require("commander");
|
|
@@ -1425,7 +1639,9 @@ async function runAllRules(themeDir, options) {
|
|
|
1425
1639
|
() => Promise.resolve().then(() => (init_inline_color_literal(), inline_color_literal_exports)),
|
|
1426
1640
|
() => Promise.resolve().then(() => (init_forbidden_script_tag(), forbidden_script_tag_exports)),
|
|
1427
1641
|
() => Promise.resolve().then(() => (init_use_app_no_availability_check(), use_app_no_availability_check_exports)),
|
|
1428
|
-
() => Promise.resolve().then(() => (init_manifest_required_fields(), manifest_required_fields_exports))
|
|
1642
|
+
() => Promise.resolve().then(() => (init_manifest_required_fields(), manifest_required_fields_exports)),
|
|
1643
|
+
() => Promise.resolve().then(() => (init_contrast_hint(), contrast_hint_exports)),
|
|
1644
|
+
() => Promise.resolve().then(() => (init_touch_target(), touch_target_exports))
|
|
1429
1645
|
];
|
|
1430
1646
|
const issues = [];
|
|
1431
1647
|
for (const load of ruleLoaders) {
|
|
@@ -1440,14 +1656,14 @@ async function runAllRules(themeDir, options) {
|
|
|
1440
1656
|
});
|
|
1441
1657
|
continue;
|
|
1442
1658
|
}
|
|
1443
|
-
const
|
|
1444
|
-
if (options.enabledRules && !options.enabledRules.has(
|
|
1659
|
+
const rule13 = mod.default;
|
|
1660
|
+
if (options.enabledRules && !options.enabledRules.has(rule13.id)) continue;
|
|
1445
1661
|
try {
|
|
1446
|
-
const ruleIssues = await
|
|
1662
|
+
const ruleIssues = await rule13.check(ctx);
|
|
1447
1663
|
for (const issue of ruleIssues) issues.push(issue);
|
|
1448
1664
|
} catch (e) {
|
|
1449
1665
|
issues.push({
|
|
1450
|
-
rule:
|
|
1666
|
+
rule: rule13.id,
|
|
1451
1667
|
severity: "warning",
|
|
1452
1668
|
message: `Rule crashed: ${e.message}`
|
|
1453
1669
|
});
|
|
@@ -1467,6 +1683,7 @@ function buildContext(themeDir) {
|
|
|
1467
1683
|
const blockSchemas = readSchemaDir(path4.join(themeDir, "schemas", "blocks"));
|
|
1468
1684
|
const locales = readLocales(path4.join(themeDir, "locales"));
|
|
1469
1685
|
const sources = readSources(themeDir);
|
|
1686
|
+
const styles = readText(path4.join(themeDir, "styles.css"));
|
|
1470
1687
|
return {
|
|
1471
1688
|
themeDir,
|
|
1472
1689
|
manifest,
|
|
@@ -1474,7 +1691,8 @@ function buildContext(themeDir) {
|
|
|
1474
1691
|
sectionSchemas,
|
|
1475
1692
|
blockSchemas,
|
|
1476
1693
|
locales,
|
|
1477
|
-
sources
|
|
1694
|
+
sources,
|
|
1695
|
+
styles
|
|
1478
1696
|
};
|
|
1479
1697
|
}
|
|
1480
1698
|
function readJson(filePath, fallback) {
|
|
@@ -1485,6 +1703,14 @@ function readJson(filePath, fallback) {
|
|
|
1485
1703
|
return fallback;
|
|
1486
1704
|
}
|
|
1487
1705
|
}
|
|
1706
|
+
function readText(filePath) {
|
|
1707
|
+
if (!fs5.existsSync(filePath)) return "";
|
|
1708
|
+
try {
|
|
1709
|
+
return fs5.readFileSync(filePath, "utf-8");
|
|
1710
|
+
} catch {
|
|
1711
|
+
return "";
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1488
1714
|
function readSchemaDir(dir) {
|
|
1489
1715
|
const out = {};
|
|
1490
1716
|
if (!fs5.existsSync(dir)) return out;
|
|
@@ -1592,41 +1818,79 @@ function groupByFile(issues) {
|
|
|
1592
1818
|
|
|
1593
1819
|
// src/commands/build.ts
|
|
1594
1820
|
var import_commander5 = require("commander");
|
|
1595
|
-
var
|
|
1821
|
+
var import_child_process4 = require("child_process");
|
|
1596
1822
|
var fs7 = __toESM(require("fs"));
|
|
1597
1823
|
var path6 = __toESM(require("path"));
|
|
1598
|
-
var buildCommand = new import_commander5.Command("build").description("Build the theme for production").option("-d, --dir <directory>", "Theme directory", ".").
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
} catch {
|
|
1610
|
-
console.error("Build failed");
|
|
1611
|
-
process.exit(1);
|
|
1612
|
-
}
|
|
1613
|
-
const distDir = path6.join(options.dir, "dist");
|
|
1614
|
-
if (fs7.existsSync(distDir)) {
|
|
1615
|
-
let totalSize = 0;
|
|
1616
|
-
const files = fs7.readdirSync(distDir, { recursive: true });
|
|
1617
|
-
for (const file of files) {
|
|
1618
|
-
const filePath = path6.join(distDir, file);
|
|
1619
|
-
if (fs7.statSync(filePath).isFile()) totalSize += fs7.statSync(filePath).size;
|
|
1824
|
+
var buildCommand = new import_commander5.Command("build").description("Build the theme for production").option("-d, --dir <directory>", "Theme directory", ".").option(
|
|
1825
|
+
"--strict",
|
|
1826
|
+
"Treat lint warnings as errors (Shopify-style a11y/quality gate)"
|
|
1827
|
+
).option("--no-lint", "Skip the lint gate (not recommended)").action(
|
|
1828
|
+
async (options) => {
|
|
1829
|
+
console.log("Validating before build...");
|
|
1830
|
+
const result = validateTheme(options.dir);
|
|
1831
|
+
if (!result.valid) {
|
|
1832
|
+
console.error("Validation failed. Fix errors before building.");
|
|
1833
|
+
result.errors.forEach((e) => console.error(` \u2717 ${e}`));
|
|
1834
|
+
process.exit(1);
|
|
1620
1835
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1836
|
+
if (options.lint !== false) {
|
|
1837
|
+
const issues = await runAllRules(
|
|
1838
|
+
path6.resolve(process.cwd(), options.dir),
|
|
1839
|
+
{ enabledRules: null }
|
|
1840
|
+
);
|
|
1841
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
1842
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
1843
|
+
if (issues.length > 0) {
|
|
1844
|
+
console.log("\nLint:");
|
|
1845
|
+
for (const i of issues) {
|
|
1846
|
+
const marker = i.severity === "error" ? "\u2718" : "\u26A0";
|
|
1847
|
+
const loc = i.file ? ` ${i.file}${i.line ? ":" + i.line : ""}` : "";
|
|
1848
|
+
console.log(` ${marker} ${i.rule}${loc} \u2014 ${i.message}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
const blocked = errors.length > 0 || options.strict && warnings.length > 0;
|
|
1852
|
+
if (blocked) {
|
|
1853
|
+
console.error(
|
|
1854
|
+
`
|
|
1855
|
+
Lint gate failed: ${errors.length} error(s), ${warnings.length} warning(s)${options.strict ? " (--strict treats warnings as errors)" : ""}.`
|
|
1856
|
+
);
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
if (warnings.length > 0) {
|
|
1860
|
+
console.log(
|
|
1861
|
+
`
|
|
1862
|
+
\u26A0 ${warnings.length} lint warning(s) \u2014 run \`numu-theme lint\` for detail, or \`--strict\` to enforce.`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
console.log("Building theme...");
|
|
1867
|
+
try {
|
|
1868
|
+
(0, import_child_process4.execSync)("npx vite build", { cwd: options.dir, stdio: "inherit" });
|
|
1869
|
+
} catch {
|
|
1870
|
+
console.error("Build failed");
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
const distDir = path6.join(options.dir, "dist");
|
|
1874
|
+
if (fs7.existsSync(distDir)) {
|
|
1875
|
+
let totalSize = 0;
|
|
1876
|
+
const files = fs7.readdirSync(distDir, { recursive: true });
|
|
1877
|
+
for (const file of files) {
|
|
1878
|
+
const filePath = path6.join(distDir, file);
|
|
1879
|
+
if (fs7.statSync(filePath).isFile())
|
|
1880
|
+
totalSize += fs7.statSync(filePath).size;
|
|
1881
|
+
}
|
|
1882
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
1883
|
+
console.log(`
|
|
1623
1884
|
Bundle size: ${sizeMB} MB`);
|
|
1624
|
-
|
|
1625
|
-
|
|
1885
|
+
if (totalSize > 5 * 1024 * 1024) {
|
|
1886
|
+
console.warn(
|
|
1887
|
+
"\u26A0 Bundle exceeds 5MB limit \u2014 optimize before submitting to marketplace"
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1626
1890
|
}
|
|
1891
|
+
console.log("\n\u2713 Build complete");
|
|
1627
1892
|
}
|
|
1628
|
-
|
|
1629
|
-
});
|
|
1893
|
+
);
|
|
1630
1894
|
|
|
1631
1895
|
// src/commands/push.ts
|
|
1632
1896
|
var import_commander6 = require("commander");
|
|
@@ -1638,10 +1902,10 @@ var fs8 = __toESM(require("fs"));
|
|
|
1638
1902
|
var import_archiver = __toESM(require("archiver"));
|
|
1639
1903
|
async function zipDirectory(sourceDir, outputPath, optsOrLegacyExcludes = {}) {
|
|
1640
1904
|
const opts = Array.isArray(optsOrLegacyExcludes) ? { excludePatterns: optsOrLegacyExcludes } : optsOrLegacyExcludes;
|
|
1641
|
-
return new Promise((
|
|
1905
|
+
return new Promise((resolve9, reject) => {
|
|
1642
1906
|
const output = fs8.createWriteStream(outputPath);
|
|
1643
1907
|
const archive = (0, import_archiver.default)("zip", { zlib: { level: 9 } });
|
|
1644
|
-
output.on("close", () =>
|
|
1908
|
+
output.on("close", () => resolve9(outputPath));
|
|
1645
1909
|
archive.on("error", reject);
|
|
1646
1910
|
archive.pipe(output);
|
|
1647
1911
|
const defaultExcludes = [
|
|
@@ -1792,7 +2056,7 @@ Submission failed (${submitRes.status}): ${JSON.stringify(submitRes.data)}`
|
|
|
1792
2056
|
|
|
1793
2057
|
// src/commands/install.ts
|
|
1794
2058
|
var import_commander8 = require("commander");
|
|
1795
|
-
var
|
|
2059
|
+
var import_child_process5 = require("child_process");
|
|
1796
2060
|
var path9 = __toESM(require("path"));
|
|
1797
2061
|
var os4 = __toESM(require("os"));
|
|
1798
2062
|
var fs10 = __toESM(require("fs"));
|
|
@@ -1853,7 +2117,7 @@ var installCommand = new import_commander8.Command("install").description(
|
|
|
1853
2117
|
console.log(` install tag: ${version}`);
|
|
1854
2118
|
console.log("Building locally (so worker can skip npm install)...");
|
|
1855
2119
|
try {
|
|
1856
|
-
(0,
|
|
2120
|
+
(0, import_child_process5.execSync)("npm run build", {
|
|
1857
2121
|
cwd: options.dir,
|
|
1858
2122
|
stdio: "inherit",
|
|
1859
2123
|
env: { ...process.env, NODE_ENV: "production" }
|
|
@@ -3704,11 +3968,11 @@ var path13 = __toESM(require("path"));
|
|
|
3704
3968
|
var https2 = __toESM(require("https"));
|
|
3705
3969
|
var http2 = __toESM(require("http"));
|
|
3706
3970
|
async function downloadToFile(url, dest) {
|
|
3707
|
-
return new Promise((
|
|
3971
|
+
return new Promise((resolve9, reject) => {
|
|
3708
3972
|
const client = url.startsWith("https:") ? https2 : http2;
|
|
3709
3973
|
const req = client.get(url, (res) => {
|
|
3710
3974
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
3711
|
-
downloadToFile(res.headers.location, dest).then(
|
|
3975
|
+
downloadToFile(res.headers.location, dest).then(resolve9, reject);
|
|
3712
3976
|
return;
|
|
3713
3977
|
}
|
|
3714
3978
|
if (res.statusCode !== 200) {
|
|
@@ -3717,7 +3981,7 @@ async function downloadToFile(url, dest) {
|
|
|
3717
3981
|
}
|
|
3718
3982
|
const out = fs14.createWriteStream(dest);
|
|
3719
3983
|
res.pipe(out);
|
|
3720
|
-
out.on("finish", () => out.close(() =>
|
|
3984
|
+
out.on("finish", () => out.close(() => resolve9()));
|
|
3721
3985
|
out.on("error", reject);
|
|
3722
3986
|
});
|
|
3723
3987
|
req.on("error", reject);
|
|
@@ -3822,10 +4086,10 @@ async function confirm(prompt) {
|
|
|
3822
4086
|
input: process.stdin,
|
|
3823
4087
|
output: process.stdout
|
|
3824
4088
|
});
|
|
3825
|
-
return new Promise((
|
|
4089
|
+
return new Promise((resolve9) => {
|
|
3826
4090
|
rl.question(`${prompt} [y/N] `, (answer) => {
|
|
3827
4091
|
rl.close();
|
|
3828
|
-
|
|
4092
|
+
resolve9(/^y(es)?$/i.test(answer.trim()));
|
|
3829
4093
|
});
|
|
3830
4094
|
});
|
|
3831
4095
|
}
|
|
@@ -3842,8 +4106,8 @@ var deleteCommand = new import_commander15.Command("delete").description("Delete
|
|
|
3842
4106
|
return;
|
|
3843
4107
|
}
|
|
3844
4108
|
}
|
|
3845
|
-
const
|
|
3846
|
-
const res = await apiRequest("DELETE",
|
|
4109
|
+
const path15 = isVersion ? `/api/v1/marketplace/themes/${encodeURIComponent(themeId)}/versions/${encodeURIComponent(options.version)}` : `/api/v1/marketplace/themes/${encodeURIComponent(themeId)}`;
|
|
4110
|
+
const res = await apiRequest("DELETE", path15);
|
|
3847
4111
|
if (res.status === 200 || res.status === 204) {
|
|
3848
4112
|
console.log(`\u2714 Deleted ${scope}.`);
|
|
3849
4113
|
return;
|
|
@@ -3861,8 +4125,773 @@ var deleteCommand = new import_commander15.Command("delete").description("Delete
|
|
|
3861
4125
|
}
|
|
3862
4126
|
);
|
|
3863
4127
|
|
|
4128
|
+
// src/commands/migrate.ts
|
|
4129
|
+
var import_commander16 = require("commander");
|
|
4130
|
+
var fs15 = __toESM(require("fs"));
|
|
4131
|
+
var path14 = __toESM(require("path"));
|
|
4132
|
+
var import_chalk = __toESM(require("chalk"));
|
|
4133
|
+
var V2_HOOK_PATTERNS = [
|
|
4134
|
+
{
|
|
4135
|
+
pattern: /from\s+["']@\/contexts\/StoreContext["']/,
|
|
4136
|
+
v2: "useStore (bazaar)",
|
|
4137
|
+
v3: "useShop() from @numueg/theme-sdk",
|
|
4138
|
+
note: "Returns Store directly; bazaar's useStore returns { store, themeSettings, ... } \u2014 destructure differently."
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
pattern: /useProductsContext|from\s+["']@\/contexts\/ProductsContext["']/,
|
|
4142
|
+
v2: "useProductsContext (bazaar)",
|
|
4143
|
+
v3: "useProducts() from @numueg/theme-sdk",
|
|
4144
|
+
note: "V3 hook is paginated + has loading state. Shape: { items, loading, error, hasMore, loadMore }. V2 returned flat array."
|
|
4145
|
+
},
|
|
4146
|
+
{
|
|
4147
|
+
pattern: /useCart\s*\(\s*\)/,
|
|
4148
|
+
v2: "useCart (bazaar)",
|
|
4149
|
+
v3: "useCart() from @numueg/theme-sdk",
|
|
4150
|
+
note: "Same name but different shape \u2014 SDK returns { cart, addItem, removeItem, ... } with async actions. Bazaar's was sync."
|
|
4151
|
+
},
|
|
4152
|
+
{
|
|
4153
|
+
pattern: /useAuth\s*\(\s*\)/,
|
|
4154
|
+
v2: "useAuth (bazaar)",
|
|
4155
|
+
v3: "useCustomer() + useCustomerActions() from @numueg/theme-sdk",
|
|
4156
|
+
note: "Customer state is split from action handlers in V3 to avoid render-on-every-action."
|
|
4157
|
+
},
|
|
4158
|
+
{
|
|
4159
|
+
pattern: /useTheme\s*\(\s*\)|from\s+["']@\/contexts\/ThemeContext["']/,
|
|
4160
|
+
v2: "useTheme (bazaar)",
|
|
4161
|
+
v3: "useThemeSettings() from @numueg/theme-sdk",
|
|
4162
|
+
note: "Returns ThemeSettingsV3; access settings via .global_settings or .templates[<page>].sections[<id>].settings."
|
|
4163
|
+
},
|
|
4164
|
+
{
|
|
4165
|
+
pattern: /useLanguage\s*\(/,
|
|
4166
|
+
v2: "useLanguage (bazaar)",
|
|
4167
|
+
v3: "useLocalization() / useDirection() from @numueg/theme-sdk",
|
|
4168
|
+
note: "Returns { locale, direction, translations, ... }. Use useTranslation() for resolving message keys."
|
|
4169
|
+
},
|
|
4170
|
+
{
|
|
4171
|
+
pattern: /editable\.section\s*\(/,
|
|
4172
|
+
v2: "editable.section() helper",
|
|
4173
|
+
v3: "<EditableText> / <EditableImage> from @numueg/theme-sdk",
|
|
4174
|
+
note: "V3 marks editable nodes via dedicated components instead of spread props."
|
|
4175
|
+
}
|
|
4176
|
+
];
|
|
4177
|
+
function migrationNotesFor(source) {
|
|
4178
|
+
const notes = [];
|
|
4179
|
+
const lines = source.split("\n");
|
|
4180
|
+
for (const { pattern, v2, v3, note } of V2_HOOK_PATTERNS) {
|
|
4181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4182
|
+
if (pattern.test(lines[i])) {
|
|
4183
|
+
notes.push({ v2, v3, note, lineHint: i + 1 });
|
|
4184
|
+
break;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
return notes;
|
|
4189
|
+
}
|
|
4190
|
+
function adapterCommentBlock(notes, v2RelativePath) {
|
|
4191
|
+
const lines = [
|
|
4192
|
+
"/**",
|
|
4193
|
+
" * \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
4194
|
+
" * V3 PORT \u2014 ADAPTER NOTES",
|
|
4195
|
+
" * \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
4196
|
+
" *",
|
|
4197
|
+
` * Source: ${v2RelativePath}`,
|
|
4198
|
+
" *",
|
|
4199
|
+
" * Generated by `numu-theme migrate`. Review and rewrite using V3",
|
|
4200
|
+
" * SDK hooks before publishing. The list below was inferred from",
|
|
4201
|
+
" * static analysis \u2014 there may be other V2 idioms not detected.",
|
|
4202
|
+
" *"
|
|
4203
|
+
];
|
|
4204
|
+
if (notes.length === 0) {
|
|
4205
|
+
lines.push(" * No V2-specific hooks detected. You may only need to:");
|
|
4206
|
+
lines.push(" * 1. Replace any bazaar-specific imports with @numueg/theme-sdk equivalents.");
|
|
4207
|
+
lines.push(" * 2. Wrap your section's root element with <Section id={...} type={...}> from the SDK.");
|
|
4208
|
+
lines.push(" * 3. Read settings from `props.instance.settings` (V3 SectionInstance shape).");
|
|
4209
|
+
} else {
|
|
4210
|
+
lines.push(" * Required swaps:");
|
|
4211
|
+
lines.push(" *");
|
|
4212
|
+
for (const n of notes) {
|
|
4213
|
+
const where = n.lineHint ? ` (line ${n.lineHint})` : "";
|
|
4214
|
+
lines.push(` * \u2022 ${n.v2}${where}`);
|
|
4215
|
+
lines.push(` * \u2192 ${n.v3}`);
|
|
4216
|
+
lines.push(` * ${n.note}`);
|
|
4217
|
+
lines.push(" *");
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
lines.push(" * When done, remove this block.");
|
|
4221
|
+
lines.push(" * \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
4222
|
+
lines.push(" */");
|
|
4223
|
+
return lines.join("\n");
|
|
4224
|
+
}
|
|
4225
|
+
function findSectionFiles(dir) {
|
|
4226
|
+
if (!fs15.existsSync(dir)) return [];
|
|
4227
|
+
const out = [];
|
|
4228
|
+
for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
|
|
4229
|
+
const full = path14.join(dir, entry.name);
|
|
4230
|
+
if (entry.isDirectory()) out.push(...findSectionFiles(full));
|
|
4231
|
+
else if (entry.isFile() && /\.(tsx|ts)$/.test(entry.name)) out.push(full);
|
|
4232
|
+
}
|
|
4233
|
+
return out;
|
|
4234
|
+
}
|
|
4235
|
+
function sectionTypeFromFilename(filename) {
|
|
4236
|
+
const base = path14.basename(filename).replace(/\.(tsx|ts)$/, "");
|
|
4237
|
+
return base.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]+/g, "-").toLowerCase();
|
|
4238
|
+
}
|
|
4239
|
+
function makeV2Bridge() {
|
|
4240
|
+
return `/**
|
|
4241
|
+
* v2-bridge \u2014 per-theme compat shim for V2 sections ported via
|
|
4242
|
+
* \`numu-theme migrate\`. DELETE this file once each section has been
|
|
4243
|
+
* rewritten to use idiomatic V3 SDK hooks + components.
|
|
4244
|
+
*/
|
|
4245
|
+
|
|
4246
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
4247
|
+
import {
|
|
4248
|
+
useV2Products,
|
|
4249
|
+
useV2Categories,
|
|
4250
|
+
useV2Auth,
|
|
4251
|
+
useV2Language,
|
|
4252
|
+
useV2Theme,
|
|
4253
|
+
} from "@numueg/theme-sdk/v2-compat";
|
|
4254
|
+
import { ProductCard, useThemeSettings } from "@numueg/theme-sdk";
|
|
4255
|
+
import type { SectionInstance } from "@numueg/theme-sdk";
|
|
4256
|
+
|
|
4257
|
+
// \u2500\u2500 Re-shaped V3 hooks under V2 names \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4258
|
+
|
|
4259
|
+
/** V2's \`useProducts\` returned \`{ products, loading }\`. Same shape. */
|
|
4260
|
+
export function useProducts() {
|
|
4261
|
+
return useV2Products();
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
/** V2's \`useCategories\` returned \`{ categories, loading }\`. Same shape. */
|
|
4265
|
+
export function useCategories() {
|
|
4266
|
+
return useV2Categories();
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
/** V2's \`useAuth\` returned \`{ user, isAuthenticated }\`. */
|
|
4270
|
+
export function useAuth() {
|
|
4271
|
+
return useV2Auth();
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
/** V2's \`useLanguage\` returned \`{ language, direction, setLanguage, t }\`. */
|
|
4275
|
+
export function useLanguage() {
|
|
4276
|
+
return useV2Language();
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
/** V2's \`useStore\` returned the full store config; here we expose
|
|
4280
|
+
* only the parts ported sections use (themeSettings nested). */
|
|
4281
|
+
export function useStore() {
|
|
4282
|
+
return useV2Theme();
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
// \u2500\u2500 V2 type re-shaping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4286
|
+
|
|
4287
|
+
/**
|
|
4288
|
+
* V2 sections expected this prop shape:
|
|
4289
|
+
*
|
|
4290
|
+
* interface SectionComponentProps {
|
|
4291
|
+
* section: { id: string; type: string; settings: Record<string, any>; ... };
|
|
4292
|
+
* ...
|
|
4293
|
+
* }
|
|
4294
|
+
*
|
|
4295
|
+
* V3 mount passes \`{ instance: SectionInstance }\`. We re-export a
|
|
4296
|
+
* compatible interface so existing destructure patterns like
|
|
4297
|
+
* \`const { section } = props\` keep compiling \u2014 the wrapper below
|
|
4298
|
+
* coerces \`instance\` \u2192 \`section\` at render time.
|
|
4299
|
+
*/
|
|
4300
|
+
export interface SectionComponentProps {
|
|
4301
|
+
section: SectionInstance & { id?: string };
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
/**
|
|
4305
|
+
* Section adapter. Migrate-generated sections still expect to be
|
|
4306
|
+
* called with \`{ section }\`; the V3 \`SECTION_REGISTRY\` calls them
|
|
4307
|
+
* with \`{ instance }\`. Wrap each export:
|
|
4308
|
+
*
|
|
4309
|
+
* export default v2Section(YourComponent);
|
|
4310
|
+
*/
|
|
4311
|
+
export function v2Section<P extends SectionComponentProps>(
|
|
4312
|
+
Component: (props: P) => ReactNode,
|
|
4313
|
+
): (props: { instance: SectionInstance }) => ReactNode {
|
|
4314
|
+
return function V2SectionAdapter({ instance }) {
|
|
4315
|
+
const props = { section: instance } as unknown as P;
|
|
4316
|
+
return Component(props);
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
// \u2500\u2500 editable.section() compat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4321
|
+
|
|
4322
|
+
/**
|
|
4323
|
+
* V2's editable helper spread DOM props on individual nodes so the
|
|
4324
|
+
* old V2 customizer could click-select them. V3 uses \`<EditableText>\`
|
|
4325
|
+
* / \`<EditableImage>\` for the same job. Returning an empty object
|
|
4326
|
+
* here means the spread is a no-op \u2014 the section still renders, just
|
|
4327
|
+
* without the inline click-to-edit affordance. Replace with the V3
|
|
4328
|
+
* components when polishing the port.
|
|
4329
|
+
*/
|
|
4330
|
+
export const editable = {
|
|
4331
|
+
section: (_sectionId: string, _key: string) => ({}),
|
|
4332
|
+
block: (_blockId: string, _key: string) => ({}),
|
|
4333
|
+
};
|
|
4334
|
+
|
|
4335
|
+
// \u2500\u2500 Misc re-exports the V2 sections often imported \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4336
|
+
|
|
4337
|
+
export { ProductCard, useThemeSettings };
|
|
4338
|
+
|
|
4339
|
+
/**
|
|
4340
|
+
* V2's \`<Link to="/...">\` came from react-router-dom. The V3 storefront
|
|
4341
|
+
* is Next.js, where \`<a href="/...">\` works (Next intercepts internal
|
|
4342
|
+
* navigation). We expose a polyfill so existing JSX renders without
|
|
4343
|
+
* react-router-dom installed. Pass-through any extra props.
|
|
4344
|
+
*/
|
|
4345
|
+
export function Link({
|
|
4346
|
+
to,
|
|
4347
|
+
children,
|
|
4348
|
+
...rest
|
|
4349
|
+
}: { to: string; children: ReactNode } & Omit<
|
|
4350
|
+
ComponentPropsWithoutRef<"a">,
|
|
4351
|
+
"href"
|
|
4352
|
+
>) {
|
|
4353
|
+
return (
|
|
4354
|
+
<a href={to} {...rest}>
|
|
4355
|
+
{children}
|
|
4356
|
+
</a>
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
/** V2's image placeholder constant. */
|
|
4361
|
+
export const PLACEHOLDER_HERO =
|
|
4362
|
+
"https://images.unsplash.com/photo-1483985988355-763728e1935b?auto=format&fit=crop&w=1200&q=60";
|
|
4363
|
+
`;
|
|
4364
|
+
}
|
|
4365
|
+
function rewriteV2Imports(source) {
|
|
4366
|
+
const PATH_REWRITES = [
|
|
4367
|
+
// The most-common bazaar context imports.
|
|
4368
|
+
[/@\/contexts\/ProductsContext/g, "../v2-bridge"],
|
|
4369
|
+
[/@\/contexts\/AuthContext/g, "../v2-bridge"],
|
|
4370
|
+
[/@\/contexts\/LanguageContext/g, "../v2-bridge"],
|
|
4371
|
+
[/@\/contexts\/StoreContext/g, "../v2-bridge"],
|
|
4372
|
+
[/@\/contexts\/ThemeContext/g, "../v2-bridge"],
|
|
4373
|
+
// V2 engine types + editable helper.
|
|
4374
|
+
[/@\/themes\/engine\/types/g, "../v2-bridge"],
|
|
4375
|
+
[/@\/themes\/engine\/editable/g, "../v2-bridge"],
|
|
4376
|
+
// bazaar's shared ProductCard component.
|
|
4377
|
+
[/@\/components\/store\/ProductCard/g, "../v2-bridge"],
|
|
4378
|
+
// The image placeholder constant.
|
|
4379
|
+
[/@\/lib\/imagePlaceholders/g, "../v2-bridge"],
|
|
4380
|
+
// react-router-dom → bridge (Link polyfill). Only the named
|
|
4381
|
+
// imports we know about; the bridge exports `Link`. Sections that
|
|
4382
|
+
// need other react-router-dom APIs will fail at build time, which
|
|
4383
|
+
// is the right outcome — they need manual conversion.
|
|
4384
|
+
[/(['"])react-router-dom\1/g, '"../v2-bridge"']
|
|
4385
|
+
];
|
|
4386
|
+
let out = source;
|
|
4387
|
+
for (const [pattern, replacement] of PATH_REWRITES) {
|
|
4388
|
+
out = out.replace(pattern, replacement);
|
|
4389
|
+
}
|
|
4390
|
+
return out;
|
|
4391
|
+
}
|
|
4392
|
+
function makeThemeJson(themeId, displayName, sectionTypes) {
|
|
4393
|
+
return {
|
|
4394
|
+
id: themeId,
|
|
4395
|
+
name: displayName,
|
|
4396
|
+
author: "",
|
|
4397
|
+
version: "0.1.0",
|
|
4398
|
+
layout: "single-column",
|
|
4399
|
+
description: `${displayName} \u2014 migrated from V2.`,
|
|
4400
|
+
error_template: "templates/error.html",
|
|
4401
|
+
loading_template: "templates/loading.html",
|
|
4402
|
+
presets: {
|
|
4403
|
+
templates: {
|
|
4404
|
+
home: {
|
|
4405
|
+
name: "Home",
|
|
4406
|
+
sections: sectionTypes.map((type) => ({
|
|
4407
|
+
type,
|
|
4408
|
+
settings: {}
|
|
4409
|
+
}))
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
function makeSettingsSchema() {
|
|
4416
|
+
return [
|
|
4417
|
+
{
|
|
4418
|
+
name: "Brand",
|
|
4419
|
+
settings: [
|
|
4420
|
+
{
|
|
4421
|
+
type: "color",
|
|
4422
|
+
id: "primary_color",
|
|
4423
|
+
label: "Primary color",
|
|
4424
|
+
default: "#111111"
|
|
4425
|
+
},
|
|
4426
|
+
{
|
|
4427
|
+
type: "color",
|
|
4428
|
+
id: "accent_color",
|
|
4429
|
+
label: "Accent color",
|
|
4430
|
+
default: "#d4af37"
|
|
4431
|
+
}
|
|
4432
|
+
]
|
|
4433
|
+
}
|
|
4434
|
+
];
|
|
4435
|
+
}
|
|
4436
|
+
function makeMainTsx(themeId, displayName, sectionTypes) {
|
|
4437
|
+
const registryEntries = sectionTypes.map((type) => ` "${type}": lazy(() => import("./sections/${type}")),`).join("\n");
|
|
4438
|
+
return `/**
|
|
4439
|
+
* ${displayName} (V3) \u2014 entry point.
|
|
4440
|
+
*
|
|
4441
|
+
* Generated by \`numu-theme migrate\`. Section components live in
|
|
4442
|
+
* src/sections/<type>.tsx and are lazy-loaded so only sections the
|
|
4443
|
+
* merchant actually uses pay the bundle cost.
|
|
4444
|
+
*/
|
|
4445
|
+
|
|
4446
|
+
import {
|
|
4447
|
+
StrictMode,
|
|
4448
|
+
lazy,
|
|
4449
|
+
Suspense,
|
|
4450
|
+
useImperativeHandle,
|
|
4451
|
+
useRef,
|
|
4452
|
+
useState,
|
|
4453
|
+
forwardRef,
|
|
4454
|
+
} from "react";
|
|
4455
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4456
|
+
import {
|
|
4457
|
+
NuMuProvider,
|
|
4458
|
+
Section,
|
|
4459
|
+
useThemeSettings,
|
|
4460
|
+
type ThemeSettingsV3,
|
|
4461
|
+
type Store,
|
|
4462
|
+
type Cart,
|
|
4463
|
+
type Customer,
|
|
4464
|
+
type SectionInstance,
|
|
4465
|
+
type MountResult,
|
|
4466
|
+
} from "@numueg/theme-sdk";
|
|
4467
|
+
import themeManifest from "../theme.json";
|
|
4468
|
+
|
|
4469
|
+
const SECTION_REGISTRY: Record<string, ReturnType<typeof lazy>> = {
|
|
4470
|
+
${registryEntries}
|
|
4471
|
+
};
|
|
4472
|
+
|
|
4473
|
+
function UnknownSection({ type }: { type: string }) {
|
|
4474
|
+
return (
|
|
4475
|
+
<section style={{ padding: "1rem", border: "1px dashed #fb923c" }}>
|
|
4476
|
+
Unknown section: <strong>{type}</strong>
|
|
4477
|
+
</section>
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
interface ResolvedSection {
|
|
4482
|
+
id: string;
|
|
4483
|
+
instance: SectionInstance;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
interface MaybeOrdered {
|
|
4487
|
+
sections?: Record<string, SectionInstance> | SectionInstance[];
|
|
4488
|
+
order?: string[];
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
function resolveSections(group: MaybeOrdered | undefined): ResolvedSection[] {
|
|
4492
|
+
if (!group) return [];
|
|
4493
|
+
if (Array.isArray(group.sections)) {
|
|
4494
|
+
return group.sections.map((instance, idx) => ({
|
|
4495
|
+
id: \`\${instance.type}-\${idx}\`,
|
|
4496
|
+
instance,
|
|
4497
|
+
}));
|
|
4498
|
+
}
|
|
4499
|
+
const map = (group.sections ?? {}) as Record<string, SectionInstance>;
|
|
4500
|
+
const order = group.order ?? Object.keys(map);
|
|
4501
|
+
return order
|
|
4502
|
+
.map((id): ResolvedSection | null => {
|
|
4503
|
+
const instance = map[id];
|
|
4504
|
+
if (!instance) return null;
|
|
4505
|
+
return { id, instance };
|
|
4506
|
+
})
|
|
4507
|
+
.filter((x): x is ResolvedSection => Boolean(x));
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
const BUILTIN_HOME = (themeManifest as unknown as { presets?: { templates?: { home?: MaybeOrdered } } })
|
|
4511
|
+
.presets?.templates?.home;
|
|
4512
|
+
|
|
4513
|
+
function RenderSection({
|
|
4514
|
+
instance,
|
|
4515
|
+
sectionId,
|
|
4516
|
+
groupId,
|
|
4517
|
+
}: {
|
|
4518
|
+
instance: SectionInstance;
|
|
4519
|
+
sectionId: string;
|
|
4520
|
+
groupId?: string;
|
|
4521
|
+
}) {
|
|
4522
|
+
const Component = SECTION_REGISTRY[instance.type];
|
|
4523
|
+
if (!Component) {
|
|
4524
|
+
return (
|
|
4525
|
+
<Section id={sectionId} type={instance.type} groupId={groupId}>
|
|
4526
|
+
<UnknownSection type={instance.type} />
|
|
4527
|
+
</Section>
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
return (
|
|
4531
|
+
<Section id={sectionId} type={instance.type} groupId={groupId}>
|
|
4532
|
+
<Suspense fallback={<div style={{ minHeight: "20vh" }} />}>
|
|
4533
|
+
<Component instance={instance} />
|
|
4534
|
+
</Suspense>
|
|
4535
|
+
</Section>
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
function ThemeApp() {
|
|
4540
|
+
const settings = useThemeSettings();
|
|
4541
|
+
const hostHome = settings.templates?.home as MaybeOrdered | undefined;
|
|
4542
|
+
const sections = resolveSections(hostHome ?? BUILTIN_HOME);
|
|
4543
|
+
return (
|
|
4544
|
+
<div data-${themeId}-app>
|
|
4545
|
+
{sections.map(({ id, instance }) => (
|
|
4546
|
+
<RenderSection key={id} sectionId={id} instance={instance} />
|
|
4547
|
+
))}
|
|
4548
|
+
</div>
|
|
4549
|
+
);
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
// \u2500\u2500 Host contract: mount(el, ctx) returns a MountResult \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4553
|
+
|
|
4554
|
+
export interface MountContext {
|
|
4555
|
+
store: Store;
|
|
4556
|
+
themeSettings: ThemeSettingsV3;
|
|
4557
|
+
initialCart?: Cart;
|
|
4558
|
+
customer?: Customer | null;
|
|
4559
|
+
locale?: string;
|
|
4560
|
+
translations?: Record<string, string>;
|
|
4561
|
+
[extra: string]: unknown;
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
interface DraftHandle {
|
|
4565
|
+
applyDraft: (next: ThemeSettingsV3) => void;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
const ThemeSettingsBridge = forwardRef<DraftHandle, { ctx: MountContext }>(
|
|
4569
|
+
function ThemeSettingsBridge({ ctx }, ref) {
|
|
4570
|
+
const [themeSettings, setThemeSettings] = useState<ThemeSettingsV3>(
|
|
4571
|
+
ctx.themeSettings,
|
|
4572
|
+
);
|
|
4573
|
+
useImperativeHandle(
|
|
4574
|
+
ref,
|
|
4575
|
+
() => ({
|
|
4576
|
+
applyDraft: (next) => setThemeSettings((prev) => (prev === next ? prev : next)),
|
|
4577
|
+
}),
|
|
4578
|
+
[],
|
|
4579
|
+
);
|
|
4580
|
+
return (
|
|
4581
|
+
<NuMuProvider
|
|
4582
|
+
store={ctx.store}
|
|
4583
|
+
themeSettings={themeSettings}
|
|
4584
|
+
initialCart={ctx.initialCart}
|
|
4585
|
+
customer={ctx.customer}
|
|
4586
|
+
locale={ctx.locale}
|
|
4587
|
+
translations={ctx.translations}
|
|
4588
|
+
>
|
|
4589
|
+
<ThemeApp />
|
|
4590
|
+
</NuMuProvider>
|
|
4591
|
+
);
|
|
4592
|
+
},
|
|
4593
|
+
);
|
|
4594
|
+
|
|
4595
|
+
let currentRoot: Root | null = null;
|
|
4596
|
+
|
|
4597
|
+
export function mount(el: HTMLElement, ctx: MountContext): MountResult {
|
|
4598
|
+
if (currentRoot) {
|
|
4599
|
+
currentRoot.unmount();
|
|
4600
|
+
currentRoot = null;
|
|
4601
|
+
}
|
|
4602
|
+
const root = createRoot(el);
|
|
4603
|
+
currentRoot = root;
|
|
4604
|
+
const handleRef = { current: null as DraftHandle | null };
|
|
4605
|
+
root.render(
|
|
4606
|
+
<StrictMode>
|
|
4607
|
+
<ThemeSettingsBridge
|
|
4608
|
+
ctx={ctx}
|
|
4609
|
+
ref={(h) => {
|
|
4610
|
+
handleRef.current = h;
|
|
4611
|
+
}}
|
|
4612
|
+
/>
|
|
4613
|
+
</StrictMode>,
|
|
4614
|
+
);
|
|
4615
|
+
return {
|
|
4616
|
+
applyDraft: (next) => handleRef.current?.applyDraft(next),
|
|
4617
|
+
cleanup: () => {
|
|
4618
|
+
root.unmount();
|
|
4619
|
+
if (currentRoot === root) currentRoot = null;
|
|
4620
|
+
handleRef.current = null;
|
|
4621
|
+
},
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
const v3Handle = {
|
|
4626
|
+
kind: "v3-mount" as const,
|
|
4627
|
+
numu_theme_version: 3 as const,
|
|
4628
|
+
mount_returns: "MountResult" as const,
|
|
4629
|
+
manifest: { id: "${themeId}", name: "${displayName}", version: "0.1.0" },
|
|
4630
|
+
mount,
|
|
4631
|
+
};
|
|
4632
|
+
export default v3Handle;
|
|
4633
|
+
`;
|
|
4634
|
+
}
|
|
4635
|
+
function makeViteConfig() {
|
|
4636
|
+
return `import { defineConfig, type PluginOption } from "vite";
|
|
4637
|
+
import react from "@vitejs/plugin-react";
|
|
4638
|
+
import { numuTheme } from "@numueg/theme-plugin";
|
|
4639
|
+
|
|
4640
|
+
export default defineConfig({
|
|
4641
|
+
plugins: [react(), numuTheme({ federate: false }) as unknown as PluginOption],
|
|
4642
|
+
server: { port: 5173 },
|
|
4643
|
+
});
|
|
4644
|
+
`;
|
|
4645
|
+
}
|
|
4646
|
+
function makePackageJson(themeId, displayName) {
|
|
4647
|
+
return {
|
|
4648
|
+
name: themeId,
|
|
4649
|
+
version: "0.1.0",
|
|
4650
|
+
private: true,
|
|
4651
|
+
description: `${displayName} (V3 port)`,
|
|
4652
|
+
type: "module",
|
|
4653
|
+
scripts: {
|
|
4654
|
+
dev: "vite",
|
|
4655
|
+
build: "vite build",
|
|
4656
|
+
preview: "vite preview --port 5173"
|
|
4657
|
+
},
|
|
4658
|
+
dependencies: {
|
|
4659
|
+
react: "^18.3.1",
|
|
4660
|
+
"react-dom": "^18.3.1",
|
|
4661
|
+
"@numueg/theme-sdk": "^0.1.0",
|
|
4662
|
+
// V2 sections commonly import these — including them in deps so
|
|
4663
|
+
// `npm install` works without manual edits. The bundle externalises
|
|
4664
|
+
// react/react-dom/@numueg/theme-sdk via federation; framer-motion
|
|
4665
|
+
// and lucide-react ship as part of the theme bundle (~30-40 KB
|
|
4666
|
+
// gzip combined). Themes that prove they don't need them can
|
|
4667
|
+
// remove these after polishing.
|
|
4668
|
+
"framer-motion": "^11.11.0",
|
|
4669
|
+
"lucide-react": "^0.454.0"
|
|
4670
|
+
},
|
|
4671
|
+
devDependencies: {
|
|
4672
|
+
"@numueg/theme-plugin": "^0.1.0",
|
|
4673
|
+
"@types/react": "^18.3.0",
|
|
4674
|
+
"@types/react-dom": "^18.3.0",
|
|
4675
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
4676
|
+
typescript: "^5.5.0",
|
|
4677
|
+
vite: "^6.0.0"
|
|
4678
|
+
}
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
function makeTsConfig() {
|
|
4682
|
+
return {
|
|
4683
|
+
compilerOptions: {
|
|
4684
|
+
target: "ES2022",
|
|
4685
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
4686
|
+
module: "ESNext",
|
|
4687
|
+
moduleResolution: "Bundler",
|
|
4688
|
+
jsx: "react-jsx",
|
|
4689
|
+
strict: true,
|
|
4690
|
+
noImplicitAny: true,
|
|
4691
|
+
strictNullChecks: true,
|
|
4692
|
+
esModuleInterop: true,
|
|
4693
|
+
resolveJsonModule: true,
|
|
4694
|
+
skipLibCheck: true,
|
|
4695
|
+
isolatedModules: true,
|
|
4696
|
+
allowSyntheticDefaultImports: true,
|
|
4697
|
+
forceConsistentCasingInFileNames: true
|
|
4698
|
+
},
|
|
4699
|
+
include: ["src", "theme.json"]
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
function makeIndexHtml(displayName) {
|
|
4703
|
+
return `<!doctype html>
|
|
4704
|
+
<html lang="en">
|
|
4705
|
+
<head>
|
|
4706
|
+
<meta charset="UTF-8" />
|
|
4707
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4708
|
+
<title>${displayName} \u2014 dev preview</title>
|
|
4709
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
4710
|
+
</head>
|
|
4711
|
+
<body>
|
|
4712
|
+
<div id="root"></div>
|
|
4713
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
4714
|
+
</body>
|
|
4715
|
+
</html>
|
|
4716
|
+
`;
|
|
4717
|
+
}
|
|
4718
|
+
function makeTemplates() {
|
|
4719
|
+
return {
|
|
4720
|
+
error: `<!-- Static BYOT error template -->
|
|
4721
|
+
<main role="alert" style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:system-ui">
|
|
4722
|
+
<div style="text-align:center;max-width:28rem">
|
|
4723
|
+
<h1 style="font-size:1.5rem;font-weight:700;color:#b91c1c">Something went wrong</h1>
|
|
4724
|
+
<p style="color:#374151;margin-top:.5rem">Please try again in a moment.</p>
|
|
4725
|
+
<button data-numu-reset type="button" style="margin-top:1.5rem;padding:.5rem 1rem;background:#1d4ed8;color:white;border-radius:.375rem;border:0;cursor:pointer">Try again</button>
|
|
4726
|
+
</div>
|
|
4727
|
+
</main>
|
|
4728
|
+
`,
|
|
4729
|
+
loading: `<!-- Static BYOT loading skeleton -->
|
|
4730
|
+
<div role="status" aria-live="polite" aria-label="Loading" style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:system-ui">
|
|
4731
|
+
<span style="font-size:.875rem;font-weight:500;color:#4b5563">Loading\u2026</span>
|
|
4732
|
+
</div>
|
|
4733
|
+
`
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
var migrateCommand = new import_commander16.Command("migrate").description("Scaffold a V3 theme project from a V2 theme directory").argument(
|
|
4737
|
+
"<v2-path>",
|
|
4738
|
+
"Path to the V2 theme directory (e.g. ../numu-egyptian-bazaar/src/themes/empire)"
|
|
4739
|
+
).option(
|
|
4740
|
+
"--out <dir>",
|
|
4741
|
+
"Output directory (default: ./<v2-id>-engine-V3)"
|
|
4742
|
+
).option(
|
|
4743
|
+
"--name <displayName>",
|
|
4744
|
+
"Override the display name (default: title-cased v2 id)"
|
|
4745
|
+
).action(
|
|
4746
|
+
async (v2Path, options) => {
|
|
4747
|
+
const absV2Path = path14.resolve(process.cwd(), v2Path);
|
|
4748
|
+
if (!fs15.existsSync(absV2Path) || !fs15.statSync(absV2Path).isDirectory()) {
|
|
4749
|
+
console.error(import_chalk.default.red(`V2 path does not exist or is not a directory: ${absV2Path}`));
|
|
4750
|
+
process.exit(1);
|
|
4751
|
+
}
|
|
4752
|
+
const v2Id = path14.basename(absV2Path).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
4753
|
+
const themeId = `${v2Id}-v3`;
|
|
4754
|
+
const displayName = options.name ?? v2Id.charAt(0).toUpperCase() + v2Id.slice(1).replace(/-/g, " ") + " (V3)";
|
|
4755
|
+
const outDir = path14.resolve(
|
|
4756
|
+
process.cwd(),
|
|
4757
|
+
options.out ?? `${v2Id}-engine-V3`
|
|
4758
|
+
);
|
|
4759
|
+
if (fs15.existsSync(outDir)) {
|
|
4760
|
+
console.error(import_chalk.default.red(`Output directory already exists: ${outDir}`));
|
|
4761
|
+
console.error(import_chalk.default.dim("Remove it first, or pass --out to a fresh location."));
|
|
4762
|
+
process.exit(1);
|
|
4763
|
+
}
|
|
4764
|
+
console.log(import_chalk.default.bold(`
|
|
4765
|
+
Migrating V2 \u2192 V3:`));
|
|
4766
|
+
console.log(` ${import_chalk.default.dim("from")} ${absV2Path}`);
|
|
4767
|
+
console.log(` ${import_chalk.default.dim(" to")} ${outDir}
|
|
4768
|
+
`);
|
|
4769
|
+
for (const d of [
|
|
4770
|
+
"src/sections",
|
|
4771
|
+
"src/v2-bridge",
|
|
4772
|
+
"schemas/sections",
|
|
4773
|
+
"schemas/blocks",
|
|
4774
|
+
"templates"
|
|
4775
|
+
]) {
|
|
4776
|
+
fs15.mkdirSync(path14.join(outDir, d), { recursive: true });
|
|
4777
|
+
}
|
|
4778
|
+
fs15.writeFileSync(
|
|
4779
|
+
path14.join(outDir, "src/v2-bridge", "index.tsx"),
|
|
4780
|
+
makeV2Bridge()
|
|
4781
|
+
);
|
|
4782
|
+
const v2Styles = path14.join(absV2Path, "styles.css");
|
|
4783
|
+
if (fs15.existsSync(v2Styles)) {
|
|
4784
|
+
fs15.copyFileSync(v2Styles, path14.join(outDir, "styles.css"));
|
|
4785
|
+
console.log(import_chalk.default.green(" \u2713 Copied styles.css"));
|
|
4786
|
+
} else {
|
|
4787
|
+
fs15.writeFileSync(
|
|
4788
|
+
path14.join(outDir, "styles.css"),
|
|
4789
|
+
`/* ${displayName} styles */
|
|
4790
|
+
`
|
|
4791
|
+
);
|
|
4792
|
+
console.log(import_chalk.default.yellow(" \u26A0 No styles.css found \u2014 created an empty one"));
|
|
4793
|
+
}
|
|
4794
|
+
const v2SectionsDir = path14.join(absV2Path, "sections");
|
|
4795
|
+
const sectionFiles = fs15.existsSync(v2SectionsDir) ? findSectionFiles(v2SectionsDir) : [];
|
|
4796
|
+
const sectionTypes = [];
|
|
4797
|
+
const allNotes = [];
|
|
4798
|
+
for (const file of sectionFiles) {
|
|
4799
|
+
if (!file.endsWith(".tsx")) continue;
|
|
4800
|
+
const type = sectionTypeFromFilename(file);
|
|
4801
|
+
sectionTypes.push(type);
|
|
4802
|
+
const v2Source = fs15.readFileSync(file, "utf-8");
|
|
4803
|
+
const notes = migrationNotesFor(v2Source);
|
|
4804
|
+
allNotes.push({ file, type, notes });
|
|
4805
|
+
const relativeV2 = path14.relative(process.cwd(), file);
|
|
4806
|
+
const header = adapterCommentBlock(notes, relativeV2);
|
|
4807
|
+
const rewritten = rewriteV2Imports(v2Source);
|
|
4808
|
+
const ported = `${header}
|
|
4809
|
+
|
|
4810
|
+
${rewritten}`;
|
|
4811
|
+
fs15.writeFileSync(
|
|
4812
|
+
path14.join(outDir, "src/sections", `${type}.tsx`),
|
|
4813
|
+
ported
|
|
4814
|
+
);
|
|
4815
|
+
fs15.writeFileSync(
|
|
4816
|
+
path14.join(outDir, "schemas/sections", `${type}.json`),
|
|
4817
|
+
JSON.stringify(
|
|
4818
|
+
{
|
|
4819
|
+
type,
|
|
4820
|
+
name: type.split("-").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" "),
|
|
4821
|
+
settings: []
|
|
4822
|
+
},
|
|
4823
|
+
null,
|
|
4824
|
+
2
|
|
4825
|
+
)
|
|
4826
|
+
);
|
|
4827
|
+
}
|
|
4828
|
+
console.log(import_chalk.default.green(` \u2713 Ported ${sectionTypes.length} section file(s)`));
|
|
4829
|
+
fs15.writeFileSync(
|
|
4830
|
+
path14.join(outDir, "theme.json"),
|
|
4831
|
+
JSON.stringify(makeThemeJson(themeId, displayName, sectionTypes), null, 2)
|
|
4832
|
+
);
|
|
4833
|
+
fs15.writeFileSync(
|
|
4834
|
+
path14.join(outDir, "settings_schema.json"),
|
|
4835
|
+
JSON.stringify(makeSettingsSchema(), null, 2)
|
|
4836
|
+
);
|
|
4837
|
+
fs15.writeFileSync(
|
|
4838
|
+
path14.join(outDir, "src/main.tsx"),
|
|
4839
|
+
makeMainTsx(themeId, displayName, sectionTypes)
|
|
4840
|
+
);
|
|
4841
|
+
fs15.writeFileSync(
|
|
4842
|
+
path14.join(outDir, "vite.config.ts"),
|
|
4843
|
+
makeViteConfig()
|
|
4844
|
+
);
|
|
4845
|
+
fs15.writeFileSync(
|
|
4846
|
+
path14.join(outDir, "package.json"),
|
|
4847
|
+
JSON.stringify(makePackageJson(themeId, displayName), null, 2)
|
|
4848
|
+
);
|
|
4849
|
+
fs15.writeFileSync(
|
|
4850
|
+
path14.join(outDir, "tsconfig.json"),
|
|
4851
|
+
JSON.stringify(makeTsConfig(), null, 2)
|
|
4852
|
+
);
|
|
4853
|
+
fs15.writeFileSync(
|
|
4854
|
+
path14.join(outDir, "index.html"),
|
|
4855
|
+
makeIndexHtml(displayName)
|
|
4856
|
+
);
|
|
4857
|
+
const tpl = makeTemplates();
|
|
4858
|
+
fs15.writeFileSync(path14.join(outDir, "templates/error.html"), tpl.error);
|
|
4859
|
+
fs15.writeFileSync(path14.join(outDir, "templates/loading.html"), tpl.loading);
|
|
4860
|
+
fs15.writeFileSync(
|
|
4861
|
+
path14.join(outDir, ".gitignore"),
|
|
4862
|
+
"node_modules/\ndist/\n.DS_Store\n"
|
|
4863
|
+
);
|
|
4864
|
+
console.log(import_chalk.default.green(" \u2713 Wrote scaffold files\n"));
|
|
4865
|
+
const filesWithNotes = allNotes.filter((n) => n.notes.length > 0);
|
|
4866
|
+
if (filesWithNotes.length > 0) {
|
|
4867
|
+
console.log(import_chalk.default.bold.yellow("Sections that need manual review:"));
|
|
4868
|
+
for (const { type, notes } of filesWithNotes) {
|
|
4869
|
+
console.log(
|
|
4870
|
+
`
|
|
4871
|
+
${import_chalk.default.cyan(type)} ${import_chalk.default.dim(`(${notes.length} V2 hooks to swap)`)}`
|
|
4872
|
+
);
|
|
4873
|
+
for (const n of notes) {
|
|
4874
|
+
console.log(
|
|
4875
|
+
` \u2022 ${import_chalk.default.dim(`L${n.lineHint ?? "?"}`)} ${n.v2} \u2192 ${import_chalk.default.green(n.v3)}`
|
|
4876
|
+
);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
console.log();
|
|
4880
|
+
}
|
|
4881
|
+
console.log(import_chalk.default.bold("\nNext steps:"));
|
|
4882
|
+
console.log(` ${import_chalk.default.cyan("cd")} ${path14.relative(process.cwd(), outDir)}`);
|
|
4883
|
+
console.log(` ${import_chalk.default.cyan("npm install")}`);
|
|
4884
|
+
console.log(` ${import_chalk.default.cyan("npm run dev")} ${import_chalk.default.dim("# dev preview on :5173")}`);
|
|
4885
|
+
console.log(` ${import_chalk.default.cyan("npx numu-theme build")} ${import_chalk.default.dim("# validate + build")}`);
|
|
4886
|
+
console.log(
|
|
4887
|
+
`
|
|
4888
|
+
${import_chalk.default.dim("Open each src/sections/*.tsx and follow the ADAPTER NOTES at the top.")}`
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
4891
|
+
);
|
|
4892
|
+
|
|
3864
4893
|
// src/index.ts
|
|
3865
|
-
var program = new
|
|
4894
|
+
var program = new import_commander17.Command();
|
|
3866
4895
|
program.name("numu-theme").description("CLI for developing, validating, building, and publishing NUMU themes").version("0.1.0");
|
|
3867
4896
|
program.addCommand(initCommand);
|
|
3868
4897
|
program.addCommand(devCommand);
|
|
@@ -3879,4 +4908,5 @@ program.addCommand(addSectionCommand);
|
|
|
3879
4908
|
program.addCommand(addBlockCommand);
|
|
3880
4909
|
program.addCommand(pullCommand);
|
|
3881
4910
|
program.addCommand(deleteCommand);
|
|
4911
|
+
program.addCommand(migrateCommand);
|
|
3882
4912
|
program.parse();
|