aem-eds-kit 0.0.0-stage → 0.4.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/CHANGELOG.md +93 -0
- package/LICENSE +21 -0
- package/README.md +377 -2
- package/dist/bin/eds.js +4214 -0
- package/dist/bin/eds.js.map +1 -0
- package/package.json +65 -4
package/dist/bin/eds.js
ADDED
|
@@ -0,0 +1,4214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/lib/logger.ts
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
function getLevel() {
|
|
24
|
+
const env = process.env.EDS_LOG_LEVEL?.toLowerCase();
|
|
25
|
+
if (env && env in LEVEL_ORDER) return env;
|
|
26
|
+
return "info";
|
|
27
|
+
}
|
|
28
|
+
function shouldLog(level) {
|
|
29
|
+
if (quiet) return level === "error";
|
|
30
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
|
|
31
|
+
}
|
|
32
|
+
var LEVEL_ORDER, currentLevel, quiet, logoShown, logger;
|
|
33
|
+
var init_logger = __esm({
|
|
34
|
+
"src/lib/logger.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
init_esm_shims();
|
|
37
|
+
LEVEL_ORDER = {
|
|
38
|
+
debug: 0,
|
|
39
|
+
info: 1,
|
|
40
|
+
warn: 2,
|
|
41
|
+
error: 3,
|
|
42
|
+
silent: 4
|
|
43
|
+
};
|
|
44
|
+
currentLevel = getLevel();
|
|
45
|
+
quiet = false;
|
|
46
|
+
logoShown = false;
|
|
47
|
+
logger = {
|
|
48
|
+
setLevel(level) {
|
|
49
|
+
currentLevel = level;
|
|
50
|
+
},
|
|
51
|
+
setQuiet(q) {
|
|
52
|
+
quiet = q;
|
|
53
|
+
},
|
|
54
|
+
debug(...args) {
|
|
55
|
+
if (shouldLog("debug")) console.error(chalk.gray("[debug]"), ...args);
|
|
56
|
+
},
|
|
57
|
+
info(...args) {
|
|
58
|
+
if (shouldLog("info")) console.error(...args);
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Print the brand logo at most once per process. The interactive menu and
|
|
62
|
+
* every command call this, so running a command directly shows the logo,
|
|
63
|
+
* while picking it from the menu (which already showed it) doesn't repeat it.
|
|
64
|
+
*/
|
|
65
|
+
logoOnce(art) {
|
|
66
|
+
if (logoShown) return;
|
|
67
|
+
logoShown = true;
|
|
68
|
+
if (shouldLog("info")) console.error(art);
|
|
69
|
+
},
|
|
70
|
+
success(...args) {
|
|
71
|
+
if (shouldLog("info")) console.error(chalk.green("\u2713"), ...args);
|
|
72
|
+
},
|
|
73
|
+
warn(...args) {
|
|
74
|
+
if (shouldLog("warn")) console.error(chalk.yellow("\u26A0"), ...args);
|
|
75
|
+
},
|
|
76
|
+
error(...args) {
|
|
77
|
+
if (shouldLog("error")) console.error(chalk.red("\u2717"), ...args);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// src/lib/ui.ts
|
|
84
|
+
import chalk2 from "chalk";
|
|
85
|
+
function statusIcon(status) {
|
|
86
|
+
return icon[status];
|
|
87
|
+
}
|
|
88
|
+
function visibleLength(s) {
|
|
89
|
+
return s.replace(ANSI, "").length;
|
|
90
|
+
}
|
|
91
|
+
function padEndVisible(s, width) {
|
|
92
|
+
const pad = width - visibleLength(s);
|
|
93
|
+
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
94
|
+
}
|
|
95
|
+
function mark() {
|
|
96
|
+
return chalk2.bgHex("#ff5a36").hex("#0d0d0c").bold(" >_ ");
|
|
97
|
+
}
|
|
98
|
+
function lockup() {
|
|
99
|
+
return `${mark()} ${chalk2.bold("aem-eds-kit")} ${chalk2.dim("by Piotr Godycki")}`;
|
|
100
|
+
}
|
|
101
|
+
function hexToRgb(hex) {
|
|
102
|
+
const h = hex.replace("#", "");
|
|
103
|
+
return [
|
|
104
|
+
Number.parseInt(h.slice(0, 2), 16),
|
|
105
|
+
Number.parseInt(h.slice(2, 4), 16),
|
|
106
|
+
Number.parseInt(h.slice(4, 6), 16)
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
function mix(a, b, t) {
|
|
110
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
111
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
112
|
+
const r = Math.round(ar + (br - ar) * t);
|
|
113
|
+
const g = Math.round(ag + (bg - ag) * t);
|
|
114
|
+
const bl = Math.round(ab + (bb - ab) * t);
|
|
115
|
+
return `#${[r, g, bl].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
116
|
+
}
|
|
117
|
+
function gradientLine(line, stops, width) {
|
|
118
|
+
const chars = [...line];
|
|
119
|
+
const span = Math.max(width - 1, 1);
|
|
120
|
+
return chars.map((ch, i) => {
|
|
121
|
+
if (ch === " ") return ch;
|
|
122
|
+
const seg = i / span * (stops.length - 1);
|
|
123
|
+
const idx = Math.min(Math.floor(seg), stops.length - 2);
|
|
124
|
+
return chalk2.hex(mix(stops[idx], stops[idx + 1], seg - idx))(ch);
|
|
125
|
+
}).join("");
|
|
126
|
+
}
|
|
127
|
+
function logo(tagline = "AEM Edge Delivery \xD7 Figma") {
|
|
128
|
+
const width = columnWidth(LOGO_ART);
|
|
129
|
+
const art = LOGO_ART.map((row) => ` ${gradientLine(row, LOGO_STOPS, width)}`).join("\n");
|
|
130
|
+
const dots = Array.from({ length: 5 }, () => brand.accent("\u25CF")).join(" ");
|
|
131
|
+
return `
|
|
132
|
+
${art}
|
|
133
|
+
|
|
134
|
+
${lockup()} ${dots} ${chalk2.dim(tagline)}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
function heading(title, subtitle) {
|
|
138
|
+
const bar = brand.red("\u258A");
|
|
139
|
+
const sub = subtitle ? ` ${chalk2.dim(subtitle)}` : "";
|
|
140
|
+
return `
|
|
141
|
+
${bar} ${chalk2.bold(title)}${sub}
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
function statusLine(status, name, message, nameWidth = 0) {
|
|
145
|
+
const padded = nameWidth ? padEndVisible(name, nameWidth) : name;
|
|
146
|
+
const msg = message ? ` ${chalk2.dim(message)}` : "";
|
|
147
|
+
return ` ${icon[status]} ${padded}${msg}`;
|
|
148
|
+
}
|
|
149
|
+
function accentLine(name, message, nameWidth = 0) {
|
|
150
|
+
const padded = nameWidth ? padEndVisible(name, nameWidth) : name;
|
|
151
|
+
const msg = message ? ` ${chalk2.dim(message)}` : "";
|
|
152
|
+
return ` ${brand.accent("\u2714")} ${padded}${msg}`;
|
|
153
|
+
}
|
|
154
|
+
function columnWidth(items) {
|
|
155
|
+
return items.reduce((max, s) => Math.max(max, visibleLength(s)), 0);
|
|
156
|
+
}
|
|
157
|
+
function box(lines, accent = brand.red) {
|
|
158
|
+
const inner = columnWidth(lines);
|
|
159
|
+
const top = accent(`\u256D${"\u2500".repeat(inner + 2)}\u256E`);
|
|
160
|
+
const bot = accent(`\u2570${"\u2500".repeat(inner + 2)}\u256F`);
|
|
161
|
+
const body = lines.map((l) => `${accent("\u2502")} ${padEndVisible(l, inner)} ${accent("\u2502")}`).join("\n");
|
|
162
|
+
return `${top}
|
|
163
|
+
${body}
|
|
164
|
+
${bot}`;
|
|
165
|
+
}
|
|
166
|
+
function summary(counts) {
|
|
167
|
+
const parts = [];
|
|
168
|
+
if (counts.pass) parts.push(`${icon.pass} ${counts.pass} passed`);
|
|
169
|
+
if (counts.warn) parts.push(`${icon.warn} ${counts.warn} ${plural(counts.warn, "warning")}`);
|
|
170
|
+
if (counts.fail) parts.push(`${icon.fail} ${counts.fail} failed`);
|
|
171
|
+
if (counts.info) parts.push(`${icon.info} ${counts.info} info`);
|
|
172
|
+
return parts.join(chalk2.dim(" "));
|
|
173
|
+
}
|
|
174
|
+
function plural(n, word) {
|
|
175
|
+
return n === 1 ? word : `${word}s`;
|
|
176
|
+
}
|
|
177
|
+
var brand, icon, ANSI, LOGO_ART, LOGO_STOPS;
|
|
178
|
+
var init_ui = __esm({
|
|
179
|
+
"src/lib/ui.ts"() {
|
|
180
|
+
"use strict";
|
|
181
|
+
init_esm_shims();
|
|
182
|
+
brand = {
|
|
183
|
+
// Warm sweep for the logo wordmark (red → ember → orange)
|
|
184
|
+
red: chalk2.hex("#FA0F00"),
|
|
185
|
+
ember: chalk2.hex("#FF4B1F"),
|
|
186
|
+
orange: chalk2.hex("#FF7B00"),
|
|
187
|
+
// Accent
|
|
188
|
+
blue: chalk2.hex("#2680EB"),
|
|
189
|
+
// Signature accent (matches the landing's `#FF5A36`)
|
|
190
|
+
accent: chalk2.hex("#ff5a36"),
|
|
191
|
+
// Five-colour dot mark (the CLI signature)
|
|
192
|
+
dotOrange: chalk2.hex("#F24E1E"),
|
|
193
|
+
dotPurple: chalk2.hex("#A259FF"),
|
|
194
|
+
dotBlue: chalk2.hex("#1ABCFE"),
|
|
195
|
+
dotGreen: chalk2.hex("#0ACF83"),
|
|
196
|
+
dotPink: chalk2.hex("#FF7262"),
|
|
197
|
+
// Semantic roles
|
|
198
|
+
ok: chalk2.hex("#2D9D78"),
|
|
199
|
+
warn: chalk2.hex("#E68619"),
|
|
200
|
+
err: chalk2.hex("#D7373F"),
|
|
201
|
+
info: chalk2.hex("#2680EB")
|
|
202
|
+
};
|
|
203
|
+
icon = {
|
|
204
|
+
pass: brand.ok("\u2713"),
|
|
205
|
+
warn: brand.warn("\u26A0"),
|
|
206
|
+
fail: brand.err("\u2717"),
|
|
207
|
+
info: brand.info("\u2139"),
|
|
208
|
+
bullet: chalk2.dim("\u2022"),
|
|
209
|
+
arrow: chalk2.dim("\u21B3"),
|
|
210
|
+
figma: brand.dotPurple("\u25C6")
|
|
211
|
+
};
|
|
212
|
+
ANSI = /\[[0-9;]*m/g;
|
|
213
|
+
LOGO_ART = [
|
|
214
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588",
|
|
215
|
+
"\u2588 \u2588 \u2588 \u2588 ",
|
|
216
|
+
"\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588\u2588",
|
|
217
|
+
"\u2588 \u2588 \u2588 \u2588",
|
|
218
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"
|
|
219
|
+
];
|
|
220
|
+
LOGO_STOPS = ["#ff3b1e", "#ff5a36", "#ff7d52"];
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// src/lib/schemas.ts
|
|
225
|
+
import { z } from "zod";
|
|
226
|
+
var figmaUrlSchema, edsConfigSchema, edsProjectSchema, edsMetaSchema, kebabCaseRegex;
|
|
227
|
+
var init_schemas = __esm({
|
|
228
|
+
"src/lib/schemas.ts"() {
|
|
229
|
+
"use strict";
|
|
230
|
+
init_esm_shims();
|
|
231
|
+
figmaUrlSchema = z.object({
|
|
232
|
+
fileKey: z.string().min(1),
|
|
233
|
+
nodeId: z.string().optional(),
|
|
234
|
+
fileName: z.string().optional()
|
|
235
|
+
});
|
|
236
|
+
edsConfigSchema = z.object({
|
|
237
|
+
agent: z.enum(["claude", "cursor", "codex"]).optional(),
|
|
238
|
+
figma: z.object({
|
|
239
|
+
mcpEndpoint: z.string().url().optional(),
|
|
240
|
+
token: z.string().optional()
|
|
241
|
+
}).optional(),
|
|
242
|
+
admin: z.object({
|
|
243
|
+
org: z.string().optional(),
|
|
244
|
+
site: z.string().optional(),
|
|
245
|
+
ref: z.string().default("main")
|
|
246
|
+
}).optional()
|
|
247
|
+
});
|
|
248
|
+
edsProjectSchema = z.object({
|
|
249
|
+
root: z.string(),
|
|
250
|
+
hasFstab: z.boolean(),
|
|
251
|
+
hasBlocks: z.boolean(),
|
|
252
|
+
hasHeadHtml: z.boolean()
|
|
253
|
+
});
|
|
254
|
+
edsMetaSchema = z.object({
|
|
255
|
+
figmaFileKey: z.string(),
|
|
256
|
+
figmaNodeId: z.string().optional(),
|
|
257
|
+
lastSyncedAt: z.string().datetime(),
|
|
258
|
+
promptVersion: z.string(),
|
|
259
|
+
agentUsed: z.string().optional()
|
|
260
|
+
});
|
|
261
|
+
kebabCaseRegex = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// src/lib/project.ts
|
|
266
|
+
import { existsSync } from "fs";
|
|
267
|
+
import path2 from "path";
|
|
268
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
269
|
+
let dir = path2.resolve(startDir);
|
|
270
|
+
const { root } = path2.parse(dir);
|
|
271
|
+
while (dir !== root) {
|
|
272
|
+
if (existsSync(path2.join(dir, "fstab.yaml"))) {
|
|
273
|
+
return dir;
|
|
274
|
+
}
|
|
275
|
+
dir = path2.dirname(dir);
|
|
276
|
+
}
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
279
|
+
function detectProject(rootDir) {
|
|
280
|
+
return edsProjectSchema.parse({
|
|
281
|
+
root: rootDir,
|
|
282
|
+
hasFstab: existsSync(path2.join(rootDir, "fstab.yaml")),
|
|
283
|
+
hasBlocks: existsSync(path2.join(rootDir, "blocks")),
|
|
284
|
+
hasHeadHtml: existsSync(path2.join(rootDir, "head.html"))
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
var init_project = __esm({
|
|
288
|
+
"src/lib/project.ts"() {
|
|
289
|
+
"use strict";
|
|
290
|
+
init_esm_shims();
|
|
291
|
+
init_schemas();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// src/commands/block/create.ts
|
|
296
|
+
var create_exports = {};
|
|
297
|
+
__export(create_exports, {
|
|
298
|
+
createBlock: () => createBlock,
|
|
299
|
+
ueModelFor: () => ueModelFor
|
|
300
|
+
});
|
|
301
|
+
import { existsSync as existsSync2 } from "fs";
|
|
302
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
303
|
+
import path3 from "path";
|
|
304
|
+
import chalk3 from "chalk";
|
|
305
|
+
function titleCase(name) {
|
|
306
|
+
return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
307
|
+
}
|
|
308
|
+
function ueModelFor(name) {
|
|
309
|
+
const title = titleCase(name);
|
|
310
|
+
const model = {
|
|
311
|
+
definitions: [
|
|
312
|
+
{
|
|
313
|
+
title,
|
|
314
|
+
id: name,
|
|
315
|
+
plugins: {
|
|
316
|
+
xwalk: {
|
|
317
|
+
page: {
|
|
318
|
+
resourceType: "core/franklin/components/block/v1/block",
|
|
319
|
+
template: { name: title, model: name }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
models: [
|
|
326
|
+
{
|
|
327
|
+
id: name,
|
|
328
|
+
// Starter fields showcasing good patterns (title+titleType collapse
|
|
329
|
+
// into a heading; richtext for body). Extend from the full catalog —
|
|
330
|
+
// see docs/universal-editor-fields.md.
|
|
331
|
+
fields: [
|
|
332
|
+
{
|
|
333
|
+
component: "text",
|
|
334
|
+
name: "title",
|
|
335
|
+
label: "Title",
|
|
336
|
+
valueType: "string"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
component: "select",
|
|
340
|
+
name: "titleType",
|
|
341
|
+
label: "Title Type",
|
|
342
|
+
valueType: "string",
|
|
343
|
+
value: "h2",
|
|
344
|
+
options: [
|
|
345
|
+
{ name: "H1", value: "h1" },
|
|
346
|
+
{ name: "H2", value: "h2" },
|
|
347
|
+
{ name: "H3", value: "h3" },
|
|
348
|
+
{ name: "H4", value: "h4" }
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
component: "richtext",
|
|
353
|
+
name: "text",
|
|
354
|
+
value: "",
|
|
355
|
+
label: "Text",
|
|
356
|
+
valueType: "string"
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
filters: []
|
|
362
|
+
};
|
|
363
|
+
return `${JSON.stringify(model, null, 2)}
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
async function createBlock(name, options = {}) {
|
|
367
|
+
if (!kebabCaseRegex.test(name)) {
|
|
368
|
+
logger.error(
|
|
369
|
+
`Invalid block name "${name}". Use kebab-case (e.g., "hero-banner", "card-list").`
|
|
370
|
+
);
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const projectRoot = findProjectRoot();
|
|
375
|
+
if (!projectRoot) {
|
|
376
|
+
logger.error(
|
|
377
|
+
"Not inside an EDS project (no fstab.yaml found). Run this from your project root."
|
|
378
|
+
);
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const blocksDir = path3.join(projectRoot, "blocks", name);
|
|
383
|
+
if (existsSync2(blocksDir)) {
|
|
384
|
+
logger.error(`Block "${name}" already exists at ${blocksDir}`);
|
|
385
|
+
process.exitCode = 1;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
await mkdir(blocksDir, { recursive: true });
|
|
389
|
+
const jsContent = `export default function decorate(block) {
|
|
390
|
+
// TODO: implement ${name} block
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
const cssContent = `.${name} {
|
|
394
|
+
/* TODO: style ${name} block */
|
|
395
|
+
}
|
|
396
|
+
`;
|
|
397
|
+
await writeFile(path3.join(blocksDir, `${name}.js`), jsContent);
|
|
398
|
+
await writeFile(path3.join(blocksDir, `${name}.css`), cssContent);
|
|
399
|
+
const withUe = options.ueModel !== false;
|
|
400
|
+
if (withUe) {
|
|
401
|
+
await writeFile(path3.join(blocksDir, `_${name}.json`), ueModelFor(name));
|
|
402
|
+
}
|
|
403
|
+
logger.success(
|
|
404
|
+
`Block "${name}" created at ${chalk3.cyan(path3.relative(projectRoot, blocksDir))}${withUe ? chalk3.dim(" (+ Universal Editor model)") : ""}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
var init_create = __esm({
|
|
408
|
+
"src/commands/block/create.ts"() {
|
|
409
|
+
"use strict";
|
|
410
|
+
init_esm_shims();
|
|
411
|
+
init_logger();
|
|
412
|
+
init_project();
|
|
413
|
+
init_schemas();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// src/lib/agents/detect.ts
|
|
418
|
+
import { existsSync as existsSync3 } from "fs";
|
|
419
|
+
import { readFile } from "fs/promises";
|
|
420
|
+
import { homedir } from "os";
|
|
421
|
+
import path4 from "path";
|
|
422
|
+
async function whichBinary(name) {
|
|
423
|
+
const { execaCommand } = await import("execa");
|
|
424
|
+
try {
|
|
425
|
+
const result = await execaCommand(`which ${name}`);
|
|
426
|
+
return result.stdout.trim() || void 0;
|
|
427
|
+
} catch {
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function findFigmaServerKey(node) {
|
|
432
|
+
if (!node || typeof node !== "object") return void 0;
|
|
433
|
+
const obj = node;
|
|
434
|
+
const servers = obj.mcpServers;
|
|
435
|
+
if (servers && typeof servers === "object") {
|
|
436
|
+
for (const key of Object.keys(servers)) {
|
|
437
|
+
if (key.toLowerCase().includes("figma")) return key;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const value of Object.values(obj)) {
|
|
441
|
+
const found = findFigmaServerKey(value);
|
|
442
|
+
if (found) return found;
|
|
443
|
+
}
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
async function findFigmaMcpServer(configPaths) {
|
|
447
|
+
for (const configPath of configPaths) {
|
|
448
|
+
try {
|
|
449
|
+
if (!existsSync3(configPath)) continue;
|
|
450
|
+
const content = await readFile(configPath, "utf-8");
|
|
451
|
+
try {
|
|
452
|
+
const key = findFigmaServerKey(JSON.parse(content));
|
|
453
|
+
if (key) return key;
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
if (content.toLowerCase().includes("figma")) return "figma";
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
async function detectAgent(preferred) {
|
|
463
|
+
if (preferred) {
|
|
464
|
+
const def = AGENT_DEFS.find((d) => d.type === preferred);
|
|
465
|
+
if (def) {
|
|
466
|
+
const binPath = await whichBinary(def.binary);
|
|
467
|
+
if (binPath) {
|
|
468
|
+
const figmaMcpServerName = await findFigmaMcpServer(def.mcpConfigPaths);
|
|
469
|
+
return {
|
|
470
|
+
type: def.type,
|
|
471
|
+
path: binPath,
|
|
472
|
+
configDir: existsSync3(def.configDir) ? def.configDir : void 0,
|
|
473
|
+
hasFigmaMcp: !!figmaMcpServerName,
|
|
474
|
+
figmaMcpServerName
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
logger.warn(`Preferred agent "${preferred}" not found in PATH`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const def of AGENT_DEFS) {
|
|
481
|
+
const binPath = await whichBinary(def.binary);
|
|
482
|
+
if (binPath) {
|
|
483
|
+
const figmaMcpServerName = await findFigmaMcpServer(def.mcpConfigPaths);
|
|
484
|
+
return {
|
|
485
|
+
type: def.type,
|
|
486
|
+
path: binPath,
|
|
487
|
+
configDir: existsSync3(def.configDir) ? def.configDir : void 0,
|
|
488
|
+
hasFigmaMcp: !!figmaMcpServerName,
|
|
489
|
+
figmaMcpServerName
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
async function detectAllAgents() {
|
|
496
|
+
const agents = [];
|
|
497
|
+
for (const def of AGENT_DEFS) {
|
|
498
|
+
const binPath = await whichBinary(def.binary);
|
|
499
|
+
if (binPath) {
|
|
500
|
+
const figmaMcpServerName = await findFigmaMcpServer(def.mcpConfigPaths);
|
|
501
|
+
agents.push({
|
|
502
|
+
type: def.type,
|
|
503
|
+
path: binPath,
|
|
504
|
+
configDir: existsSync3(def.configDir) ? def.configDir : void 0,
|
|
505
|
+
hasFigmaMcp: !!figmaMcpServerName,
|
|
506
|
+
figmaMcpServerName
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return agents;
|
|
511
|
+
}
|
|
512
|
+
var AGENT_DEFS;
|
|
513
|
+
var init_detect = __esm({
|
|
514
|
+
"src/lib/agents/detect.ts"() {
|
|
515
|
+
"use strict";
|
|
516
|
+
init_esm_shims();
|
|
517
|
+
init_logger();
|
|
518
|
+
AGENT_DEFS = [
|
|
519
|
+
{
|
|
520
|
+
type: "claude",
|
|
521
|
+
binary: "claude",
|
|
522
|
+
configDir: path4.join(homedir(), ".claude"),
|
|
523
|
+
mcpConfigPaths: [
|
|
524
|
+
path4.join(homedir(), ".claude", "plugins.json"),
|
|
525
|
+
path4.join(homedir(), ".claude.json")
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
type: "cursor",
|
|
530
|
+
binary: "cursor",
|
|
531
|
+
configDir: path4.join(homedir(), ".cursor"),
|
|
532
|
+
mcpConfigPaths: [path4.join(homedir(), ".cursor", "mcp.json")]
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
type: "codex",
|
|
536
|
+
binary: "codex",
|
|
537
|
+
configDir: path4.join(homedir(), ".codex"),
|
|
538
|
+
mcpConfigPaths: []
|
|
539
|
+
}
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// src/lib/agents/invoke.ts
|
|
545
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
546
|
+
import { tmpdir } from "os";
|
|
547
|
+
import path5 from "path";
|
|
548
|
+
import readline from "readline";
|
|
549
|
+
import chalk4 from "chalk";
|
|
550
|
+
async function savePromptToFile(prompt, projectRoot) {
|
|
551
|
+
const base = projectRoot ? path5.join(projectRoot, "tmp") : tmpdir();
|
|
552
|
+
const dir = path5.join(base, "eds-cli");
|
|
553
|
+
await mkdir2(dir, { recursive: true });
|
|
554
|
+
const filename = `from-design-${Date.now()}.md`;
|
|
555
|
+
const filePath = path5.join(dir, filename);
|
|
556
|
+
await writeFile2(filePath, prompt, "utf-8");
|
|
557
|
+
return filePath;
|
|
558
|
+
}
|
|
559
|
+
async function invokeAgent(agent, promptFile, cwd) {
|
|
560
|
+
const { execa } = await import("execa");
|
|
561
|
+
const prompt = await readFile2(promptFile, "utf-8");
|
|
562
|
+
try {
|
|
563
|
+
switch (agent.type) {
|
|
564
|
+
case "claude": {
|
|
565
|
+
logger.info(`Running Claude Code ${chalk4.dim("- live log:")}`);
|
|
566
|
+
const figmaServers = [
|
|
567
|
+
...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
|
|
568
|
+
"figma-sse",
|
|
569
|
+
"figma-desktop",
|
|
570
|
+
"figma",
|
|
571
|
+
"Figma",
|
|
572
|
+
"plugin_figma_figma"
|
|
573
|
+
// Claude Code plugin: plugin:figma:figma
|
|
574
|
+
];
|
|
575
|
+
const allowedTools = [
|
|
576
|
+
...new Set(figmaServers.map((s) => `mcp__${s}`)),
|
|
577
|
+
"Edit",
|
|
578
|
+
"Write",
|
|
579
|
+
"Read"
|
|
580
|
+
].join(",");
|
|
581
|
+
const sub = execa(
|
|
582
|
+
"claude",
|
|
583
|
+
[
|
|
584
|
+
"-p",
|
|
585
|
+
prompt,
|
|
586
|
+
"--output-format",
|
|
587
|
+
"stream-json",
|
|
588
|
+
"--verbose",
|
|
589
|
+
"--allowedTools",
|
|
590
|
+
allowedTools
|
|
591
|
+
],
|
|
592
|
+
{ cwd, buffer: false, stdio: ["ignore", "pipe", "inherit"] }
|
|
593
|
+
);
|
|
594
|
+
await renderClaudeStream(sub, cwd);
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case "cursor": {
|
|
598
|
+
logger.info("Running Cursor agent...");
|
|
599
|
+
await execa("cursor", ["--prompt", prompt], {
|
|
600
|
+
cwd,
|
|
601
|
+
stdio: "inherit"
|
|
602
|
+
});
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "codex": {
|
|
606
|
+
logger.info("Running Codex...");
|
|
607
|
+
await execa("codex", [prompt], {
|
|
608
|
+
cwd,
|
|
609
|
+
stdio: "inherit"
|
|
610
|
+
});
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return true;
|
|
615
|
+
} catch (err) {
|
|
616
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
617
|
+
logger.error(`Agent ${agent.type} failed: ${msg}`);
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function shortName(name) {
|
|
622
|
+
return name.includes("figma") ? name.split("__").pop() ?? name : name.toLowerCase();
|
|
623
|
+
}
|
|
624
|
+
async function renderClaudeStream(sub, cwd) {
|
|
625
|
+
const ora4 = (await import("ora")).default;
|
|
626
|
+
const start = Date.now();
|
|
627
|
+
const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
|
|
628
|
+
const spinner = ora4({ text: `thinking ${chalk4.dim(elapsed())}`, color: "yellow" }).start();
|
|
629
|
+
let activity = "thinking";
|
|
630
|
+
const timer = setInterval(() => {
|
|
631
|
+
spinner.text = `${activity} ${chalk4.dim(elapsed())}`;
|
|
632
|
+
}, 300);
|
|
633
|
+
const pending = /* @__PURE__ */ new Map();
|
|
634
|
+
const emit = (line) => {
|
|
635
|
+
spinner.stop();
|
|
636
|
+
logger.info(` ${line}`);
|
|
637
|
+
spinner.start();
|
|
638
|
+
};
|
|
639
|
+
let result;
|
|
640
|
+
try {
|
|
641
|
+
if (sub.stdout) {
|
|
642
|
+
const rl = readline.createInterface({ input: sub.stdout });
|
|
643
|
+
for await (const line of rl) {
|
|
644
|
+
const trimmed = line.trim();
|
|
645
|
+
if (!trimmed) continue;
|
|
646
|
+
let evt;
|
|
647
|
+
try {
|
|
648
|
+
evt = JSON.parse(trimmed);
|
|
649
|
+
} catch {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const content = Array.isArray(evt.message?.content) ? evt.message.content : [];
|
|
653
|
+
if (evt.type === "assistant") {
|
|
654
|
+
for (const block2 of content) {
|
|
655
|
+
if (block2.type === "text" && "text" in block2) {
|
|
656
|
+
const first = block2.text.trim().split("\n")[0].slice(0, 100);
|
|
657
|
+
if (first) emit(chalk4.dim(first));
|
|
658
|
+
} else if (block2.type === "tool_use" && "name" in block2) {
|
|
659
|
+
const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
|
|
660
|
+
pending.set(id, {
|
|
661
|
+
label: toolLabel(block2.name, block2.input, cwd),
|
|
662
|
+
start: Date.now()
|
|
663
|
+
});
|
|
664
|
+
activity = shortName(block2.name);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} else if (evt.type === "user") {
|
|
668
|
+
for (const block2 of content) {
|
|
669
|
+
if (block2.type === "tool_result" && "tool_use_id" in block2 && block2.tool_use_id) {
|
|
670
|
+
const p = pending.get(block2.tool_use_id);
|
|
671
|
+
if (p) {
|
|
672
|
+
const dur = `${((Date.now() - p.start) / 1e3).toFixed(1)}s`;
|
|
673
|
+
const failed = "is_error" in block2 && block2.is_error ? ` ${chalk4.red("failed")}` : "";
|
|
674
|
+
emit(`${p.label} ${chalk4.dim(dur)}${failed}`);
|
|
675
|
+
pending.delete(block2.tool_use_id);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} else if (evt.type === "result") {
|
|
680
|
+
const dur = typeof evt.duration_ms === "number" ? `${(evt.duration_ms / 1e3).toFixed(1)}s` : "";
|
|
681
|
+
const cost = typeof evt.total_cost_usd === "number" ? `$${evt.total_cost_usd.toFixed(3)}` : "";
|
|
682
|
+
result = [dur, cost].filter(Boolean).join(" \xB7 ") || "done";
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const p of pending.values()) emit(`${p.label} ${chalk4.dim("\u2026")}`);
|
|
687
|
+
clearInterval(timer);
|
|
688
|
+
if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk4.dim(result)}`);
|
|
689
|
+
else spinner.stop();
|
|
690
|
+
} catch (err) {
|
|
691
|
+
clearInterval(timer);
|
|
692
|
+
spinner.stop();
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
await sub;
|
|
696
|
+
}
|
|
697
|
+
function toolLabel(name, input, cwd) {
|
|
698
|
+
const rel = (key) => {
|
|
699
|
+
const p = input?.[key];
|
|
700
|
+
if (typeof p !== "string") return "";
|
|
701
|
+
return p.startsWith(cwd) ? path5.relative(cwd, p) : path5.basename(p);
|
|
702
|
+
};
|
|
703
|
+
if (name.includes("figma")) {
|
|
704
|
+
const short = name.split("__").pop() ?? name;
|
|
705
|
+
return `${icon.figma} ${chalk4.bold(short)} ${chalk4.dim("Figma MCP")}`;
|
|
706
|
+
}
|
|
707
|
+
switch (name) {
|
|
708
|
+
case "Write":
|
|
709
|
+
case "Edit":
|
|
710
|
+
case "NotebookEdit":
|
|
711
|
+
return `${brand.accent("\u270E")} ${chalk4.cyan(rel("file_path"))}`;
|
|
712
|
+
case "Read":
|
|
713
|
+
return chalk4.dim(`\xB7 read ${rel("file_path")}`);
|
|
714
|
+
case "Bash": {
|
|
715
|
+
const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
|
|
716
|
+
return chalk4.dim(`$ ${cmd}`);
|
|
717
|
+
}
|
|
718
|
+
default:
|
|
719
|
+
return `${brand.blue("\u2192")} ${name}`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
var init_invoke = __esm({
|
|
723
|
+
"src/lib/agents/invoke.ts"() {
|
|
724
|
+
"use strict";
|
|
725
|
+
init_esm_shims();
|
|
726
|
+
init_logger();
|
|
727
|
+
init_ui();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// src/lib/config.ts
|
|
732
|
+
import { existsSync as existsSync4 } from "fs";
|
|
733
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
734
|
+
import { homedir as homedir2 } from "os";
|
|
735
|
+
import path6 from "path";
|
|
736
|
+
async function loadJsonFile(filePath) {
|
|
737
|
+
try {
|
|
738
|
+
const content = await readFile3(filePath, "utf-8");
|
|
739
|
+
return JSON.parse(content);
|
|
740
|
+
} catch {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function loadConfig(projectRoot) {
|
|
745
|
+
const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
|
|
746
|
+
const projectRaw = projectRoot ? await loadJsonFile(path6.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
|
|
747
|
+
const merged = {
|
|
748
|
+
...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
|
|
749
|
+
...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
|
|
750
|
+
};
|
|
751
|
+
const result = edsConfigSchema.safeParse(merged);
|
|
752
|
+
if (!result.success) {
|
|
753
|
+
logger.debug("Config validation issues:", result.error.flatten());
|
|
754
|
+
return edsConfigSchema.parse({});
|
|
755
|
+
}
|
|
756
|
+
return result.data;
|
|
757
|
+
}
|
|
758
|
+
function globalConfigDir() {
|
|
759
|
+
return GLOBAL_CONFIG_DIR;
|
|
760
|
+
}
|
|
761
|
+
function globalConfigPath() {
|
|
762
|
+
return GLOBAL_CONFIG_PATH;
|
|
763
|
+
}
|
|
764
|
+
function globalConfigExists() {
|
|
765
|
+
return existsSync4(GLOBAL_CONFIG_PATH);
|
|
766
|
+
}
|
|
767
|
+
var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
|
|
768
|
+
var init_config = __esm({
|
|
769
|
+
"src/lib/config.ts"() {
|
|
770
|
+
"use strict";
|
|
771
|
+
init_esm_shims();
|
|
772
|
+
init_logger();
|
|
773
|
+
init_schemas();
|
|
774
|
+
GLOBAL_CONFIG_DIR = path6.join(homedir2(), ".eds");
|
|
775
|
+
GLOBAL_CONFIG_PATH = path6.join(GLOBAL_CONFIG_DIR, "config.json");
|
|
776
|
+
PROJECT_CONFIG_NAME = ".edsrc.json";
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// src/lib/figma/node-id.ts
|
|
781
|
+
function parseFigmaUrl(input) {
|
|
782
|
+
if (/^\d+[-:]\d+$/.test(input.trim())) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Input "${input}" looks like a node-id but not a full URL. Provide a Figma URL or use --file-key.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
let url;
|
|
788
|
+
try {
|
|
789
|
+
url = new URL(input.trim());
|
|
790
|
+
} catch {
|
|
791
|
+
throw new Error(`Invalid Figma URL: "${input}"`);
|
|
792
|
+
}
|
|
793
|
+
if (!url.hostname.endsWith("figma.com")) {
|
|
794
|
+
throw new Error(`Not a Figma URL: "${input}"`);
|
|
795
|
+
}
|
|
796
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
797
|
+
if (segments.length < 2) {
|
|
798
|
+
throw new Error(`Cannot extract fileKey from URL: "${input}"`);
|
|
799
|
+
}
|
|
800
|
+
const type = segments[0];
|
|
801
|
+
let fileKey;
|
|
802
|
+
let fileName;
|
|
803
|
+
if (segments[2] === "branch" && segments[3]) {
|
|
804
|
+
fileKey = segments[3];
|
|
805
|
+
fileName = segments[4] ? decodeURIComponent(segments[4]) : void 0;
|
|
806
|
+
} else {
|
|
807
|
+
fileKey = segments[1];
|
|
808
|
+
fileName = segments[2] ? decodeURIComponent(segments[2]) : void 0;
|
|
809
|
+
}
|
|
810
|
+
const rawNodeId = url.searchParams.get("node-id");
|
|
811
|
+
const nodeId = rawNodeId ? normalizeNodeId(rawNodeId) : void 0;
|
|
812
|
+
return { fileKey, nodeId, fileName };
|
|
813
|
+
}
|
|
814
|
+
function normalizeNodeId(nodeId) {
|
|
815
|
+
return decodeURIComponent(nodeId).replace(/-/g, ":");
|
|
816
|
+
}
|
|
817
|
+
var init_node_id = __esm({
|
|
818
|
+
"src/lib/figma/node-id.ts"() {
|
|
819
|
+
"use strict";
|
|
820
|
+
init_esm_shims();
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// src/lib/figma/prompt-builder.ts
|
|
825
|
+
import { existsSync as existsSync5 } from "fs";
|
|
826
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
827
|
+
import path7 from "path";
|
|
828
|
+
function contentSourceGuidance(source, cfHint, blockName) {
|
|
829
|
+
if (!source || source === "document" || source === "ue") return "";
|
|
830
|
+
const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
|
|
831
|
+
const gql = "`${window.location.origin}/graphql/execute.json/<project>/<query>;path=<cfPath>`";
|
|
832
|
+
if (source === "cf") {
|
|
833
|
+
return `
|
|
834
|
+
## Content source: Content Fragment
|
|
835
|
+
This block renders an AEM **Content Fragment**${q} - not inline-authored copy.
|
|
836
|
+
- UE model: use an \`aem-content-fragment\` field (e.g. \`name: "fragment"\`) for the CF reference.
|
|
837
|
+
- \`decorate(block)\` must be **async**: read the CF reference from the authored DOM (a link to \`/content/dam/...\`), fetch its fields through an AEM GraphQL **persisted query** (${gql}), then build the DOM from the returned fields.
|
|
838
|
+
- Handle loading / empty / error states. Do not hardcode copy that the CF owns.
|
|
839
|
+
- \`.${blockName}\` styling still follows the EDS conventions below.
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
return `
|
|
843
|
+
## Content source: Mixed (Universal Editor + Content Fragment)
|
|
844
|
+
This block mixes **two sources in one model and one \`decorate(block)\`**:
|
|
845
|
+
- **Inline (UE-authored)** parts \u2192 normal fields (text / richtext / image / select) read straight from the authored DOM cells, with semantic collapsing.
|
|
846
|
+
- **Content Fragment** part${q} \u2192 an \`aem-content-fragment\` field; \`decorate()\` is **async** and also fetches that CF via a GraphQL persisted query (${gql}) and renders its fields.
|
|
847
|
+
- Field order in the model = cell order in the authored DOM. Read the inline cells by position; detect the CF reference by its \`/content/dam/\` link. Merge both into the final markup, and handle the CF loading/empty/error states.
|
|
848
|
+
`;
|
|
849
|
+
}
|
|
850
|
+
async function buildPrompt(ctx) {
|
|
851
|
+
const tokensHint = await getTokensHint(ctx.projectRoot);
|
|
852
|
+
const contentSourceSection = contentSourceGuidance(ctx.contentSource, ctx.cfHint, ctx.blockName);
|
|
853
|
+
const withScreenshot = ctx.screenshot !== false;
|
|
854
|
+
const verifyStep = withScreenshot ? `## Step 5 \u2014 Pixel-Perfect Self-Verification (do not skip)
|
|
855
|
+
After writing the files, **verify against the screenshot from Step 1**:
|
|
856
|
+
1. Re-open the \`get_screenshot\` image and compare it to your implementation region by region.
|
|
857
|
+
2. Check each axis: spacing, font sizes/weights/line-heights, colors, border-radius, shadows, alignment, and overall proportions.
|
|
858
|
+
3. For every mismatch, adjust the CSS and re-check. Repeat until the rendered block is indistinguishable from the screenshot.
|
|
859
|
+
4. Briefly state what you verified and any value you had to infer.` : `## Step 5 \u2014 Structural Self-Verification (do not skip)
|
|
860
|
+
No screenshot was fetched (token-lean mode). Verify against the **exact values from \`get_design_context\`**:
|
|
861
|
+
1. Re-check every extracted value (spacing, fonts, colors, radius, shadows, alignment) against your CSS.
|
|
862
|
+
2. Fix any mismatch.
|
|
863
|
+
3. Briefly state what you verified and any value you had to infer.`;
|
|
864
|
+
const provider = ctx.provider ?? "figma";
|
|
865
|
+
const providerName = PROVIDER_NAMES[provider];
|
|
866
|
+
const fetchSection = provider === "figma" ? `## Step 1 \u2014 Fetch the Design
|
|
867
|
+
Call the Figma MCP tools for this selection:
|
|
868
|
+
- \`get_design_context\` \u2014 structure, layout, measurements, text content
|
|
869
|
+
- \`get_variable_defs\` \u2014 design tokens (variables) to map to CSS custom properties
|
|
870
|
+
${withScreenshot ? "- `get_screenshot` \u2014 the ground-truth visual reference for Step 5. Request it at `maxDimension: 1024` (enough to verify; do not request larger)." : "- (screenshot skipped to save tokens \u2014 pass `excludeScreenshot: true` to `get_design_context`)"}
|
|
871
|
+
|
|
872
|
+
**Token efficiency:** call each tool **once** for the target node only \u2014 don't re-fetch and don't walk sibling/parent nodes you don't need.${withScreenshot ? "" : " You will verify structurally against the `get_design_context` measurements, not a picture."}
|
|
873
|
+
|
|
874
|
+
Target:
|
|
875
|
+
- fileKey: \`${ctx.figma?.fileKey}\`
|
|
876
|
+
${ctx.figma?.nodeId ? `- node: \`${ctx.figma.nodeId}\`` : "- (no node selected \u2014 use the full page)"}
|
|
877
|
+
|
|
878
|
+
${ctx.figma?.nodeId ? `Use node ID \`${ctx.figma.nodeId}\` in file \`${ctx.figma.fileKey}\`.` : `Use file key \`${ctx.figma?.fileKey}\` (no specific node selected \u2014 analyze the full page or ask the user to select a frame).`}` : `## Step 1 \u2014 Fetch the Design
|
|
879
|
+
Read the design from **${providerName}** through your ${providerName} MCP connection (use its design-context / inspect / screenshot tools). Design reference: \`${ctx.designRef ?? "(the selected design)"}\`.
|
|
880
|
+
Extract the **exact** structure, layout, colours, typography, spacing, radius and effects \u2014 do not approximate.${withScreenshot ? " Capture a screenshot for the Step 5 pixel check." : " No screenshot (token-lean) \u2014 verify structurally against the extracted values."}
|
|
881
|
+
|
|
882
|
+
**Token efficiency:** fetch once for the target only.`;
|
|
883
|
+
const ueModelSection = ctx.withUeModel ? `
|
|
884
|
+
## Step 4b \u2014 Universal Editor Authoring (REQUIRED, not optional)
|
|
885
|
+
Authors must be able to drop and edit this block in the Universal Editor, so generate its UE configuration **in the same pass** as the code.
|
|
886
|
+
|
|
887
|
+
Create \`blocks/${ctx.blockName}/_${ctx.blockName}.json\` using the EDS block-plugin (xwalk) format with three top-level arrays:
|
|
888
|
+
|
|
889
|
+
\`\`\`json
|
|
890
|
+
{
|
|
891
|
+
"definitions": [
|
|
892
|
+
{
|
|
893
|
+
"title": "<Human Title>",
|
|
894
|
+
"id": "${ctx.blockName}",
|
|
895
|
+
"plugins": {
|
|
896
|
+
"xwalk": {
|
|
897
|
+
"page": {
|
|
898
|
+
"resourceType": "core/franklin/components/block/v1/block",
|
|
899
|
+
"template": { "name": "<Human Title>", "model": "${ctx.blockName}" }
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
],
|
|
905
|
+
"models": [
|
|
906
|
+
{ "id": "${ctx.blockName}", "fields": [ /* one field per authorable piece of content */ ] }
|
|
907
|
+
],
|
|
908
|
+
"filters": []
|
|
909
|
+
}
|
|
910
|
+
\`\`\`
|
|
911
|
+
|
|
912
|
+
Derive the **model fields from the Figma design**, not boilerplate. Pick the correct field \`component\` for each authorable element from the **full Universal Editor field catalog** (17 types) \u2014 do not default everything to text:
|
|
913
|
+
|
|
914
|
+
| component | valueType | Use for |
|
|
915
|
+
|---|---|---|
|
|
916
|
+
| \`text\` | string | Single-line: titles, labels, alt text, short strings |
|
|
917
|
+
| \`textarea\` | string | Multi-line plain text: descriptions, notes |
|
|
918
|
+
| \`richtext\` | string | Formatted copy: body with bold/italic/lists/links |
|
|
919
|
+
| \`reference\` | string | AEM asset (image/video/doc) from DAM. \`multi:true\` for many |
|
|
920
|
+
| \`aem-content\` | any | Page link / URL / content path (content picker) |
|
|
921
|
+
| \`aem-content-fragment\` | any | Content Fragment reference |
|
|
922
|
+
| \`aem-experience-fragment\` | any | Experience Fragment reference |
|
|
923
|
+
| \`aem-tag\` | string | Tag picker for categorization |
|
|
924
|
+
| \`select\` | string | Single choice dropdown \u2014 **requires \`options\`** |
|
|
925
|
+
| \`multiselect\` | string | Multiple choice (often \`name:"classes"\` \u2192 CSS variants) \u2014 **requires \`options\`** |
|
|
926
|
+
| \`checkbox-group\` | string[] | Multiple independent toggles \u2014 **requires \`options\`** |
|
|
927
|
+
| \`radio-group\` | string | Mutually exclusive choice \u2014 **requires \`options\`** |
|
|
928
|
+
| \`boolean\` | boolean | Single on/off toggle |
|
|
929
|
+
| \`number\` | number | Counts, limits (validation: \`numberMin\`/\`numberMax\`) |
|
|
930
|
+
| \`date-time\` | date | Date/time picker |
|
|
931
|
+
| \`container\` | any | Group nested \`fields\`; \`multi:true\` = repeatable items (cards/slides) |
|
|
932
|
+
| \`tab\` | any | Organize the property panel into tabs (UI only, not data) |
|
|
933
|
+
|
|
934
|
+
Rules:
|
|
935
|
+
- Every field needs \`component\`, \`name\`, \`label\`. Include the enforced \`valueType\` from the table. Add \`value\` (default), \`description\`, \`required\`, \`condition\` (JSON Logic) where useful.
|
|
936
|
+
- \`options\` format for select/multiselect/checkbox-group/radio-group: \`[{ "name": "Display", "value": "stored" }]\` (multiselect also supports grouped \`children\`).
|
|
937
|
+
- **Semantic collapsing** \u2014 name paired fields so EDS collapses them into one element:
|
|
938
|
+
- \`image\` (reference) + \`imageAlt\` (text) \u2192 \`<picture><img alt="\u2026">\`
|
|
939
|
+
- \`link\` (aem-content) + \`linkText\` (text) + \`linkTitle\` (text) + \`linkType\` (text) \u2192 \`<a href title>text</a>\`
|
|
940
|
+
- \`title\` (text) + \`titleType\` (select h1\u2013h6) \u2192 \`<h2>title</h2>\` at the chosen level
|
|
941
|
+
- \`classes\` (multiselect) \u2192 values become CSS classes on the block root
|
|
942
|
+
- fields prefixed \`group_\` share one cell
|
|
943
|
+
- Map Figma **text layers**: headings \u2192 \`title\`+\`titleType\`; body \u2192 \`richtext\`; short labels \u2192 \`text\`.
|
|
944
|
+
- Map **image/media layers** \u2192 \`reference\` (+ \`imageAlt\`). **Links/CTAs** \u2192 \`link\`/\`linkText\`.
|
|
945
|
+
- Map **component variants** \u2192 a \`multiselect\` named \`classes\` (or a \`select\`) whose options are the variant values (e.g. Theme dark/light, Layout centered) \u2192 CSS modifier classes on the block root. Keep JS/CSS variant handling in sync.
|
|
946
|
+
- **Repeatable groups** (card lists, carousels, tabs): either a \`container\` field with \`multi:true\` and nested \`fields\`, or \u2014 for true child blocks \u2014 a container definition (\`template.filter\`) + an item definition (resourceType \`core/franklin/components/block/v1/block/item\`) + a matching \`filters\` entry.
|
|
947
|
+
- Field \`name\`s must match what \`decorate()\` reads from the authored DOM. No underscores in names when using xwalk (except the \`group_\` grouping prefix).
|
|
948
|
+
|
|
949
|
+
**Make multifields and dropdowns actually work \u2014 the config is only half; \`decorate()\` MUST consume every field:**
|
|
950
|
+
- **Repeatable items** (cards, slides, accordion, list rows) \u2192 model them as a **container block**, not a flat model: (a) a container definition whose \`template\` has a \`"filter"\` (not \`"model"\`), (b) an **item** definition with \`"resourceType": "core/franklin/components/block/v1/block/item"\` and its own \`model\`, (c) a \`filters\` entry \`{ "id": "${ctx.blockName}", "components": ["${ctx.blockName}-item"] }\`. The authored block renders **one row per item** \u2014 \`decorate()\` MUST iterate them: \`[...block.children].forEach((row) => { /* build one card */ })\`. A flat model with no iteration = a block that only ever shows one item.
|
|
951
|
+
- **Inline repeatable group** (e.g. 2\u20133 CTAs inside a hero) \u2192 a \`container\` field with \`"multi": true\` and nested \`fields\` (no child-block needed).
|
|
952
|
+
- **Dropdowns**: a \`select\`/\`radio-group\` value arrives as a cell in the authored DOM \u2014 read it in \`decorate()\` and act on it (e.g. read \`titleType\` and swap the heading tag to \`h1..h6\`). A \`multiselect\` named \`classes\` is applied as CSS classes on the block root automatically \u2014 just write \`.${ctx.blockName}.<value>\` styles; any other \`multiselect\`/\`checkbox-group\` arrives as a CSV cell you parse.
|
|
953
|
+
- **Self-check before finishing:** every field \`name\` in the model must be read by \`decorate()\` (or applied via \`classes\`). List each field and where the JS/CSS consumes it. Unconsumed fields = broken authoring.
|
|
954
|
+
|
|
955
|
+
If the project uses a **single aggregated model file** (\`component-definition.json\` + \`component-models.json\` + \`component-filters.json\`) instead of per-block \`_${ctx.blockName}.json\`, detect that convention and add the block's definition/model/filter to those files instead (and register the block in the \`section\` filter). Prefer the per-block \`_${ctx.blockName}.json\` when neither exists. Full field reference and a repeatable-cards round-trip example: \`docs/universal-editor-fields.md\`.
|
|
956
|
+
` : "";
|
|
957
|
+
return `# Pixel-Perfect EDS Block from ${providerName}
|
|
958
|
+
|
|
959
|
+
## Task
|
|
960
|
+
Generate an AEM Edge Delivery Services block named **\`${ctx.blockName}\`** that reproduces the ${providerName} design **1:1 \u2014 pixel perfect**. Exact spacing, colors, typography, and layout. Not "close enough" \u2014 identical.
|
|
961
|
+
|
|
962
|
+
${fetchSection}
|
|
963
|
+
|
|
964
|
+
## Step 2 \u2014 Extract Exact Values
|
|
965
|
+
From the design data you fetched, record the **precise** values \u2014 do not approximate:
|
|
966
|
+
1. **Layout**: Auto Layout direction, gap, padding, alignment \u2192 Flexbox/Grid with exact \`gap\`/\`padding\`
|
|
967
|
+
2. **Spacing**: every margin/padding in px \u2014 convert to \`rem\` (\xF716) or reuse a matching project token
|
|
968
|
+
3. **Colors**: exact hex / token for every fill, stroke, shadow \u2014 prefer bound variables over raw hex
|
|
969
|
+
4. **Typography**: font-family, font-size, font-weight, line-height, letter-spacing per text layer. If the design uses a font **not already loaded by the project**, add it to the project's font pipeline (\`styles/fonts.css\` / \`loadFonts\` in \`scripts/scripts.js\`, or a \`<link>\` managed by EDS) \u2014 **never** \`@import\` or a third-party \`<link>\` inside the block CSS (it is render-blocking and fails the loading audit). Always declare a sensible fallback stack.
|
|
970
|
+
5. **Dimensions**: fixed vs. fluid (\`Fill\`\u2192fluid, \`Hug\`\u2192auto, fixed px\u2192fixed) and border-radius
|
|
971
|
+
6. **Effects**: box-shadow, blur, opacity, gradients \u2014 replicate exactly
|
|
972
|
+
|
|
973
|
+
## Step 3 \u2014 Map to Semantic EDS Structure
|
|
974
|
+
- Map layer names/roles to semantic HTML (\`<h1>\`/\`<h2>\` by text-style level, \`<p>\`, \`<a class="button">\`, \`<picture>\`, \`<figure>\`, \`<nav>\`\u2026)
|
|
975
|
+
- A \`decorate(block)\` function receives a \`<div class="${ctx.blockName}">\` whose children are the **authored content** coming from the document (EDS block table rows \u2192 nested divs). Your job is to read that existing DOM and enhance/restructure it \u2014 **not** to hardcode copy.
|
|
976
|
+
- Assume one row per logical content group; the first cell of each row holds the content. Normalize whatever markup EDS hands you.
|
|
977
|
+
- Map Figma component **variant properties** to modifier classes on the block root (e.g. \`Theme=dark\` \u2192 \`.${ctx.blockName}.dark\`, \`Layout=centered\` \u2192 \`.${ctx.blockName}.centered\`). Read variants via metadata; handle each in CSS.
|
|
978
|
+
- Follow any Figma **annotations** for behavior (autoplay, collapsed-by-default, load eagerly, truncation, tab order).
|
|
979
|
+
|
|
980
|
+
## Step 4 \u2014 Generate Block Files
|
|
981
|
+
|
|
982
|
+
Create these files in \`blocks/${ctx.blockName}/\`:
|
|
983
|
+
|
|
984
|
+
### \`${ctx.blockName}.js\`
|
|
985
|
+
\`\`\`javascript
|
|
986
|
+
// EDS block: ${ctx.blockName}
|
|
987
|
+
// Generated from Figma design
|
|
988
|
+
export default function decorate(block) {
|
|
989
|
+
// - Vanilla JS only (no frameworks)
|
|
990
|
+
// - Read the authored children already inside \`block\`; restructure into semantic HTML
|
|
991
|
+
// - Add class names prefixed with "${ctx.blockName}-"
|
|
992
|
+
// - Wire interactions from Figma annotations (if any)
|
|
993
|
+
// - Eager-load above-fold images, lazy-load below-fold
|
|
994
|
+
}
|
|
995
|
+
\`\`\`
|
|
996
|
+
|
|
997
|
+
### \`${ctx.blockName}.css\`
|
|
998
|
+
\`\`\`css
|
|
999
|
+
/* EDS block: ${ctx.blockName} */
|
|
1000
|
+
/* Map Figma design tokens to CSS custom properties; reuse project tokens below where they match */
|
|
1001
|
+
/* Mobile-first; exact values from Step 2 */
|
|
1002
|
+
\`\`\`
|
|
1003
|
+
${ueModelSection}${contentSourceSection}
|
|
1004
|
+
## Placeholders & assets \u2014 be visible before Universal Editor takes over
|
|
1005
|
+
Content images and copy come from **authoring** (the document / DAM via UE), not the repo. Do **not** ship design-tool CDN URLs (Figma / Google Stitch links are short-lived) and do **not** bundle content photos into the repo.
|
|
1006
|
+
- In \`decorate()\`, read the image/text from the authored DOM. When none is present yet, render a lightweight **placeholder** so the block is visible *before* an author sets the real content: an inline SVG data-URI grey box (or a CSS placeholder) at the correct \`aspect-ratio\`, and sensible placeholder copy.
|
|
1007
|
+
- Provide the mock for the preview in \`blocks/${ctx.blockName}/_${ctx.blockName}.preview.html\` (used by \`eds block preview\`). Keep placeholder copy there, not hardcoded in the block.
|
|
1008
|
+
- Only bundle genuinely **static, decorative** assets (e.g. a small icon) as an optimized inline/\`icons/\` **SVG**, referenced by project-relative path. Never bundle authorable content images.
|
|
1009
|
+
- Always set \`width\`/\`height\` or \`aspect-ratio\` to avoid CLS.
|
|
1010
|
+
|
|
1011
|
+
${verifyStep}
|
|
1012
|
+
|
|
1013
|
+
## EDS Block Conventions
|
|
1014
|
+
- **Full-width by default**: the block/section spans the **full viewport width** (full-bleed) \u2014 never a fixed or centered fixed-width box. Constrain only the *inner content* to a sensible \`max-width\` (centered) for line-length/readability, with fluid horizontal padding. The design frame's width (e.g. 1280px) is the content cap, not the section width.
|
|
1015
|
+
- **Mobile-first & fluid**: base styles target the smallest screen; layer breakpoints upward. The section must look right at any width, not only the design frame's.
|
|
1016
|
+
- **No frameworks**: vanilla JS and CSS only
|
|
1017
|
+
- **\`decorate(block)\`**: enhance the authored DOM; never inject hardcoded copy that should be authored
|
|
1018
|
+
- **Semantic HTML**: correct elements and heading hierarchy
|
|
1019
|
+
- **Class naming**: prefix all classes with the block name: \`.${ctx.blockName}-item\`, \`.${ctx.blockName}-title\`
|
|
1020
|
+
- **Loading strategy**: eager (above-fold/hero), lazy (below-fold/carousels), delayed (analytics)
|
|
1021
|
+
- **Images**: always \`alt\` (empty for decorative), \`<picture>\` with WebP where applicable
|
|
1022
|
+
- **Accessibility**: ARIA where needed, keyboard navigation, focus states matching the design
|
|
1023
|
+
- **Performance**: no heavy dependencies, minimal DOM work, CSS for animations
|
|
1024
|
+
${tokensHint}
|
|
1025
|
+
|
|
1026
|
+
## Output Requirements
|
|
1027
|
+
1. All files in \`blocks/${ctx.blockName}/\`${ctx.withUeModel ? ` \u2014 JS, CSS **and** the Universal Editor model (\`_${ctx.blockName}.json\`)` : ""}
|
|
1028
|
+
2. Production-ready, Lighthouse-friendly (aim for 100), no \`console.log\`
|
|
1029
|
+
3. Brief inline comments only for non-obvious logic
|
|
1030
|
+
4. **Minimise chat output (save tokens):** write the files directly with your editing tools \u2014 do **not** paste the generated code back into the chat, and skip step-by-step narration. End with a **\u22643-line** summary: files created + any value you had to infer.
|
|
1031
|
+
5. Mobile-first CSS with breakpoints at 600px and 900px
|
|
1032
|
+
6. Prefer existing project tokens over new hardcoded values${ctx.withUeModel ? "\n7. UE model field `name`s must match what `decorate()` reads, and every Figma variant must appear as a select option wired to a CSS modifier" : ""}
|
|
1033
|
+
|
|
1034
|
+
---
|
|
1035
|
+
*Prompt version: ${PROMPT_VERSION}*
|
|
1036
|
+
`;
|
|
1037
|
+
}
|
|
1038
|
+
async function getTokensHint(projectRoot) {
|
|
1039
|
+
const stylesPath = path7.join(projectRoot, "styles", "styles.css");
|
|
1040
|
+
if (!existsSync5(stylesPath)) return "";
|
|
1041
|
+
try {
|
|
1042
|
+
const css = await readFile4(stylesPath, "utf-8");
|
|
1043
|
+
const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
|
|
1044
|
+
if (!rootMatch) return "";
|
|
1045
|
+
const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
|
|
1046
|
+
if (!props) return "";
|
|
1047
|
+
return `
|
|
1048
|
+
## Existing Project Tokens
|
|
1049
|
+
The project already defines these CSS custom properties in \`styles/styles.css\`.
|
|
1050
|
+
Reuse them instead of hardcoding values:
|
|
1051
|
+
|
|
1052
|
+
\`\`\`css
|
|
1053
|
+
:root {
|
|
1054
|
+
${props}
|
|
1055
|
+
}
|
|
1056
|
+
\`\`\`
|
|
1057
|
+
`;
|
|
1058
|
+
} catch {
|
|
1059
|
+
return "";
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
var PROMPT_VERSION, PROVIDER_NAMES;
|
|
1063
|
+
var init_prompt_builder = __esm({
|
|
1064
|
+
"src/lib/figma/prompt-builder.ts"() {
|
|
1065
|
+
"use strict";
|
|
1066
|
+
init_esm_shims();
|
|
1067
|
+
PROMPT_VERSION = "0.5.0";
|
|
1068
|
+
PROVIDER_NAMES = {
|
|
1069
|
+
figma: "Figma",
|
|
1070
|
+
stitch: "Google Stitch",
|
|
1071
|
+
canva: "Canva",
|
|
1072
|
+
sketch: "Sketch"
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// src/lib/preview/harness.ts
|
|
1078
|
+
function esc(s) {
|
|
1079
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1080
|
+
}
|
|
1081
|
+
function buildInner(opts) {
|
|
1082
|
+
const { blockName, sampleContent } = opts;
|
|
1083
|
+
return `<!DOCTYPE html>
|
|
1084
|
+
<html lang="en">
|
|
1085
|
+
<head>
|
|
1086
|
+
<meta charset="utf-8" />
|
|
1087
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1088
|
+
<link rel="stylesheet" href="/blocks/${blockName}/${blockName}.css" />
|
|
1089
|
+
<style>*{box-sizing:border-box}html,body{margin:0;padding:0}</style>
|
|
1090
|
+
</head>
|
|
1091
|
+
<body>
|
|
1092
|
+
${sampleContent}
|
|
1093
|
+
<script type="module">
|
|
1094
|
+
import decorate from '/blocks/${blockName}/${blockName}.js';
|
|
1095
|
+
const block = document.querySelector('.${blockName}');
|
|
1096
|
+
if (block) { try { decorate(block); } catch (e) { console.error('decorate() failed:', e); } }
|
|
1097
|
+
</script>
|
|
1098
|
+
<script>
|
|
1099
|
+
// Live reload: refresh when eds detects a change to the block.
|
|
1100
|
+
try { new EventSource('/__eds_events').onmessage = () => location.reload(); } catch (e) { /* no SSE */ }
|
|
1101
|
+
</script>
|
|
1102
|
+
</body>
|
|
1103
|
+
</html>`;
|
|
1104
|
+
}
|
|
1105
|
+
function buildOuter(opts) {
|
|
1106
|
+
const { blockName, breakpoints, figmaUrl, frameRoute, placeholder } = opts;
|
|
1107
|
+
const figmaBps = breakpoints.filter((b) => b.figma);
|
|
1108
|
+
const def = (figmaBps.length ? figmaBps : breakpoints).reduce(
|
|
1109
|
+
(m, b) => b.width > m.width ? b : m,
|
|
1110
|
+
breakpoints[0]
|
|
1111
|
+
);
|
|
1112
|
+
const options = breakpoints.map(
|
|
1113
|
+
(b) => `<option value="${b.width}"${b.width === def.width ? " selected" : ""}>${esc(b.label)}</option>`
|
|
1114
|
+
).join("\n ");
|
|
1115
|
+
const figmaLink = figmaUrl ? `<a class="figma-link" href="${esc(figmaUrl)}" target="_blank" rel="noopener">
|
|
1116
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
|
1117
|
+
Open in Figma
|
|
1118
|
+
</a>` : "";
|
|
1119
|
+
const hint = placeholder ? `<span class="hint">placeholder content \u2014 add <code>blocks/${blockName}/_${blockName}.preview.html</code></span>` : "";
|
|
1120
|
+
return `<!DOCTYPE html>
|
|
1121
|
+
<html lang="en">
|
|
1122
|
+
<head>
|
|
1123
|
+
<meta charset="utf-8" />
|
|
1124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1125
|
+
<title>eds preview \u2014 ${esc(blockName)}</title>
|
|
1126
|
+
<style>
|
|
1127
|
+
*{box-sizing:border-box}
|
|
1128
|
+
html,body{margin:0;padding:0;background:#1f1f1f;color:#eaeaea;font-family:ui-sans-serif,-apple-system,"Segoe UI",sans-serif}
|
|
1129
|
+
.bar{position:sticky;top:0;z-index:10;display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:12px 18px;font-size:13px;background:#141414;border-bottom:1px solid #333}
|
|
1130
|
+
.bar .label{color:#8a8a8a;font-weight:600}
|
|
1131
|
+
.bar .block{color:#e6e6e6;font-weight:700}
|
|
1132
|
+
select#bp{appearance:none;cursor:pointer;color:#eaeaea;background:#232323;border:1px solid #3a3a3a;border-radius:8px;padding:7px 34px 7px 13px;font:inherit;font-size:12.5px;font-weight:600;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;background-size:14px}
|
|
1133
|
+
select#bp:hover{border-color:#6a6a6a;background-color:#2e2e2e}
|
|
1134
|
+
.hint{color:#8a8a8a;font-size:12px}
|
|
1135
|
+
.hint code{color:#bdbdbd}
|
|
1136
|
+
.figma-link{margin-left:auto;display:inline-flex;align-items:center;gap:7px;color:#cfcfcf;text-decoration:none;font-weight:600;font-size:12.5px;padding:7px 13px;border:1px solid #3a3a3a;border-radius:8px;background:#232323}
|
|
1137
|
+
.figma-link svg{width:15px;height:15px}
|
|
1138
|
+
.figma-link:hover{border-color:#6a6a6a;background:#2e2e2e;color:#fff}
|
|
1139
|
+
.stage{padding:30px 28px;display:flex;justify-content:center;overflow-x:auto}
|
|
1140
|
+
.device{position:relative;background:#fff;border:1px solid #000;transition:width .2s ease}
|
|
1141
|
+
.device::before{content:attr(data-w) "px";position:absolute;top:-20px;left:0;font-size:11px;color:#8a8a8a;font-family:ui-monospace,Menlo,monospace}
|
|
1142
|
+
iframe{display:block;width:100%;height:820px;border:0}
|
|
1143
|
+
</style>
|
|
1144
|
+
</head>
|
|
1145
|
+
<body>
|
|
1146
|
+
<div class="bar">
|
|
1147
|
+
<span class="label">Block</span><span class="block">${esc(blockName)}</span>
|
|
1148
|
+
<label class="label" for="bp" style="margin-left:8px">Breakpoint</label>
|
|
1149
|
+
<select id="bp">
|
|
1150
|
+
${options}
|
|
1151
|
+
<option value="full">Full ca\u0142a szeroko\u015B\u0107</option>
|
|
1152
|
+
</select>
|
|
1153
|
+
${hint}
|
|
1154
|
+
${figmaLink}
|
|
1155
|
+
</div>
|
|
1156
|
+
<div class="stage">
|
|
1157
|
+
<div class="device" id="device" data-w="${def.width}" style="width:${def.width}px">
|
|
1158
|
+
<iframe id="frame" src="${frameRoute}" title="${esc(blockName)} preview"></iframe>
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
<script>
|
|
1162
|
+
const device = document.getElementById('device');
|
|
1163
|
+
const bp = document.getElementById('bp');
|
|
1164
|
+
function setWidth(w){
|
|
1165
|
+
if(w==='full'){device.style.width='100%';device.dataset.w=Math.round(device.getBoundingClientRect().width);}
|
|
1166
|
+
else{device.style.width=w+'px';device.dataset.w=w;}
|
|
1167
|
+
}
|
|
1168
|
+
bp.addEventListener('change',()=>setWidth(bp.value==='full'?'full':Number(bp.value)));
|
|
1169
|
+
setWidth(Number(bp.value));
|
|
1170
|
+
</script>
|
|
1171
|
+
</body>
|
|
1172
|
+
</html>`;
|
|
1173
|
+
}
|
|
1174
|
+
var init_harness = __esm({
|
|
1175
|
+
"src/lib/preview/harness.ts"() {
|
|
1176
|
+
"use strict";
|
|
1177
|
+
init_esm_shims();
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// src/lib/preview/server.ts
|
|
1182
|
+
import { createReadStream, existsSync as existsSync6, statSync } from "fs";
|
|
1183
|
+
import http from "http";
|
|
1184
|
+
import path8 from "path";
|
|
1185
|
+
function startServer(root, routes, port, host = "127.0.0.1") {
|
|
1186
|
+
const resolvedRoot = path8.resolve(root);
|
|
1187
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1188
|
+
const server = http.createServer((req, res) => {
|
|
1189
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
1190
|
+
if (urlPath === PING_ROUTE) {
|
|
1191
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1192
|
+
res.end(JSON.stringify({ eds: true }));
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (urlPath === EVENTS_ROUTE) {
|
|
1196
|
+
res.writeHead(200, {
|
|
1197
|
+
"content-type": "text/event-stream",
|
|
1198
|
+
"cache-control": "no-cache",
|
|
1199
|
+
connection: "keep-alive"
|
|
1200
|
+
});
|
|
1201
|
+
res.write(": connected\n\n");
|
|
1202
|
+
clients.add(res);
|
|
1203
|
+
req.on("close", () => clients.delete(res));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const route = routes[urlPath];
|
|
1207
|
+
if (route) {
|
|
1208
|
+
res.writeHead(200, { "content-type": route.type });
|
|
1209
|
+
res.end(route.body);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const filePath = path8.join(resolvedRoot, urlPath);
|
|
1213
|
+
if (!filePath.startsWith(resolvedRoot)) {
|
|
1214
|
+
res.writeHead(403);
|
|
1215
|
+
res.end("Forbidden");
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (!existsSync6(filePath) || !statSync(filePath).isFile()) {
|
|
1219
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1220
|
+
res.end(`Not found: ${urlPath}`);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
res.writeHead(200, {
|
|
1224
|
+
"content-type": MIME[path8.extname(filePath).toLowerCase()] ?? "application/octet-stream"
|
|
1225
|
+
});
|
|
1226
|
+
createReadStream(filePath).pipe(res);
|
|
1227
|
+
});
|
|
1228
|
+
return new Promise((resolve, reject) => {
|
|
1229
|
+
let attempts = 0;
|
|
1230
|
+
const tryListen = (p) => {
|
|
1231
|
+
server.once("error", (err) => {
|
|
1232
|
+
if (err.code === "EADDRINUSE" && attempts < 10) {
|
|
1233
|
+
attempts++;
|
|
1234
|
+
tryListen(p + 1);
|
|
1235
|
+
} else {
|
|
1236
|
+
reject(err);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
server.listen(p, host, () => {
|
|
1240
|
+
resolve({
|
|
1241
|
+
url: `http://${host}:${p}/`,
|
|
1242
|
+
port: p,
|
|
1243
|
+
reload: () => {
|
|
1244
|
+
for (const c of clients) c.write("data: reload\n\n");
|
|
1245
|
+
},
|
|
1246
|
+
close: () => new Promise((res) => {
|
|
1247
|
+
for (const c of clients) c.end();
|
|
1248
|
+
server.close(() => res());
|
|
1249
|
+
})
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
};
|
|
1253
|
+
tryListen(port);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
var MIME, PING_ROUTE, EVENTS_ROUTE;
|
|
1257
|
+
var init_server = __esm({
|
|
1258
|
+
"src/lib/preview/server.ts"() {
|
|
1259
|
+
"use strict";
|
|
1260
|
+
init_esm_shims();
|
|
1261
|
+
MIME = {
|
|
1262
|
+
".html": "text/html; charset=utf-8",
|
|
1263
|
+
".js": "text/javascript; charset=utf-8",
|
|
1264
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
1265
|
+
".css": "text/css; charset=utf-8",
|
|
1266
|
+
".json": "application/json; charset=utf-8",
|
|
1267
|
+
".svg": "image/svg+xml",
|
|
1268
|
+
".png": "image/png",
|
|
1269
|
+
".jpg": "image/jpeg",
|
|
1270
|
+
".jpeg": "image/jpeg",
|
|
1271
|
+
".webp": "image/webp",
|
|
1272
|
+
".gif": "image/gif",
|
|
1273
|
+
".avif": "image/avif",
|
|
1274
|
+
".woff": "font/woff",
|
|
1275
|
+
".woff2": "font/woff2",
|
|
1276
|
+
".ico": "image/x-icon"
|
|
1277
|
+
};
|
|
1278
|
+
PING_ROUTE = "/__eds_ping";
|
|
1279
|
+
EVENTS_ROUTE = "/__eds_events";
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// src/commands/block/preview.ts
|
|
1284
|
+
var preview_exports = {};
|
|
1285
|
+
__export(preview_exports, {
|
|
1286
|
+
previewBlock: () => previewBlock
|
|
1287
|
+
});
|
|
1288
|
+
import { existsSync as existsSync7, watch } from "fs";
|
|
1289
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1290
|
+
import path9 from "path";
|
|
1291
|
+
import chalk5 from "chalk";
|
|
1292
|
+
function labelFor(width) {
|
|
1293
|
+
if (width <= 480) return `${width} mobile`;
|
|
1294
|
+
if (width <= 1024) return `${width} tablet`;
|
|
1295
|
+
return `${width} desktop`;
|
|
1296
|
+
}
|
|
1297
|
+
function resolveBreakpoints(widthsFlag, meta) {
|
|
1298
|
+
if (widthsFlag) {
|
|
1299
|
+
const widths = widthsFlag.split(",").map((w) => Number.parseInt(w.trim(), 10)).filter((w) => Number.isFinite(w) && w > 0);
|
|
1300
|
+
if (widths.length) return widths.map((w) => ({ width: w, label: labelFor(w), figma: false }));
|
|
1301
|
+
}
|
|
1302
|
+
const metaBps = meta?.breakpoints;
|
|
1303
|
+
if (Array.isArray(metaBps) && metaBps.length) {
|
|
1304
|
+
return metaBps.map((b) => ({
|
|
1305
|
+
width: Number(b.width),
|
|
1306
|
+
label: typeof b.label === "string" ? b.label : labelFor(Number(b.width)),
|
|
1307
|
+
figma: b.source === "figma"
|
|
1308
|
+
})).filter((b) => Number.isFinite(b.width) && b.width > 0);
|
|
1309
|
+
}
|
|
1310
|
+
return DEFAULT_WIDTHS.map((w) => ({ width: w, label: labelFor(w), figma: false }));
|
|
1311
|
+
}
|
|
1312
|
+
function figmaUrlFromMeta(meta) {
|
|
1313
|
+
if (!meta?.figmaFileKey || typeof meta.figmaFileKey !== "string") return void 0;
|
|
1314
|
+
const node = typeof meta.figmaNodeId === "string" ? meta.figmaNodeId.replace(":", "-") : void 0;
|
|
1315
|
+
const base = `https://www.figma.com/design/${meta.figmaFileKey}/preview`;
|
|
1316
|
+
return node ? `${base}?node-id=${node}` : base;
|
|
1317
|
+
}
|
|
1318
|
+
function defaultSample(blockName) {
|
|
1319
|
+
return `<div class="${blockName} block">
|
|
1320
|
+
<div><div>
|
|
1321
|
+
<h2>${blockName} heading</h2>
|
|
1322
|
+
<p>Sample body copy for the preview. Replace with real authored content.</p>
|
|
1323
|
+
<p><a href="#">Primary action</a></p>
|
|
1324
|
+
<p><a href="#">Secondary action</a></p>
|
|
1325
|
+
</div></div>
|
|
1326
|
+
</div>`;
|
|
1327
|
+
}
|
|
1328
|
+
async function openInBrowser(url) {
|
|
1329
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1330
|
+
try {
|
|
1331
|
+
const { execa } = await import("execa");
|
|
1332
|
+
await execa(cmd, [url], { stdio: "ignore", shell: process.platform === "win32" });
|
|
1333
|
+
} catch {
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async function previewBlock(name, options = {}) {
|
|
1337
|
+
const projectRoot = findProjectRoot();
|
|
1338
|
+
if (!projectRoot) {
|
|
1339
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
1340
|
+
process.exitCode = 1;
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const blockDir = path9.join(projectRoot, "blocks", name);
|
|
1344
|
+
if (!existsSync7(blockDir)) {
|
|
1345
|
+
logger.error(`Block "${name}" not found at blocks/${name}/.`);
|
|
1346
|
+
process.exitCode = 1;
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (!existsSync7(path9.join(blockDir, `${name}.css`)) && !existsSync7(path9.join(blockDir, `${name}.js`))) {
|
|
1350
|
+
logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
|
|
1351
|
+
process.exitCode = 1;
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
let meta = null;
|
|
1355
|
+
const metaPath = path9.join(blockDir, ".eds-meta.json");
|
|
1356
|
+
if (existsSync7(metaPath)) {
|
|
1357
|
+
try {
|
|
1358
|
+
meta = JSON.parse(await readFile5(metaPath, "utf-8"));
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const samplePath = path9.join(blockDir, `_${name}.preview.html`);
|
|
1363
|
+
let sampleContent;
|
|
1364
|
+
let placeholder = false;
|
|
1365
|
+
if (existsSync7(samplePath)) {
|
|
1366
|
+
sampleContent = await readFile5(samplePath, "utf-8");
|
|
1367
|
+
} else {
|
|
1368
|
+
sampleContent = defaultSample(name);
|
|
1369
|
+
placeholder = true;
|
|
1370
|
+
}
|
|
1371
|
+
const breakpoints = resolveBreakpoints(options.widths, meta);
|
|
1372
|
+
const figmaUrl = figmaUrlFromMeta(meta);
|
|
1373
|
+
const frameRoute = "/__eds_preview_frame";
|
|
1374
|
+
const inner = buildInner({ blockName: name, breakpoints, sampleContent, frameRoute });
|
|
1375
|
+
const outer = buildOuter({
|
|
1376
|
+
blockName: name,
|
|
1377
|
+
breakpoints,
|
|
1378
|
+
sampleContent,
|
|
1379
|
+
figmaUrl,
|
|
1380
|
+
frameRoute,
|
|
1381
|
+
placeholder
|
|
1382
|
+
});
|
|
1383
|
+
const port = Number.parseInt(options.port ?? "8777", 10);
|
|
1384
|
+
const server = await startServer(
|
|
1385
|
+
projectRoot,
|
|
1386
|
+
{
|
|
1387
|
+
"/": { body: outer, type: "text/html; charset=utf-8" },
|
|
1388
|
+
[frameRoute]: { body: inner, type: "text/html; charset=utf-8" }
|
|
1389
|
+
},
|
|
1390
|
+
Number.isFinite(port) ? port : 8777
|
|
1391
|
+
);
|
|
1392
|
+
logger.logoOnce(logo(`Preview \u2014 ${name}`));
|
|
1393
|
+
logger.info(heading("Live preview", server.url));
|
|
1394
|
+
logger.info(
|
|
1395
|
+
statusLine(
|
|
1396
|
+
"pass",
|
|
1397
|
+
"breakpoints",
|
|
1398
|
+
`${breakpoints.map((b) => `${b.width}${b.figma ? "\u2713" : ""}`).join(", ")}, Full`
|
|
1399
|
+
)
|
|
1400
|
+
);
|
|
1401
|
+
if (placeholder) {
|
|
1402
|
+
logger.info(
|
|
1403
|
+
statusLine(
|
|
1404
|
+
"warn",
|
|
1405
|
+
"content",
|
|
1406
|
+
`placeholder \u2014 add blocks/${name}/_${name}.preview.html for real content`
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
let reloadTimer;
|
|
1411
|
+
const watcher = watch(blockDir, { recursive: true }, () => {
|
|
1412
|
+
clearTimeout(reloadTimer);
|
|
1413
|
+
reloadTimer = setTimeout(() => server.reload(), 150);
|
|
1414
|
+
});
|
|
1415
|
+
logger.info("");
|
|
1416
|
+
logger.info(` ${chalk5.bold("\u279C")} ${chalk5.cyan(server.url)}`);
|
|
1417
|
+
logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
|
|
1418
|
+
logger.info(chalk5.dim(" Press Ctrl+C to stop.\n"));
|
|
1419
|
+
if (options.open !== false) await openInBrowser(server.url);
|
|
1420
|
+
const stop = async () => {
|
|
1421
|
+
watcher.close();
|
|
1422
|
+
await server.close();
|
|
1423
|
+
process.exit(0);
|
|
1424
|
+
};
|
|
1425
|
+
process.on("SIGINT", stop);
|
|
1426
|
+
process.on("SIGTERM", stop);
|
|
1427
|
+
}
|
|
1428
|
+
var DEFAULT_WIDTHS;
|
|
1429
|
+
var init_preview = __esm({
|
|
1430
|
+
"src/commands/block/preview.ts"() {
|
|
1431
|
+
"use strict";
|
|
1432
|
+
init_esm_shims();
|
|
1433
|
+
init_logger();
|
|
1434
|
+
init_harness();
|
|
1435
|
+
init_server();
|
|
1436
|
+
init_project();
|
|
1437
|
+
init_ui();
|
|
1438
|
+
DEFAULT_WIDTHS = [375, 768, 1280];
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// src/commands/block/from-design.ts
|
|
1443
|
+
var from_design_exports = {};
|
|
1444
|
+
__export(from_design_exports, {
|
|
1445
|
+
blockFromDesign: () => blockFromDesign
|
|
1446
|
+
});
|
|
1447
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1448
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
1449
|
+
import path10 from "path";
|
|
1450
|
+
import chalk6 from "chalk";
|
|
1451
|
+
import ora from "ora";
|
|
1452
|
+
async function previewRunning(port = 8777) {
|
|
1453
|
+
try {
|
|
1454
|
+
const ctrl = new AbortController();
|
|
1455
|
+
const t = setTimeout(() => ctrl.abort(), 400);
|
|
1456
|
+
const res = await fetch(`http://127.0.0.1:${port}/__eds_ping`, { signal: ctrl.signal });
|
|
1457
|
+
clearTimeout(t);
|
|
1458
|
+
if (!res.ok) return false;
|
|
1459
|
+
const data = await res.json();
|
|
1460
|
+
return data?.eds === true;
|
|
1461
|
+
} catch {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
async function runWizard(figmaUrl) {
|
|
1466
|
+
const { input, select, confirm } = await import("@inquirer/prompts");
|
|
1467
|
+
const provider = await select({
|
|
1468
|
+
message: "1/7 Design source",
|
|
1469
|
+
choices: [
|
|
1470
|
+
{ name: "Figma", value: "figma" },
|
|
1471
|
+
{ name: "Google Stitch", value: "stitch" },
|
|
1472
|
+
{ name: "Canva", value: "canva" },
|
|
1473
|
+
{ name: "Sketch", value: "sketch" }
|
|
1474
|
+
],
|
|
1475
|
+
default: "figma"
|
|
1476
|
+
});
|
|
1477
|
+
const name = await input({
|
|
1478
|
+
message: "2/7 Component name",
|
|
1479
|
+
validate: (v) => kebabCaseRegex.test(v.trim()) || "Use kebab-case, e.g. hero-banner"
|
|
1480
|
+
});
|
|
1481
|
+
const url = figmaUrl ?? await input({
|
|
1482
|
+
message: `3/7 ${PROVIDER_NAMES[provider]} link`,
|
|
1483
|
+
validate: (v) => {
|
|
1484
|
+
if (!v.trim()) return "Paste the design link";
|
|
1485
|
+
if (provider !== "figma") return true;
|
|
1486
|
+
try {
|
|
1487
|
+
parseFigmaUrl(v.trim());
|
|
1488
|
+
return true;
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
return err.message;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
const source = await select({
|
|
1495
|
+
message: "4/7 Where does the content come from?",
|
|
1496
|
+
choices: [
|
|
1497
|
+
{ name: "Document authoring (default EDS)", value: "document" },
|
|
1498
|
+
{ name: "Universal Editor", value: "ue" },
|
|
1499
|
+
{ name: "Content Fragment", value: "cf" },
|
|
1500
|
+
{ name: "Mixed - Universal Editor + Content Fragment", value: "mixed" }
|
|
1501
|
+
],
|
|
1502
|
+
default: "document"
|
|
1503
|
+
});
|
|
1504
|
+
let cfHint;
|
|
1505
|
+
if (source === "cf" || source === "mixed") {
|
|
1506
|
+
const hint = await input({
|
|
1507
|
+
message: " CF model / GraphQL persisted-query name (optional)"
|
|
1508
|
+
});
|
|
1509
|
+
cfHint = hint.trim() || void 0;
|
|
1510
|
+
}
|
|
1511
|
+
const ueModel = await confirm({
|
|
1512
|
+
message: "5/7 Generate the Universal Editor model?",
|
|
1513
|
+
default: true
|
|
1514
|
+
});
|
|
1515
|
+
const screenshot = await confirm({
|
|
1516
|
+
message: "6/7 Fetch a screenshot for a pixel-perfect check? (off = fewer tokens)",
|
|
1517
|
+
default: true
|
|
1518
|
+
});
|
|
1519
|
+
const agents = await detectAllAgents();
|
|
1520
|
+
const runMode = await select({
|
|
1521
|
+
message: "7/7 Run with",
|
|
1522
|
+
choices: [
|
|
1523
|
+
...agents.map((a) => ({
|
|
1524
|
+
name: `${a.type}${provider === "figma" && !a.hasFigmaMcp ? " (Figma MCP not detected)" : ""}`,
|
|
1525
|
+
value: `agent:${a.type}`
|
|
1526
|
+
})),
|
|
1527
|
+
{ name: "Preview the prompt only (--dry-run)", value: "dry-run" },
|
|
1528
|
+
{ name: "Save the prompt, run it later (--agent none)", value: "none" }
|
|
1529
|
+
],
|
|
1530
|
+
default: agents[0] ? `agent:${agents[0].type}` : "dry-run"
|
|
1531
|
+
});
|
|
1532
|
+
const sourceLabel = PROVIDER_NAMES[provider].toLowerCase();
|
|
1533
|
+
logger.info(heading("Summary"));
|
|
1534
|
+
const w = columnWidth(["component", sourceLabel, "content", "ue model", "screenshot", "run"]);
|
|
1535
|
+
logger.info(accentLine("component", name, w));
|
|
1536
|
+
const designRef = provider === "figma" ? (() => {
|
|
1537
|
+
const parsed = parseFigmaUrl(url.trim());
|
|
1538
|
+
return parsed.nodeId ? `node ${parsed.nodeId}` : parsed.fileKey;
|
|
1539
|
+
})() : url.trim();
|
|
1540
|
+
logger.info(accentLine(sourceLabel, designRef, w));
|
|
1541
|
+
logger.info(accentLine("content", source + (cfHint ? ` (${cfHint})` : ""), w));
|
|
1542
|
+
logger.info(accentLine("ue model", ueModel ? "yes" : "no", w));
|
|
1543
|
+
logger.info(accentLine("screenshot", screenshot ? "yes" : "no (token-lean)", w));
|
|
1544
|
+
logger.info(accentLine("run", runMode.replace("agent:", ""), w));
|
|
1545
|
+
logger.info("");
|
|
1546
|
+
const go = await confirm({ message: "Generate this block?", default: true });
|
|
1547
|
+
if (!go) return null;
|
|
1548
|
+
const opts = {
|
|
1549
|
+
figmaUrl: url.trim(),
|
|
1550
|
+
name,
|
|
1551
|
+
provider,
|
|
1552
|
+
source,
|
|
1553
|
+
cfHint,
|
|
1554
|
+
ueModel,
|
|
1555
|
+
screenshot
|
|
1556
|
+
};
|
|
1557
|
+
if (runMode === "dry-run") opts.dryRun = true;
|
|
1558
|
+
else if (runMode === "none") opts.agent = "none";
|
|
1559
|
+
else {
|
|
1560
|
+
opts.agent = runMode.replace("agent:", "");
|
|
1561
|
+
opts.yes = true;
|
|
1562
|
+
}
|
|
1563
|
+
return opts;
|
|
1564
|
+
}
|
|
1565
|
+
function inferBlockName(fileName) {
|
|
1566
|
+
if (!fileName) return "design-block";
|
|
1567
|
+
return fileName.replace(/[-_\s]+/g, "-").replace(/[^a-z0-9-]/gi, "").toLowerCase().replace(/^-+|-+$/g, "") || "design-block";
|
|
1568
|
+
}
|
|
1569
|
+
async function blockFromDesign(figmaUrlArg, optionsArg) {
|
|
1570
|
+
const projectRoot = findProjectRoot();
|
|
1571
|
+
if (!projectRoot) {
|
|
1572
|
+
logger.error(
|
|
1573
|
+
"Not inside an EDS project (no fstab.yaml found). Run this from your project root."
|
|
1574
|
+
);
|
|
1575
|
+
process.exitCode = 1;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
logger.logoOnce(logo("Design \u2192 EDS block"));
|
|
1579
|
+
let designUrl = figmaUrlArg;
|
|
1580
|
+
let options = optionsArg;
|
|
1581
|
+
if (!designUrl) {
|
|
1582
|
+
const wiz = await runWizard(designUrl);
|
|
1583
|
+
if (!wiz) {
|
|
1584
|
+
logger.info("Cancelled.");
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
designUrl = wiz.figmaUrl;
|
|
1588
|
+
options = { ...options, ...wiz };
|
|
1589
|
+
}
|
|
1590
|
+
const provider = options.provider ?? "figma";
|
|
1591
|
+
const providerName = PROVIDER_NAMES[provider];
|
|
1592
|
+
let figma2 = null;
|
|
1593
|
+
const designRef = designUrl;
|
|
1594
|
+
if (provider === "figma") {
|
|
1595
|
+
const spinner = ora("Parsing Figma URL...").start();
|
|
1596
|
+
try {
|
|
1597
|
+
figma2 = parseFigmaUrl(designUrl);
|
|
1598
|
+
spinner.succeed(
|
|
1599
|
+
`Parsed Figma URL ${chalk6.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
|
|
1600
|
+
);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
spinner.fail(err.message);
|
|
1603
|
+
process.exitCode = 1;
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
} else {
|
|
1607
|
+
logger.info(` ${icon.pass} ${providerName} design ${chalk6.dim(designRef)}`);
|
|
1608
|
+
}
|
|
1609
|
+
const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
|
|
1610
|
+
if (!kebabCaseRegex.test(blockName)) {
|
|
1611
|
+
logger.error(
|
|
1612
|
+
`Invalid block name "${blockName}". Use --name with a kebab-case name (e.g., --name hero-banner).`
|
|
1613
|
+
);
|
|
1614
|
+
process.exitCode = 1;
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const promptSpinner = ora("Reading design tokens & building prompt...").start();
|
|
1618
|
+
const prompt = await buildPrompt({
|
|
1619
|
+
provider,
|
|
1620
|
+
figma: figma2,
|
|
1621
|
+
designRef,
|
|
1622
|
+
blockName,
|
|
1623
|
+
projectRoot,
|
|
1624
|
+
withUeModel: options.ueModel !== false,
|
|
1625
|
+
contentSource: options.source ?? "document",
|
|
1626
|
+
cfHint: options.cfHint,
|
|
1627
|
+
screenshot: options.screenshot !== false
|
|
1628
|
+
});
|
|
1629
|
+
const promptFile = await savePromptToFile(prompt, projectRoot);
|
|
1630
|
+
const hasTokens = existsSync8(path10.join(projectRoot, "styles", "styles.css"));
|
|
1631
|
+
promptSpinner.succeed(
|
|
1632
|
+
hasTokens ? `Loaded design tokens ${chalk6.dim("styles/styles.css")}` : `Built prompt ${chalk6.dim("no project tokens found")}`
|
|
1633
|
+
);
|
|
1634
|
+
logger.info(chalk6.dim(` prompt \u2192 ${promptFile}`));
|
|
1635
|
+
if (options.dryRun) {
|
|
1636
|
+
logger.info("");
|
|
1637
|
+
logger.info(chalk6.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
|
|
1638
|
+
logger.info(`Prompt file: ${chalk6.cyan(promptFile)}`);
|
|
1639
|
+
logger.info(`Block name: ${chalk6.cyan(blockName)}`);
|
|
1640
|
+
logger.info(`Prompt version: ${PROMPT_VERSION}`);
|
|
1641
|
+
logger.info("");
|
|
1642
|
+
logger.info("Preview:");
|
|
1643
|
+
logger.info(chalk6.dim("\u2500".repeat(60)));
|
|
1644
|
+
console.log(prompt);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const config = await loadConfig(projectRoot);
|
|
1648
|
+
const preferredAgent = options.agent || config.agent;
|
|
1649
|
+
if (options.agent === "none") {
|
|
1650
|
+
logger.info("Agent skipped (--agent none). Use the prompt file manually:");
|
|
1651
|
+
logger.info(` ${chalk6.cyan(promptFile)}`);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const agentSpinner = ora("Detecting AI agent...").start();
|
|
1655
|
+
const agent = await detectAgent(preferredAgent);
|
|
1656
|
+
if (!agent) {
|
|
1657
|
+
agentSpinner.fail("No AI agent found (claude, cursor, or codex).");
|
|
1658
|
+
logger.info(`Install Claude Code: ${chalk6.cyan("npm install -g @anthropic-ai/claude-code")}`);
|
|
1659
|
+
logger.info(`Or use the prompt file manually: ${chalk6.cyan(promptFile)}`);
|
|
1660
|
+
process.exitCode = 1;
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
agentSpinner.succeed(`Agent ready ${chalk6.cyan(agent.type)} ${chalk6.dim(agent.path)}`);
|
|
1664
|
+
if (provider === "figma" && !agent.hasFigmaMcp) {
|
|
1665
|
+
logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
|
|
1666
|
+
logger.info(`Run ${chalk6.cyan("eds figma setup")} to configure it.`);
|
|
1667
|
+
}
|
|
1668
|
+
if (!options.yes) {
|
|
1669
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
1670
|
+
const proceed = await confirm({
|
|
1671
|
+
message: `Run ${agent.type} with the generated prompt?`,
|
|
1672
|
+
default: true
|
|
1673
|
+
});
|
|
1674
|
+
if (!proceed) {
|
|
1675
|
+
logger.info(`Prompt saved to: ${chalk6.cyan(promptFile)}`);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
logger.info(
|
|
1680
|
+
` ${chalk6.dim("\u2192")} Handing off to ${chalk6.cyan(agent.type)} ${chalk6.dim(`via ${providerName} MCP`)}`
|
|
1681
|
+
);
|
|
1682
|
+
if (provider === "figma") {
|
|
1683
|
+
logger.info(chalk6.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
|
|
1684
|
+
}
|
|
1685
|
+
const success = await invokeAgent(agent, promptFile, projectRoot);
|
|
1686
|
+
if (!success) {
|
|
1687
|
+
process.exitCode = 1;
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const blockDir = path10.join(projectRoot, "blocks", blockName);
|
|
1691
|
+
const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
|
|
1692
|
+
if (existsSync8(blockDir)) {
|
|
1693
|
+
const meta = {
|
|
1694
|
+
provider,
|
|
1695
|
+
designRef,
|
|
1696
|
+
figmaFileKey: figma2?.fileKey ?? null,
|
|
1697
|
+
figmaNodeId: figma2?.nodeId ?? null,
|
|
1698
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1699
|
+
promptVersion: PROMPT_VERSION,
|
|
1700
|
+
agentUsed: agent.type
|
|
1701
|
+
};
|
|
1702
|
+
await writeFile3(path10.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
|
|
1703
|
+
`);
|
|
1704
|
+
}
|
|
1705
|
+
if (!ok) {
|
|
1706
|
+
logger.warn(
|
|
1707
|
+
`Block "${blockName}" generated with issues \u2014 review the output above before publishing.`
|
|
1708
|
+
);
|
|
1709
|
+
process.exitCode = 1;
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
logger.success(`Block "${blockName}" generation complete.`);
|
|
1713
|
+
if (options.serve !== false) {
|
|
1714
|
+
if (await previewRunning()) {
|
|
1715
|
+
logger.info(
|
|
1716
|
+
chalk6.dim(
|
|
1717
|
+
" A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
|
|
1718
|
+
)
|
|
1719
|
+
);
|
|
1720
|
+
} else {
|
|
1721
|
+
logger.info("");
|
|
1722
|
+
const { previewBlock: previewBlock2 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
|
|
1723
|
+
await previewBlock2(blockName, {});
|
|
1724
|
+
}
|
|
1725
|
+
} else {
|
|
1726
|
+
logger.info(` ${chalk6.dim("\u2192")} Next: ${chalk6.cyan(`eds block preview ${blockName}`)}`);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
|
|
1730
|
+
const checks = [];
|
|
1731
|
+
const jsPath = path10.join(blockDir, `${blockName}.js`);
|
|
1732
|
+
const cssPath = path10.join(blockDir, `${blockName}.css`);
|
|
1733
|
+
const hasJs = existsSync8(jsPath);
|
|
1734
|
+
const hasCss = existsSync8(cssPath);
|
|
1735
|
+
checks.push({
|
|
1736
|
+
status: hasJs ? "pass" : "fail",
|
|
1737
|
+
name: `${blockName}.js`,
|
|
1738
|
+
message: hasJs ? "created" : "missing \u2014 agent did not write the JS file"
|
|
1739
|
+
});
|
|
1740
|
+
checks.push({
|
|
1741
|
+
status: hasCss ? "pass" : "fail",
|
|
1742
|
+
name: `${blockName}.css`,
|
|
1743
|
+
message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
|
|
1744
|
+
});
|
|
1745
|
+
if (hasJs) {
|
|
1746
|
+
const js = await readFile6(jsPath, "utf-8");
|
|
1747
|
+
checks.push({
|
|
1748
|
+
status: /export\s+default/.test(js) ? "pass" : "fail",
|
|
1749
|
+
name: "decorate export",
|
|
1750
|
+
message: /export\s+default/.test(js) ? "found" : "no default export \u2014 block won't load"
|
|
1751
|
+
});
|
|
1752
|
+
if (js.includes("console.log")) {
|
|
1753
|
+
checks.push({
|
|
1754
|
+
status: "warn",
|
|
1755
|
+
name: "no console.log",
|
|
1756
|
+
message: "found console.log \u2014 remove before production"
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (hasCss) {
|
|
1761
|
+
const css = await readFile6(cssPath, "utf-8");
|
|
1762
|
+
checks.push({
|
|
1763
|
+
status: css.includes(`.${blockName}`) ? "pass" : "warn",
|
|
1764
|
+
name: "scoped CSS",
|
|
1765
|
+
message: css.includes(`.${blockName}`) ? `scoped to .${blockName}` : `no .${blockName} selector \u2014 styles may leak`
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
if (withUeModel) {
|
|
1769
|
+
const hasPerBlock = existsSync8(path10.join(blockDir, `_${blockName}.json`));
|
|
1770
|
+
const modelsFile = path10.join(blockDir, "..", "..", "component-models.json");
|
|
1771
|
+
let hasAggregated = false;
|
|
1772
|
+
if (!hasPerBlock && existsSync8(modelsFile)) {
|
|
1773
|
+
try {
|
|
1774
|
+
hasAggregated = (await readFile6(modelsFile, "utf-8")).includes(`"${blockName}"`);
|
|
1775
|
+
} catch {
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const hasModel = hasPerBlock || hasAggregated;
|
|
1779
|
+
checks.push({
|
|
1780
|
+
status: hasModel ? "pass" : "warn",
|
|
1781
|
+
name: "Universal Editor model",
|
|
1782
|
+
message: hasModel ? hasPerBlock ? `_${blockName}.json created` : "added to component-models.json" : `missing \u2014 no _${blockName}.json (run with default UE generation, or --no-ue-model to skip)`
|
|
1783
|
+
});
|
|
1784
|
+
if (hasPerBlock && hasJs) {
|
|
1785
|
+
try {
|
|
1786
|
+
const modelRaw = await readFile6(path10.join(blockDir, `_${blockName}.json`), "utf-8");
|
|
1787
|
+
const js = await readFile6(jsPath, "utf-8");
|
|
1788
|
+
const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
|
|
1789
|
+
const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
|
|
1790
|
+
if (isRepeatable) {
|
|
1791
|
+
checks.push({
|
|
1792
|
+
status: iterates ? "pass" : "warn",
|
|
1793
|
+
name: "multifield wired",
|
|
1794
|
+
message: iterates ? "decorate() iterates items" : "repeatable model but decorate() never iterates block.children \u2014 only one item will render"
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
} catch {
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
logger.info(heading("Verification"));
|
|
1802
|
+
const nameWidth = columnWidth(checks.map((c) => c.name));
|
|
1803
|
+
for (const c of checks) {
|
|
1804
|
+
logger.info(statusLine(c.status, c.name, c.message, nameWidth));
|
|
1805
|
+
}
|
|
1806
|
+
logger.info("");
|
|
1807
|
+
return !checks.some((c) => c.status === "fail");
|
|
1808
|
+
}
|
|
1809
|
+
var init_from_design = __esm({
|
|
1810
|
+
"src/commands/block/from-design.ts"() {
|
|
1811
|
+
"use strict";
|
|
1812
|
+
init_esm_shims();
|
|
1813
|
+
init_detect();
|
|
1814
|
+
init_invoke();
|
|
1815
|
+
init_config();
|
|
1816
|
+
init_node_id();
|
|
1817
|
+
init_prompt_builder();
|
|
1818
|
+
init_logger();
|
|
1819
|
+
init_project();
|
|
1820
|
+
init_schemas();
|
|
1821
|
+
init_ui();
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
// src/commands/block/list.ts
|
|
1826
|
+
var list_exports = {};
|
|
1827
|
+
__export(list_exports, {
|
|
1828
|
+
listBlocks: () => listBlocks
|
|
1829
|
+
});
|
|
1830
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1831
|
+
import { readFile as readFile7, readdir } from "fs/promises";
|
|
1832
|
+
import path11 from "path";
|
|
1833
|
+
import chalk7 from "chalk";
|
|
1834
|
+
async function listBlocks(options = {}) {
|
|
1835
|
+
const projectRoot = findProjectRoot();
|
|
1836
|
+
if (!projectRoot) {
|
|
1837
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
1838
|
+
process.exitCode = 1;
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const blocksDir = path11.join(projectRoot, "blocks");
|
|
1842
|
+
if (!existsSync9(blocksDir)) {
|
|
1843
|
+
logger.info("No blocks/ directory found.");
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
const entries = await readdir(blocksDir, { withFileTypes: true });
|
|
1847
|
+
const blocks = [];
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
if (!entry.isDirectory()) continue;
|
|
1850
|
+
const name = entry.name;
|
|
1851
|
+
const blockDir = path11.join(blocksDir, name);
|
|
1852
|
+
const info = {
|
|
1853
|
+
name,
|
|
1854
|
+
hasJs: existsSync9(path11.join(blockDir, `${name}.js`)),
|
|
1855
|
+
hasCss: existsSync9(path11.join(blockDir, `${name}.css`)),
|
|
1856
|
+
hasUeModel: existsSync9(path11.join(blockDir, `_${name}.json`))
|
|
1857
|
+
};
|
|
1858
|
+
const metaFile = path11.join(blockDir, ".eds-meta.json");
|
|
1859
|
+
if (existsSync9(metaFile)) {
|
|
1860
|
+
try {
|
|
1861
|
+
const meta = JSON.parse(await readFile7(metaFile, "utf-8"));
|
|
1862
|
+
info.figmaSource = {
|
|
1863
|
+
fileKey: meta.figmaFileKey,
|
|
1864
|
+
nodeId: meta.figmaNodeId,
|
|
1865
|
+
lastSyncedAt: meta.lastSyncedAt
|
|
1866
|
+
};
|
|
1867
|
+
} catch {
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
blocks.push(info);
|
|
1871
|
+
}
|
|
1872
|
+
if (options.json) {
|
|
1873
|
+
console.log(JSON.stringify(blocks, null, 2));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (blocks.length === 0) {
|
|
1877
|
+
logger.info("No blocks found.");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
logger.info(heading("Blocks", `${blocks.length} in project`));
|
|
1881
|
+
const nameWidth = columnWidth(blocks.map((b) => b.name));
|
|
1882
|
+
for (const b of blocks) {
|
|
1883
|
+
const badges = [
|
|
1884
|
+
b.hasJs ? brand.ok("js") : chalk7.dim.strikethrough("js"),
|
|
1885
|
+
b.hasCss ? brand.ok("css") : chalk7.dim.strikethrough("css")
|
|
1886
|
+
];
|
|
1887
|
+
if (b.hasUeModel) badges.push(brand.blue("ue-model"));
|
|
1888
|
+
if (b.figmaSource) badges.push(`${icon.figma} ${brand.dotPurple("figma")}`);
|
|
1889
|
+
const name = padEndVisible(chalk7.bold(b.name), nameWidth);
|
|
1890
|
+
logger.info(` ${name} ${chalk7.dim("[")}${badges.join(" ")}${chalk7.dim("]")}`);
|
|
1891
|
+
if (b.figmaSource) {
|
|
1892
|
+
const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
|
|
1893
|
+
logger.info(
|
|
1894
|
+
` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk7.dim(
|
|
1895
|
+
`figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
|
|
1896
|
+
)}`
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
logger.info("");
|
|
1901
|
+
}
|
|
1902
|
+
var init_list = __esm({
|
|
1903
|
+
"src/commands/block/list.ts"() {
|
|
1904
|
+
"use strict";
|
|
1905
|
+
init_esm_shims();
|
|
1906
|
+
init_logger();
|
|
1907
|
+
init_project();
|
|
1908
|
+
init_ui();
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
// src/lib/scaffold/blocks.ts
|
|
1913
|
+
function json(value) {
|
|
1914
|
+
return `${JSON.stringify(value, null, 2)}
|
|
1915
|
+
`;
|
|
1916
|
+
}
|
|
1917
|
+
function blockById(id) {
|
|
1918
|
+
return STANDARD_BLOCKS.find((b) => b.id === id);
|
|
1919
|
+
}
|
|
1920
|
+
var blockDef, containerDef, itemDef, STANDARD_BLOCKS;
|
|
1921
|
+
var init_blocks = __esm({
|
|
1922
|
+
"src/lib/scaffold/blocks.ts"() {
|
|
1923
|
+
"use strict";
|
|
1924
|
+
init_esm_shims();
|
|
1925
|
+
blockDef = (title, id, model) => ({
|
|
1926
|
+
title,
|
|
1927
|
+
id,
|
|
1928
|
+
plugins: {
|
|
1929
|
+
xwalk: {
|
|
1930
|
+
page: {
|
|
1931
|
+
resourceType: "core/franklin/components/block/v1/block",
|
|
1932
|
+
template: { name: title, model }
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
containerDef = (title, id, filter) => ({
|
|
1938
|
+
title,
|
|
1939
|
+
id,
|
|
1940
|
+
plugins: {
|
|
1941
|
+
xwalk: {
|
|
1942
|
+
page: {
|
|
1943
|
+
resourceType: "core/franklin/components/block/v1/block",
|
|
1944
|
+
template: { name: title, filter }
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
itemDef = (title, id, model) => ({
|
|
1950
|
+
title,
|
|
1951
|
+
id,
|
|
1952
|
+
plugins: {
|
|
1953
|
+
xwalk: {
|
|
1954
|
+
page: {
|
|
1955
|
+
resourceType: "core/franklin/components/block/v1/block/item",
|
|
1956
|
+
template: { name: title, model }
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
STANDARD_BLOCKS = [
|
|
1962
|
+
// ── hero ──────────────────────────────────────────────
|
|
1963
|
+
{
|
|
1964
|
+
id: "hero",
|
|
1965
|
+
title: "Hero",
|
|
1966
|
+
js: `// Block: hero
|
|
1967
|
+
export default function decorate(block) {
|
|
1968
|
+
block.classList.add('hero');
|
|
1969
|
+
const picture = block.querySelector('picture');
|
|
1970
|
+
if (picture) picture.closest('div')?.classList.add('hero-image');
|
|
1971
|
+
}
|
|
1972
|
+
`,
|
|
1973
|
+
css: `/* Block: hero */
|
|
1974
|
+
.hero {
|
|
1975
|
+
position: relative;
|
|
1976
|
+
display: grid;
|
|
1977
|
+
min-height: 60vh;
|
|
1978
|
+
align-content: center;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.hero .hero-image img {
|
|
1982
|
+
position: absolute;
|
|
1983
|
+
inset: 0;
|
|
1984
|
+
width: 100%;
|
|
1985
|
+
height: 100%;
|
|
1986
|
+
object-fit: cover;
|
|
1987
|
+
z-index: -1;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
.hero h1,
|
|
1991
|
+
.hero p {
|
|
1992
|
+
max-width: 720px;
|
|
1993
|
+
margin: 0 auto;
|
|
1994
|
+
padding: 0 var(--spacing-m, 16px);
|
|
1995
|
+
}
|
|
1996
|
+
`,
|
|
1997
|
+
model: json({
|
|
1998
|
+
definitions: [blockDef("Hero", "hero", "hero")],
|
|
1999
|
+
models: [
|
|
2000
|
+
{
|
|
2001
|
+
id: "hero",
|
|
2002
|
+
fields: [
|
|
2003
|
+
{ component: "reference", valueType: "string", name: "image", label: "Image" },
|
|
2004
|
+
{ component: "text", valueType: "string", name: "imageAlt", label: "Alt" },
|
|
2005
|
+
{ component: "text", valueType: "string", name: "title", label: "Title" },
|
|
2006
|
+
{ component: "richtext", valueType: "string", name: "text", value: "", label: "Text" }
|
|
2007
|
+
]
|
|
2008
|
+
}
|
|
2009
|
+
],
|
|
2010
|
+
filters: []
|
|
2011
|
+
})
|
|
2012
|
+
},
|
|
2013
|
+
// ── cards (container / multifield) ────────────────────
|
|
2014
|
+
{
|
|
2015
|
+
id: "cards",
|
|
2016
|
+
title: "Cards",
|
|
2017
|
+
js: `// Block: cards
|
|
2018
|
+
export default function decorate(block) {
|
|
2019
|
+
const ul = document.createElement('ul');
|
|
2020
|
+
[...block.children].forEach((row) => {
|
|
2021
|
+
const li = document.createElement('li');
|
|
2022
|
+
while (row.firstElementChild) li.append(row.firstElementChild);
|
|
2023
|
+
[...li.children].forEach((cell) => {
|
|
2024
|
+
cell.className = cell.querySelector('picture') ? 'cards-card-image' : 'cards-card-body';
|
|
2025
|
+
});
|
|
2026
|
+
ul.append(li);
|
|
2027
|
+
});
|
|
2028
|
+
block.textContent = '';
|
|
2029
|
+
block.append(ul);
|
|
2030
|
+
}
|
|
2031
|
+
`,
|
|
2032
|
+
css: `/* Block: cards */
|
|
2033
|
+
.cards ul {
|
|
2034
|
+
display: grid;
|
|
2035
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
|
|
2036
|
+
gap: var(--spacing-m, 16px);
|
|
2037
|
+
margin: 0;
|
|
2038
|
+
padding: 0;
|
|
2039
|
+
list-style: none;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
.cards li {
|
|
2043
|
+
border: 1px solid var(--color-line, #e5e5e5);
|
|
2044
|
+
border-radius: 12px;
|
|
2045
|
+
overflow: hidden;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
.cards .cards-card-image img {
|
|
2049
|
+
width: 100%;
|
|
2050
|
+
aspect-ratio: 16 / 9;
|
|
2051
|
+
object-fit: cover;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
.cards .cards-card-body {
|
|
2055
|
+
padding: var(--spacing-m, 16px);
|
|
2056
|
+
}
|
|
2057
|
+
`,
|
|
2058
|
+
model: json({
|
|
2059
|
+
definitions: [containerDef("Cards", "cards", "cards"), itemDef("Card", "card", "card")],
|
|
2060
|
+
models: [
|
|
2061
|
+
{
|
|
2062
|
+
id: "card",
|
|
2063
|
+
fields: [
|
|
2064
|
+
{ component: "reference", valueType: "string", name: "image", label: "Image" },
|
|
2065
|
+
{ component: "text", valueType: "string", name: "imageAlt", label: "Alt" },
|
|
2066
|
+
{ component: "text", valueType: "string", name: "title", label: "Title" },
|
|
2067
|
+
{ component: "richtext", valueType: "string", name: "text", value: "", label: "Text" },
|
|
2068
|
+
{ component: "aem-content", name: "link", label: "Link" }
|
|
2069
|
+
]
|
|
2070
|
+
}
|
|
2071
|
+
],
|
|
2072
|
+
filters: [{ id: "cards", components: ["card"] }]
|
|
2073
|
+
})
|
|
2074
|
+
},
|
|
2075
|
+
// ── columns ───────────────────────────────────────────
|
|
2076
|
+
{
|
|
2077
|
+
id: "columns",
|
|
2078
|
+
title: "Columns",
|
|
2079
|
+
js: `// Block: columns
|
|
2080
|
+
export default function decorate(block) {
|
|
2081
|
+
const cols = [...block.firstElementChild.children];
|
|
2082
|
+
block.classList.add(\`columns-\${cols.length}-cols\`);
|
|
2083
|
+
[...block.children].forEach((row) => {
|
|
2084
|
+
[...row.children].forEach((col) => {
|
|
2085
|
+
const pic = col.querySelector('picture');
|
|
2086
|
+
if (pic && pic.closest('div') === col) col.classList.add('columns-img-col');
|
|
2087
|
+
});
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
`,
|
|
2091
|
+
css: `/* Block: columns */
|
|
2092
|
+
.columns > div {
|
|
2093
|
+
display: grid;
|
|
2094
|
+
gap: var(--spacing-l, 32px);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
@media (width >= 900px) {
|
|
2098
|
+
.columns > div {
|
|
2099
|
+
grid-template-columns: repeat(var(--columns, 2), 1fr);
|
|
2100
|
+
}
|
|
2101
|
+
.columns-2-cols > div { --columns: 2; }
|
|
2102
|
+
.columns-3-cols > div { --columns: 3; }
|
|
2103
|
+
.columns-4-cols > div { --columns: 4; }
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
.columns img { width: 100%; height: auto; }
|
|
2107
|
+
`,
|
|
2108
|
+
model: json({
|
|
2109
|
+
definitions: [blockDef("Columns", "columns", "columns")],
|
|
2110
|
+
models: [
|
|
2111
|
+
{
|
|
2112
|
+
id: "columns",
|
|
2113
|
+
fields: [
|
|
2114
|
+
{
|
|
2115
|
+
component: "richtext",
|
|
2116
|
+
valueType: "string",
|
|
2117
|
+
name: "content",
|
|
2118
|
+
value: "",
|
|
2119
|
+
label: "Content"
|
|
2120
|
+
}
|
|
2121
|
+
]
|
|
2122
|
+
}
|
|
2123
|
+
],
|
|
2124
|
+
filters: []
|
|
2125
|
+
})
|
|
2126
|
+
},
|
|
2127
|
+
// ── accordion (container / <details>) ─────────────────
|
|
2128
|
+
{
|
|
2129
|
+
id: "accordion",
|
|
2130
|
+
title: "Accordion",
|
|
2131
|
+
js: `// Block: accordion
|
|
2132
|
+
export default function decorate(block) {
|
|
2133
|
+
[...block.children].forEach((row) => {
|
|
2134
|
+
const [label, body] = row.children;
|
|
2135
|
+
const details = document.createElement('details');
|
|
2136
|
+
const summary = document.createElement('summary');
|
|
2137
|
+
summary.className = 'accordion-item-label';
|
|
2138
|
+
if (label) summary.append(...label.childNodes);
|
|
2139
|
+
const content = document.createElement('div');
|
|
2140
|
+
content.className = 'accordion-item-body';
|
|
2141
|
+
if (body) content.append(...body.childNodes);
|
|
2142
|
+
details.append(summary, content);
|
|
2143
|
+
row.replaceWith(details);
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
`,
|
|
2147
|
+
css: `/* Block: accordion */
|
|
2148
|
+
.accordion details {
|
|
2149
|
+
border-bottom: 1px solid var(--color-line, #e5e5e5);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.accordion summary {
|
|
2153
|
+
cursor: pointer;
|
|
2154
|
+
padding: var(--spacing-m, 16px) 0;
|
|
2155
|
+
font-weight: 600;
|
|
2156
|
+
list-style: none;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
.accordion summary::after { content: '+'; float: right; }
|
|
2160
|
+
.accordion details[open] summary::after { content: '\u2212'; }
|
|
2161
|
+
|
|
2162
|
+
.accordion .accordion-item-body {
|
|
2163
|
+
padding-bottom: var(--spacing-m, 16px);
|
|
2164
|
+
}
|
|
2165
|
+
`,
|
|
2166
|
+
model: json({
|
|
2167
|
+
definitions: [
|
|
2168
|
+
containerDef("Accordion", "accordion", "accordion"),
|
|
2169
|
+
itemDef("Accordion Item", "accordion-item", "accordion-item")
|
|
2170
|
+
],
|
|
2171
|
+
models: [
|
|
2172
|
+
{
|
|
2173
|
+
id: "accordion-item",
|
|
2174
|
+
fields: [
|
|
2175
|
+
{ component: "text", valueType: "string", name: "label", label: "Label" },
|
|
2176
|
+
{ component: "richtext", valueType: "string", name: "body", value: "", label: "Body" }
|
|
2177
|
+
]
|
|
2178
|
+
}
|
|
2179
|
+
],
|
|
2180
|
+
filters: [{ id: "accordion", components: ["accordion-item"] }]
|
|
2181
|
+
})
|
|
2182
|
+
},
|
|
2183
|
+
// ── embed ─────────────────────────────────────────────
|
|
2184
|
+
{
|
|
2185
|
+
id: "embed",
|
|
2186
|
+
title: "Embed",
|
|
2187
|
+
js: `// Block: embed
|
|
2188
|
+
export default function decorate(block) {
|
|
2189
|
+
const link = block.querySelector('a');
|
|
2190
|
+
const url = link?.href;
|
|
2191
|
+
block.textContent = '';
|
|
2192
|
+
if (!url) return;
|
|
2193
|
+
const wrapper = document.createElement('div');
|
|
2194
|
+
wrapper.className = 'embed-wrapper';
|
|
2195
|
+
const iframe = document.createElement('iframe');
|
|
2196
|
+
iframe.src = url;
|
|
2197
|
+
iframe.loading = 'lazy';
|
|
2198
|
+
iframe.title = 'Embedded content';
|
|
2199
|
+
iframe.setAttribute('allow', 'encrypted-media; fullscreen');
|
|
2200
|
+
wrapper.append(iframe);
|
|
2201
|
+
block.append(wrapper);
|
|
2202
|
+
}
|
|
2203
|
+
`,
|
|
2204
|
+
css: `/* Block: embed */
|
|
2205
|
+
.embed .embed-wrapper {
|
|
2206
|
+
position: relative;
|
|
2207
|
+
aspect-ratio: 16 / 9;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
.embed iframe {
|
|
2211
|
+
position: absolute;
|
|
2212
|
+
inset: 0;
|
|
2213
|
+
width: 100%;
|
|
2214
|
+
height: 100%;
|
|
2215
|
+
border: 0;
|
|
2216
|
+
}
|
|
2217
|
+
`,
|
|
2218
|
+
model: json({
|
|
2219
|
+
definitions: [blockDef("Embed", "embed", "embed")],
|
|
2220
|
+
models: [
|
|
2221
|
+
{
|
|
2222
|
+
id: "embed",
|
|
2223
|
+
fields: [{ component: "aem-content", name: "link", label: "Embed URL" }]
|
|
2224
|
+
}
|
|
2225
|
+
],
|
|
2226
|
+
filters: []
|
|
2227
|
+
})
|
|
2228
|
+
}
|
|
2229
|
+
];
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
// src/lib/scaffold/ue.ts
|
|
2234
|
+
function fieldReferenceFields() {
|
|
2235
|
+
return [
|
|
2236
|
+
{ component: "tab", name: "basics", label: "Basics" },
|
|
2237
|
+
{ component: "text", valueType: "string", name: "text", label: "Text (single line)" },
|
|
2238
|
+
{
|
|
2239
|
+
component: "textarea",
|
|
2240
|
+
valueType: "string",
|
|
2241
|
+
name: "textarea",
|
|
2242
|
+
label: "Textarea (multi-line)"
|
|
2243
|
+
},
|
|
2244
|
+
{ component: "richtext", valueType: "string", name: "richtext", value: "", label: "Rich text" },
|
|
2245
|
+
{ component: "number", valueType: "number", name: "number", label: "Number" },
|
|
2246
|
+
{ component: "boolean", valueType: "boolean", name: "boolean", value: false, label: "Boolean" },
|
|
2247
|
+
{ component: "date-time", valueType: "date", name: "date", label: "Date / time" },
|
|
2248
|
+
{ component: "tab", name: "selection", label: "Selection" },
|
|
2249
|
+
{
|
|
2250
|
+
component: "select",
|
|
2251
|
+
valueType: "string",
|
|
2252
|
+
name: "select",
|
|
2253
|
+
label: "Select",
|
|
2254
|
+
value: "a",
|
|
2255
|
+
options: [
|
|
2256
|
+
{ name: "Option A", value: "a" },
|
|
2257
|
+
{ name: "Option B", value: "b" }
|
|
2258
|
+
]
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
component: "radio-group",
|
|
2262
|
+
valueType: "string",
|
|
2263
|
+
name: "radio",
|
|
2264
|
+
label: "Radio group",
|
|
2265
|
+
value: "x",
|
|
2266
|
+
options: [
|
|
2267
|
+
{ name: "X", value: "x" },
|
|
2268
|
+
{ name: "Y", value: "y" }
|
|
2269
|
+
]
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
component: "multiselect",
|
|
2273
|
+
valueType: "string",
|
|
2274
|
+
name: "classes",
|
|
2275
|
+
label: "Multiselect (CSS classes)",
|
|
2276
|
+
options: [
|
|
2277
|
+
{ name: "Highlight", value: "highlight" },
|
|
2278
|
+
{ name: "Dark", value: "dark" }
|
|
2279
|
+
]
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
component: "checkbox-group",
|
|
2283
|
+
valueType: "string[]",
|
|
2284
|
+
name: "toggles",
|
|
2285
|
+
label: "Checkbox group",
|
|
2286
|
+
options: [
|
|
2287
|
+
{ name: "Show title", value: "show-title" },
|
|
2288
|
+
{ name: "Show image", value: "show-image" }
|
|
2289
|
+
]
|
|
2290
|
+
},
|
|
2291
|
+
{ component: "tab", name: "media", label: "Media & content" },
|
|
2292
|
+
{ component: "reference", valueType: "string", name: "image", label: "Image (DAM asset)" },
|
|
2293
|
+
{ component: "text", valueType: "string", name: "imageAlt", label: "Image alt" },
|
|
2294
|
+
{ component: "aem-content", name: "link", label: "Link (page / URL)" },
|
|
2295
|
+
{ component: "aem-content-fragment", name: "contentFragment", label: "Content Fragment" },
|
|
2296
|
+
{
|
|
2297
|
+
component: "aem-experience-fragment",
|
|
2298
|
+
name: "experienceFragment",
|
|
2299
|
+
label: "Experience Fragment"
|
|
2300
|
+
},
|
|
2301
|
+
{ component: "aem-tag", valueType: "string", name: "tags", label: "Tags" },
|
|
2302
|
+
{ component: "tab", name: "repeatable", label: "Multifield" },
|
|
2303
|
+
{
|
|
2304
|
+
component: "container",
|
|
2305
|
+
name: "items",
|
|
2306
|
+
label: "Items (repeatable)",
|
|
2307
|
+
multi: true,
|
|
2308
|
+
collapsible: true,
|
|
2309
|
+
fields: [
|
|
2310
|
+
{ component: "text", valueType: "string", name: "itemTitle", label: "Title" },
|
|
2311
|
+
{ component: "richtext", valueType: "string", name: "itemText", value: "", label: "Text" },
|
|
2312
|
+
{ component: "aem-content", name: "itemLink", label: "Link" }
|
|
2313
|
+
]
|
|
2314
|
+
}
|
|
2315
|
+
];
|
|
2316
|
+
}
|
|
2317
|
+
function fieldReferenceModelFile() {
|
|
2318
|
+
const model = {
|
|
2319
|
+
definitions: [
|
|
2320
|
+
{
|
|
2321
|
+
title: "Field Reference",
|
|
2322
|
+
id: "field-reference",
|
|
2323
|
+
plugins: {
|
|
2324
|
+
xwalk: {
|
|
2325
|
+
page: {
|
|
2326
|
+
resourceType: "core/franklin/components/block/v1/block",
|
|
2327
|
+
template: { name: "Field Reference", model: "field-reference" }
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
],
|
|
2333
|
+
models: [{ id: "field-reference", fields: fieldReferenceFields() }],
|
|
2334
|
+
filters: []
|
|
2335
|
+
};
|
|
2336
|
+
return `${JSON.stringify(model, null, 2)}
|
|
2337
|
+
`;
|
|
2338
|
+
}
|
|
2339
|
+
function mergeById(existing, additions) {
|
|
2340
|
+
const seen = new Set(existing.map((e) => e.id));
|
|
2341
|
+
return [...existing, ...additions.filter((a) => !seen.has(a.id))];
|
|
2342
|
+
}
|
|
2343
|
+
function mergeDefinitions(existing, defaultContent, blockDefs) {
|
|
2344
|
+
const groups = existing?.groups ? [...existing.groups] : [];
|
|
2345
|
+
const ensure = (id, title, comps) => {
|
|
2346
|
+
let g = groups.find((x) => x.id === id);
|
|
2347
|
+
if (!g) {
|
|
2348
|
+
g = { id, title, components: [] };
|
|
2349
|
+
groups.push(g);
|
|
2350
|
+
}
|
|
2351
|
+
g.components = mergeById(g.components, comps);
|
|
2352
|
+
};
|
|
2353
|
+
ensure("default", "Default Content", defaultContent);
|
|
2354
|
+
ensure("blocks", "Blocks", blockDefs);
|
|
2355
|
+
return { groups };
|
|
2356
|
+
}
|
|
2357
|
+
function mergeSectionFilter(existing, ids) {
|
|
2358
|
+
const filters = existing ? [...existing] : [];
|
|
2359
|
+
let section = filters.find((f) => f.id === "section");
|
|
2360
|
+
if (!section) {
|
|
2361
|
+
section = { id: "section", components: ["text", "image", "button", "title"] };
|
|
2362
|
+
filters.push(section);
|
|
2363
|
+
}
|
|
2364
|
+
section.components = [.../* @__PURE__ */ new Set([...section.components, ...ids])];
|
|
2365
|
+
return filters;
|
|
2366
|
+
}
|
|
2367
|
+
var DEFAULT_CONTENT_DEFINITIONS, DEFAULT_CONTENT_MODELS, FIELD_REFERENCE_JS, FIELD_REFERENCE_CSS;
|
|
2368
|
+
var init_ue = __esm({
|
|
2369
|
+
"src/lib/scaffold/ue.ts"() {
|
|
2370
|
+
"use strict";
|
|
2371
|
+
init_esm_shims();
|
|
2372
|
+
DEFAULT_CONTENT_DEFINITIONS = [
|
|
2373
|
+
{
|
|
2374
|
+
title: "Text",
|
|
2375
|
+
id: "text",
|
|
2376
|
+
plugins: {
|
|
2377
|
+
xwalk: {
|
|
2378
|
+
page: {
|
|
2379
|
+
resourceType: "core/franklin/components/text/v1/text",
|
|
2380
|
+
template: { text: "<p>Text</p>" }
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
title: "Image",
|
|
2387
|
+
id: "image",
|
|
2388
|
+
plugins: {
|
|
2389
|
+
xwalk: {
|
|
2390
|
+
page: {
|
|
2391
|
+
resourceType: "core/franklin/components/image/v1/image",
|
|
2392
|
+
template: { image: "", imageAlt: "" }
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
title: "Button",
|
|
2399
|
+
id: "button",
|
|
2400
|
+
plugins: {
|
|
2401
|
+
xwalk: {
|
|
2402
|
+
page: {
|
|
2403
|
+
resourceType: "core/franklin/components/button/v1/button",
|
|
2404
|
+
template: { text: "Button", href: "", type: "" }
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
title: "Title",
|
|
2411
|
+
id: "title",
|
|
2412
|
+
plugins: {
|
|
2413
|
+
xwalk: {
|
|
2414
|
+
page: {
|
|
2415
|
+
resourceType: "core/franklin/components/title/v1/title",
|
|
2416
|
+
template: { title: "Title", type: "h2" }
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
];
|
|
2422
|
+
DEFAULT_CONTENT_MODELS = [
|
|
2423
|
+
{
|
|
2424
|
+
id: "title",
|
|
2425
|
+
fields: [
|
|
2426
|
+
{ component: "text", valueType: "string", name: "title", label: "Title" },
|
|
2427
|
+
{
|
|
2428
|
+
component: "select",
|
|
2429
|
+
valueType: "string",
|
|
2430
|
+
name: "type",
|
|
2431
|
+
label: "Type",
|
|
2432
|
+
value: "h2",
|
|
2433
|
+
options: [
|
|
2434
|
+
{ name: "H1", value: "h1" },
|
|
2435
|
+
{ name: "H2", value: "h2" },
|
|
2436
|
+
{ name: "H3", value: "h3" },
|
|
2437
|
+
{ name: "H4", value: "h4" },
|
|
2438
|
+
{ name: "H5", value: "h5" },
|
|
2439
|
+
{ name: "H6", value: "h6" }
|
|
2440
|
+
]
|
|
2441
|
+
}
|
|
2442
|
+
]
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
id: "image",
|
|
2446
|
+
fields: [
|
|
2447
|
+
{ component: "reference", valueType: "string", name: "image", label: "Image", multi: false },
|
|
2448
|
+
{ component: "text", valueType: "string", name: "imageAlt", label: "Alt Text" }
|
|
2449
|
+
]
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
id: "button",
|
|
2453
|
+
fields: [
|
|
2454
|
+
{ component: "text", valueType: "string", name: "text", label: "Text" },
|
|
2455
|
+
{ component: "aem-content", name: "href", label: "Link" },
|
|
2456
|
+
{
|
|
2457
|
+
component: "select",
|
|
2458
|
+
valueType: "string",
|
|
2459
|
+
name: "type",
|
|
2460
|
+
label: "Type",
|
|
2461
|
+
options: [
|
|
2462
|
+
{ name: "Default", value: "" },
|
|
2463
|
+
{ name: "Primary", value: "primary" },
|
|
2464
|
+
{ name: "Secondary", value: "secondary" }
|
|
2465
|
+
]
|
|
2466
|
+
}
|
|
2467
|
+
]
|
|
2468
|
+
}
|
|
2469
|
+
];
|
|
2470
|
+
FIELD_REFERENCE_JS = `// Block: field-reference
|
|
2471
|
+
// A living reference of every Universal Editor field type (see _field-reference.json).
|
|
2472
|
+
// Authors use it to see what's available; delete it before launch.
|
|
2473
|
+
export default function decorate(block) {
|
|
2474
|
+
// Default/document authoring hands the field values in as rows - nothing to
|
|
2475
|
+
// enhance here beyond a scoping class. In Universal Editor the fields above
|
|
2476
|
+
// are what you drop and edit.
|
|
2477
|
+
block.classList.add('field-reference');
|
|
2478
|
+
}
|
|
2479
|
+
`;
|
|
2480
|
+
FIELD_REFERENCE_CSS = `/* Block: field-reference */
|
|
2481
|
+
.field-reference {
|
|
2482
|
+
display: grid;
|
|
2483
|
+
gap: var(--spacing-m, 16px);
|
|
2484
|
+
padding: var(--spacing-l, 32px) 0;
|
|
2485
|
+
}
|
|
2486
|
+
`;
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
// src/commands/scaffold.ts
|
|
2491
|
+
var scaffold_exports = {};
|
|
2492
|
+
__export(scaffold_exports, {
|
|
2493
|
+
scaffoldBlocks: () => scaffoldBlocks,
|
|
2494
|
+
scaffoldInteractive: () => scaffoldInteractive,
|
|
2495
|
+
scaffoldUe: () => scaffoldUe
|
|
2496
|
+
});
|
|
2497
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2498
|
+
import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2499
|
+
import path12 from "path";
|
|
2500
|
+
async function readJson(file) {
|
|
2501
|
+
if (!existsSync10(file)) return null;
|
|
2502
|
+
try {
|
|
2503
|
+
return JSON.parse(await readFile8(file, "utf-8"));
|
|
2504
|
+
} catch {
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
async function writeJson(file, data) {
|
|
2509
|
+
await writeFile4(file, `${JSON.stringify(data, null, 2)}
|
|
2510
|
+
`);
|
|
2511
|
+
}
|
|
2512
|
+
async function scaffoldUe() {
|
|
2513
|
+
const projectRoot = findProjectRoot();
|
|
2514
|
+
if (!projectRoot) {
|
|
2515
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
2516
|
+
process.exitCode = 1;
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
logger.logoOnce(logo("Scaffold - Universal Editor"));
|
|
2520
|
+
const created = [];
|
|
2521
|
+
const blockDir = path12.join(projectRoot, "blocks", "field-reference");
|
|
2522
|
+
await mkdir3(blockDir, { recursive: true });
|
|
2523
|
+
await writeFile4(path12.join(blockDir, "field-reference.js"), FIELD_REFERENCE_JS);
|
|
2524
|
+
await writeFile4(path12.join(blockDir, "field-reference.css"), FIELD_REFERENCE_CSS);
|
|
2525
|
+
await writeFile4(path12.join(blockDir, "_field-reference.json"), fieldReferenceModelFile());
|
|
2526
|
+
created.push("blocks/field-reference/ (all 17 fields + multifield)");
|
|
2527
|
+
const defFile = path12.join(projectRoot, "component-definition.json");
|
|
2528
|
+
const modelsFile = path12.join(projectRoot, "component-models.json");
|
|
2529
|
+
const filtersFile = path12.join(projectRoot, "component-filters.json");
|
|
2530
|
+
const blockDefs = [
|
|
2531
|
+
{
|
|
2532
|
+
title: "Field Reference",
|
|
2533
|
+
id: "field-reference",
|
|
2534
|
+
plugins: {
|
|
2535
|
+
xwalk: {
|
|
2536
|
+
page: {
|
|
2537
|
+
resourceType: "core/franklin/components/block/v1/block",
|
|
2538
|
+
template: { name: "Field Reference", model: "field-reference" }
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
];
|
|
2544
|
+
const definitions = mergeDefinitions(
|
|
2545
|
+
await readJson(defFile),
|
|
2546
|
+
DEFAULT_CONTENT_DEFINITIONS,
|
|
2547
|
+
blockDefs
|
|
2548
|
+
);
|
|
2549
|
+
await writeJson(defFile, definitions);
|
|
2550
|
+
created.push("component-definition.json (default content + field-reference)");
|
|
2551
|
+
const existingModels = await readJson(modelsFile) ?? [];
|
|
2552
|
+
const fieldRefModel = JSON.parse(fieldReferenceModelFile()).models[0];
|
|
2553
|
+
const models = mergeById(existingModels, [...DEFAULT_CONTENT_MODELS, fieldRefModel]);
|
|
2554
|
+
await writeJson(modelsFile, models);
|
|
2555
|
+
created.push("component-models.json (title / image / button / field-reference)");
|
|
2556
|
+
const filters = mergeSectionFilter(await readJson(filtersFile), ["field-reference"]);
|
|
2557
|
+
await writeJson(filtersFile, filters);
|
|
2558
|
+
created.push("component-filters.json (section filter)");
|
|
2559
|
+
logger.info(heading("Created / merged"));
|
|
2560
|
+
const names = created.map((c) => c.split(" ")[0]);
|
|
2561
|
+
const w = columnWidth(names);
|
|
2562
|
+
for (const c of created) {
|
|
2563
|
+
const [name, ...rest] = c.split(" ");
|
|
2564
|
+
logger.info(accentLine(name, rest.join(" ").replace(/^\((.*)\)$/, "$1"), w));
|
|
2565
|
+
}
|
|
2566
|
+
logger.info("");
|
|
2567
|
+
logger.info(box(["Universal Editor scaffold ready"]));
|
|
2568
|
+
}
|
|
2569
|
+
async function scaffoldBlocks(names) {
|
|
2570
|
+
const projectRoot = findProjectRoot();
|
|
2571
|
+
if (!projectRoot) {
|
|
2572
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
2573
|
+
process.exitCode = 1;
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
logger.logoOnce(logo("Scaffold \u2014 standard blocks"));
|
|
2577
|
+
let selected;
|
|
2578
|
+
if (names?.length) {
|
|
2579
|
+
selected = names.map((n) => blockById(n.trim())).filter((b) => !!b);
|
|
2580
|
+
const unknown = names.filter((n) => !blockById(n.trim()));
|
|
2581
|
+
if (unknown.length) {
|
|
2582
|
+
logger.warn(
|
|
2583
|
+
`Unknown block(s): ${unknown.join(", ")}. Available: ${STANDARD_BLOCKS.map((b) => b.id).join(", ")}`
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
} else {
|
|
2587
|
+
selected = STANDARD_BLOCKS;
|
|
2588
|
+
}
|
|
2589
|
+
const createdIds = [];
|
|
2590
|
+
const skipped = [];
|
|
2591
|
+
for (const b of selected) {
|
|
2592
|
+
const dir = path12.join(projectRoot, "blocks", b.id);
|
|
2593
|
+
if (existsSync10(dir)) {
|
|
2594
|
+
skipped.push(b.id);
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
await mkdir3(dir, { recursive: true });
|
|
2598
|
+
await writeFile4(path12.join(dir, `${b.id}.js`), b.js);
|
|
2599
|
+
await writeFile4(path12.join(dir, `${b.id}.css`), b.css);
|
|
2600
|
+
await writeFile4(path12.join(dir, `_${b.id}.json`), b.model);
|
|
2601
|
+
createdIds.push(b.id);
|
|
2602
|
+
}
|
|
2603
|
+
const filtersFile = path12.join(projectRoot, "component-filters.json");
|
|
2604
|
+
const filters = mergeSectionFilter(
|
|
2605
|
+
await readJson(filtersFile),
|
|
2606
|
+
selected.map((b) => b.id)
|
|
2607
|
+
);
|
|
2608
|
+
await writeJson(filtersFile, filters);
|
|
2609
|
+
logger.info(heading("Blocks"));
|
|
2610
|
+
const w = columnWidth([...createdIds, ...skipped, "component-filters"]);
|
|
2611
|
+
for (const id of createdIds) logger.info(accentLine(id, "created", w));
|
|
2612
|
+
for (const id of skipped) logger.info(statusLine("warn", id, "already exists \u2014 skipped", w));
|
|
2613
|
+
logger.info("");
|
|
2614
|
+
logger.info(box([`${createdIds.length} block(s) scaffolded + registered`]));
|
|
2615
|
+
}
|
|
2616
|
+
async function scaffoldInteractive() {
|
|
2617
|
+
const { select, checkbox } = await import("@inquirer/prompts");
|
|
2618
|
+
logger.logoOnce(logo("Scaffold"));
|
|
2619
|
+
const what = await select({
|
|
2620
|
+
message: "What do you want to scaffold?",
|
|
2621
|
+
choices: [
|
|
2622
|
+
{ name: "Universal Editor config + field-reference (all 17 field types)", value: "ue" },
|
|
2623
|
+
{ name: "Standard blocks (hero, cards, columns, accordion, embed)", value: "blocks" },
|
|
2624
|
+
{ name: "Both", value: "both" }
|
|
2625
|
+
]
|
|
2626
|
+
});
|
|
2627
|
+
if (what === "ue" || what === "both") await scaffoldUe();
|
|
2628
|
+
if (what === "blocks" || what === "both") {
|
|
2629
|
+
const picked = await checkbox({
|
|
2630
|
+
message: "Which blocks?",
|
|
2631
|
+
choices: STANDARD_BLOCKS.map((b) => ({ name: b.title, value: b.id, checked: true }))
|
|
2632
|
+
});
|
|
2633
|
+
await scaffoldBlocks(picked);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
var init_scaffold = __esm({
|
|
2637
|
+
"src/commands/scaffold.ts"() {
|
|
2638
|
+
"use strict";
|
|
2639
|
+
init_esm_shims();
|
|
2640
|
+
init_logger();
|
|
2641
|
+
init_project();
|
|
2642
|
+
init_blocks();
|
|
2643
|
+
init_ue();
|
|
2644
|
+
init_ui();
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
// src/lib/integrations.ts
|
|
2649
|
+
function integrationById(id) {
|
|
2650
|
+
return INTEGRATIONS.find((i) => i.id === id);
|
|
2651
|
+
}
|
|
2652
|
+
function wrapBlock(name, code) {
|
|
2653
|
+
return `
|
|
2654
|
+
// \u2500\u2500 ${name} (eds integrate) \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
|
|
2655
|
+
{
|
|
2656
|
+
${code.split("\n").map((l) => l ? ` ${l}` : l).join("\n")}
|
|
2657
|
+
}
|
|
2658
|
+
`;
|
|
2659
|
+
}
|
|
2660
|
+
var INTEGRATIONS;
|
|
2661
|
+
var init_integrations = __esm({
|
|
2662
|
+
"src/lib/integrations.ts"() {
|
|
2663
|
+
"use strict";
|
|
2664
|
+
init_esm_shims();
|
|
2665
|
+
INTEGRATIONS = [
|
|
2666
|
+
{
|
|
2667
|
+
id: "gtm",
|
|
2668
|
+
name: "Google Tag Manager",
|
|
2669
|
+
fields: [{ name: "id", message: "GTM container ID", placeholder: "GTM-XXXXXXX" }],
|
|
2670
|
+
code: (v) => `// Google Tag Manager (${v.id}) \u2014 delayed phase, after LCP.
|
|
2671
|
+
(function (w, d, s, l, i) {
|
|
2672
|
+
w[l] = w[l] || [];
|
|
2673
|
+
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
|
2674
|
+
const f = d.getElementsByTagName(s)[0];
|
|
2675
|
+
const j = d.createElement(s);
|
|
2676
|
+
j.async = true;
|
|
2677
|
+
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i;
|
|
2678
|
+
f.parentNode.insertBefore(j, f);
|
|
2679
|
+
})(window, document, 'script', 'dataLayer', '${v.id}');`
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
id: "ga4",
|
|
2683
|
+
name: "Google Analytics 4",
|
|
2684
|
+
fields: [{ name: "id", message: "Measurement ID", placeholder: "G-XXXXXXXXXX" }],
|
|
2685
|
+
code: (v) => `// Google Analytics 4 (${v.id}) \u2014 delayed phase.
|
|
2686
|
+
const gaScript = document.createElement('script');
|
|
2687
|
+
gaScript.async = true;
|
|
2688
|
+
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=${v.id}';
|
|
2689
|
+
document.head.appendChild(gaScript);
|
|
2690
|
+
window.dataLayer = window.dataLayer || [];
|
|
2691
|
+
function gtag() { window.dataLayer.push(arguments); }
|
|
2692
|
+
gtag('js', new Date());
|
|
2693
|
+
gtag('config', '${v.id}');`
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
id: "chat",
|
|
2697
|
+
name: "Chat widget (facade)",
|
|
2698
|
+
fields: [
|
|
2699
|
+
{ name: "label", message: "Button label", placeholder: "Chat with us" },
|
|
2700
|
+
{ name: "src", message: "Chat widget script URL" }
|
|
2701
|
+
],
|
|
2702
|
+
code: (v) => `// Chat widget \u2014 facade pattern: load the heavy script only on first interaction.
|
|
2703
|
+
const chatBtn = document.createElement('button');
|
|
2704
|
+
chatBtn.type = 'button';
|
|
2705
|
+
chatBtn.className = 'chat-facade';
|
|
2706
|
+
chatBtn.textContent = ${JSON.stringify(v.label || "Chat")};
|
|
2707
|
+
chatBtn.addEventListener('click', () => {
|
|
2708
|
+
const s = document.createElement('script');
|
|
2709
|
+
s.async = true;
|
|
2710
|
+
s.src = ${JSON.stringify(v.src || "")};
|
|
2711
|
+
document.body.appendChild(s);
|
|
2712
|
+
chatBtn.remove();
|
|
2713
|
+
}, { once: true });
|
|
2714
|
+
document.body.appendChild(chatBtn);`
|
|
2715
|
+
},
|
|
2716
|
+
{
|
|
2717
|
+
id: "cookiebot",
|
|
2718
|
+
name: "Cookie consent (Cookiebot)",
|
|
2719
|
+
fields: [{ name: "id", message: "Cookiebot domain group ID (CBID)" }],
|
|
2720
|
+
code: (v) => `// Cookiebot consent \u2014 load early in delayed; configure GTM consent mode to respect it.
|
|
2721
|
+
const cb = document.createElement('script');
|
|
2722
|
+
cb.id = 'Cookiebot';
|
|
2723
|
+
cb.src = 'https://consent.cookiebot.com/uc.js';
|
|
2724
|
+
cb.setAttribute('data-cbid', '${v.id}');
|
|
2725
|
+
cb.async = true;
|
|
2726
|
+
document.head.appendChild(cb);`
|
|
2727
|
+
},
|
|
2728
|
+
{
|
|
2729
|
+
id: "custom",
|
|
2730
|
+
name: "Custom third-party script",
|
|
2731
|
+
fields: [{ name: "url", message: "Script URL" }],
|
|
2732
|
+
code: (v) => `// Custom third-party script \u2014 delayed phase.
|
|
2733
|
+
const s = document.createElement('script');
|
|
2734
|
+
s.async = true;
|
|
2735
|
+
s.src = ${JSON.stringify(v.url || "")};
|
|
2736
|
+
document.head.appendChild(s);`
|
|
2737
|
+
}
|
|
2738
|
+
];
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
// src/commands/integrate.ts
|
|
2743
|
+
var integrate_exports = {};
|
|
2744
|
+
__export(integrate_exports, {
|
|
2745
|
+
integrate: () => integrate
|
|
2746
|
+
});
|
|
2747
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2748
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
2749
|
+
import path13 from "path";
|
|
2750
|
+
import chalk8 from "chalk";
|
|
2751
|
+
async function integrate(type) {
|
|
2752
|
+
const projectRoot = findProjectRoot();
|
|
2753
|
+
if (!projectRoot) {
|
|
2754
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
2755
|
+
process.exitCode = 1;
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
logger.logoOnce(logo("Integrate \u2014 third-party service"));
|
|
2759
|
+
const { select, input } = await import("@inquirer/prompts");
|
|
2760
|
+
const chosenId = type || await select({
|
|
2761
|
+
message: "Which service do you want to add?",
|
|
2762
|
+
choices: INTEGRATIONS.map((i) => ({ name: i.name, value: i.id }))
|
|
2763
|
+
});
|
|
2764
|
+
const integration = integrationById(chosenId);
|
|
2765
|
+
if (!integration) {
|
|
2766
|
+
logger.error(
|
|
2767
|
+
`Unknown integration "${chosenId}". Options: ${INTEGRATIONS.map((i) => i.id).join(", ")}`
|
|
2768
|
+
);
|
|
2769
|
+
process.exitCode = 1;
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const values = {};
|
|
2773
|
+
for (const f of integration.fields) {
|
|
2774
|
+
const answer = await input({ message: ` ${f.message}`, default: f.placeholder });
|
|
2775
|
+
values[f.name] = answer.trim();
|
|
2776
|
+
}
|
|
2777
|
+
const block2 = wrapBlock(integration.name, integration.code(values));
|
|
2778
|
+
const delayedPath = path13.join(projectRoot, "scripts", "delayed.js");
|
|
2779
|
+
await mkdir4(path13.dirname(delayedPath), { recursive: true });
|
|
2780
|
+
const existing = existsSync11(delayedPath) ? await readFile9(delayedPath, "utf-8") : DELAYED_HEADER;
|
|
2781
|
+
await writeFile5(delayedPath, `${existing.replace(/\s*$/, "")}
|
|
2782
|
+
${block2}`);
|
|
2783
|
+
logger.info(heading("Added"));
|
|
2784
|
+
logger.info(accentLine(integration.name, "\u2192 scripts/delayed.js"));
|
|
2785
|
+
logger.info("");
|
|
2786
|
+
logger.info(
|
|
2787
|
+
chalk8.dim(" Loads after LCP (delayed phase) \u2014 zero impact on your Core Web Vitals.")
|
|
2788
|
+
);
|
|
2789
|
+
if (integration.id === "gtm" || integration.id === "ga4") {
|
|
2790
|
+
logger.info(
|
|
2791
|
+
chalk8.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
if (integration.id === "chat") {
|
|
2795
|
+
logger.info(
|
|
2796
|
+
chalk8.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
logger.info("");
|
|
2800
|
+
logger.info(box(["Integration added"]));
|
|
2801
|
+
}
|
|
2802
|
+
var DELAYED_HEADER;
|
|
2803
|
+
var init_integrate = __esm({
|
|
2804
|
+
"src/commands/integrate.ts"() {
|
|
2805
|
+
"use strict";
|
|
2806
|
+
init_esm_shims();
|
|
2807
|
+
init_integrations();
|
|
2808
|
+
init_logger();
|
|
2809
|
+
init_project();
|
|
2810
|
+
init_ui();
|
|
2811
|
+
DELAYED_HEADER = `// delayed.js \u2014 third-party / non-critical scripts.
|
|
2812
|
+
// Loaded in the EDS delayed phase (after LCP), so nothing here blocks the page.
|
|
2813
|
+
`;
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
// src/commands/figma/setup.ts
|
|
2818
|
+
var setup_exports = {};
|
|
2819
|
+
__export(setup_exports, {
|
|
2820
|
+
figmaSetup: () => figmaSetup
|
|
2821
|
+
});
|
|
2822
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2823
|
+
import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
|
|
2824
|
+
import chalk9 from "chalk";
|
|
2825
|
+
async function figmaSetup() {
|
|
2826
|
+
const { select, confirm } = await import("@inquirer/prompts");
|
|
2827
|
+
logger.logoOnce(logo());
|
|
2828
|
+
logger.info(heading("Figma MCP Setup Wizard"));
|
|
2829
|
+
logger.info("Detecting installed AI agents...");
|
|
2830
|
+
const agents = await detectAllAgents();
|
|
2831
|
+
let selectedAgent;
|
|
2832
|
+
if (agents.length === 0) {
|
|
2833
|
+
logger.warn("No AI agents found (claude, cursor, codex).");
|
|
2834
|
+
logger.info(`Install Claude Code: ${chalk9.cyan("npm install -g @anthropic-ai/claude-code")}`);
|
|
2835
|
+
selectedAgent = "none";
|
|
2836
|
+
} else {
|
|
2837
|
+
logger.success(`Found: ${agents.map((a) => chalk9.cyan(a.type)).join(", ")}`);
|
|
2838
|
+
selectedAgent = await select({
|
|
2839
|
+
message: "Which agent do you want to use with eds-cli?",
|
|
2840
|
+
choices: [
|
|
2841
|
+
...agents.map((a) => ({
|
|
2842
|
+
name: `${a.type}${a.hasFigmaMcp ? chalk9.green(" (Figma MCP detected)") : ""}`,
|
|
2843
|
+
value: a.type
|
|
2844
|
+
})),
|
|
2845
|
+
{ name: "None (manual mode)", value: "none" }
|
|
2846
|
+
]
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
if (selectedAgent !== "none") {
|
|
2850
|
+
const agent = agents.find((a) => a.type === selectedAgent);
|
|
2851
|
+
if (agent && !agent.hasFigmaMcp) {
|
|
2852
|
+
logger.warn(`Figma MCP is not configured for ${selectedAgent}.
|
|
2853
|
+
`);
|
|
2854
|
+
logger.info("Install it with one of these methods:\n");
|
|
2855
|
+
switch (selectedAgent) {
|
|
2856
|
+
case "claude":
|
|
2857
|
+
logger.info(
|
|
2858
|
+
` ${chalk9.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
|
|
2859
|
+
);
|
|
2860
|
+
logger.info(" or for Figma Desktop app:");
|
|
2861
|
+
logger.info(
|
|
2862
|
+
` ${chalk9.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
|
|
2863
|
+
);
|
|
2864
|
+
break;
|
|
2865
|
+
case "cursor":
|
|
2866
|
+
logger.info(` Add to ${chalk9.cyan("~/.cursor/mcp.json")}:`);
|
|
2867
|
+
logger.info(
|
|
2868
|
+
chalk9.dim(
|
|
2869
|
+
JSON.stringify(
|
|
2870
|
+
{
|
|
2871
|
+
mcpServers: {
|
|
2872
|
+
figma: { url: "https://mcp.figma.com/sse" }
|
|
2873
|
+
}
|
|
2874
|
+
},
|
|
2875
|
+
null,
|
|
2876
|
+
2
|
|
2877
|
+
)
|
|
2878
|
+
)
|
|
2879
|
+
);
|
|
2880
|
+
break;
|
|
2881
|
+
case "codex":
|
|
2882
|
+
logger.info(
|
|
2883
|
+
" See Figma MCP docs: https://help.figma.com/hc/en-us/articles/32132100833559"
|
|
2884
|
+
);
|
|
2885
|
+
break;
|
|
2886
|
+
}
|
|
2887
|
+
logger.info("");
|
|
2888
|
+
} else if (agent?.hasFigmaMcp) {
|
|
2889
|
+
logger.success(`Figma MCP is already configured for ${selectedAgent}!`);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
logger.info(
|
|
2893
|
+
chalk9.dim(
|
|
2894
|
+
"\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
|
|
2895
|
+
)
|
|
2896
|
+
);
|
|
2897
|
+
const shouldSave = await confirm({
|
|
2898
|
+
message: "Save preferences to ~/.eds/config.json?",
|
|
2899
|
+
default: true
|
|
2900
|
+
});
|
|
2901
|
+
if (shouldSave) {
|
|
2902
|
+
const configDir = globalConfigDir();
|
|
2903
|
+
if (!existsSync12(configDir)) {
|
|
2904
|
+
await mkdir5(configDir, { recursive: true });
|
|
2905
|
+
}
|
|
2906
|
+
const config = {};
|
|
2907
|
+
if (selectedAgent !== "none") {
|
|
2908
|
+
config.agent = selectedAgent;
|
|
2909
|
+
}
|
|
2910
|
+
await writeFile6(globalConfigPath(), `${JSON.stringify(config, null, 2)}
|
|
2911
|
+
`, {
|
|
2912
|
+
mode: 384
|
|
2913
|
+
});
|
|
2914
|
+
logger.success(`Config saved to ${chalk9.cyan(globalConfigPath())}`);
|
|
2915
|
+
}
|
|
2916
|
+
logger.info(chalk9.bold("\nSetup complete!"));
|
|
2917
|
+
if (selectedAgent !== "none") {
|
|
2918
|
+
logger.info(`Try: ${chalk9.cyan('eds block from-design "<figma-url>" --dry-run')}`);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
var init_setup = __esm({
|
|
2922
|
+
"src/commands/figma/setup.ts"() {
|
|
2923
|
+
"use strict";
|
|
2924
|
+
init_esm_shims();
|
|
2925
|
+
init_detect();
|
|
2926
|
+
init_config();
|
|
2927
|
+
init_logger();
|
|
2928
|
+
init_ui();
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
// src/commands/doctor.ts
|
|
2933
|
+
var doctor_exports = {};
|
|
2934
|
+
__export(doctor_exports, {
|
|
2935
|
+
doctor: () => doctor
|
|
2936
|
+
});
|
|
2937
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2938
|
+
import { readFile as readFile10, readdir as readdir2 } from "fs/promises";
|
|
2939
|
+
import path14 from "path";
|
|
2940
|
+
import { parse as parseYaml } from "yaml";
|
|
2941
|
+
async function doctor(options = {}) {
|
|
2942
|
+
const projectRoot = findProjectRoot();
|
|
2943
|
+
if (!projectRoot) {
|
|
2944
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
2945
|
+
process.exitCode = 1;
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
const project = detectProject(projectRoot);
|
|
2949
|
+
if (!options.json) {
|
|
2950
|
+
logger.logoOnce(logo("Doctor"));
|
|
2951
|
+
logger.info(heading("Doctor", projectRoot));
|
|
2952
|
+
}
|
|
2953
|
+
const results = [];
|
|
2954
|
+
results.push({
|
|
2955
|
+
name: "fstab.yaml exists",
|
|
2956
|
+
status: project.hasFstab ? "pass" : "fail",
|
|
2957
|
+
message: project.hasFstab ? "Found" : "Missing \u2014 required for EDS projects"
|
|
2958
|
+
});
|
|
2959
|
+
if (project.hasFstab) {
|
|
2960
|
+
try {
|
|
2961
|
+
const content = await readFile10(path14.join(projectRoot, "fstab.yaml"), "utf-8");
|
|
2962
|
+
parseYaml(content);
|
|
2963
|
+
results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
|
|
2964
|
+
} catch {
|
|
2965
|
+
results.push({
|
|
2966
|
+
name: "fstab.yaml valid YAML",
|
|
2967
|
+
status: "fail",
|
|
2968
|
+
message: "Invalid YAML syntax"
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
results.push({
|
|
2973
|
+
name: "head.html exists",
|
|
2974
|
+
status: project.hasHeadHtml ? "pass" : "warn",
|
|
2975
|
+
message: project.hasHeadHtml ? "Found" : "Missing \u2014 recommended for metadata/scripts"
|
|
2976
|
+
});
|
|
2977
|
+
results.push({
|
|
2978
|
+
name: "blocks/ directory",
|
|
2979
|
+
status: project.hasBlocks ? "pass" : "warn",
|
|
2980
|
+
message: project.hasBlocks ? "Found" : "Missing \u2014 no blocks yet"
|
|
2981
|
+
});
|
|
2982
|
+
if (project.hasBlocks) {
|
|
2983
|
+
const blocksDir = path14.join(projectRoot, "blocks");
|
|
2984
|
+
const entries = await readdir2(blocksDir, { withFileTypes: true });
|
|
2985
|
+
let aggregatedModels = "";
|
|
2986
|
+
const aggregatedPath = path14.join(projectRoot, "component-models.json");
|
|
2987
|
+
if (existsSync13(aggregatedPath)) {
|
|
2988
|
+
try {
|
|
2989
|
+
aggregatedModels = await readFile10(aggregatedPath, "utf-8");
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
for (const entry of entries) {
|
|
2994
|
+
if (!entry.isDirectory()) continue;
|
|
2995
|
+
const blockName = entry.name;
|
|
2996
|
+
const jsFile = path14.join(blocksDir, blockName, `${blockName}.js`);
|
|
2997
|
+
const hasJs = existsSync13(jsFile);
|
|
2998
|
+
results.push({
|
|
2999
|
+
name: `blocks/${blockName} has JS`,
|
|
3000
|
+
status: hasJs ? "pass" : "warn",
|
|
3001
|
+
message: hasJs ? "Found" : `Missing ${blockName}.js`
|
|
3002
|
+
});
|
|
3003
|
+
if (hasJs) {
|
|
3004
|
+
const jsContent = await readFile10(jsFile, "utf-8");
|
|
3005
|
+
if (jsContent.includes("console.log")) {
|
|
3006
|
+
results.push({
|
|
3007
|
+
name: `blocks/${blockName} no console.log`,
|
|
3008
|
+
status: "warn",
|
|
3009
|
+
message: "Found console.log \u2014 remove before production"
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
const hasPerBlockModel = existsSync13(path14.join(blocksDir, blockName, `_${blockName}.json`));
|
|
3014
|
+
const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
|
|
3015
|
+
results.push({
|
|
3016
|
+
name: `blocks/${blockName} UE model`,
|
|
3017
|
+
status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
|
|
3018
|
+
message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model \u2014 authors can't edit this block in UE"
|
|
3019
|
+
});
|
|
3020
|
+
const metaFile = path14.join(blocksDir, blockName, ".eds-meta.json");
|
|
3021
|
+
if (existsSync13(metaFile)) {
|
|
3022
|
+
try {
|
|
3023
|
+
const meta = JSON.parse(await readFile10(metaFile, "utf-8"));
|
|
3024
|
+
const syncDate = new Date(meta.lastSyncedAt);
|
|
3025
|
+
const daysSinceSync = Math.floor(
|
|
3026
|
+
(Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
3027
|
+
);
|
|
3028
|
+
results.push({
|
|
3029
|
+
name: `blocks/${blockName} Figma sync`,
|
|
3030
|
+
status: daysSinceSync > 30 ? "warn" : "pass",
|
|
3031
|
+
message: daysSinceSync > 30 ? `Last synced ${daysSinceSync} days ago \u2014 design may have changed` : `Synced ${daysSinceSync} days ago`
|
|
3032
|
+
});
|
|
3033
|
+
} catch {
|
|
3034
|
+
results.push({
|
|
3035
|
+
name: `blocks/${blockName} .eds-meta.json`,
|
|
3036
|
+
status: "warn",
|
|
3037
|
+
message: "Invalid metadata file"
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
const queryFile = path14.join(projectRoot, "helix-query.yaml");
|
|
3044
|
+
if (existsSync13(queryFile)) {
|
|
3045
|
+
try {
|
|
3046
|
+
const content = await readFile10(queryFile, "utf-8");
|
|
3047
|
+
parseYaml(content);
|
|
3048
|
+
results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
|
|
3049
|
+
} catch {
|
|
3050
|
+
results.push({
|
|
3051
|
+
name: "helix-query.yaml valid",
|
|
3052
|
+
status: "fail",
|
|
3053
|
+
message: "Invalid YAML syntax"
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
results.push({
|
|
3058
|
+
name: "~/.eds/config.json",
|
|
3059
|
+
status: globalConfigExists() ? "pass" : "warn",
|
|
3060
|
+
message: globalConfigExists() ? "Found" : 'Missing \u2014 run "eds figma setup" to configure'
|
|
3061
|
+
});
|
|
3062
|
+
const fails0 = results.filter((r) => r.status === "fail");
|
|
3063
|
+
const warns0 = results.filter((r) => r.status === "warn");
|
|
3064
|
+
if (options.json) {
|
|
3065
|
+
const summary2 = {
|
|
3066
|
+
pass: results.filter((r) => r.status === "pass").length,
|
|
3067
|
+
warn: warns0.length,
|
|
3068
|
+
fail: fails0.length
|
|
3069
|
+
};
|
|
3070
|
+
console.log(JSON.stringify({ projectRoot, results, summary: summary2 }, null, 2));
|
|
3071
|
+
if (fails0.length > 0) process.exitCode = 1;
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const nameWidth = columnWidth(results.map((r) => r.name));
|
|
3075
|
+
for (const r of results) {
|
|
3076
|
+
logger.info(statusLine(r.status, r.name, r.message, nameWidth));
|
|
3077
|
+
}
|
|
3078
|
+
const fails = results.filter((r) => r.status === "fail");
|
|
3079
|
+
const warns = results.filter((r) => r.status === "warn");
|
|
3080
|
+
const passes = results.filter((r) => r.status === "pass");
|
|
3081
|
+
logger.info("");
|
|
3082
|
+
logger.info(
|
|
3083
|
+
box([summary({ pass: passes.length, warn: warns.length, fail: fails.length })])
|
|
3084
|
+
);
|
|
3085
|
+
if (fails.length > 0) process.exitCode = 1;
|
|
3086
|
+
}
|
|
3087
|
+
var init_doctor = __esm({
|
|
3088
|
+
"src/commands/doctor.ts"() {
|
|
3089
|
+
"use strict";
|
|
3090
|
+
init_esm_shims();
|
|
3091
|
+
init_config();
|
|
3092
|
+
init_logger();
|
|
3093
|
+
init_project();
|
|
3094
|
+
init_ui();
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
// src/commands/audit-loading.ts
|
|
3099
|
+
var audit_loading_exports = {};
|
|
3100
|
+
__export(audit_loading_exports, {
|
|
3101
|
+
auditLoading: () => auditLoading
|
|
3102
|
+
});
|
|
3103
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3104
|
+
import { readFile as readFile11, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
3105
|
+
import path15 from "path";
|
|
3106
|
+
import chalk10 from "chalk";
|
|
3107
|
+
async function auditLoading(options = {}) {
|
|
3108
|
+
const projectRoot = findProjectRoot();
|
|
3109
|
+
if (!projectRoot) {
|
|
3110
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
3111
|
+
process.exitCode = 1;
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (!options.json) {
|
|
3115
|
+
logger.logoOnce(logo("Loading Audit"));
|
|
3116
|
+
logger.info(heading("Loading Order Audit", projectRoot));
|
|
3117
|
+
}
|
|
3118
|
+
const results = [];
|
|
3119
|
+
await checkHeadHtml(projectRoot, results);
|
|
3120
|
+
await checkScriptsJs(projectRoot, results);
|
|
3121
|
+
await checkStylesCss(projectRoot, results);
|
|
3122
|
+
await checkDelayedJs(projectRoot, results);
|
|
3123
|
+
await checkBlocks(projectRoot, results);
|
|
3124
|
+
await checkAemJs(projectRoot, results);
|
|
3125
|
+
if (options.json) {
|
|
3126
|
+
const summary2 = {
|
|
3127
|
+
pass: results.filter((r) => r.status === "pass").length,
|
|
3128
|
+
warn: results.filter((r) => r.status === "warn").length,
|
|
3129
|
+
fail: results.filter((r) => r.status === "fail").length,
|
|
3130
|
+
info: results.filter((r) => r.status === "info").length
|
|
3131
|
+
};
|
|
3132
|
+
console.log(JSON.stringify({ projectRoot, results, summary: summary2 }, null, 2));
|
|
3133
|
+
if (summary2.fail > 0) process.exitCode = 1;
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
printResults(results);
|
|
3137
|
+
}
|
|
3138
|
+
async function checkHeadHtml(root, results) {
|
|
3139
|
+
const filePath = path15.join(root, "head.html");
|
|
3140
|
+
if (!existsSync14(filePath)) {
|
|
3141
|
+
results.push({
|
|
3142
|
+
rule: "head.html exists",
|
|
3143
|
+
status: "warn",
|
|
3144
|
+
message: "Missing head.html"
|
|
3145
|
+
});
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
const content = await readFile11(filePath, "utf-8");
|
|
3149
|
+
const lines = content.split("\n");
|
|
3150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3151
|
+
const line = lines[i];
|
|
3152
|
+
if (/<script\b/i.test(line) && !/type\s*=\s*["']module["']/i.test(line) && !/<\/script>/.test(line)) {
|
|
3153
|
+
results.push({
|
|
3154
|
+
rule: "No render-blocking scripts in head.html",
|
|
3155
|
+
status: "fail",
|
|
3156
|
+
message: 'Script without type="module" blocks rendering',
|
|
3157
|
+
file: "head.html",
|
|
3158
|
+
line: i + 1
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const stylesheetMatches = content.match(/<link[^>]+rel=["']stylesheet["'][^>]*>/gi) || [];
|
|
3163
|
+
const extraStylesheets = stylesheetMatches.filter(
|
|
3164
|
+
(tag) => !tag.includes("styles/styles.css") && !tag.includes("styles.css")
|
|
3165
|
+
);
|
|
3166
|
+
if (extraStylesheets.length > 0) {
|
|
3167
|
+
results.push({
|
|
3168
|
+
rule: "No extra stylesheets in head.html",
|
|
3169
|
+
status: "warn",
|
|
3170
|
+
message: `${extraStylesheets.length} extra stylesheet(s) \u2014 each competes with LCP budget`,
|
|
3171
|
+
file: "head.html"
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
if (/<link[^>]+rel=["'](preload|preconnect)["']/i.test(content)) {
|
|
3175
|
+
results.push({
|
|
3176
|
+
rule: "No preload/preconnect in head.html",
|
|
3177
|
+
status: "warn",
|
|
3178
|
+
message: "preload/preconnect can hurt EDS LCP \u2014 resources compete with the 100KB budget",
|
|
3179
|
+
file: "head.html"
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
if (/<script\b[^>]*>[\s\S]*?[^\s<][\s\S]*?<\/script>/i.test(content)) {
|
|
3183
|
+
results.push({
|
|
3184
|
+
rule: "No inline scripts in head.html",
|
|
3185
|
+
status: "warn",
|
|
3186
|
+
message: "Inline scripts block rendering \u2014 move to delayed.js",
|
|
3187
|
+
file: "head.html"
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
const thirdPartyPattern = /(?:src|href)=["'](https?:\/\/(?!(?:localhost|127\.0\.0\.1))[^"']+)["']/gi;
|
|
3191
|
+
for (const match of content.matchAll(thirdPartyPattern)) {
|
|
3192
|
+
const url = match[1];
|
|
3193
|
+
if (!url.includes("fonts.googleapis.com")) {
|
|
3194
|
+
results.push({
|
|
3195
|
+
rule: "No third-party resources in head.html",
|
|
3196
|
+
status: "warn",
|
|
3197
|
+
message: `Third-party resource: ${url} \u2014 move to delayed.js`,
|
|
3198
|
+
file: "head.html"
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
if (/<link[^>]+as=["']font["']/i.test(content)) {
|
|
3203
|
+
results.push({
|
|
3204
|
+
rule: "No font preloads in head.html",
|
|
3205
|
+
status: "warn",
|
|
3206
|
+
message: "Font preloads consume LCP bandwidth budget \u2014 let loadFonts() handle them",
|
|
3207
|
+
file: "head.html"
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
if (!results.some((r) => r.file === "head.html")) {
|
|
3211
|
+
results.push({
|
|
3212
|
+
rule: "head.html is clean",
|
|
3213
|
+
status: "pass",
|
|
3214
|
+
message: "No render-blocking issues found",
|
|
3215
|
+
file: "head.html"
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
async function checkScriptsJs(root, results) {
|
|
3220
|
+
const filePath = path15.join(root, "scripts", "scripts.js");
|
|
3221
|
+
if (!existsSync14(filePath)) {
|
|
3222
|
+
results.push({
|
|
3223
|
+
rule: "scripts/scripts.js exists",
|
|
3224
|
+
status: "warn",
|
|
3225
|
+
message: "Missing scripts.js \u2014 cannot audit loading phases"
|
|
3226
|
+
});
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
const content = await readFile11(filePath, "utf-8");
|
|
3230
|
+
const lines = content.split("\n");
|
|
3231
|
+
const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
|
|
3232
|
+
const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
|
|
3233
|
+
const hasLoadDelayed = /function\s+loadDelayed|loadDelayed\s*=/i.test(content);
|
|
3234
|
+
results.push({
|
|
3235
|
+
rule: "loadEager phase exists",
|
|
3236
|
+
status: hasLoadEager ? "pass" : "fail",
|
|
3237
|
+
message: hasLoadEager ? "Found" : "Missing loadEager \u2014 LCP optimization depends on it",
|
|
3238
|
+
file: "scripts/scripts.js"
|
|
3239
|
+
});
|
|
3240
|
+
results.push({
|
|
3241
|
+
rule: "loadLazy phase exists",
|
|
3242
|
+
status: hasLoadLazy ? "pass" : "fail",
|
|
3243
|
+
message: hasLoadLazy ? "Found" : "Missing loadLazy \u2014 below-fold content won't load properly",
|
|
3244
|
+
file: "scripts/scripts.js"
|
|
3245
|
+
});
|
|
3246
|
+
results.push({
|
|
3247
|
+
rule: "loadDelayed phase exists",
|
|
3248
|
+
status: hasLoadDelayed ? "pass" : "warn",
|
|
3249
|
+
message: hasLoadDelayed ? "Found" : "Missing loadDelayed \u2014 third-party scripts may block LCP",
|
|
3250
|
+
file: "scripts/scripts.js"
|
|
3251
|
+
});
|
|
3252
|
+
const delayedTimeoutMatch = content.match(/setTimeout\s*\([^,]+,\s*(\d+)\s*\)/);
|
|
3253
|
+
if (hasLoadDelayed && delayedTimeoutMatch) {
|
|
3254
|
+
const delay = Number.parseInt(delayedTimeoutMatch[1], 10);
|
|
3255
|
+
if (delay < 3e3) {
|
|
3256
|
+
results.push({
|
|
3257
|
+
rule: "Delayed phase waits >= 3s",
|
|
3258
|
+
status: "warn",
|
|
3259
|
+
message: `delayed.js loads after ${delay}ms \u2014 should be >= 3000ms for LCP safety`,
|
|
3260
|
+
file: "scripts/scripts.js"
|
|
3261
|
+
});
|
|
3262
|
+
} else {
|
|
3263
|
+
results.push({
|
|
3264
|
+
rule: "Delayed phase waits >= 3s",
|
|
3265
|
+
status: "pass",
|
|
3266
|
+
message: `${delay}ms delay before loading delayed.js`,
|
|
3267
|
+
file: "scripts/scripts.js"
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
if (hasLoadEager) {
|
|
3272
|
+
const eagerSection = content.match(/loadEager[\s\S]*?(?=function\s|const\s+load|$)/)?.[0] || "";
|
|
3273
|
+
if (/loadSections\s*\(/.test(eagerSection)) {
|
|
3274
|
+
results.push({
|
|
3275
|
+
rule: "loadEager loads only first section",
|
|
3276
|
+
status: "fail",
|
|
3277
|
+
message: "loadEager calls loadSections() \u2014 should only load first section for fast LCP",
|
|
3278
|
+
file: "scripts/scripts.js"
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3283
|
+
const line = lines[i];
|
|
3284
|
+
if (/import\s.*from\s+['"]https?:\/\//i.test(line)) {
|
|
3285
|
+
results.push({
|
|
3286
|
+
rule: "No third-party imports at top level",
|
|
3287
|
+
status: "warn",
|
|
3288
|
+
message: `Third-party import at line ${i + 1} \u2014 move to delayed.js if not critical`,
|
|
3289
|
+
file: "scripts/scripts.js",
|
|
3290
|
+
line: i + 1
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
if (hasLoadEager) {
|
|
3295
|
+
const eagerBody = extractFunctionBody(content, "loadEager");
|
|
3296
|
+
if (eagerBody && /loadHeader|loadFooter/.test(eagerBody)) {
|
|
3297
|
+
results.push({
|
|
3298
|
+
rule: "Header/footer not in loadEager",
|
|
3299
|
+
status: "warn",
|
|
3300
|
+
message: "Header/footer in loadEager delays LCP \u2014 move to loadLazy",
|
|
3301
|
+
file: "scripts/scripts.js"
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
async function checkStylesCss(root, results) {
|
|
3307
|
+
const filePath = path15.join(root, "styles", "styles.css");
|
|
3308
|
+
if (!existsSync14(filePath)) return;
|
|
3309
|
+
const stats = await stat3(filePath);
|
|
3310
|
+
const sizeKb = stats.size / 1024;
|
|
3311
|
+
results.push({
|
|
3312
|
+
rule: "styles.css size within LCP budget",
|
|
3313
|
+
status: sizeKb > 40 ? "warn" : "pass",
|
|
3314
|
+
message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
|
|
3315
|
+
file: "styles/styles.css"
|
|
3316
|
+
});
|
|
3317
|
+
const content = await readFile11(filePath, "utf-8");
|
|
3318
|
+
if (/@import\s/.test(content)) {
|
|
3319
|
+
results.push({
|
|
3320
|
+
rule: "No @import in styles.css",
|
|
3321
|
+
status: "warn",
|
|
3322
|
+
message: "@import creates a request chain \u2014 inline the CSS or use lazy-styles.css",
|
|
3323
|
+
file: "styles/styles.css"
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
async function checkDelayedJs(root, results) {
|
|
3328
|
+
const filePath = path15.join(root, "scripts", "delayed.js");
|
|
3329
|
+
if (!existsSync14(filePath)) {
|
|
3330
|
+
results.push({
|
|
3331
|
+
rule: "scripts/delayed.js exists",
|
|
3332
|
+
status: "info",
|
|
3333
|
+
message: "No delayed.js \u2014 third-party scripts should go here"
|
|
3334
|
+
});
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const content = await readFile11(filePath, "utf-8");
|
|
3338
|
+
const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
3339
|
+
if (!stripped) {
|
|
3340
|
+
results.push({
|
|
3341
|
+
rule: "delayed.js has content",
|
|
3342
|
+
status: "info",
|
|
3343
|
+
message: "delayed.js is empty \u2014 add analytics/martech here",
|
|
3344
|
+
file: "scripts/delayed.js"
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
async function checkBlocks(root, results) {
|
|
3349
|
+
const blocksDir = path15.join(root, "blocks");
|
|
3350
|
+
if (!existsSync14(blocksDir)) return;
|
|
3351
|
+
const entries = await readdir3(blocksDir, { withFileTypes: true });
|
|
3352
|
+
for (const entry of entries) {
|
|
3353
|
+
if (!entry.isDirectory()) continue;
|
|
3354
|
+
const blockName = entry.name;
|
|
3355
|
+
const jsFile = path15.join(blocksDir, blockName, `${blockName}.js`);
|
|
3356
|
+
if (!existsSync14(jsFile)) continue;
|
|
3357
|
+
const content = await readFile11(jsFile, "utf-8");
|
|
3358
|
+
const lines = content.split("\n");
|
|
3359
|
+
const heavyLibs = [
|
|
3360
|
+
"jquery",
|
|
3361
|
+
"lodash",
|
|
3362
|
+
"moment",
|
|
3363
|
+
"rxjs",
|
|
3364
|
+
"d3",
|
|
3365
|
+
"three",
|
|
3366
|
+
"gsap",
|
|
3367
|
+
"anime",
|
|
3368
|
+
"chart.js",
|
|
3369
|
+
"swiper"
|
|
3370
|
+
];
|
|
3371
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3372
|
+
const line = lines[i].toLowerCase();
|
|
3373
|
+
for (const lib of heavyLibs) {
|
|
3374
|
+
if (line.includes("from") && line.includes(lib) && /import\s/.test(line)) {
|
|
3375
|
+
results.push({
|
|
3376
|
+
rule: "No heavy imports in blocks",
|
|
3377
|
+
status: "warn",
|
|
3378
|
+
message: `Imports "${lib}" \u2014 heavy library delays section loading`,
|
|
3379
|
+
file: `blocks/${blockName}/${blockName}.js`,
|
|
3380
|
+
line: i + 1
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (/document\.write/i.test(content)) {
|
|
3386
|
+
results.push({
|
|
3387
|
+
rule: "No document.write in blocks",
|
|
3388
|
+
status: "fail",
|
|
3389
|
+
message: "document.write blocks parsing and can break the page",
|
|
3390
|
+
file: `blocks/${blockName}/${blockName}.js`
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const stats = await stat3(jsFile);
|
|
3394
|
+
const sizeKb = stats.size / 1024;
|
|
3395
|
+
if (sizeKb > 20) {
|
|
3396
|
+
results.push({
|
|
3397
|
+
rule: "Block JS size reasonable",
|
|
3398
|
+
status: "warn",
|
|
3399
|
+
message: `${sizeKb.toFixed(1)} KB \u2014 consider splitting or lazy-loading parts`,
|
|
3400
|
+
file: `blocks/${blockName}/${blockName}.js`
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
for (const entry of entries) {
|
|
3405
|
+
if (!entry.isDirectory()) continue;
|
|
3406
|
+
const blockName = entry.name;
|
|
3407
|
+
const cssFile = path15.join(blocksDir, blockName, `${blockName}.css`);
|
|
3408
|
+
if (!existsSync14(cssFile)) continue;
|
|
3409
|
+
const content = await readFile11(cssFile, "utf-8");
|
|
3410
|
+
const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
|
|
3411
|
+
for (const selectorMatch of content.matchAll(selectorPattern)) {
|
|
3412
|
+
const selector = selectorMatch[1];
|
|
3413
|
+
if (!["body", "html", "main"].includes(selector)) {
|
|
3414
|
+
results.push({
|
|
3415
|
+
rule: "Block CSS is scoped",
|
|
3416
|
+
status: "warn",
|
|
3417
|
+
message: `Bare element selector "${selector}" \u2014 may leak styles and cause CLS. Use .${blockName} prefix`,
|
|
3418
|
+
file: `blocks/${blockName}/${blockName}.css`
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
async function checkAemJs(root, results) {
|
|
3425
|
+
const filePath = path15.join(root, "scripts", "aem.js");
|
|
3426
|
+
if (!existsSync14(filePath)) {
|
|
3427
|
+
results.push({
|
|
3428
|
+
rule: "scripts/aem.js exists",
|
|
3429
|
+
status: "warn",
|
|
3430
|
+
message: "Missing aem.js (or lib-franklin.js) \u2014 the EDS framework library"
|
|
3431
|
+
});
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
results.push({
|
|
3435
|
+
rule: "aem.js present",
|
|
3436
|
+
status: "pass",
|
|
3437
|
+
message: "Found \u2014 remember: never modify this file",
|
|
3438
|
+
file: "scripts/aem.js"
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
function extractFunctionBody(source, funcName) {
|
|
3442
|
+
const pattern = new RegExp(`(?:async\\s+)?function\\s+${funcName}\\s*\\([^)]*\\)\\s*\\{`);
|
|
3443
|
+
const match = pattern.exec(source);
|
|
3444
|
+
if (!match) return null;
|
|
3445
|
+
let depth = 1;
|
|
3446
|
+
let i = match.index + match[0].length;
|
|
3447
|
+
const start = i;
|
|
3448
|
+
while (i < source.length && depth > 0) {
|
|
3449
|
+
if (source[i] === "{") depth++;
|
|
3450
|
+
else if (source[i] === "}") depth--;
|
|
3451
|
+
i++;
|
|
3452
|
+
}
|
|
3453
|
+
return source.slice(start, i - 1);
|
|
3454
|
+
}
|
|
3455
|
+
function printResults(results) {
|
|
3456
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3457
|
+
for (const r of results) {
|
|
3458
|
+
const key = r.file || "general";
|
|
3459
|
+
const bucket = grouped.get(key) ?? [];
|
|
3460
|
+
bucket.push(r);
|
|
3461
|
+
grouped.set(key, bucket);
|
|
3462
|
+
}
|
|
3463
|
+
for (const [file, fileResults] of grouped) {
|
|
3464
|
+
logger.info(` ${icon.bullet} ${chalk10.underline(file)}`);
|
|
3465
|
+
for (const r of fileResults) {
|
|
3466
|
+
const loc = r.line ? chalk10.dim(`:${r.line}`) : "";
|
|
3467
|
+
logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
|
|
3468
|
+
if (r.status !== "pass") {
|
|
3469
|
+
logger.info(chalk10.dim(` ${r.message}`));
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
logger.info("");
|
|
3473
|
+
}
|
|
3474
|
+
const counts = {
|
|
3475
|
+
pass: results.filter((r) => r.status === "pass").length,
|
|
3476
|
+
warn: results.filter((r) => r.status === "warn").length,
|
|
3477
|
+
fail: results.filter((r) => r.status === "fail").length,
|
|
3478
|
+
info: results.filter((r) => r.status === "info").length
|
|
3479
|
+
};
|
|
3480
|
+
logger.info(box([summary(counts)]));
|
|
3481
|
+
if (counts.fail > 0) process.exitCode = 1;
|
|
3482
|
+
}
|
|
3483
|
+
var init_audit_loading = __esm({
|
|
3484
|
+
"src/commands/audit-loading.ts"() {
|
|
3485
|
+
"use strict";
|
|
3486
|
+
init_esm_shims();
|
|
3487
|
+
init_logger();
|
|
3488
|
+
init_project();
|
|
3489
|
+
init_ui();
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
// src/commands/audit-security.ts
|
|
3494
|
+
var audit_security_exports = {};
|
|
3495
|
+
__export(audit_security_exports, {
|
|
3496
|
+
auditSecurity: () => auditSecurity
|
|
3497
|
+
});
|
|
3498
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3499
|
+
import { readFile as readFile12, readdir as readdir4 } from "fs/promises";
|
|
3500
|
+
import path16 from "path";
|
|
3501
|
+
import chalk11 from "chalk";
|
|
3502
|
+
async function* walk(dir) {
|
|
3503
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3504
|
+
for (const entry of entries) {
|
|
3505
|
+
const full = path16.join(dir, entry.name);
|
|
3506
|
+
if (entry.isDirectory()) {
|
|
3507
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
3508
|
+
yield* walk(full);
|
|
3509
|
+
} else if (entry.isFile()) {
|
|
3510
|
+
yield full;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function scanContent(content, relFile, ext) {
|
|
3515
|
+
const findings = [];
|
|
3516
|
+
const lines = content.split("\n");
|
|
3517
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3518
|
+
const line = lines[i];
|
|
3519
|
+
let innerHtmlDynamicHit = false;
|
|
3520
|
+
for (const rule of RULES) {
|
|
3521
|
+
if (!rule.ext.includes(ext)) continue;
|
|
3522
|
+
if (rule.id === "innerhtml-static" && innerHtmlDynamicHit) continue;
|
|
3523
|
+
if (!rule.re.test(line)) continue;
|
|
3524
|
+
if (rule.extra && !rule.extra(line)) continue;
|
|
3525
|
+
if (rule.id === "innerhtml-dynamic") innerHtmlDynamicHit = true;
|
|
3526
|
+
findings.push({
|
|
3527
|
+
severity: rule.severity,
|
|
3528
|
+
rule: rule.id,
|
|
3529
|
+
message: rule.message,
|
|
3530
|
+
file: relFile,
|
|
3531
|
+
line: i + 1,
|
|
3532
|
+
snippet: line.trim().slice(0, 100)
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
return findings;
|
|
3537
|
+
}
|
|
3538
|
+
async function auditDependencies(root) {
|
|
3539
|
+
if (!existsSync15(path16.join(root, "package.json"))) {
|
|
3540
|
+
return { status: "skipped", note: "no package.json" };
|
|
3541
|
+
}
|
|
3542
|
+
const hasLock = existsSync15(path16.join(root, "package-lock.json")) || existsSync15(path16.join(root, "npm-shrinkwrap.json"));
|
|
3543
|
+
if (!hasLock && !existsSync15(path16.join(root, "node_modules"))) {
|
|
3544
|
+
return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
|
|
3545
|
+
}
|
|
3546
|
+
try {
|
|
3547
|
+
const { execa } = await import("execa");
|
|
3548
|
+
const { stdout } = await execa("npm", ["audit", "--json"], {
|
|
3549
|
+
cwd: root,
|
|
3550
|
+
reject: false
|
|
3551
|
+
});
|
|
3552
|
+
const data = JSON.parse(stdout);
|
|
3553
|
+
const v = data?.metadata?.vulnerabilities ?? {};
|
|
3554
|
+
const counts = {
|
|
3555
|
+
critical: v.critical ?? 0,
|
|
3556
|
+
high: v.high ?? 0,
|
|
3557
|
+
moderate: v.moderate ?? 0,
|
|
3558
|
+
low: v.low ?? 0
|
|
3559
|
+
};
|
|
3560
|
+
const total = counts.critical + counts.high + counts.moderate + counts.low;
|
|
3561
|
+
return { status: total > 0 ? "vulnerable" : "ok", counts };
|
|
3562
|
+
} catch (err) {
|
|
3563
|
+
return { status: "skipped", note: `npm audit failed: ${err.message}` };
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
async function auditSecurity(options = {}) {
|
|
3567
|
+
const projectRoot = findProjectRoot();
|
|
3568
|
+
if (!projectRoot) {
|
|
3569
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
3570
|
+
process.exitCode = 1;
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const findings = [];
|
|
3574
|
+
for await (const file of walk(projectRoot)) {
|
|
3575
|
+
const ext = path16.extname(file).slice(1).toLowerCase();
|
|
3576
|
+
if (!SCAN_EXT.has(ext)) continue;
|
|
3577
|
+
const rel = path16.relative(projectRoot, file);
|
|
3578
|
+
if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
|
|
3579
|
+
const content = await readFile12(file, "utf-8");
|
|
3580
|
+
findings.push(...scanContent(content, rel, ext));
|
|
3581
|
+
}
|
|
3582
|
+
const deps = await auditDependencies(projectRoot);
|
|
3583
|
+
const bySeverity = (s) => findings.filter((f) => f.severity === s);
|
|
3584
|
+
const codeCounts = {
|
|
3585
|
+
critical: bySeverity("critical").length,
|
|
3586
|
+
high: bySeverity("high").length,
|
|
3587
|
+
moderate: bySeverity("moderate").length,
|
|
3588
|
+
low: bySeverity("low").length
|
|
3589
|
+
};
|
|
3590
|
+
const hardFail = codeCounts.critical > 0 || codeCounts.high > 0 || (deps.counts?.critical ?? 0) > 0 || (deps.counts?.high ?? 0) > 0;
|
|
3591
|
+
if (options.json) {
|
|
3592
|
+
console.log(
|
|
3593
|
+
JSON.stringify(
|
|
3594
|
+
{ projectRoot, dependencies: deps, findings, summary: { code: codeCounts } },
|
|
3595
|
+
null,
|
|
3596
|
+
2
|
|
3597
|
+
)
|
|
3598
|
+
);
|
|
3599
|
+
if (hardFail) process.exitCode = 1;
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
logger.logoOnce(logo("Security Audit"));
|
|
3603
|
+
logger.info(heading("Dependencies", "npm audit"));
|
|
3604
|
+
if (deps.status === "skipped") {
|
|
3605
|
+
logger.info(statusLine("info", "npm audit", deps.note ?? "skipped"));
|
|
3606
|
+
} else if (deps.status === "ok") {
|
|
3607
|
+
logger.info(statusLine("pass", "npm audit", "no known vulnerabilities"));
|
|
3608
|
+
} else {
|
|
3609
|
+
const c = deps.counts ?? { critical: 0, high: 0, moderate: 0, low: 0 };
|
|
3610
|
+
logger.info(
|
|
3611
|
+
statusLine(
|
|
3612
|
+
c.critical + c.high > 0 ? "fail" : "warn",
|
|
3613
|
+
"npm audit",
|
|
3614
|
+
`${c.critical} critical, ${c.high} high, ${c.moderate} moderate, ${c.low} low \u2014 run \`npm audit fix\``
|
|
3615
|
+
)
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
logger.info(heading("Exploit scan", "XSS \xB7 secrets \xB7 injection \xB7 tabnabbing"));
|
|
3619
|
+
if (findings.length === 0) {
|
|
3620
|
+
logger.info(statusLine("pass", "code", "no risky patterns found"));
|
|
3621
|
+
} else {
|
|
3622
|
+
const sorted = [...findings].sort(
|
|
3623
|
+
(a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
|
|
3624
|
+
);
|
|
3625
|
+
for (const f of sorted) {
|
|
3626
|
+
const meta = SEVERITY_META[f.severity];
|
|
3627
|
+
const loc = chalk11.dim(`${f.file}:${f.line}`);
|
|
3628
|
+
logger.info(` ${statusIcon(meta.status)} ${chalk11.bold(meta.label)} ${f.message} ${loc}`);
|
|
3629
|
+
logger.info(chalk11.dim(` ${f.snippet}`));
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const summaryLine = summary({
|
|
3633
|
+
fail: codeCounts.critical + codeCounts.high + (deps.counts?.critical ?? 0) + (deps.counts?.high ?? 0),
|
|
3634
|
+
warn: codeCounts.moderate + (deps.counts?.moderate ?? 0),
|
|
3635
|
+
info: codeCounts.low + (deps.counts?.low ?? 0)
|
|
3636
|
+
});
|
|
3637
|
+
logger.info("");
|
|
3638
|
+
logger.info(
|
|
3639
|
+
box([summaryLine || `${icon.pass} no vulnerabilities or risky patterns found`])
|
|
3640
|
+
);
|
|
3641
|
+
if (hardFail) process.exitCode = 1;
|
|
3642
|
+
}
|
|
3643
|
+
var RULES, SEVERITY_ORDER, SEVERITY_META, IGNORE_DIRS, SCAN_EXT;
|
|
3644
|
+
var init_audit_security = __esm({
|
|
3645
|
+
"src/commands/audit-security.ts"() {
|
|
3646
|
+
"use strict";
|
|
3647
|
+
init_esm_shims();
|
|
3648
|
+
init_logger();
|
|
3649
|
+
init_project();
|
|
3650
|
+
init_ui();
|
|
3651
|
+
RULES = [
|
|
3652
|
+
{
|
|
3653
|
+
id: "eval",
|
|
3654
|
+
severity: "critical",
|
|
3655
|
+
re: /\beval\s*\(/,
|
|
3656
|
+
message: "eval() executes arbitrary code \u2014 prime XSS / RCE vector",
|
|
3657
|
+
ext: ["js", "mjs", "html"]
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
id: "aws-key",
|
|
3661
|
+
severity: "critical",
|
|
3662
|
+
re: /\bAKIA[0-9A-Z]{16}\b/,
|
|
3663
|
+
message: "Hardcoded AWS access key id",
|
|
3664
|
+
ext: ["js", "mjs", "html", "json"]
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
id: "private-key",
|
|
3668
|
+
severity: "critical",
|
|
3669
|
+
re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,
|
|
3670
|
+
message: "Embedded private key material",
|
|
3671
|
+
ext: ["js", "mjs", "html", "json", "pem", "txt"]
|
|
3672
|
+
},
|
|
3673
|
+
{
|
|
3674
|
+
id: "new-function",
|
|
3675
|
+
severity: "high",
|
|
3676
|
+
re: /\bnew\s+Function\s*\(/,
|
|
3677
|
+
message: "new Function() runs strings as code",
|
|
3678
|
+
ext: ["js", "mjs", "html"]
|
|
3679
|
+
},
|
|
3680
|
+
{
|
|
3681
|
+
id: "document-write",
|
|
3682
|
+
severity: "high",
|
|
3683
|
+
re: /document\.write(?:ln)?\s*\(/,
|
|
3684
|
+
message: "document.write() enables XSS and blocks parsing",
|
|
3685
|
+
ext: ["js", "mjs", "html"]
|
|
3686
|
+
},
|
|
3687
|
+
{
|
|
3688
|
+
id: "innerhtml-dynamic",
|
|
3689
|
+
severity: "high",
|
|
3690
|
+
re: /\.(?:inner|outer)HTML\s*=\s*[^;\n]*(?:\$\{|\+|concat)/,
|
|
3691
|
+
message: "Dynamic innerHTML assignment \u2014 sanitize or use textContent / DOM APIs",
|
|
3692
|
+
ext: ["js", "mjs"]
|
|
3693
|
+
},
|
|
3694
|
+
{
|
|
3695
|
+
id: "insert-adjacent-html",
|
|
3696
|
+
severity: "high",
|
|
3697
|
+
re: /insertAdjacentHTML\s*\(/,
|
|
3698
|
+
message: "insertAdjacentHTML with untrusted data is an XSS vector",
|
|
3699
|
+
ext: ["js", "mjs"]
|
|
3700
|
+
},
|
|
3701
|
+
{
|
|
3702
|
+
id: "js-url",
|
|
3703
|
+
severity: "high",
|
|
3704
|
+
re: /(?:href|src)\s*=\s*["']?\s*javascript:/i,
|
|
3705
|
+
message: "javascript: URL \u2014 XSS vector",
|
|
3706
|
+
ext: ["js", "mjs", "html"]
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
id: "secret",
|
|
3710
|
+
severity: "high",
|
|
3711
|
+
re: /(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|client[_-]?secret|passwd|password)\s*[:=]\s*["'][^"']{8,}["']/i,
|
|
3712
|
+
message: "Possible hardcoded secret / credential",
|
|
3713
|
+
ext: ["js", "mjs", "html", "json"],
|
|
3714
|
+
// Skip obvious placeholders.
|
|
3715
|
+
extra: (line) => !/(example|placeholder|your[-_]|changeme|xxxx|<[^>]+>|\$\{)/i.test(line)
|
|
3716
|
+
},
|
|
3717
|
+
{
|
|
3718
|
+
id: "bearer-token",
|
|
3719
|
+
severity: "high",
|
|
3720
|
+
re: /Bearer\s+[A-Za-z0-9\-._~+/]{20,}/,
|
|
3721
|
+
message: "Hardcoded bearer token",
|
|
3722
|
+
ext: ["js", "mjs", "html", "json"]
|
|
3723
|
+
},
|
|
3724
|
+
{
|
|
3725
|
+
id: "innerhtml-static",
|
|
3726
|
+
severity: "moderate",
|
|
3727
|
+
re: /\.(?:inner|outer)HTML\s*=/,
|
|
3728
|
+
message: "innerHTML assignment \u2014 confirm the content is trusted / sanitized",
|
|
3729
|
+
ext: ["js", "mjs"]
|
|
3730
|
+
},
|
|
3731
|
+
{
|
|
3732
|
+
id: "insecure-url",
|
|
3733
|
+
severity: "moderate",
|
|
3734
|
+
re: /(?:href|src)\s*=\s*["']http:\/\/(?!localhost|127\.0\.0\.1)/i,
|
|
3735
|
+
message: "Insecure http:// resource \u2014 mixed content, use https://",
|
|
3736
|
+
ext: ["html", "js", "mjs"]
|
|
3737
|
+
},
|
|
3738
|
+
{
|
|
3739
|
+
id: "target-blank",
|
|
3740
|
+
severity: "moderate",
|
|
3741
|
+
re: /target\s*=\s*["']_blank["']/,
|
|
3742
|
+
message: 'target="_blank" without rel="noopener" \u2014 reverse tabnabbing',
|
|
3743
|
+
ext: ["html", "js", "mjs"],
|
|
3744
|
+
extra: (line) => !/rel\s*=\s*["'][^"']*noopener/i.test(line)
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
id: "postmessage-wildcard",
|
|
3748
|
+
severity: "moderate",
|
|
3749
|
+
re: /postMessage\s*\([^,]+,\s*["']\*["']\s*\)/,
|
|
3750
|
+
message: "postMessage to '*' origin \u2014 restrict the target origin",
|
|
3751
|
+
ext: ["js", "mjs"]
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
id: "inline-handler",
|
|
3755
|
+
severity: "low",
|
|
3756
|
+
re: /\son[a-z]+\s*=\s*["']/i,
|
|
3757
|
+
message: "Inline event handler \u2014 move to addEventListener (CSP-unfriendly)",
|
|
3758
|
+
ext: ["html"]
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
id: "console-log",
|
|
3762
|
+
severity: "low",
|
|
3763
|
+
re: /console\.(?:log|debug)\s*\(/,
|
|
3764
|
+
message: "console.log / debug left in code \u2014 potential info leak",
|
|
3765
|
+
ext: ["js", "mjs"]
|
|
3766
|
+
}
|
|
3767
|
+
];
|
|
3768
|
+
SEVERITY_ORDER = ["critical", "high", "moderate", "low"];
|
|
3769
|
+
SEVERITY_META = {
|
|
3770
|
+
critical: { status: "fail", label: "CRIT" },
|
|
3771
|
+
high: { status: "fail", label: "HIGH" },
|
|
3772
|
+
moderate: { status: "warn", label: "MOD " },
|
|
3773
|
+
low: { status: "info", label: "LOW " }
|
|
3774
|
+
};
|
|
3775
|
+
IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".vscode", "coverage"]);
|
|
3776
|
+
SCAN_EXT = /* @__PURE__ */ new Set(["js", "mjs", "html", "json", "pem", "txt"]);
|
|
3777
|
+
}
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
// src/lib/admin-api.ts
|
|
3781
|
+
async function adminRequest(action, config, pagePath) {
|
|
3782
|
+
const ref = config.ref || "main";
|
|
3783
|
+
const normalizedPath = pagePath.startsWith("/") ? pagePath : `/${pagePath}`;
|
|
3784
|
+
const url = `${ADMIN_BASE}/${action}/${config.org}/${config.site}/${ref}${normalizedPath}`;
|
|
3785
|
+
logger.debug(`POST ${url}`);
|
|
3786
|
+
const response = await fetch(url, {
|
|
3787
|
+
method: "POST",
|
|
3788
|
+
headers: { "Content-Type": "application/json" }
|
|
3789
|
+
});
|
|
3790
|
+
const body = await response.json().catch(() => ({}));
|
|
3791
|
+
const data = body;
|
|
3792
|
+
if (!response.ok) {
|
|
3793
|
+
return {
|
|
3794
|
+
ok: false,
|
|
3795
|
+
status: response.status,
|
|
3796
|
+
path: normalizedPath,
|
|
3797
|
+
message: body.message || `HTTP ${response.status}`
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
return {
|
|
3801
|
+
ok: true,
|
|
3802
|
+
status: response.status,
|
|
3803
|
+
path: normalizedPath,
|
|
3804
|
+
previewUrl: data.preview?.url,
|
|
3805
|
+
liveUrl: data.live?.url
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
async function previewPage(config, pagePath) {
|
|
3809
|
+
return adminRequest("preview", config, pagePath);
|
|
3810
|
+
}
|
|
3811
|
+
async function publishPage(config, pagePath) {
|
|
3812
|
+
return adminRequest("live", config, pagePath);
|
|
3813
|
+
}
|
|
3814
|
+
async function previewPages(config, paths) {
|
|
3815
|
+
return Promise.all(paths.map((p) => previewPage(config, p)));
|
|
3816
|
+
}
|
|
3817
|
+
async function publishPages(config, paths) {
|
|
3818
|
+
return Promise.all(paths.map((p) => publishPage(config, p)));
|
|
3819
|
+
}
|
|
3820
|
+
var ADMIN_BASE;
|
|
3821
|
+
var init_admin_api = __esm({
|
|
3822
|
+
"src/lib/admin-api.ts"() {
|
|
3823
|
+
"use strict";
|
|
3824
|
+
init_esm_shims();
|
|
3825
|
+
init_logger();
|
|
3826
|
+
ADMIN_BASE = "https://admin.hlx.page";
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3830
|
+
// src/commands/preview.ts
|
|
3831
|
+
var preview_exports2 = {};
|
|
3832
|
+
__export(preview_exports2, {
|
|
3833
|
+
preview: () => preview,
|
|
3834
|
+
resolveAdminConfig: () => resolveAdminConfig
|
|
3835
|
+
});
|
|
3836
|
+
import chalk12 from "chalk";
|
|
3837
|
+
import ora2 from "ora";
|
|
3838
|
+
async function preview(paths, options) {
|
|
3839
|
+
const projectRoot = findProjectRoot();
|
|
3840
|
+
const config = await loadConfig(projectRoot);
|
|
3841
|
+
const adminConfig = resolveAdminConfig(config, options);
|
|
3842
|
+
if (!adminConfig) return;
|
|
3843
|
+
const spinner = ora2(`Previewing ${paths.length} path(s)...`).start();
|
|
3844
|
+
const results = paths.length === 1 ? [await previewPage(adminConfig, paths[0])] : await previewPages(adminConfig, paths);
|
|
3845
|
+
spinner.stop();
|
|
3846
|
+
let hasErrors = false;
|
|
3847
|
+
for (const r of results) {
|
|
3848
|
+
if (r.ok) {
|
|
3849
|
+
logger.success(`${r.path} \u2192 ${chalk12.cyan(r.previewUrl || "preview updated")}`);
|
|
3850
|
+
} else {
|
|
3851
|
+
logger.error(`${r.path} \u2014 ${r.message}`);
|
|
3852
|
+
hasErrors = true;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
if (hasErrors) process.exitCode = 1;
|
|
3856
|
+
}
|
|
3857
|
+
function resolveAdminConfig(config, options) {
|
|
3858
|
+
const org = options.org || config.admin?.org;
|
|
3859
|
+
const site = options.site || config.admin?.site;
|
|
3860
|
+
const ref = options.ref || config.admin?.ref || "main";
|
|
3861
|
+
if (!org || !site) {
|
|
3862
|
+
logger.error("Missing --org and --site. Set them via flags or in .edsrc.json:");
|
|
3863
|
+
logger.info(chalk12.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
|
|
3864
|
+
process.exitCode = 1;
|
|
3865
|
+
return void 0;
|
|
3866
|
+
}
|
|
3867
|
+
return { org, site, ref };
|
|
3868
|
+
}
|
|
3869
|
+
var init_preview2 = __esm({
|
|
3870
|
+
"src/commands/preview.ts"() {
|
|
3871
|
+
"use strict";
|
|
3872
|
+
init_esm_shims();
|
|
3873
|
+
init_admin_api();
|
|
3874
|
+
init_config();
|
|
3875
|
+
init_logger();
|
|
3876
|
+
init_project();
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
|
|
3880
|
+
// src/commands/publish.ts
|
|
3881
|
+
var publish_exports = {};
|
|
3882
|
+
__export(publish_exports, {
|
|
3883
|
+
publish: () => publish
|
|
3884
|
+
});
|
|
3885
|
+
import chalk13 from "chalk";
|
|
3886
|
+
import ora3 from "ora";
|
|
3887
|
+
async function publish(paths, options) {
|
|
3888
|
+
const projectRoot = findProjectRoot();
|
|
3889
|
+
const config = await loadConfig(projectRoot);
|
|
3890
|
+
const adminConfig = resolveAdminConfig(config, options);
|
|
3891
|
+
if (!adminConfig) return;
|
|
3892
|
+
const spinner = ora3(`Publishing ${paths.length} path(s)...`).start();
|
|
3893
|
+
const results = paths.length === 1 ? [await publishPage(adminConfig, paths[0])] : await publishPages(adminConfig, paths);
|
|
3894
|
+
spinner.stop();
|
|
3895
|
+
let hasErrors = false;
|
|
3896
|
+
for (const r of results) {
|
|
3897
|
+
if (r.ok) {
|
|
3898
|
+
logger.success(`${r.path} \u2192 ${chalk13.cyan(r.liveUrl || "published")}`);
|
|
3899
|
+
} else {
|
|
3900
|
+
logger.error(`${r.path} \u2014 ${r.message}`);
|
|
3901
|
+
hasErrors = true;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (hasErrors) process.exitCode = 1;
|
|
3905
|
+
}
|
|
3906
|
+
var init_publish = __esm({
|
|
3907
|
+
"src/commands/publish.ts"() {
|
|
3908
|
+
"use strict";
|
|
3909
|
+
init_esm_shims();
|
|
3910
|
+
init_admin_api();
|
|
3911
|
+
init_config();
|
|
3912
|
+
init_logger();
|
|
3913
|
+
init_project();
|
|
3914
|
+
init_preview2();
|
|
3915
|
+
}
|
|
3916
|
+
});
|
|
3917
|
+
|
|
3918
|
+
// src/commands/admin.ts
|
|
3919
|
+
var admin_exports = {};
|
|
3920
|
+
__export(admin_exports, {
|
|
3921
|
+
adminWizard: () => adminWizard
|
|
3922
|
+
});
|
|
3923
|
+
async function adminWizard(initialOp) {
|
|
3924
|
+
const projectRoot = findProjectRoot();
|
|
3925
|
+
if (!projectRoot) {
|
|
3926
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
3927
|
+
process.exitCode = 1;
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
logger.logoOnce(logo("Admin API \u2014 preview / publish"));
|
|
3931
|
+
const { select, input, confirm } = await import("@inquirer/prompts");
|
|
3932
|
+
const config = await loadConfig(projectRoot);
|
|
3933
|
+
const admin = config.admin ?? {};
|
|
3934
|
+
const op = initialOp ?? await select({
|
|
3935
|
+
message: "What do you want to do?",
|
|
3936
|
+
choices: [
|
|
3937
|
+
{ name: "Preview pages (staging)", value: "preview" },
|
|
3938
|
+
{ name: "Publish pages (go live)", value: "publish" }
|
|
3939
|
+
]
|
|
3940
|
+
});
|
|
3941
|
+
const org = (await input({ message: "GitHub org / owner", default: admin.org })).trim();
|
|
3942
|
+
const site = (await input({ message: "Repository / site", default: admin.site })).trim();
|
|
3943
|
+
const ref = (await input({ message: "Git ref", default: admin.ref || "main" })).trim();
|
|
3944
|
+
const pathsRaw = await input({
|
|
3945
|
+
message: "Paths (space- or comma-separated)",
|
|
3946
|
+
default: "/"
|
|
3947
|
+
});
|
|
3948
|
+
const paths = pathsRaw.split(/[\s,]+/).filter(Boolean);
|
|
3949
|
+
if (!org || !site || paths.length === 0) {
|
|
3950
|
+
logger.error("org, site and at least one path are required.");
|
|
3951
|
+
process.exitCode = 1;
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
logger.info(heading("Summary"));
|
|
3955
|
+
const w = columnWidth(["operation", "org", "site", "ref", "paths"]);
|
|
3956
|
+
logger.info(accentLine("operation", op, w));
|
|
3957
|
+
logger.info(accentLine("org", org, w));
|
|
3958
|
+
logger.info(accentLine("site", site, w));
|
|
3959
|
+
logger.info(accentLine("ref", ref, w));
|
|
3960
|
+
logger.info(accentLine("paths", paths.join(", "), w));
|
|
3961
|
+
logger.info("");
|
|
3962
|
+
if (op === "publish") {
|
|
3963
|
+
const go = await confirm({
|
|
3964
|
+
message: `Publish ${paths.length} path(s) to LIVE?`,
|
|
3965
|
+
default: false
|
|
3966
|
+
});
|
|
3967
|
+
if (!go) {
|
|
3968
|
+
logger.info("Cancelled.");
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
const options = { org, site, ref };
|
|
3973
|
+
if (op === "preview") {
|
|
3974
|
+
const { preview: preview2 } = await Promise.resolve().then(() => (init_preview2(), preview_exports2));
|
|
3975
|
+
await preview2(paths, options);
|
|
3976
|
+
} else {
|
|
3977
|
+
const { publish: publish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
3978
|
+
await publish2(paths, options);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
var init_admin = __esm({
|
|
3982
|
+
"src/commands/admin.ts"() {
|
|
3983
|
+
"use strict";
|
|
3984
|
+
init_esm_shims();
|
|
3985
|
+
init_config();
|
|
3986
|
+
init_logger();
|
|
3987
|
+
init_project();
|
|
3988
|
+
init_ui();
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3992
|
+
// src/commands/menu.ts
|
|
3993
|
+
var menu_exports = {};
|
|
3994
|
+
__export(menu_exports, {
|
|
3995
|
+
mainMenu: () => mainMenu
|
|
3996
|
+
});
|
|
3997
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3998
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
3999
|
+
import path17 from "path";
|
|
4000
|
+
async function listBlockNames(projectRoot) {
|
|
4001
|
+
const dir = path17.join(projectRoot, "blocks");
|
|
4002
|
+
if (!existsSync16(dir)) return [];
|
|
4003
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4004
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4005
|
+
}
|
|
4006
|
+
async function mainMenu() {
|
|
4007
|
+
const { select, input } = await import("@inquirer/prompts");
|
|
4008
|
+
logger.logoOnce(logo("What do you want to do?"));
|
|
4009
|
+
const action = await select({
|
|
4010
|
+
message: "Choose an action",
|
|
4011
|
+
choices: [
|
|
4012
|
+
{ name: "Generate a block from a design (Figma/Stitch/Canva/Sketch)", value: "from-design" },
|
|
4013
|
+
{ name: "Scaffold Universal Editor components", value: "scaffold" },
|
|
4014
|
+
{ name: "Create an empty block", value: "create" },
|
|
4015
|
+
{ name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
|
|
4016
|
+
{ name: "Preview a block in the browser", value: "preview-block" },
|
|
4017
|
+
{ name: "Preview / publish pages (Admin API)", value: "admin" },
|
|
4018
|
+
{ name: "Audit the project", value: "audit" },
|
|
4019
|
+
{ name: "Configure Figma MCP", value: "figma-setup" }
|
|
4020
|
+
]
|
|
4021
|
+
});
|
|
4022
|
+
switch (action) {
|
|
4023
|
+
case "from-design": {
|
|
4024
|
+
const { blockFromDesign: blockFromDesign2 } = await Promise.resolve().then(() => (init_from_design(), from_design_exports));
|
|
4025
|
+
await blockFromDesign2(void 0, {});
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
case "scaffold": {
|
|
4029
|
+
const { scaffoldInteractive: scaffoldInteractive2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
|
|
4030
|
+
await scaffoldInteractive2();
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
case "create": {
|
|
4034
|
+
const name = await input({
|
|
4035
|
+
message: "Block name",
|
|
4036
|
+
validate: (v) => kebabCaseRegex.test(v.trim()) || "Use kebab-case, e.g. hero-banner"
|
|
4037
|
+
});
|
|
4038
|
+
const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));
|
|
4039
|
+
await createBlock2(name.trim(), {});
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
case "integrate": {
|
|
4043
|
+
const { integrate: integrate2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
|
|
4044
|
+
await integrate2(void 0);
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
case "preview-block": {
|
|
4048
|
+
const projectRoot = findProjectRoot();
|
|
4049
|
+
if (!projectRoot) {
|
|
4050
|
+
logger.error("Not inside an EDS project (no fstab.yaml found).");
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
const blocks = await listBlockNames(projectRoot);
|
|
4054
|
+
if (blocks.length === 0) {
|
|
4055
|
+
logger.warn("No blocks found in blocks/.");
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
const name = await select({
|
|
4059
|
+
message: "Which block?",
|
|
4060
|
+
choices: blocks.map((b) => ({ name: b, value: b }))
|
|
4061
|
+
});
|
|
4062
|
+
const { previewBlock: previewBlock2 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
|
|
4063
|
+
await previewBlock2(name, {});
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
case "admin": {
|
|
4067
|
+
const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
|
|
4068
|
+
await adminWizard2();
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
case "audit": {
|
|
4072
|
+
const which = await select({
|
|
4073
|
+
message: "Which audit?",
|
|
4074
|
+
choices: [
|
|
4075
|
+
{ name: "Doctor \u2014 overall project health", value: "doctor" },
|
|
4076
|
+
{ name: "Loading \u2014 three-phase / LCP budget", value: "loading" },
|
|
4077
|
+
{ name: "Security \u2014 vulnerabilities & exploit patterns", value: "security" }
|
|
4078
|
+
]
|
|
4079
|
+
});
|
|
4080
|
+
if (which === "doctor") {
|
|
4081
|
+
const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
4082
|
+
await doctor2({});
|
|
4083
|
+
} else if (which === "loading") {
|
|
4084
|
+
const { auditLoading: auditLoading2 } = await Promise.resolve().then(() => (init_audit_loading(), audit_loading_exports));
|
|
4085
|
+
await auditLoading2({});
|
|
4086
|
+
} else {
|
|
4087
|
+
const { auditSecurity: auditSecurity2 } = await Promise.resolve().then(() => (init_audit_security(), audit_security_exports));
|
|
4088
|
+
await auditSecurity2({});
|
|
4089
|
+
}
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
case "figma-setup": {
|
|
4093
|
+
const { figmaSetup: figmaSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
4094
|
+
await figmaSetup2();
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
var init_menu = __esm({
|
|
4100
|
+
"src/commands/menu.ts"() {
|
|
4101
|
+
"use strict";
|
|
4102
|
+
init_esm_shims();
|
|
4103
|
+
init_logger();
|
|
4104
|
+
init_project();
|
|
4105
|
+
init_schemas();
|
|
4106
|
+
init_ui();
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
|
|
4110
|
+
// src/bin/eds.ts
|
|
4111
|
+
init_esm_shims();
|
|
4112
|
+
init_logger();
|
|
4113
|
+
init_ui();
|
|
4114
|
+
import chalk14 from "chalk";
|
|
4115
|
+
import { Command } from "commander";
|
|
4116
|
+
var program = new Command();
|
|
4117
|
+
program.name("eds").description("CLI for AEM Edge Delivery Services with Figma integration").version("0.4.0").option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-error output").option("--json", "Output in JSON format").option("--no-color", "Disable color output").showHelpAfterError("(add --help for usage)").hook("preAction", (thisCommand) => {
|
|
4118
|
+
const opts = thisCommand.opts();
|
|
4119
|
+
if (opts.verbose) logger.setLevel("debug");
|
|
4120
|
+
if (opts.quiet) logger.setQuiet(true);
|
|
4121
|
+
if (opts.color === false) chalk14.level = 0;
|
|
4122
|
+
});
|
|
4123
|
+
program.addHelpText("beforeAll", () => logo());
|
|
4124
|
+
var block = program.command("block").description("Manage EDS blocks");
|
|
4125
|
+
block.command("create <name>").description("Scaffold a new EDS block (with Universal Editor model by default)").option("--no-ue-model", "Skip the Universal Editor model (generated by default)").action(async (name, options) => {
|
|
4126
|
+
const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));
|
|
4127
|
+
await createBlock2(name, options);
|
|
4128
|
+
});
|
|
4129
|
+
block.command("from-design [design-url]").description(
|
|
4130
|
+
"Generate an EDS block from a design (Figma/Stitch/Canva/Sketch; interactive wizard when run with no URL)"
|
|
4131
|
+
).option("--name <name>", "Override the inferred block name").option("--agent <type>", "Force agent type (claude|cursor|codex|none)").option("--dry-run", "Generate prompt only, do not invoke agent").option("--source <type>", "Content source: document|ue|cf|mixed (default: document)").option("--no-ue-model", "Skip the Universal Editor model (generated by default)").option("--no-screenshot", "Skip the screenshot fetch (fewer tokens; structural verification)").option("--no-serve", "Don't auto-start the live preview after generating").option("--yes", "Skip confirmation prompts").action(async (designUrl, options) => {
|
|
4132
|
+
const { blockFromDesign: blockFromDesign2 } = await Promise.resolve().then(() => (init_from_design(), from_design_exports));
|
|
4133
|
+
await blockFromDesign2(designUrl, options);
|
|
4134
|
+
});
|
|
4135
|
+
block.command("list").description("List all blocks in the project").action(async (options) => {
|
|
4136
|
+
const { listBlocks: listBlocks2 } = await Promise.resolve().then(() => (init_list(), list_exports));
|
|
4137
|
+
await listBlocks2({ ...options, json: program.opts().json });
|
|
4138
|
+
});
|
|
4139
|
+
block.command("preview <name>").description("Serve a live, breakpoint-switchable preview of a block in the browser").option("--port <port>", "Port to serve on (default: 8777, falls back if taken)").option("--widths <list>", "Comma-separated breakpoint widths (e.g. 375,768,1280)").option("--no-open", "Do not open the browser automatically").action(async (name, options) => {
|
|
4140
|
+
const { previewBlock: previewBlock2 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
|
|
4141
|
+
await previewBlock2(name, options);
|
|
4142
|
+
});
|
|
4143
|
+
var scaffold = program.command("scaffold").description("Scaffold standard UE components and blocks").action(async () => {
|
|
4144
|
+
const { scaffoldInteractive: scaffoldInteractive2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
|
|
4145
|
+
await scaffoldInteractive2();
|
|
4146
|
+
});
|
|
4147
|
+
scaffold.command("ue").description("Scaffold default-content UE components + a field-reference block (all 17 fields)").action(async () => {
|
|
4148
|
+
const { scaffoldUe: scaffoldUe2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
|
|
4149
|
+
await scaffoldUe2();
|
|
4150
|
+
});
|
|
4151
|
+
scaffold.command("blocks [names...]").description("Scaffold standard blocks (hero, cards, columns, accordion, embed) with UE models").action(async (names) => {
|
|
4152
|
+
const { scaffoldBlocks: scaffoldBlocks2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
|
|
4153
|
+
await scaffoldBlocks2(names);
|
|
4154
|
+
});
|
|
4155
|
+
program.command("integrate [type]").description("Add a third-party service (GTM, GA4, chat, consent) to scripts/delayed.js").action(async (type) => {
|
|
4156
|
+
const { integrate: integrate2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
|
|
4157
|
+
await integrate2(type);
|
|
4158
|
+
});
|
|
4159
|
+
var figma = program.command("figma").description("Figma MCP integration");
|
|
4160
|
+
figma.command("setup").description("Configure Figma MCP for your AI agent").action(async () => {
|
|
4161
|
+
const { figmaSetup: figmaSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
4162
|
+
await figmaSetup2();
|
|
4163
|
+
});
|
|
4164
|
+
program.command("doctor").description("Audit your EDS project for common issues").action(async () => {
|
|
4165
|
+
const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
4166
|
+
await doctor2({ json: program.opts().json });
|
|
4167
|
+
});
|
|
4168
|
+
var audit = program.command("audit").description("Run project audits");
|
|
4169
|
+
audit.command("loading").description("Check loading order (eager/lazy/delayed phases, LCP budget)").action(async () => {
|
|
4170
|
+
const { auditLoading: auditLoading2 } = await Promise.resolve().then(() => (init_audit_loading(), audit_loading_exports));
|
|
4171
|
+
await auditLoading2({ json: program.opts().json });
|
|
4172
|
+
});
|
|
4173
|
+
audit.command("security").description(
|
|
4174
|
+
"Scan for vulnerabilities (npm audit) and exploit patterns (XSS, secrets, tabnabbing)"
|
|
4175
|
+
).action(async () => {
|
|
4176
|
+
const { auditSecurity: auditSecurity2 } = await Promise.resolve().then(() => (init_audit_security(), audit_security_exports));
|
|
4177
|
+
await auditSecurity2({ json: program.opts().json });
|
|
4178
|
+
});
|
|
4179
|
+
program.command("preview [paths...]").description("Preview pages via Admin API (interactive wizard when run with no paths)").option("--org <org>", "GitHub org/owner").option("--site <site>", "Repository name").option("--ref <ref>", "Git ref (default: main)").action(async (paths, options) => {
|
|
4180
|
+
if (!paths || paths.length === 0) {
|
|
4181
|
+
const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
|
|
4182
|
+
await adminWizard2("preview");
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
const { preview: preview2 } = await Promise.resolve().then(() => (init_preview2(), preview_exports2));
|
|
4186
|
+
await preview2(paths, options);
|
|
4187
|
+
});
|
|
4188
|
+
program.command("publish [paths...]").description("Publish pages to live via Admin API (interactive wizard when run with no paths)").option("--org <org>", "GitHub org/owner").option("--site <site>", "Repository name").option("--ref <ref>", "Git ref (default: main)").action(async (paths, options) => {
|
|
4189
|
+
if (!paths || paths.length === 0) {
|
|
4190
|
+
const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
|
|
4191
|
+
await adminWizard2("publish");
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const { publish: publish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
4195
|
+
await publish2(paths, options);
|
|
4196
|
+
});
|
|
4197
|
+
(async () => {
|
|
4198
|
+
try {
|
|
4199
|
+
if (process.argv.slice(2).length === 0) {
|
|
4200
|
+
if (process.stdout.isTTY) {
|
|
4201
|
+
const { mainMenu: mainMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));
|
|
4202
|
+
await mainMenu2();
|
|
4203
|
+
} else {
|
|
4204
|
+
program.outputHelp();
|
|
4205
|
+
}
|
|
4206
|
+
} else {
|
|
4207
|
+
await program.parseAsync();
|
|
4208
|
+
}
|
|
4209
|
+
} catch (err) {
|
|
4210
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
4211
|
+
process.exitCode = 1;
|
|
4212
|
+
}
|
|
4213
|
+
})();
|
|
4214
|
+
//# sourceMappingURL=eds.js.map
|