@feeef.dev/cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -35
- package/dist/bin.js +221 -58
- package/dist/bin.js.map +1 -1
- package/dist/index.js +221 -58
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/scaffolds/blank/AGENTS.md +14 -11
- package/scaffolds/blank/README.md +8 -4
- package/scaffolds/blank/docs/00-INDEX.md +6 -4
- package/scaffolds/blank/docs/04-WORKFLOW.md +29 -24
- package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +3 -1
- package/scaffolds/blank/package.json +3 -2
- package/vendor/template-kit/bin/feeef-template.mjs +32 -0
- package/vendor/template-kit/lib/build.mjs +22 -6
- package/vendor/template-kit/lib/library.mjs +2 -2
- package/vendor/template-kit/lib/smoke.mjs +258 -0
package/dist/bin.js
CHANGED
|
@@ -94,6 +94,13 @@ async function loadBuild() {
|
|
|
94
94
|
}
|
|
95
95
|
return import(pathToFileURL(buildPath).href);
|
|
96
96
|
}
|
|
97
|
+
async function loadSmoke() {
|
|
98
|
+
const smokePath = path2.join(templateKitRoot(), "lib", "smoke.mjs");
|
|
99
|
+
if (!fs2.existsSync(smokePath)) {
|
|
100
|
+
throw new Error(`template-kit smoke missing at ${smokePath}`);
|
|
101
|
+
}
|
|
102
|
+
return import(pathToFileURL(smokePath).href);
|
|
103
|
+
}
|
|
97
104
|
async function loadUnpack() {
|
|
98
105
|
const unpackPath = path2.join(templateKitRoot(), "lib", "unpack.mjs");
|
|
99
106
|
if (!fs2.existsSync(unpackPath)) {
|
|
@@ -145,22 +152,22 @@ function createClient(opts = {}) {
|
|
|
145
152
|
if (requireAuth && !token) {
|
|
146
153
|
throw new Error("Not signed in. Run: feeef signin");
|
|
147
154
|
}
|
|
148
|
-
const
|
|
155
|
+
const http3 = createHttp(baseURL, token);
|
|
149
156
|
const ff = new FeeeF({
|
|
150
157
|
apiKey: token || "",
|
|
151
158
|
baseURL,
|
|
152
|
-
client:
|
|
159
|
+
client: http3,
|
|
153
160
|
cache: false
|
|
154
161
|
});
|
|
155
|
-
return { ff, http:
|
|
162
|
+
return { ff, http: http3, cfg, baseURL, token };
|
|
156
163
|
}
|
|
157
164
|
async function signinWithPassword(input) {
|
|
158
165
|
const baseURL = (input.apiUrl || defaultApiUrl()).replace(/\/$/, "");
|
|
159
|
-
const
|
|
166
|
+
const http3 = createHttp(baseURL, void 0);
|
|
160
167
|
const ff = new FeeeF({
|
|
161
168
|
apiKey: "",
|
|
162
169
|
baseURL,
|
|
163
|
-
client:
|
|
170
|
+
client: http3,
|
|
164
171
|
cache: false
|
|
165
172
|
});
|
|
166
173
|
const auth = await ff.users.signin({
|
|
@@ -660,14 +667,14 @@ function resolveThemeDir(dirArg) {
|
|
|
660
667
|
if (dirArg) {
|
|
661
668
|
const abs = path4.resolve(dirArg);
|
|
662
669
|
if (!fs4.existsSync(abs)) {
|
|
663
|
-
throw new Error(`
|
|
670
|
+
throw new Error(`Template directory not found: ${abs}`);
|
|
664
671
|
}
|
|
665
672
|
return findThemeRoot(abs) || abs;
|
|
666
673
|
}
|
|
667
674
|
const fromCwd = findThemeRoot(process.cwd());
|
|
668
675
|
if (fromCwd) return fromCwd;
|
|
669
676
|
throw new Error(
|
|
670
|
-
"No
|
|
677
|
+
"No template found. Run inside a template folder (feeef.template.json) or: feeef template init"
|
|
671
678
|
);
|
|
672
679
|
}
|
|
673
680
|
|
|
@@ -977,7 +984,7 @@ async function runTemplateBuild(dirArg, opts = {}) {
|
|
|
977
984
|
const srcDir = resolveThemeDir(dirArg);
|
|
978
985
|
const outPath = path7.join(srcDir, "dist", "data.json");
|
|
979
986
|
const s = spinner2();
|
|
980
|
-
s.start(opts.checkOnly ? "Checking
|
|
987
|
+
s.start(opts.checkOnly ? "Checking template\u2026" : "Building template\u2026");
|
|
981
988
|
const { buildTemplate } = await loadBuild();
|
|
982
989
|
const result = buildTemplate({
|
|
983
990
|
srcDir,
|
|
@@ -1030,7 +1037,7 @@ function assertStorefrontPath() {
|
|
|
1030
1037
|
throw new Error(
|
|
1031
1038
|
[
|
|
1032
1039
|
"Lithium storefront not found.",
|
|
1033
|
-
"Set it once (
|
|
1040
|
+
"Set it once (template repos do not need storefront source inside them):",
|
|
1034
1041
|
" feeef config set storefrontPath /path/to/storefront",
|
|
1035
1042
|
"or:",
|
|
1036
1043
|
" export FEEEF_STOREFRONT_PATH=/path/to/storefront"
|
|
@@ -1040,6 +1047,85 @@ function assertStorefrontPath() {
|
|
|
1040
1047
|
return root;
|
|
1041
1048
|
}
|
|
1042
1049
|
|
|
1050
|
+
// src/dev-error-server.ts
|
|
1051
|
+
import http2 from "http";
|
|
1052
|
+
|
|
1053
|
+
// src/dev-error-format.ts
|
|
1054
|
+
function formatFeeefLiveErrorLine(body) {
|
|
1055
|
+
const debug = body.debug || {};
|
|
1056
|
+
const kind = String(body.kind || "error");
|
|
1057
|
+
const message = String(body.message || "unknown");
|
|
1058
|
+
const lines = [
|
|
1059
|
+
`[feeef:custom-live] ${kind}`,
|
|
1060
|
+
debug.sourcePath ? ` path: ${debug.sourcePath}` : null,
|
|
1061
|
+
debug.instanceId ? ` instanceId: ${debug.instanceId}` : null,
|
|
1062
|
+
debug.title ? ` title: ${debug.title}` : null,
|
|
1063
|
+
debug.type ? ` type: ${debug.type}` : null,
|
|
1064
|
+
` message: ${message}`,
|
|
1065
|
+
body.props && typeof body.props === "object" ? ` props: ${JSON.stringify(body.props)}` : null,
|
|
1066
|
+
body.href ? ` href: ${body.href}` : null
|
|
1067
|
+
].filter(Boolean);
|
|
1068
|
+
return lines.join("\n");
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/dev-error-server.ts
|
|
1072
|
+
async function startDevErrorServer(opts) {
|
|
1073
|
+
const preferred = opts?.preferredPort ?? (Number(process.env["FEEEF_DEV_ERROR_PORT"]) || 0);
|
|
1074
|
+
const server = http2.createServer((req, res) => {
|
|
1075
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1076
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1077
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1078
|
+
if (req.method === "OPTIONS") {
|
|
1079
|
+
res.writeHead(204);
|
|
1080
|
+
res.end();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "/health")) {
|
|
1084
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
1085
|
+
res.end("feeef-dev-error-ok");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (req.method === "POST" && req.url?.startsWith("/feeef-error")) {
|
|
1089
|
+
const chunks = [];
|
|
1090
|
+
req.on("data", (c) => chunks.push(c));
|
|
1091
|
+
req.on("end", () => {
|
|
1092
|
+
try {
|
|
1093
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1094
|
+
const body = JSON.parse(raw);
|
|
1095
|
+
const line = formatFeeefLiveErrorLine(body);
|
|
1096
|
+
console.error("\n" + line + "\n");
|
|
1097
|
+
res.writeHead(204);
|
|
1098
|
+
res.end();
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
console.error("[feeef:custom-live] bad error payload", e);
|
|
1101
|
+
res.writeHead(400);
|
|
1102
|
+
res.end("bad json");
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
res.writeHead(404);
|
|
1108
|
+
res.end("not found");
|
|
1109
|
+
});
|
|
1110
|
+
await new Promise((resolve, reject) => {
|
|
1111
|
+
server.once("error", reject);
|
|
1112
|
+
server.listen(preferred, "127.0.0.1", () => resolve());
|
|
1113
|
+
});
|
|
1114
|
+
const addr = server.address();
|
|
1115
|
+
if (!addr || typeof addr === "string") {
|
|
1116
|
+
throw new Error("Failed to bind feeef dev error server");
|
|
1117
|
+
}
|
|
1118
|
+
const port = addr.port;
|
|
1119
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1120
|
+
return {
|
|
1121
|
+
port,
|
|
1122
|
+
url,
|
|
1123
|
+
close: () => new Promise((resolve) => {
|
|
1124
|
+
server.close(() => resolve());
|
|
1125
|
+
})
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1043
1129
|
// src/commands/template/dev.ts
|
|
1044
1130
|
function killChildren(children) {
|
|
1045
1131
|
for (const c of children) {
|
|
@@ -1073,7 +1159,7 @@ async function resolveBoundStore(srcDir) {
|
|
|
1073
1159
|
});
|
|
1074
1160
|
if (!storeRef) {
|
|
1075
1161
|
throw new Error(
|
|
1076
|
-
"No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live
|
|
1162
|
+
"No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live template stays unchanged.)"
|
|
1077
1163
|
);
|
|
1078
1164
|
}
|
|
1079
1165
|
const { ff } = createClient({ requireAuth: true });
|
|
@@ -1090,10 +1176,21 @@ async function runRemoteDraftPreview(srcDir) {
|
|
|
1090
1176
|
const { storeId, slug } = await resolveBoundStore(srcDir);
|
|
1091
1177
|
const { ff } = createClient({ requireAuth: true });
|
|
1092
1178
|
const s = spinner2();
|
|
1093
|
-
s.start("Building
|
|
1179
|
+
s.start("Building template\u2026");
|
|
1094
1180
|
const { buildTemplate } = await loadBuild();
|
|
1095
1181
|
buildTemplate({ srcDir, outPath, write: true });
|
|
1096
1182
|
s.stop("dist ready");
|
|
1183
|
+
const errorServer = await startDevErrorServer();
|
|
1184
|
+
info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
|
|
1185
|
+
const withDevLog = (previewUrl) => {
|
|
1186
|
+
try {
|
|
1187
|
+
const u = new URL(previewUrl);
|
|
1188
|
+
u.searchParams.set("feeef_dev_log", errorServer.url);
|
|
1189
|
+
return u.toString();
|
|
1190
|
+
} catch {
|
|
1191
|
+
return previewUrl;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1097
1194
|
let lastPreviewUrl;
|
|
1098
1195
|
let pushInFlight = false;
|
|
1099
1196
|
let pushQueued = false;
|
|
@@ -1110,8 +1207,8 @@ async function runRemoteDraftPreview(srcDir) {
|
|
|
1110
1207
|
}
|
|
1111
1208
|
const data = JSON.parse(fs7.readFileSync(outPath, "utf8"));
|
|
1112
1209
|
const result = await ff.stores.putTemplatePreview(storeId, { data });
|
|
1113
|
-
lastPreviewUrl = result.previewUrl;
|
|
1114
|
-
ok(`Draft preview updated \u2192 ${pc.cyan(
|
|
1210
|
+
lastPreviewUrl = withDevLog(result.previewUrl);
|
|
1211
|
+
ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
|
|
1115
1212
|
} catch (e) {
|
|
1116
1213
|
fail(e);
|
|
1117
1214
|
} finally {
|
|
@@ -1131,12 +1228,13 @@ async function runRemoteDraftPreview(srcDir) {
|
|
|
1131
1228
|
}
|
|
1132
1229
|
note2(
|
|
1133
1230
|
[
|
|
1134
|
-
`
|
|
1135
|
-
`Store
|
|
1136
|
-
`Preview
|
|
1231
|
+
`Template ${srcDir}`,
|
|
1232
|
+
`Store ${slug} (${storeId})`,
|
|
1233
|
+
`Preview ${lastPreviewUrl ?? "(pending)"}`,
|
|
1137
1234
|
"",
|
|
1138
1235
|
"Live customers still see metadata.templateData.",
|
|
1139
1236
|
"Exit preview in the browser: banner \u2192 Exit, or /?preview=0",
|
|
1237
|
+
`CustomLive errors print here via ${errorServer.url}`,
|
|
1140
1238
|
"Stopping the CLI clears the draft."
|
|
1141
1239
|
].join("\n"),
|
|
1142
1240
|
"Remote draft preview"
|
|
@@ -1164,6 +1262,10 @@ async function runRemoteDraftPreview(srcDir) {
|
|
|
1164
1262
|
watching = false;
|
|
1165
1263
|
debouncedPush.cancel();
|
|
1166
1264
|
killChildren([watch]);
|
|
1265
|
+
try {
|
|
1266
|
+
await errorServer.close();
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1167
1269
|
try {
|
|
1168
1270
|
await ff.stores.clearTemplatePreview(storeId);
|
|
1169
1271
|
info("Cleared draft preview on server");
|
|
@@ -1193,6 +1295,8 @@ async function runLocalStorefrontPreview(srcDir, opts) {
|
|
|
1193
1295
|
process.env["FEEEF_STOREFRONT_PATH"] = path9.resolve(opts.storefront);
|
|
1194
1296
|
}
|
|
1195
1297
|
const storefrontRoot = assertStorefrontPath();
|
|
1298
|
+
const errorServer = await startDevErrorServer();
|
|
1299
|
+
info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
|
|
1196
1300
|
const cfg = loadConfig();
|
|
1197
1301
|
const storeRef = resolveStoreIdFromProject({
|
|
1198
1302
|
themeRoot: srcDir,
|
|
@@ -1219,16 +1323,26 @@ async function runLocalStorefrontPreview(srcDir, opts) {
|
|
|
1219
1323
|
}
|
|
1220
1324
|
}
|
|
1221
1325
|
const s = spinner2();
|
|
1222
|
-
s.start("Building
|
|
1326
|
+
s.start("Building template\u2026");
|
|
1223
1327
|
const { buildTemplate } = await loadBuild();
|
|
1224
1328
|
buildTemplate({ srcDir, outPath, write: true });
|
|
1225
1329
|
s.stop("dist ready");
|
|
1226
1330
|
const previewHost = slug ? `http://${slug}.localhost.feeef.org:${port}` : `http://localhost:${port}`;
|
|
1331
|
+
const previewWithLog = (() => {
|
|
1332
|
+
try {
|
|
1333
|
+
const u = new URL(previewHost);
|
|
1334
|
+
u.searchParams.set("feeef_dev_log", errorServer.url);
|
|
1335
|
+
return u.toString();
|
|
1336
|
+
} catch {
|
|
1337
|
+
return previewHost;
|
|
1338
|
+
}
|
|
1339
|
+
})();
|
|
1227
1340
|
note2(
|
|
1228
1341
|
[
|
|
1229
|
-
`
|
|
1342
|
+
`Template ${srcDir}`,
|
|
1230
1343
|
`Storefront ${storefrontRoot}`,
|
|
1231
|
-
`Preview
|
|
1344
|
+
`Preview ${previewWithLog}`,
|
|
1345
|
+
`Errors ${errorServer.url}`,
|
|
1232
1346
|
"Internal --local mode (not for public docs)."
|
|
1233
1347
|
].join("\n"),
|
|
1234
1348
|
"Local Next preview"
|
|
@@ -1237,6 +1351,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
|
|
|
1237
1351
|
...process.env,
|
|
1238
1352
|
FEEEF_TEMPLATE_DATA_PATH: outPath,
|
|
1239
1353
|
FEEEF_TEMPLATE_LIBRARY_PATH: libraryPath,
|
|
1354
|
+
FEEEF_DEV_LOG_URL: errorServer.url,
|
|
1240
1355
|
PORT: String(port)
|
|
1241
1356
|
};
|
|
1242
1357
|
if (slug) {
|
|
@@ -1255,7 +1370,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
|
|
|
1255
1370
|
const children = [watch, next];
|
|
1256
1371
|
const shutdown = (code = 0) => {
|
|
1257
1372
|
killChildren(children);
|
|
1258
|
-
process.exit(code);
|
|
1373
|
+
void errorServer.close().finally(() => process.exit(code));
|
|
1259
1374
|
};
|
|
1260
1375
|
process.on("SIGINT", () => shutdown(0));
|
|
1261
1376
|
process.on("SIGTERM", () => shutdown(0));
|
|
@@ -1263,7 +1378,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
|
|
|
1263
1378
|
watch.on("exit", (code) => {
|
|
1264
1379
|
if (code && code !== 0) shutdown(code);
|
|
1265
1380
|
});
|
|
1266
|
-
ok(`Open ${pc.cyan(
|
|
1381
|
+
ok(`Open ${pc.cyan(previewWithLog)}`);
|
|
1267
1382
|
outro2("Watching \u2014 Ctrl+C to stop");
|
|
1268
1383
|
await new Promise(() => {
|
|
1269
1384
|
});
|
|
@@ -1344,15 +1459,16 @@ function packageNameFromFolder(name) {
|
|
|
1344
1459
|
return name.toLowerCase();
|
|
1345
1460
|
}
|
|
1346
1461
|
var DEFAULT_PACKAGE_JSON = {
|
|
1347
|
-
name: "feeef-
|
|
1462
|
+
name: "feeef-template",
|
|
1348
1463
|
private: true,
|
|
1349
1464
|
version: "0.1.0",
|
|
1350
|
-
description: "Feeef Lithium
|
|
1465
|
+
description: "Feeef Lithium template",
|
|
1351
1466
|
type: "module",
|
|
1352
1467
|
engines: { node: ">=18" },
|
|
1353
1468
|
scripts: {
|
|
1354
1469
|
build: "feeef template build",
|
|
1355
1470
|
check: "feeef template check",
|
|
1471
|
+
test: "feeef template smoke",
|
|
1356
1472
|
dev: "feeef template dev",
|
|
1357
1473
|
publish: "feeef template publish",
|
|
1358
1474
|
typecheck: "tsc --noEmit"
|
|
@@ -1468,7 +1584,9 @@ function ensureNodePackage(dest, folderName) {
|
|
|
1468
1584
|
pkg.scripts = { ...DEFAULT_PACKAGE_JSON.scripts };
|
|
1469
1585
|
}
|
|
1470
1586
|
const scripts = pkg.scripts;
|
|
1471
|
-
|
|
1587
|
+
for (const [key, value] of Object.entries(DEFAULT_PACKAGE_JSON.scripts)) {
|
|
1588
|
+
if (!scripts[key]) scripts[key] = value;
|
|
1589
|
+
}
|
|
1472
1590
|
const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
|
|
1473
1591
|
for (const [name, version] of Object.entries(EDITOR_DEV_DEPS)) {
|
|
1474
1592
|
if (!devDeps[name]) devDeps[name] = version;
|
|
@@ -2048,7 +2166,7 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
|
|
|
2048
2166
|
);
|
|
2049
2167
|
const { unpackTemplate } = await loadUnpack();
|
|
2050
2168
|
const unpackSpin = spinner2();
|
|
2051
|
-
unpackSpin.start("Unpacking
|
|
2169
|
+
unpackSpin.start("Unpacking template files\u2026");
|
|
2052
2170
|
const stats = unpackTemplate({
|
|
2053
2171
|
inputPath: dataPath,
|
|
2054
2172
|
outDir: dest,
|
|
@@ -2121,7 +2239,7 @@ async function runTemplateInit(nameArg, opts = {}) {
|
|
|
2121
2239
|
options: [
|
|
2122
2240
|
{
|
|
2123
2241
|
value: "blank",
|
|
2124
|
-
label: "Blank
|
|
2242
|
+
label: "Blank template",
|
|
2125
2243
|
hint: resolveBlankKitGitUrl() ? "git clone official / monorepo kit" : "local scaffold"
|
|
2126
2244
|
},
|
|
2127
2245
|
{
|
|
@@ -2137,8 +2255,8 @@ async function runTemplateInit(nameArg, opts = {}) {
|
|
|
2137
2255
|
]
|
|
2138
2256
|
});
|
|
2139
2257
|
const name = nameArg || await text2({
|
|
2140
|
-
message: "
|
|
2141
|
-
placeholder: "my-
|
|
2258
|
+
message: "Template folder name",
|
|
2259
|
+
placeholder: "my-template",
|
|
2142
2260
|
validate: validateFolderName
|
|
2143
2261
|
});
|
|
2144
2262
|
const dest = path13.resolve(process.cwd(), name);
|
|
@@ -2161,7 +2279,7 @@ async function runTemplateInit(nameArg, opts = {}) {
|
|
|
2161
2279
|
info(
|
|
2162
2280
|
"Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build"
|
|
2163
2281
|
);
|
|
2164
|
-
outro2("
|
|
2282
|
+
outro2("Template scaffold ready");
|
|
2165
2283
|
return;
|
|
2166
2284
|
}
|
|
2167
2285
|
const { ff } = createClient({ requireAuth: false });
|
|
@@ -2179,7 +2297,7 @@ async function runTemplateInit(nameArg, opts = {}) {
|
|
|
2179
2297
|
info(
|
|
2180
2298
|
"Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build\n npm run dev"
|
|
2181
2299
|
);
|
|
2182
|
-
outro2("
|
|
2300
|
+
outro2("Template cloned and ready");
|
|
2183
2301
|
} catch (e) {
|
|
2184
2302
|
fail(e);
|
|
2185
2303
|
}
|
|
@@ -2262,7 +2380,7 @@ async function runUse(storeArg, opts) {
|
|
|
2262
2380
|
const themeRoot = findThemeRoot(process.cwd());
|
|
2263
2381
|
if (!themeRoot) {
|
|
2264
2382
|
fail(
|
|
2265
|
-
"No feeef.template.json nearby. Run from a
|
|
2383
|
+
"No feeef.template.json nearby. Run from a template folder or feeef template init first."
|
|
2266
2384
|
);
|
|
2267
2385
|
}
|
|
2268
2386
|
const { ff } = createClient();
|
|
@@ -2291,7 +2409,7 @@ async function runUse(storeArg, opts) {
|
|
|
2291
2409
|
if (opts.apiUrl) patch.apiUrl = opts.apiUrl;
|
|
2292
2410
|
const { rcPath } = saveProjectRc(themeRoot, patch);
|
|
2293
2411
|
saveConfig({ defaultStoreId: storeId });
|
|
2294
|
-
ok(`Linked
|
|
2412
|
+
ok(`Linked template \u2192 store ${slug || storeId}`);
|
|
2295
2413
|
info(rcPath);
|
|
2296
2414
|
outro2("Next: feeef template publish --apply");
|
|
2297
2415
|
} catch (e) {
|
|
@@ -2312,7 +2430,7 @@ var SOURCE_TAG_PREFIX = "feeef-source:";
|
|
|
2312
2430
|
async function runTemplatePublish(dirArg, opts) {
|
|
2313
2431
|
intro2("feeef template publish");
|
|
2314
2432
|
try {
|
|
2315
|
-
const { ff, http:
|
|
2433
|
+
const { ff, http: http3, cfg } = createClient();
|
|
2316
2434
|
const srcDir = resolveThemeDir(dirArg);
|
|
2317
2435
|
const meta = loadTemplateMeta(srcDir);
|
|
2318
2436
|
const rc = loadProjectRc(srcDir);
|
|
@@ -2334,7 +2452,7 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2334
2452
|
}
|
|
2335
2453
|
const store = await ff.stores.find({ id: storeId });
|
|
2336
2454
|
const title = opts.title || rc.title || meta.name || path14.basename(srcDir);
|
|
2337
|
-
info(`
|
|
2455
|
+
info(`Template ${srcDir}`);
|
|
2338
2456
|
info(`Store ${store.slug || storeId} (${policy})`);
|
|
2339
2457
|
const outPath = path14.join(srcDir, "dist", "data.json");
|
|
2340
2458
|
const s = spinner2();
|
|
@@ -2520,7 +2638,7 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2520
2638
|
if (typeof ff.storeTemplates.replaceLocales === "function") {
|
|
2521
2639
|
await ff.storeTemplates.replaceLocales(templateId, locales);
|
|
2522
2640
|
} else {
|
|
2523
|
-
await
|
|
2641
|
+
await http3.put(`/store_templates/${templateId}/locales`, { locales });
|
|
2524
2642
|
}
|
|
2525
2643
|
s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
|
|
2526
2644
|
}
|
|
@@ -2544,8 +2662,48 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2544
2662
|
}
|
|
2545
2663
|
}
|
|
2546
2664
|
|
|
2547
|
-
// src/commands/template/
|
|
2665
|
+
// src/commands/template/smoke.ts
|
|
2548
2666
|
import path15 from "path";
|
|
2667
|
+
async function runTemplateSmoke(dirArg, opts = {}) {
|
|
2668
|
+
intro2("feeef template smoke");
|
|
2669
|
+
try {
|
|
2670
|
+
const srcDir = resolveThemeDir(dirArg);
|
|
2671
|
+
const s = spinner2();
|
|
2672
|
+
s.start(
|
|
2673
|
+
opts.noBuild ? "Smoking custom components (existing dist)\u2026" : "Building + smoking custom components\u2026"
|
|
2674
|
+
);
|
|
2675
|
+
const { smokeTheme } = await loadSmoke();
|
|
2676
|
+
const result = await smokeTheme({
|
|
2677
|
+
srcDir,
|
|
2678
|
+
build: !opts.noBuild
|
|
2679
|
+
});
|
|
2680
|
+
s.stop(
|
|
2681
|
+
`Smoked ${result.total} custom component${result.total === 1 ? "" : "s"}`
|
|
2682
|
+
);
|
|
2683
|
+
for (const f of result.failed) {
|
|
2684
|
+
const label = f.item.sourcePath || f.item.instanceId || f.item.loc || "?";
|
|
2685
|
+
warn(`\u2717 ${label}`);
|
|
2686
|
+
console.error(pc.red(` ${f.error.message}`));
|
|
2687
|
+
}
|
|
2688
|
+
if (result.failed.length) {
|
|
2689
|
+
fail(
|
|
2690
|
+
new Error(
|
|
2691
|
+
`smoke failed \u2014 ${result.failed.length}/${result.total} components`
|
|
2692
|
+
)
|
|
2693
|
+
);
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
ok(
|
|
2697
|
+
`All ${result.passed} custom component${result.passed === 1 ? "" : "s"} rendered`
|
|
2698
|
+
);
|
|
2699
|
+
outro2(path15.relative(process.cwd(), srcDir) || srcDir);
|
|
2700
|
+
} catch (e) {
|
|
2701
|
+
fail(e);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// src/commands/template/remove.ts
|
|
2706
|
+
import path16 from "path";
|
|
2549
2707
|
async function runTemplateRemovePage(pageId, opts = {}) {
|
|
2550
2708
|
intro2("feeef template remove page");
|
|
2551
2709
|
try {
|
|
@@ -2564,7 +2722,7 @@ async function runTemplateRemovePage(pageId, opts = {}) {
|
|
|
2564
2722
|
}
|
|
2565
2723
|
}
|
|
2566
2724
|
const { pageId: id, removedPath } = removePage(themeRoot, pageId);
|
|
2567
|
-
ok(`Removed ${
|
|
2725
|
+
ok(`Removed ${path16.relative(themeRoot, removedPath)}`);
|
|
2568
2726
|
outro2(`Page "${id}" deleted`);
|
|
2569
2727
|
} catch (err) {
|
|
2570
2728
|
fail(err instanceof Error ? err.message : String(err));
|
|
@@ -2590,7 +2748,7 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
|
|
|
2590
2748
|
const { removedPath } = removeComponent(themeRoot, pageId, componentId, {
|
|
2591
2749
|
...opts.section ? { section: opts.section } : {}
|
|
2592
2750
|
});
|
|
2593
|
-
ok(`Removed ${
|
|
2751
|
+
ok(`Removed ${path16.relative(themeRoot, removedPath)}`);
|
|
2594
2752
|
outro2("Done");
|
|
2595
2753
|
} catch (err) {
|
|
2596
2754
|
fail(err instanceof Error ? err.message : String(err));
|
|
@@ -2599,12 +2757,12 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
|
|
|
2599
2757
|
|
|
2600
2758
|
// src/commands/template/watch.ts
|
|
2601
2759
|
import { spawn as spawn2 } from "child_process";
|
|
2602
|
-
import
|
|
2760
|
+
import path17 from "path";
|
|
2603
2761
|
async function runTemplateWatch(dirArg) {
|
|
2604
2762
|
intro2("feeef template watch");
|
|
2605
2763
|
try {
|
|
2606
2764
|
const srcDir = resolveThemeDir(dirArg);
|
|
2607
|
-
const outPath =
|
|
2765
|
+
const outPath = path17.join(srcDir, "dist", "data.json");
|
|
2608
2766
|
const s = spinner2();
|
|
2609
2767
|
s.start("Initial build\u2026");
|
|
2610
2768
|
const { buildTemplate } = await loadBuild();
|
|
@@ -2632,7 +2790,7 @@ async function runTemplateWatch(dirArg) {
|
|
|
2632
2790
|
// src/index.ts
|
|
2633
2791
|
function createProgram() {
|
|
2634
2792
|
const program = new Command();
|
|
2635
|
-
program.name("feeef").description("Feeef developer CLI \u2014
|
|
2793
|
+
program.name("feeef").description("Feeef developer CLI \u2014 templates today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
|
|
2636
2794
|
if (thisCommand.args.length === 0) return;
|
|
2637
2795
|
banner();
|
|
2638
2796
|
});
|
|
@@ -2660,18 +2818,18 @@ function createProgram() {
|
|
|
2660
2818
|
runConfigSet(key, value);
|
|
2661
2819
|
});
|
|
2662
2820
|
program.command("use").description(
|
|
2663
|
-
"Link
|
|
2664
|
-
).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this
|
|
2821
|
+
"Link template to a store (.feeefrc); omit store to pick from your list"
|
|
2822
|
+
).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
|
|
2665
2823
|
await runUse(store, opts);
|
|
2666
2824
|
});
|
|
2667
|
-
program.command("publish").description("Build + publish current
|
|
2825
|
+
program.command("publish").description("Build + publish current template (alias of template publish)").argument("[dir]", "Template directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
|
|
2668
2826
|
await runTemplatePublish(dir, opts);
|
|
2669
2827
|
});
|
|
2670
2828
|
const template = program.command("template").description(
|
|
2671
|
-
"
|
|
2829
|
+
"Template kit: init, add/remove page|component, build, check, smoke, watch, dev, publish"
|
|
2672
2830
|
);
|
|
2673
2831
|
template.command("init").description(
|
|
2674
|
-
"Scaffold a
|
|
2832
|
+
"Scaffold a template (blank, git kit, or public marketplace)"
|
|
2675
2833
|
).argument("[name]", "Folder name").option("--blank", "Use the blank scaffold (skip picker)").option("--from <templateId>", "Clone a public template by id").option("--git <url>", "Clone a kit repo (depth 1), Adonis/Laravel style").option("-q, --search <query>", "Pre-fill marketplace search").action(async (name, opts) => {
|
|
2676
2834
|
await runTemplateInit(name, {
|
|
2677
2835
|
blank: opts.blank,
|
|
@@ -2680,8 +2838,8 @@ function createProgram() {
|
|
|
2680
2838
|
git: opts.git
|
|
2681
2839
|
});
|
|
2682
2840
|
});
|
|
2683
|
-
const templateAdd = template.command("add").description("Add a page or component to the current
|
|
2684
|
-
templateAdd.command("page").description("Create pages/<pageId>/ with flat components/ + update manifest").argument("<pageId>", "Page id (e.g. home, checkout, thank_you)").option("--dir <path>", "
|
|
2841
|
+
const templateAdd = template.command("add").description("Add a page or component to the current template");
|
|
2842
|
+
templateAdd.command("page").description("Create pages/<pageId>/ with flat components/ + update manifest").argument("<pageId>", "Page id (e.g. home, checkout, thank_you)").option("--dir <path>", "Template directory").action(async (pageId, opts) => {
|
|
2685
2843
|
await runTemplateAddPage(pageId, { dir: opts.dir });
|
|
2686
2844
|
});
|
|
2687
2845
|
templateAdd.command("component").description(
|
|
@@ -2689,42 +2847,47 @@ function createProgram() {
|
|
|
2689
2847
|
).argument("<pageId>", "Page id").argument("<componentId>", "Component file id (e.g. hero)").option("--title <title>", "meta.title (default: Title Case of id)").option(
|
|
2690
2848
|
"--section <id>",
|
|
2691
2849
|
"Legacy: write under sections/<id>/components/ instead of flat"
|
|
2692
|
-
).option("--dir <path>", "
|
|
2850
|
+
).option("--dir <path>", "Template directory").action(async (pageId, componentId, opts) => {
|
|
2693
2851
|
await runTemplateAddComponent(pageId, componentId, {
|
|
2694
2852
|
dir: opts.dir,
|
|
2695
2853
|
title: opts.title,
|
|
2696
2854
|
section: opts.section
|
|
2697
2855
|
});
|
|
2698
2856
|
});
|
|
2699
|
-
const templateRemove = template.command("remove").description("Remove a page or component from the current
|
|
2700
|
-
templateRemove.command("page").description("Delete pages/<pageId>/ and drop it from feeef.template.json").argument("<pageId>", "Page id").option("-y, --yes", "Skip confirmation").option("--dir <path>", "
|
|
2857
|
+
const templateRemove = template.command("remove").description("Remove a page or component from the current template");
|
|
2858
|
+
templateRemove.command("page").description("Delete pages/<pageId>/ and drop it from feeef.template.json").argument("<pageId>", "Page id").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Template directory").action(async (pageId, opts) => {
|
|
2701
2859
|
await runTemplateRemovePage(pageId, { dir: opts.dir, yes: opts.yes });
|
|
2702
2860
|
});
|
|
2703
|
-
templateRemove.command("component").description("Delete a component file/folder under a page").argument("<pageId>", "Page id").argument("<componentId>", "Component id").option("--section <id>", "Legacy: only search sections/<id>/components/").option("-y, --yes", "Skip confirmation").option("--dir <path>", "
|
|
2861
|
+
templateRemove.command("component").description("Delete a component file/folder under a page").argument("<pageId>", "Page id").argument("<componentId>", "Component id").option("--section <id>", "Legacy: only search sections/<id>/components/").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Template directory").action(async (pageId, componentId, opts) => {
|
|
2704
2862
|
await runTemplateRemoveComponent(pageId, componentId, {
|
|
2705
2863
|
dir: opts.dir,
|
|
2706
2864
|
section: opts.section,
|
|
2707
2865
|
yes: opts.yes
|
|
2708
2866
|
});
|
|
2709
2867
|
});
|
|
2710
|
-
template.command("build").description("Compile
|
|
2868
|
+
template.command("build").description("Compile template \u2192 dist/data.json").argument("[dir]", "Template directory").action(async (dir) => {
|
|
2711
2869
|
await runTemplateBuild(dir);
|
|
2712
2870
|
});
|
|
2713
|
-
template.command("check").description("Validate
|
|
2871
|
+
template.command("check").description("Validate template without writing dist").argument("[dir]", "Template directory").action(async (dir) => {
|
|
2714
2872
|
await runTemplateBuild(dir, { checkOnly: true });
|
|
2715
2873
|
});
|
|
2716
|
-
template.command("
|
|
2874
|
+
template.command("smoke").description(
|
|
2875
|
+
"Build + SSR-eval every custom App() (catches broken components)"
|
|
2876
|
+
).argument("[dir]", "Template directory").option("--no-build", "Use existing dist/data.json only").action(async (dir, opts) => {
|
|
2877
|
+
await runTemplateSmoke(dir, { noBuild: Boolean(opts.noBuild) });
|
|
2878
|
+
});
|
|
2879
|
+
template.command("watch").description("Rebuild on file changes").argument("[dir]", "Template directory").action(async (dir) => {
|
|
2717
2880
|
await runTemplateWatch(dir);
|
|
2718
2881
|
});
|
|
2719
2882
|
template.command("dev").description(
|
|
2720
|
-
"Watch + push draft preview to hosted storefront (never overwrites live
|
|
2721
|
-
).argument("[dir]", "
|
|
2883
|
+
"Watch + push draft preview to hosted storefront (never overwrites live template)"
|
|
2884
|
+
).argument("[dir]", "Template directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
|
|
2722
2885
|
"--local",
|
|
2723
2886
|
"Employee escape hatch: spawn local Next (requires storefront)"
|
|
2724
2887
|
).action(async (dir, opts) => {
|
|
2725
2888
|
await runTemplateDev(dir, opts);
|
|
2726
2889
|
});
|
|
2727
|
-
template.command("publish").description("Build, upload components + template + locales, optional install").argument("[dir]", "
|
|
2890
|
+
template.command("publish").description("Build, upload components + template + locales, optional install").argument("[dir]", "Template directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
|
|
2728
2891
|
await runTemplatePublish(dir, opts);
|
|
2729
2892
|
});
|
|
2730
2893
|
const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");
|