@webmaster-droid/cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +480 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,20 +22,89 @@ import generateImport from "@babel/generator";
|
|
|
22
22
|
import * as t from "@babel/types";
|
|
23
23
|
var traverse = traverseImport.default ?? traverseImport;
|
|
24
24
|
var generate = generateImport.default ?? generateImport;
|
|
25
|
+
var PARSER_PLUGINS = ["typescript", "jsx"];
|
|
26
|
+
function parseModule(source) {
|
|
27
|
+
return parse(source, {
|
|
28
|
+
sourceType: "module",
|
|
29
|
+
plugins: PARSER_PLUGINS
|
|
30
|
+
});
|
|
31
|
+
}
|
|
25
32
|
function normalizeText(text) {
|
|
26
33
|
return text.replace(/\s+/g, " ").trim();
|
|
27
34
|
}
|
|
28
35
|
function defaultPathFor(file, line, kind) {
|
|
29
|
-
const
|
|
30
|
-
|
|
36
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\//, "").replace(/\.[tj]sx?$/, "");
|
|
37
|
+
const stem = normalized.split("/").filter((segment) => segment && segment !== "." && segment !== "..").map(
|
|
38
|
+
(segment) => segment.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "")
|
|
39
|
+
).filter(Boolean).join(".");
|
|
40
|
+
return `pages.todo.${stem || "file"}.${kind}.${line}`;
|
|
41
|
+
}
|
|
42
|
+
function escapeJsxString(value) {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
function applyReplacements(source, replacements) {
|
|
46
|
+
if (replacements.length === 0) {
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
const sorted = [...replacements].sort((a, b) => b.start - a.start);
|
|
50
|
+
let out = source;
|
|
51
|
+
for (const replacement of sorted) {
|
|
52
|
+
out = out.slice(0, replacement.start) + replacement.value + out.slice(replacement.end);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function hasEditableTextImportDeclaration(node) {
|
|
57
|
+
return node.source.value === "@webmaster-droid/web" && node.specifiers.some(
|
|
58
|
+
(specifier) => t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported) && specifier.imported.name === "EditableText"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function ensureEditableTextImport(source) {
|
|
62
|
+
const ast = parseModule(source);
|
|
63
|
+
const body = ast.program.body;
|
|
64
|
+
let lastImportEnd = 0;
|
|
65
|
+
let moduleImport = null;
|
|
66
|
+
for (const node of body) {
|
|
67
|
+
if (!t.isImportDeclaration(node)) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
lastImportEnd = node.end ?? lastImportEnd;
|
|
71
|
+
if (node.source.value === "@webmaster-droid/web" && node.importKind !== "type" && !moduleImport) {
|
|
72
|
+
moduleImport = node;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (moduleImport && hasEditableTextImportDeclaration(moduleImport)) {
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
if (moduleImport) {
|
|
79
|
+
const hasNamespaceSpecifier = moduleImport.specifiers.some(
|
|
80
|
+
(specifier) => t.isImportNamespaceSpecifier(specifier)
|
|
81
|
+
);
|
|
82
|
+
if (!hasNamespaceSpecifier && moduleImport.start !== null && moduleImport.end !== null) {
|
|
83
|
+
const updatedImport = t.cloneNode(moduleImport);
|
|
84
|
+
updatedImport.specifiers.push(
|
|
85
|
+
t.importSpecifier(t.identifier("EditableText"), t.identifier("EditableText"))
|
|
86
|
+
);
|
|
87
|
+
const importSource = generate(updatedImport, { compact: false }).code;
|
|
88
|
+
return source.slice(0, moduleImport.start) + importSource + source.slice(moduleImport.end);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const importLine = `import { EditableText } from "@webmaster-droid/web";
|
|
92
|
+
`;
|
|
93
|
+
if (lastImportEnd > 0) {
|
|
94
|
+
let insertionPoint = lastImportEnd;
|
|
95
|
+
if (source.slice(insertionPoint, insertionPoint + 2) === "\r\n") {
|
|
96
|
+
insertionPoint += 2;
|
|
97
|
+
} else if (source[insertionPoint] === "\n") {
|
|
98
|
+
insertionPoint += 1;
|
|
99
|
+
}
|
|
100
|
+
return source.slice(0, insertionPoint) + importLine + source.slice(insertionPoint);
|
|
101
|
+
}
|
|
102
|
+
return `${importLine}${source}`;
|
|
31
103
|
}
|
|
32
104
|
function transformEditableTextCodemod(source, filePath, cwd) {
|
|
33
|
-
const ast =
|
|
34
|
-
sourceType: "module",
|
|
35
|
-
plugins: ["typescript", "jsx"]
|
|
36
|
-
});
|
|
105
|
+
const ast = parseModule(source);
|
|
37
106
|
let touched = false;
|
|
38
|
-
|
|
107
|
+
const replacements = [];
|
|
39
108
|
traverse(ast, {
|
|
40
109
|
JSXElement(pathNode) {
|
|
41
110
|
const children = pathNode.node.children;
|
|
@@ -52,22 +121,17 @@ function transformEditableTextCodemod(source, filePath, cwd) {
|
|
|
52
121
|
const loc = nonWhitespace[0].loc?.start.line ?? 0;
|
|
53
122
|
const rel = path.relative(cwd, filePath);
|
|
54
123
|
const pathHint = defaultPathFor(rel, loc, "text");
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[],
|
|
66
|
-
true
|
|
67
|
-
);
|
|
68
|
-
pathNode.node.children = [t.jsxExpressionContainer(editableEl)];
|
|
124
|
+
const targetNode = nonWhitespace[0];
|
|
125
|
+
if (typeof targetNode.start !== "number" || typeof targetNode.end !== "number") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const replacement = `<EditableText path=${escapeJsxString(pathHint)} fallback=${escapeJsxString(text)} />`;
|
|
129
|
+
replacements.push({
|
|
130
|
+
start: targetNode.start,
|
|
131
|
+
end: targetNode.end,
|
|
132
|
+
value: replacement
|
|
133
|
+
});
|
|
69
134
|
touched = true;
|
|
70
|
-
needsEditableTextImport = true;
|
|
71
135
|
}
|
|
72
136
|
});
|
|
73
137
|
if (!touched) {
|
|
@@ -76,28 +140,12 @@ function transformEditableTextCodemod(source, filePath, cwd) {
|
|
|
76
140
|
next: source
|
|
77
141
|
};
|
|
78
142
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
);
|
|
86
|
-
if (!hasImport) {
|
|
87
|
-
body.unshift(
|
|
88
|
-
t.importDeclaration(
|
|
89
|
-
[
|
|
90
|
-
t.importSpecifier(
|
|
91
|
-
t.identifier("EditableText"),
|
|
92
|
-
t.identifier("EditableText")
|
|
93
|
-
)
|
|
94
|
-
],
|
|
95
|
-
t.stringLiteral("@webmaster-droid/web")
|
|
96
|
-
)
|
|
97
|
-
);
|
|
98
|
-
}
|
|
143
|
+
let next = applyReplacements(source, replacements);
|
|
144
|
+
next = ensureEditableTextImport(next);
|
|
145
|
+
parseModule(next);
|
|
146
|
+
if (source.endsWith("\n") && !next.endsWith("\n")) {
|
|
147
|
+
next += "\n";
|
|
99
148
|
}
|
|
100
|
-
const next = generate(ast, { retainLines: true }, source).code;
|
|
101
149
|
return {
|
|
102
150
|
changed: next !== source,
|
|
103
151
|
next
|
|
@@ -126,6 +174,177 @@ async function readJson(filePath) {
|
|
|
126
174
|
const raw = await fs.readFile(filePath, "utf8");
|
|
127
175
|
return JSON.parse(raw);
|
|
128
176
|
}
|
|
177
|
+
function isRecord(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function splitPath(pathValue) {
|
|
181
|
+
return pathValue.replace(/\[(\d+)\]/g, ".$1").split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
function readByPath(input, pathValue) {
|
|
184
|
+
const segments = splitPath(pathValue);
|
|
185
|
+
let current = input;
|
|
186
|
+
for (const segment of segments) {
|
|
187
|
+
if (Array.isArray(current)) {
|
|
188
|
+
const index = Number(segment);
|
|
189
|
+
if (Number.isNaN(index)) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
current = current[index];
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!isRecord(current)) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
current = current[segment];
|
|
199
|
+
}
|
|
200
|
+
return current;
|
|
201
|
+
}
|
|
202
|
+
function writeByPath(input, pathValue, value) {
|
|
203
|
+
const segments = splitPath(pathValue);
|
|
204
|
+
if (segments.length === 0) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
let current = input;
|
|
208
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
209
|
+
const segment = segments[index];
|
|
210
|
+
if (Array.isArray(current)) {
|
|
211
|
+
const itemIndex = Number(segment);
|
|
212
|
+
if (Number.isNaN(itemIndex)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (current[itemIndex] === void 0) {
|
|
216
|
+
const next = segments[index + 1];
|
|
217
|
+
current[itemIndex] = /^\d+$/.test(next) ? [] : {};
|
|
218
|
+
}
|
|
219
|
+
current = current[itemIndex];
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!isRecord(current)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (current[segment] === void 0) {
|
|
226
|
+
const next = segments[index + 1];
|
|
227
|
+
current[segment] = /^\d+$/.test(next) ? [] : {};
|
|
228
|
+
}
|
|
229
|
+
current = current[segment];
|
|
230
|
+
}
|
|
231
|
+
const leaf = segments.at(-1);
|
|
232
|
+
if (!leaf) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(current)) {
|
|
236
|
+
const itemIndex = Number(leaf);
|
|
237
|
+
if (Number.isNaN(itemIndex)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
current[itemIndex] = value;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (!isRecord(current)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
current[leaf] = value;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
function createSeedDocument() {
|
|
250
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
|
+
return {
|
|
252
|
+
meta: {
|
|
253
|
+
schemaVersion: 1,
|
|
254
|
+
contentVersion: "seed_v1",
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
updatedBy: "seed-generator"
|
|
257
|
+
},
|
|
258
|
+
themeTokens: {},
|
|
259
|
+
layout: {},
|
|
260
|
+
pages: {},
|
|
261
|
+
seo: {}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function normalizeSeedDocument(input) {
|
|
265
|
+
const defaults = createSeedDocument();
|
|
266
|
+
if (!isRecord(input)) {
|
|
267
|
+
return defaults;
|
|
268
|
+
}
|
|
269
|
+
const output = {
|
|
270
|
+
...defaults,
|
|
271
|
+
...input
|
|
272
|
+
};
|
|
273
|
+
const metaValue = isRecord(output.meta) ? output.meta : {};
|
|
274
|
+
output.meta = {
|
|
275
|
+
...defaults.meta,
|
|
276
|
+
...metaValue
|
|
277
|
+
};
|
|
278
|
+
for (const key of ["themeTokens", "layout", "pages", "seo"]) {
|
|
279
|
+
if (!isRecord(output[key])) {
|
|
280
|
+
output[key] = {};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
function extractIdentifierName(value) {
|
|
286
|
+
if (t2.isJSXIdentifier(value)) {
|
|
287
|
+
return value.name;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
function staticTemplateLiteralValue(template) {
|
|
292
|
+
if (template.expressions.length > 0) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return template.quasis.map((part) => part.value.cooked ?? part.value.raw).join("");
|
|
296
|
+
}
|
|
297
|
+
function resolveAttributeValue(value) {
|
|
298
|
+
if (!value) {
|
|
299
|
+
return { kind: "missing" };
|
|
300
|
+
}
|
|
301
|
+
if (t2.isStringLiteral(value)) {
|
|
302
|
+
return { kind: "static", value: value.value };
|
|
303
|
+
}
|
|
304
|
+
if (!t2.isJSXExpressionContainer(value)) {
|
|
305
|
+
return { kind: "dynamic" };
|
|
306
|
+
}
|
|
307
|
+
const expression = value.expression;
|
|
308
|
+
if (t2.isStringLiteral(expression)) {
|
|
309
|
+
return { kind: "static", value: expression.value };
|
|
310
|
+
}
|
|
311
|
+
if (t2.isTemplateLiteral(expression)) {
|
|
312
|
+
const staticValue = staticTemplateLiteralValue(expression);
|
|
313
|
+
if (staticValue !== null) {
|
|
314
|
+
return { kind: "static", value: staticValue };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { kind: "dynamic" };
|
|
318
|
+
}
|
|
319
|
+
function findAttribute(node, name) {
|
|
320
|
+
for (const attribute of node.attributes) {
|
|
321
|
+
if (!t2.isJSXAttribute(attribute)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!t2.isJSXIdentifier(attribute.name)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (attribute.name.name === name) {
|
|
328
|
+
return attribute;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
var EDITABLE_COMPONENT_PATHS = {
|
|
334
|
+
EditableText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
335
|
+
EditableRichText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
336
|
+
EditableImage: [
|
|
337
|
+
{ pathProp: "path", fallbackProp: "fallbackSrc" },
|
|
338
|
+
{ pathProp: "altPath", fallbackProp: "fallbackAlt" }
|
|
339
|
+
],
|
|
340
|
+
EditableLink: [
|
|
341
|
+
{ pathProp: "hrefPath", fallbackProp: "fallbackHref" },
|
|
342
|
+
{ pathProp: "labelPath", fallbackProp: "fallbackLabel" }
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
function isSeedableEditablePath(pathValue) {
|
|
346
|
+
return pathValue.startsWith("pages.") || pathValue.startsWith("layout.") || pathValue.startsWith("seo.") || pathValue.startsWith("themeTokens.");
|
|
347
|
+
}
|
|
129
348
|
program.name("webmaster-droid").description("Webmaster Droid CLI").version(CLI_VERSION);
|
|
130
349
|
program.command("init").description("Initialize webmaster-droid environment template in current project").option("--backend <backend>", "backend (supabase|aws)", "supabase").option("--out <dir>", "output dir", ".").action(async (opts) => {
|
|
131
350
|
const backendRaw = String(opts.backend ?? "supabase").trim().toLowerCase();
|
|
@@ -222,6 +441,173 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
|
|
|
222
441
|
await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
223
442
|
console.log(`Wrote manifest: ${output}`);
|
|
224
443
|
});
|
|
444
|
+
program.command("seed").description("Generate CMS seed document from Editable component paths").argument("<srcDir>", "source directory").option("--out <file>", "seed output file", "cms/seed.from-editables.json").option("--base <file>", "merge into an existing seed document").option("--json", "emit machine-readable JSON output", false).action(async (srcDir, opts) => {
|
|
445
|
+
try {
|
|
446
|
+
const root = path2.resolve(process.cwd(), srcDir);
|
|
447
|
+
let rootStat;
|
|
448
|
+
try {
|
|
449
|
+
rootStat = await fs.stat(root);
|
|
450
|
+
} catch {
|
|
451
|
+
throw new Error(`Source directory not found: ${root}`);
|
|
452
|
+
}
|
|
453
|
+
if (!rootStat.isDirectory()) {
|
|
454
|
+
throw new Error(`Source path is not a directory: ${root}`);
|
|
455
|
+
}
|
|
456
|
+
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
457
|
+
cwd: root,
|
|
458
|
+
absolute: true,
|
|
459
|
+
ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
460
|
+
});
|
|
461
|
+
const staticPaths = /* @__PURE__ */ new Map();
|
|
462
|
+
const dynamicPaths = [];
|
|
463
|
+
const invalidPaths = [];
|
|
464
|
+
for (const file of files) {
|
|
465
|
+
const code = await fs.readFile(file, "utf8");
|
|
466
|
+
const ast = parse2(code, {
|
|
467
|
+
sourceType: "module",
|
|
468
|
+
plugins: ["typescript", "jsx"]
|
|
469
|
+
});
|
|
470
|
+
traverse2(ast, {
|
|
471
|
+
JSXOpeningElement(pathNode) {
|
|
472
|
+
const componentName = extractIdentifierName(pathNode.node.name);
|
|
473
|
+
if (!componentName) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const specs = EDITABLE_COMPONENT_PATHS[componentName];
|
|
477
|
+
if (!specs) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const spec of specs) {
|
|
481
|
+
const pathAttr = findAttribute(pathNode.node, spec.pathProp);
|
|
482
|
+
const pathValue = resolveAttributeValue(pathAttr?.value);
|
|
483
|
+
if (pathValue.kind === "missing") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const relFile = path2.relative(process.cwd(), file);
|
|
487
|
+
if (pathValue.kind === "dynamic") {
|
|
488
|
+
dynamicPaths.push({
|
|
489
|
+
file: relFile,
|
|
490
|
+
line: pathAttr?.loc?.start.line,
|
|
491
|
+
prop: spec.pathProp
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const normalizedPath = pathValue.value.trim();
|
|
496
|
+
if (!normalizedPath) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (!isSeedableEditablePath(normalizedPath)) {
|
|
500
|
+
invalidPaths.push({
|
|
501
|
+
file: relFile,
|
|
502
|
+
line: pathAttr?.loc?.start.line,
|
|
503
|
+
path: normalizedPath
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const fallbackAttr = findAttribute(pathNode.node, spec.fallbackProp);
|
|
508
|
+
const fallbackValue = resolveAttributeValue(fallbackAttr?.value);
|
|
509
|
+
const fallback = fallbackValue.kind === "static" ? fallbackValue.value : "";
|
|
510
|
+
const existing = staticPaths.get(normalizedPath);
|
|
511
|
+
if (!existing) {
|
|
512
|
+
staticPaths.set(normalizedPath, {
|
|
513
|
+
fallback,
|
|
514
|
+
source: relFile,
|
|
515
|
+
line: pathAttr?.loc?.start.line
|
|
516
|
+
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!existing.fallback && fallback) {
|
|
520
|
+
staticPaths.set(normalizedPath, {
|
|
521
|
+
fallback,
|
|
522
|
+
source: relFile,
|
|
523
|
+
line: pathAttr?.loc?.start.line
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const baseFile = opts.base ? path2.resolve(process.cwd(), String(opts.base)) : null;
|
|
531
|
+
const baseSeed = baseFile ? await readJson(baseFile) : createSeedDocument();
|
|
532
|
+
const seedDocument = normalizeSeedDocument(baseSeed);
|
|
533
|
+
let writtenPaths = 0;
|
|
534
|
+
let preservedPaths = 0;
|
|
535
|
+
let writeFailures = 0;
|
|
536
|
+
for (const [seedPath, entry] of staticPaths.entries()) {
|
|
537
|
+
const existingValue = readByPath(seedDocument, seedPath);
|
|
538
|
+
if (existingValue !== void 0) {
|
|
539
|
+
preservedPaths += 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const written = writeByPath(seedDocument, seedPath, entry.fallback);
|
|
543
|
+
if (written) {
|
|
544
|
+
writtenPaths += 1;
|
|
545
|
+
} else {
|
|
546
|
+
writeFailures += 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const output = path2.resolve(process.cwd(), opts.out);
|
|
550
|
+
await ensureDir(output);
|
|
551
|
+
await fs.writeFile(output, JSON.stringify(seedDocument, null, 2) + "\n", "utf8");
|
|
552
|
+
const report = {
|
|
553
|
+
outputPath: output,
|
|
554
|
+
source: root,
|
|
555
|
+
baseFile,
|
|
556
|
+
totalFiles: files.length,
|
|
557
|
+
discoveredStaticPaths: staticPaths.size,
|
|
558
|
+
writtenPaths,
|
|
559
|
+
preservedPaths,
|
|
560
|
+
dynamicPathSkips: dynamicPaths.length,
|
|
561
|
+
invalidPathSkips: invalidPaths.length,
|
|
562
|
+
writeFailures
|
|
563
|
+
};
|
|
564
|
+
if (opts.json) {
|
|
565
|
+
emitCliEnvelope({
|
|
566
|
+
ok: writeFailures === 0,
|
|
567
|
+
command: "seed",
|
|
568
|
+
version: CLI_VERSION,
|
|
569
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
570
|
+
data: report,
|
|
571
|
+
errors: writeFailures > 0 ? [`Failed to write ${writeFailures} discovered path(s) into seed document.`] : void 0
|
|
572
|
+
}, writeFailures > 0);
|
|
573
|
+
if (writeFailures > 0) {
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(
|
|
579
|
+
`Seed generated. Static paths: ${staticPaths.size}. Written: ${writtenPaths}. Preserved: ${preservedPaths}. Output: ${output}`
|
|
580
|
+
);
|
|
581
|
+
if (dynamicPaths.length > 0) {
|
|
582
|
+
console.log(
|
|
583
|
+
`Skipped ${dynamicPaths.length} dynamic path expression(s). Convert them manually or use concrete index paths.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (invalidPaths.length > 0) {
|
|
587
|
+
console.log(
|
|
588
|
+
`Skipped ${invalidPaths.length} non-editable path(s) outside pages/layout/seo/themeTokens.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (writeFailures > 0) {
|
|
592
|
+
throw new Error(`Failed to write ${writeFailures} discovered path(s) into seed document.`);
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (!opts.json) {
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
emitCliEnvelope(
|
|
599
|
+
{
|
|
600
|
+
ok: false,
|
|
601
|
+
command: "seed",
|
|
602
|
+
version: CLI_VERSION,
|
|
603
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
604
|
+
errors: [errorToMessage(error)]
|
|
605
|
+
},
|
|
606
|
+
true
|
|
607
|
+
);
|
|
608
|
+
process.exitCode = 1;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
225
611
|
program.command("scan").description("Scan source files for static content candidates").argument("<srcDir>", "source directory").option("--out <file>", "report output", ".webmaster-droid/scan-report.json").option("--json", "emit machine-readable JSON output", false).action(async (srcDir, opts) => {
|
|
226
612
|
try {
|
|
227
613
|
const root = path2.resolve(process.cwd(), srcDir);
|
|
@@ -323,20 +709,28 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
323
709
|
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
324
710
|
});
|
|
325
711
|
const changed = [];
|
|
712
|
+
const failures = [];
|
|
326
713
|
for (const file of files) {
|
|
327
|
-
const source = await fs.readFile(file, "utf8");
|
|
328
|
-
const transformed = transformEditableTextCodemod(source, file, process.cwd());
|
|
329
|
-
if (!transformed.changed) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const next = transformed.next;
|
|
333
714
|
const relFile = path2.relative(process.cwd(), file);
|
|
334
|
-
|
|
335
|
-
file
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
715
|
+
try {
|
|
716
|
+
const source = await fs.readFile(file, "utf8");
|
|
717
|
+
const transformed = transformEditableTextCodemod(source, file, root);
|
|
718
|
+
if (!transformed.changed) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const next = transformed.next;
|
|
722
|
+
changed.push({
|
|
723
|
+
file: relFile,
|
|
724
|
+
patch: createTwoFilesPatch(relFile, relFile, source, next)
|
|
725
|
+
});
|
|
726
|
+
if (opts.apply) {
|
|
727
|
+
await fs.writeFile(file, next, "utf8");
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
failures.push({
|
|
731
|
+
file: relFile,
|
|
732
|
+
error: errorToMessage(error)
|
|
733
|
+
});
|
|
340
734
|
}
|
|
341
735
|
}
|
|
342
736
|
const output = path2.resolve(process.cwd(), opts.out);
|
|
@@ -345,10 +739,41 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
345
739
|
source: root,
|
|
346
740
|
apply: Boolean(opts.apply),
|
|
347
741
|
changedFiles: changed.length,
|
|
742
|
+
failedFiles: failures.length,
|
|
743
|
+
failures,
|
|
348
744
|
changes: changed
|
|
349
745
|
};
|
|
350
746
|
await ensureDir(output);
|
|
351
747
|
await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
748
|
+
if (failures.length > 0) {
|
|
749
|
+
const errorMessage = failures.map((item) => `${item.file}: ${item.error}`).join("; ");
|
|
750
|
+
if (opts.json) {
|
|
751
|
+
emitCliEnvelope(
|
|
752
|
+
{
|
|
753
|
+
ok: false,
|
|
754
|
+
command: "codemod",
|
|
755
|
+
version: CLI_VERSION,
|
|
756
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
data: {
|
|
758
|
+
reportPath: output,
|
|
759
|
+
source: root,
|
|
760
|
+
apply: Boolean(opts.apply),
|
|
761
|
+
changedFiles: changed.length
|
|
762
|
+
},
|
|
763
|
+
errors: [errorMessage]
|
|
764
|
+
},
|
|
765
|
+
true
|
|
766
|
+
);
|
|
767
|
+
process.exitCode = 1;
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
console.error(`Codemod encountered ${failures.length} file error(s). Report: ${output}`);
|
|
771
|
+
for (const failure of failures) {
|
|
772
|
+
console.error(`- ${failure.file}: ${failure.error}`);
|
|
773
|
+
}
|
|
774
|
+
process.exitCode = 1;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
352
777
|
if (opts.json) {
|
|
353
778
|
emitCliEnvelope({
|
|
354
779
|
ok: true,
|