design-embed 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +98 -2
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +273 -0
  5. package/dist/core-BLV62TaX.mjs +907 -0
  6. package/dist/index.d.mts +273 -0
  7. package/dist/index.mjs +2 -0
  8. package/package.json +6 -19
  9. package/src/cli.ts +8 -16
  10. package/src/commands/compile.ts +25 -110
  11. package/src/commands/generateTests.ts +14 -96
  12. package/src/commands/init.ts +52 -55
  13. package/src/commands/plugin.ts +6 -21
  14. package/src/config/index.ts +302 -0
  15. package/{node_modules/@design-embed/core/src → src/core}/index.ts +151 -163
  16. package/src/core/nodes.ts +74 -0
  17. package/src/core/plugins/pluginApi.ts +44 -0
  18. package/src/core/types.ts +120 -0
  19. package/src/index.ts +48 -2
  20. package/src/targets/html.ts +621 -0
  21. package/dist/args.js +0 -36
  22. package/dist/cli.js +0 -35
  23. package/dist/commands/check.js +0 -4
  24. package/dist/commands/compile.js +0 -157
  25. package/dist/commands/generateTests.js +0 -113
  26. package/dist/commands/init.js +0 -102
  27. package/dist/commands/plugin.js +0 -68
  28. package/dist/index.js +0 -2
  29. package/node_modules/@design-embed/config/README.md +0 -5
  30. package/node_modules/@design-embed/config/dist/index.js +0 -283
  31. package/node_modules/@design-embed/config/package.json +0 -19
  32. package/node_modules/@design-embed/config/src/index.ts +0 -518
  33. package/node_modules/@design-embed/core/README.md +0 -5
  34. package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +0 -3
  35. package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +0 -35
  36. package/node_modules/@design-embed/core/dist/index.js +0 -351
  37. package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +0 -29
  38. package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +0 -1
  39. package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +0 -25
  40. package/node_modules/@design-embed/core/package.json +0 -19
  41. package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +0 -78
  42. package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
  43. /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/diagnostic.ts +0 -0
  44. /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/jsonDiagnostic.ts +0 -0
  45. /package/{node_modules/@design-embed/core/src → src/core}/pipeline/checkMode.ts +0 -0
@@ -0,0 +1,621 @@
1
+ import type {
2
+ DesignNode,
3
+ Diagnostic,
4
+ GeneratedFile,
5
+ PropValue,
6
+ TargetEmitInput,
7
+ TargetEmitResult,
8
+ TargetEmitter,
9
+ TargetTestGenerateInput,
10
+ TargetTestGenerateResult,
11
+ TargetTestGenerator,
12
+ } from "../core/index.ts";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Public API
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface HtmlTargetOptions {
19
+ domModel?: "light" | "shadow";
20
+ }
21
+
22
+ export class HtmlTarget implements TargetEmitter, TargetTestGenerator {
23
+ private readonly domModel: "light" | "shadow";
24
+
25
+ constructor(options: HtmlTargetOptions = {}) {
26
+ this.domModel = options.domModel ?? "light";
27
+ }
28
+
29
+ emit({ nodes, css, config }: TargetEmitInput): TargetEmitResult {
30
+ const viewsDir = String(config?.output?.viewsDir ?? "src/generated/views");
31
+ const viewName = config?.output?.viewName ?? "index";
32
+
33
+ const components = collectComponents(nodes);
34
+ const scriptTag =
35
+ components.length > 0
36
+ ? `<script defer src="./${viewName}.js"></script>\n`
37
+ : "";
38
+
39
+ const files: GeneratedFile[] = [
40
+ {
41
+ path: `${viewsDir}/${viewName}.html`,
42
+ contents: emitHtml(nodes, css) + scriptTag,
43
+ },
44
+ ];
45
+
46
+ if (components.length > 0) {
47
+ files.push({
48
+ path: `${viewsDir}/${viewName}.ts`,
49
+ contents: emitWebComponentFile(components, this.domModel),
50
+ });
51
+ }
52
+
53
+ return { files };
54
+ }
55
+
56
+ generateTests(input: TargetTestGenerateInput): TargetTestGenerateResult {
57
+ return htmlTestGenerator.generateTests(input);
58
+ }
59
+ }
60
+
61
+ export const htmlTarget: TargetEmitter & TargetTestGenerator = new HtmlTarget();
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Test generator (unchanged)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const htmlTestGenerator: TargetTestGenerator = {
68
+ generateTests({
69
+ html,
70
+ css,
71
+ config,
72
+ }: TargetTestGenerateInput): TargetTestGenerateResult {
73
+ const diagnostics: Diagnostic[] = [];
74
+ const tests = config.tests;
75
+
76
+ if (tests?.runner && tests.runner !== "playwright") {
77
+ diagnostics.push({
78
+ code: "TEST_RUNNER_UNSUPPORTED",
79
+ message: `Unsupported test runner: ${tests.runner}`,
80
+ severity: "error",
81
+ });
82
+ return { files: [], diagnostics };
83
+ }
84
+
85
+ const viewsDir = config.output?.viewsDir ?? "src/generated/views";
86
+ const viewName = config.output?.viewName ?? "index";
87
+ const outputDir = tests?.outputDir ?? `${viewsDir}/tests`;
88
+ const fixturePath = `${outputDir}/${viewName}.reference.html`;
89
+ const specPath = `${outputDir}/${viewName}.spec.ts`;
90
+ const outputHtmlPath = `${viewsDir}/${viewName}.html`;
91
+ const referenceHtml = `${css?.trim() ? `<style>\n${css}\n</style>\n` : ""}${html}`;
92
+
93
+ return {
94
+ diagnostics,
95
+ files: [
96
+ {
97
+ path: fixturePath,
98
+ contents: referenceHtml.endsWith("\n")
99
+ ? referenceHtml
100
+ : `${referenceHtml}\n`,
101
+ },
102
+ {
103
+ path: specPath,
104
+ contents: emitHtmlVisualSpec({
105
+ viewName,
106
+ relativeOutputPath: toRelativeFilePath(specPath, outputHtmlPath),
107
+ fixtureFileName: `${viewName}.reference.html`,
108
+ viewports: tests?.viewports ?? [
109
+ { name: "default", width: 1440, height: 900 },
110
+ ],
111
+ states: tests?.states ?? [{ name: "default" }],
112
+ assertions: {
113
+ screenshot: tests?.assertions?.screenshot ?? true,
114
+ layout: tests?.assertions?.layout ?? true,
115
+ layoutTolerance: tests?.assertions?.layoutTolerance ?? 0,
116
+ selectors: tests?.assertions?.selectors ?? [":scope", ":scope *"],
117
+ screenshotThreshold:
118
+ tests?.assertions?.screenshotThreshold ?? 0.1,
119
+ screenshotMaxDiffPixels:
120
+ tests?.assertions?.screenshotMaxDiffPixels ?? 0,
121
+ },
122
+ }),
123
+ },
124
+ ],
125
+ };
126
+ },
127
+ };
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // HTML emit
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function emitHtml(nodes: DesignNode[], css?: string): string {
134
+ const body = nodes.map((node) => emitNode(node, 0)).join("");
135
+ if (!css?.trim()) {
136
+ return body;
137
+ }
138
+ return `<style>\n${css.trim()}\n</style>\n${body}\n`;
139
+ }
140
+
141
+ function emitNode(node: DesignNode, depth: number): string {
142
+ const indent = "\t".repeat(depth);
143
+ if (node.kind === "text") {
144
+ return `${indent}${escapeHtml(node.text ?? "")}\n`;
145
+ }
146
+ if (node.kind === "component") {
147
+ return emitComponentHtml(node, depth);
148
+ }
149
+
150
+ const attributes = Object.entries(node.attributes ?? {})
151
+ .sort(([left], [right]) => left.localeCompare(right))
152
+ .map(([name, value]) =>
153
+ value === "" ? name : `${name}="${escapeAttribute(value)}"`,
154
+ )
155
+ .join(" ");
156
+ const openTag = attributes
157
+ ? `<${node.tagName} ${attributes}>`
158
+ : `<${node.tagName}>`;
159
+ const children = node.children ?? [];
160
+
161
+ if (children.length === 0) {
162
+ return `${indent}${openTag}</${node.tagName}>\n`;
163
+ }
164
+
165
+ return `${indent}${openTag}\n${children
166
+ .map((child) => emitNode(child, depth + 1))
167
+ .join("")}${indent}</${node.tagName}>\n`;
168
+ }
169
+
170
+ function emitComponentHtml(node: DesignNode, depth: number): string {
171
+ const indent = "\t".repeat(depth);
172
+ const tag = toCustomElementTag(node.component ?? "component");
173
+
174
+ const attrParts = Object.entries(node.props ?? {})
175
+ .filter(([name, prop]) => name !== "children" && prop.kind !== "children")
176
+ .sort(([left], [right]) => left.localeCompare(right))
177
+ .flatMap(([name, prop]) => {
178
+ const part = formatPropAsAttribute(name, prop);
179
+ return part !== null ? [part] : [];
180
+ })
181
+ .join(" ");
182
+ const openTag = attrParts ? `<${tag} ${attrParts}>` : `<${tag}>`;
183
+
184
+ const childrenProp = node.props?.children;
185
+ if (childrenProp?.kind === "text") {
186
+ return `${indent}${openTag}${escapeHtml(childrenProp.value)}</${tag}>\n`;
187
+ }
188
+ if (childrenProp?.kind === "children") {
189
+ const kids = childrenProp.value;
190
+ if (kids.length === 0) {
191
+ return `${indent}${openTag}</${tag}>\n`;
192
+ }
193
+ return `${indent}${openTag}\n${kids
194
+ .map((child) => emitNode(child, depth + 1))
195
+ .join("")}${indent}</${tag}>\n`;
196
+ }
197
+
198
+ const children = node.children ?? [];
199
+ if (children.length === 0) {
200
+ return `${indent}${openTag}</${tag}>\n`;
201
+ }
202
+ return `${indent}${openTag}\n${children
203
+ .map((child) => emitNode(child, depth + 1))
204
+ .join("")}${indent}</${tag}>\n`;
205
+ }
206
+
207
+ function formatPropAsAttribute(name: string, prop: PropValue): string | null {
208
+ if (prop.kind === "children") {
209
+ return null;
210
+ }
211
+ if (prop.value === false) {
212
+ return null;
213
+ }
214
+ if (prop.value === true) {
215
+ return name;
216
+ }
217
+ const value = String(prop.value);
218
+ return value === "" ? name : `${name}="${escapeAttribute(value)}"`;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Web component emit
223
+ // ---------------------------------------------------------------------------
224
+
225
+ const VOID_ELEMENTS = new Set([
226
+ "area",
227
+ "base",
228
+ "br",
229
+ "col",
230
+ "embed",
231
+ "hr",
232
+ "img",
233
+ "input",
234
+ "link",
235
+ "meta",
236
+ "param",
237
+ "source",
238
+ "track",
239
+ "wbr",
240
+ ]);
241
+
242
+ interface PropBinding {
243
+ propName: string;
244
+ attrName: string;
245
+ }
246
+
247
+ interface ComponentInfo {
248
+ tagName: string;
249
+ className: string;
250
+ observedAttributes: string[];
251
+ sourceTagName?: string;
252
+ staticAttrEntries: [string, string][];
253
+ propBindings: PropBinding[];
254
+ }
255
+
256
+ function collectComponents(nodes: DesignNode[]): ComponentInfo[] {
257
+ const seen = new Map<string, ComponentInfo>();
258
+
259
+ function visit(node: DesignNode): void {
260
+ if (node.kind === "component") {
261
+ const key = `${node.importPath ?? ""}::${node.importName ?? ""}`;
262
+ if (!seen.has(key) && node.importPath && node.importName) {
263
+ seen.set(key, buildComponentInfo(node));
264
+ }
265
+ for (const child of node.children ?? []) {
266
+ visit(child);
267
+ }
268
+ for (const prop of Object.values(node.props ?? {})) {
269
+ if (prop.kind === "children") {
270
+ for (const child of prop.value) {
271
+ visit(child);
272
+ }
273
+ }
274
+ }
275
+ } else if (node.kind === "element") {
276
+ for (const child of node.children ?? []) {
277
+ visit(child);
278
+ }
279
+ }
280
+ }
281
+
282
+ for (const node of nodes) {
283
+ visit(node);
284
+ }
285
+
286
+ return Array.from(seen.values());
287
+ }
288
+
289
+ function buildComponentInfo(node: DesignNode): ComponentInfo {
290
+ const importName = node.importName ?? node.component ?? "Component";
291
+ const tagName = toCustomElementTag(importName);
292
+ const className = toPascalCase(tagName);
293
+
294
+ const observedAttributes: string[] = [];
295
+ const propBindings: PropBinding[] = [];
296
+ const mappedOriginalAttrs = new Set<string>();
297
+
298
+ for (const [name, prop] of Object.entries(node.props ?? {})) {
299
+ if (prop.kind === "literal") {
300
+ observedAttributes.push(name);
301
+ if (prop.attribute) {
302
+ propBindings.push({ propName: name, attrName: prop.attribute });
303
+ mappedOriginalAttrs.add(prop.attribute);
304
+ }
305
+ }
306
+ }
307
+ observedAttributes.sort();
308
+ propBindings.sort((a, b) => a.attrName.localeCompare(b.attrName));
309
+
310
+ const sourceEl = node.sourceElement;
311
+ let sourceTagName: string | undefined;
312
+ let staticAttrEntries: [string, string][] = [];
313
+
314
+ if (sourceEl?.kind === "element" && sourceEl.tagName) {
315
+ sourceTagName = sourceEl.tagName;
316
+ staticAttrEntries = Object.entries(sourceEl.attributes ?? {})
317
+ .filter(([attr]) => !mappedOriginalAttrs.has(attr))
318
+ .sort(([a], [b]) => a.localeCompare(b));
319
+ }
320
+
321
+ return {
322
+ tagName,
323
+ className,
324
+ observedAttributes,
325
+ sourceTagName,
326
+ staticAttrEntries,
327
+ propBindings,
328
+ };
329
+ }
330
+
331
+ function emitWebComponentFile(
332
+ components: ComponentInfo[],
333
+ domModel: "light" | "shadow",
334
+ ): string {
335
+ const classes = components
336
+ .map((c) => emitWebComponentClass(c, domModel))
337
+ .join("\n\n");
338
+
339
+ const registrations = components
340
+ .map((c) => `customElements.define("${c.tagName}", ${c.className});`)
341
+ .join("\n");
342
+
343
+ return `${classes}\n\n${registrations}\n`;
344
+ }
345
+
346
+ function emitWebComponentClass(
347
+ info: ComponentInfo,
348
+ domModel: "light" | "shadow",
349
+ ): string {
350
+ const hasShadow = domModel === "shadow";
351
+ const {
352
+ className,
353
+ observedAttributes,
354
+ sourceTagName,
355
+ staticAttrEntries,
356
+ propBindings,
357
+ } = info;
358
+
359
+ const attrArray =
360
+ observedAttributes.length === 0
361
+ ? "[]"
362
+ : `[${observedAttributes.map((a) => JSON.stringify(a)).join(", ")}]`;
363
+
364
+ const shadowSetup = hasShadow
365
+ ? `\tprivate shadow: ShadowRoot;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.shadow = this.attachShadow({ mode: "open" });\n\t}\n\n`
366
+ : "";
367
+
368
+ let renderBody: string;
369
+
370
+ if (sourceTagName && !hasShadow) {
371
+ const isVoid = VOID_ELEMENTS.has(sourceTagName);
372
+ const lines: string[] = ["\t\tif (!this.parentNode) return;"];
373
+
374
+ if (observedAttributes.length > 0) {
375
+ for (const a of observedAttributes) {
376
+ lines.push(`\t\tconst ${a} = this.getAttribute(${JSON.stringify(a)});`);
377
+ }
378
+ }
379
+
380
+ lines.push(
381
+ `\t\tconst el = document.createElement(${JSON.stringify(sourceTagName)});`,
382
+ );
383
+
384
+ for (const [attr, value] of staticAttrEntries) {
385
+ lines.push(
386
+ `\t\tel.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});`,
387
+ );
388
+ }
389
+ for (const { propName, attrName } of propBindings) {
390
+ lines.push(
391
+ `\t\tif (${propName} !== null) el.setAttribute(${JSON.stringify(attrName)}, ${propName});`,
392
+ );
393
+ }
394
+
395
+ if (!isVoid) {
396
+ lines.push("\t\tel.innerHTML = this.innerHTML;");
397
+ }
398
+
399
+ lines.push("\t\tthis.replaceWith(el);");
400
+ renderBody = lines.join("\n");
401
+ } else {
402
+ const attrVars = observedAttributes
403
+ .map((a) => `\t\tconst ${a} = this.getAttribute("${a}");`)
404
+ .join("\n");
405
+ const fallbackLines: string[] = [];
406
+ if (attrVars) fallbackLines.push(attrVars);
407
+ if (hasShadow)
408
+ fallbackLines.push(`\t\tthis.shadow.innerHTML = \`<slot></slot>\`;`);
409
+ renderBody = fallbackLines.join("\n");
410
+ }
411
+
412
+ const renderMethod = renderBody
413
+ ? `\tprivate render(): void {\n${renderBody}\n\t}`
414
+ : `\tprivate render(): void {\n\t}`;
415
+
416
+ return `class ${className} extends HTMLElement {
417
+ ${shadowSetup}\tstatic get observedAttributes(): string[] {
418
+ \t\treturn ${attrArray};
419
+ \t}
420
+
421
+ \tconnectedCallback(): void {
422
+ \t\tthis.render();
423
+ \t}
424
+
425
+ \tattributeChangedCallback(): void {
426
+ \t\tthis.render();
427
+ \t}
428
+
429
+ ${renderMethod}
430
+ }`;
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Playwright test generator
435
+ // ---------------------------------------------------------------------------
436
+
437
+ interface HtmlVisualSpecInput {
438
+ viewName: string;
439
+ relativeOutputPath: string;
440
+ fixtureFileName: string;
441
+ viewports: Array<{ name?: string; width: number; height: number }>;
442
+ states: Array<{
443
+ name: string;
444
+ hover?: string;
445
+ focus?: string;
446
+ click?: string;
447
+ waitFor?: string;
448
+ }>;
449
+ assertions: {
450
+ screenshot: boolean;
451
+ layout: boolean;
452
+ layoutTolerance: number;
453
+ selectors: string[];
454
+ screenshotThreshold: number;
455
+ screenshotMaxDiffPixels: number;
456
+ };
457
+ }
458
+
459
+ function emitHtmlVisualSpec(input: HtmlVisualSpecInput): string {
460
+ const viewports = JSON.stringify(input.viewports, null, 2);
461
+ const states = JSON.stringify(input.states, null, 2);
462
+ const selectors = JSON.stringify(input.assertions.selectors, null, 2);
463
+ const screenshotEnabled = JSON.stringify(input.assertions.screenshot);
464
+ const layoutEnabled = JSON.stringify(input.assertions.layout);
465
+ const layoutTolerance = JSON.stringify(input.assertions.layoutTolerance);
466
+ const screenshotThreshold = JSON.stringify(
467
+ input.assertions.screenshotThreshold,
468
+ );
469
+ const screenshotMaxDiffPixels = JSON.stringify(
470
+ input.assertions.screenshotMaxDiffPixels,
471
+ );
472
+
473
+ return `import { readFileSync } from "node:fs";
474
+ import { dirname, resolve } from "node:path";
475
+ import { fileURLToPath } from "node:url";
476
+ import { expect, test } from "@playwright/test";
477
+ import pixelmatch from "pixelmatch";
478
+ import { PNG } from "pngjs";
479
+
480
+ const currentDir = dirname(fileURLToPath(import.meta.url));
481
+ const referenceHtml = readFileSync(resolve(currentDir, "./${input.fixtureFileName}"), "utf-8");
482
+ const outputHtmlPath = resolve(currentDir, "${input.relativeOutputPath}");
483
+ const viewports = ${viewports};
484
+ const states = ${states};
485
+ const selectors = ${selectors};
486
+ const screenshotEnabled = ${screenshotEnabled};
487
+ const layoutEnabled = ${layoutEnabled};
488
+ const layoutTolerance = ${layoutTolerance};
489
+ const screenshotThreshold = ${screenshotThreshold};
490
+ const screenshotMaxDiffPixels = ${screenshotMaxDiffPixels};
491
+
492
+ for (const viewport of viewports) {
493
+ \tfor (const state of states) {
494
+ \t\tconst viewportName = viewport.name ?? String(viewport.width) + "x" + String(viewport.height);
495
+ \t\ttest("${input.viewName} matches source at " + viewportName + " / " + state.name, async ({ page }) => {
496
+ \t\t\tawait page.setViewportSize({ width: viewport.width, height: viewport.height });
497
+
498
+ \t\t\tawait page.setContent(referenceHtml);
499
+ \t\t\tawait applyState(page, state);
500
+ \t\t\tconst expectedScreenshot = screenshotEnabled ? await page.screenshot({ fullPage: true }) : undefined;
501
+ \t\t\tconst expectedLayout = layoutEnabled ? await readLayout(page.locator("body > *").first(), selectors) : [];
502
+
503
+ \t\t\tawait page.goto("file://" + outputHtmlPath);
504
+ \t\t\tawait page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))));
505
+ \t\t\tawait applyState(page, state);
506
+ \t\t\tconst actualScreenshot = screenshotEnabled ? await page.screenshot({ fullPage: true }) : undefined;
507
+ \t\t\tconst actualLayout = layoutEnabled ? await readLayout(page.locator("body > *").first(), selectors) : [];
508
+
509
+ \t\t\tif (screenshotEnabled) {
510
+ \t\t\t\tconst expectedPng = PNG.sync.read(expectedScreenshot);
511
+ \t\t\t\tconst actualPng = PNG.sync.read(actualScreenshot);
512
+ \t\t\t\texpect(actualPng.width, "screenshot width").toBe(expectedPng.width);
513
+ \t\t\t\texpect(actualPng.height, "screenshot height").toBe(expectedPng.height);
514
+ \t\t\t\tconst diff = new PNG({ width: expectedPng.width, height: expectedPng.height });
515
+ \t\t\t\tconst diffPixelCount = pixelmatch(expectedPng.data, actualPng.data, diff.data, expectedPng.width, expectedPng.height, { threshold: screenshotThreshold });
516
+ \t\t\t\texpect(diffPixelCount, "screenshot diff pixels").toBeLessThanOrEqual(screenshotMaxDiffPixels);
517
+ \t\t\t}
518
+ \t\t\tif (layoutEnabled) {
519
+ \t\t\t\texpectLayoutToMatch(actualLayout, expectedLayout, layoutTolerance);
520
+ \t\t\t}
521
+ \t\t});
522
+ \t}
523
+ }
524
+
525
+ async function applyState(page, state) {
526
+ \tif (state.waitFor) {
527
+ \t\tawait page.waitForSelector(state.waitFor);
528
+ \t}
529
+ \tif (state.hover) {
530
+ \t\tawait page.hover(state.hover);
531
+ \t}
532
+ \tif (state.focus) {
533
+ \t\tawait page.focus(state.focus);
534
+ \t}
535
+ \tif (state.click) {
536
+ \t\tawait page.click(state.click);
537
+ \t}
538
+ }
539
+
540
+ async function readLayout(root, selectorsToRead) {
541
+ \treturn root.evaluate((element, values) => {
542
+ \t\treturn values.flatMap((selector) => {
543
+ \t\t\tconst matches = selector === ":scope" ? [element] : Array.from(element.querySelectorAll(selector));
544
+ \t\t\treturn matches.map((matchedElement, index) => {
545
+ \t\t\t\tconst rect = matchedElement.getBoundingClientRect();
546
+ \t\t\t\treturn {
547
+ \t\t\t\t\tselector,
548
+ \t\t\t\t\tindex,
549
+ \t\t\t\t\ttagName: matchedElement.tagName.toLowerCase(),
550
+ \t\t\t\t\tx: rect.x,
551
+ \t\t\t\t\ty: rect.y,
552
+ \t\t\t\t\twidth: rect.width,
553
+ \t\t\t\t\theight: rect.height,
554
+ \t\t\t\t};
555
+ \t\t\t});
556
+ \t\t});
557
+ \t}, selectorsToRead);
558
+ }
559
+
560
+ function expectLayoutToMatch(actual, expected, tolerance) {
561
+ \texpect(actual.length).toBe(expected.length);
562
+ \tfor (let index = 0; index < expected.length; index += 1) {
563
+ \t\tconst actualRect = actual[index];
564
+ \t\tconst expectedRect = expected[index];
565
+ \t\texpect(actualRect.selector).toBe(expectedRect.selector);
566
+ \t\texpect(actualRect.index).toBe(expectedRect.index);
567
+ \t\texpect(actualRect.tagName).toBe(expectedRect.tagName);
568
+ \t\tfor (const key of ["x", "y", "width", "height"]) {
569
+ \t\t\tconst drift = Math.abs(actualRect[key] - expectedRect[key]);
570
+ \t\t\texpect(drift, \`\${expectedRect.selector}[\${expectedRect.index}] \${key} drift\`).toBeLessThanOrEqual(tolerance);
571
+ \t\t}
572
+ \t}
573
+ }
574
+ `;
575
+ }
576
+
577
+ // ---------------------------------------------------------------------------
578
+ // Utilities
579
+ // ---------------------------------------------------------------------------
580
+
581
+ function toRelativeFilePath(fromFile: string, toFile: string): string {
582
+ const fromParts = fromFile.split("/").slice(0, -1);
583
+ const toParts = toFile.split("/");
584
+ while (
585
+ fromParts.length > 0 &&
586
+ toParts.length > 0 &&
587
+ fromParts[0] === toParts[0]
588
+ ) {
589
+ fromParts.shift();
590
+ toParts.shift();
591
+ }
592
+ const prefix = fromParts.map(() => "..");
593
+ const relative = [...prefix, ...toParts].join("/");
594
+ return relative.startsWith(".") ? relative : `./${relative}`;
595
+ }
596
+
597
+ function escapeHtml(value: string): string {
598
+ return value
599
+ .replace(/&/g, "&amp;")
600
+ .replace(/</g, "&lt;")
601
+ .replace(/>/g, "&gt;");
602
+ }
603
+
604
+ function escapeAttribute(value: string): string {
605
+ return escapeHtml(value).replace(/"/g, "&quot;");
606
+ }
607
+
608
+ function toCustomElementTag(name: string): string {
609
+ const kebab = name
610
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
611
+ .toLowerCase()
612
+ .replace(/^-/, "");
613
+ return kebab.includes("-") ? kebab : `${kebab}-el`;
614
+ }
615
+
616
+ function toPascalCase(kebab: string): string {
617
+ return kebab
618
+ .split("-")
619
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
620
+ .join("");
621
+ }
package/dist/args.js DELETED
@@ -1,36 +0,0 @@
1
- export function parseArgs(args) {
2
- const positionals = [];
3
- const flags = {};
4
- for (let index = 0; index < args.length; index += 1) {
5
- const value = args[index];
6
- if (!value?.startsWith("--")) {
7
- if (value) {
8
- positionals.push(value);
9
- }
10
- continue;
11
- }
12
- const next = args[index + 1];
13
- if (!next || next.startsWith("--")) {
14
- flags[value] = true;
15
- continue;
16
- }
17
- flags[value] = next;
18
- index += 1;
19
- }
20
- const [command = "compile", ...rest] = positionals;
21
- return {
22
- command,
23
- positionals: rest,
24
- flags,
25
- };
26
- }
27
- export function getStringFlag(flags, name) {
28
- const value = flags[name];
29
- return typeof value === "string" ? value : undefined;
30
- }
31
- export function getBooleanFlag(flags, name) {
32
- return flags[name] === true;
33
- }
34
- export function getFormat(flags) {
35
- return flags["--format"] === "json" ? "json" : "text";
36
- }
package/dist/cli.js DELETED
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
- import { parseArgs } from "./args.js";
3
- import { runCheckCommand } from "./commands/check.js";
4
- import { runCompileCommand } from "./commands/compile.js";
5
- import { runGenerateTestsCommand } from "./commands/generateTests.js";
6
- import { runInitCommand } from "./commands/init.js";
7
- import { runPluginCommand } from "./commands/plugin.js";
8
- async function main() {
9
- const args = process.argv.slice(2);
10
- const parsed = parseArgs(args);
11
- if (args[0] === "check") {
12
- return runCheckCommand(parsed.flags);
13
- }
14
- if (args[0] === "plugin") {
15
- return runPluginCommand(parsed.positionals[0], parsed.flags);
16
- }
17
- if (args[0] === "generate-tests") {
18
- return runGenerateTestsCommand(parsed.flags);
19
- }
20
- if (args[0] === "init") {
21
- return runInitCommand(parsed.flags);
22
- }
23
- return runCompileCommand(parsed.flags);
24
- }
25
- main()
26
- .then((code) => {
27
- if (code !== 0) {
28
- process.exit(code);
29
- }
30
- })
31
- .catch((error) => {
32
- const message = error instanceof Error ? error.message : String(error);
33
- console.error(`Pipeline failed: ${message}`);
34
- process.exit(1);
35
- });
@@ -1,4 +0,0 @@
1
- import { runCompileCommand } from "./compile.js";
2
- export async function runCheckCommand(flags) {
3
- return runCompileCommand(flags, { check: true });
4
- }