@agent-scope/cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +173 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +148 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +148 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
6
|
import { Command as Command3 } from "commander";
|
|
7
7
|
|
|
@@ -325,7 +325,7 @@ function createManifestCommand() {
|
|
|
325
325
|
|
|
326
326
|
// src/render-commands.ts
|
|
327
327
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
328
|
-
import { resolve as
|
|
328
|
+
import { resolve as resolve3 } from "path";
|
|
329
329
|
import {
|
|
330
330
|
ALL_CONTEXT_IDS,
|
|
331
331
|
ALL_STRESS_IDS,
|
|
@@ -341,9 +341,9 @@ import { Command as Command2 } from "commander";
|
|
|
341
341
|
// src/component-bundler.ts
|
|
342
342
|
import { dirname } from "path";
|
|
343
343
|
import * as esbuild from "esbuild";
|
|
344
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth) {
|
|
344
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
345
345
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
346
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
346
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
347
347
|
}
|
|
348
348
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
349
349
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -429,7 +429,10 @@ ${msg}`);
|
|
|
429
429
|
}
|
|
430
430
|
return outputFile.text;
|
|
431
431
|
}
|
|
432
|
-
function wrapInHtml(bundledScript, viewportWidth) {
|
|
432
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
433
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
434
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
435
|
+
</style>` : "";
|
|
433
436
|
return `<!DOCTYPE html>
|
|
434
437
|
<html lang="en">
|
|
435
438
|
<head>
|
|
@@ -440,6 +443,7 @@ function wrapInHtml(bundledScript, viewportWidth) {
|
|
|
440
443
|
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
441
444
|
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
442
445
|
</style>
|
|
446
|
+
${projectStyleBlock}
|
|
443
447
|
</head>
|
|
444
448
|
<body>
|
|
445
449
|
<div id="scope-root" data-reactscope-root></div>
|
|
@@ -608,6 +612,126 @@ function csvEscape(value) {
|
|
|
608
612
|
return value;
|
|
609
613
|
}
|
|
610
614
|
|
|
615
|
+
// src/tailwind-css.ts
|
|
616
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
617
|
+
import { createRequire } from "module";
|
|
618
|
+
import { resolve as resolve2 } from "path";
|
|
619
|
+
var CONFIG_FILENAMES = [
|
|
620
|
+
".reactscope/config.json",
|
|
621
|
+
".reactscope/config.js",
|
|
622
|
+
".reactscope/config.mjs"
|
|
623
|
+
];
|
|
624
|
+
var STYLE_ENTRY_CANDIDATES = [
|
|
625
|
+
"src/index.css",
|
|
626
|
+
"src/globals.css",
|
|
627
|
+
"app/globals.css",
|
|
628
|
+
"app/index.css",
|
|
629
|
+
"styles/index.css",
|
|
630
|
+
"index.css"
|
|
631
|
+
];
|
|
632
|
+
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
633
|
+
var compilerCache = null;
|
|
634
|
+
function getCachedBuild(cwd) {
|
|
635
|
+
if (compilerCache !== null && resolve2(compilerCache.cwd) === resolve2(cwd)) {
|
|
636
|
+
return compilerCache.build;
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function findStylesEntry(cwd) {
|
|
641
|
+
for (const name of CONFIG_FILENAMES) {
|
|
642
|
+
const p = resolve2(cwd, name);
|
|
643
|
+
if (!existsSync2(p)) continue;
|
|
644
|
+
try {
|
|
645
|
+
if (name.endsWith(".json")) {
|
|
646
|
+
const raw = readFileSync2(p, "utf-8");
|
|
647
|
+
const data = JSON.parse(raw);
|
|
648
|
+
const scope = data.scope;
|
|
649
|
+
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
650
|
+
if (typeof entry === "string") {
|
|
651
|
+
const full = resolve2(cwd, entry);
|
|
652
|
+
if (existsSync2(full)) return full;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const pkgPath = resolve2(cwd, "package.json");
|
|
659
|
+
if (existsSync2(pkgPath)) {
|
|
660
|
+
try {
|
|
661
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
662
|
+
const pkg = JSON.parse(raw);
|
|
663
|
+
const entry = pkg.scope?.stylesEntry;
|
|
664
|
+
if (typeof entry === "string") {
|
|
665
|
+
const full = resolve2(cwd, entry);
|
|
666
|
+
if (existsSync2(full)) return full;
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
672
|
+
const full = resolve2(cwd, candidate);
|
|
673
|
+
if (existsSync2(full)) {
|
|
674
|
+
try {
|
|
675
|
+
const content = readFileSync2(full, "utf-8");
|
|
676
|
+
if (TAILWIND_IMPORT.test(content)) return full;
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
async function getTailwindCompiler(cwd) {
|
|
684
|
+
const cached = getCachedBuild(cwd);
|
|
685
|
+
if (cached !== null) return cached;
|
|
686
|
+
const entryPath = findStylesEntry(cwd);
|
|
687
|
+
if (entryPath === null) return null;
|
|
688
|
+
let compile;
|
|
689
|
+
try {
|
|
690
|
+
const require2 = createRequire(resolve2(cwd, "package.json"));
|
|
691
|
+
const tailwind = require2("tailwindcss");
|
|
692
|
+
const fn = tailwind.compile;
|
|
693
|
+
if (typeof fn !== "function") return null;
|
|
694
|
+
compile = fn;
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const entryContent = readFileSync2(entryPath, "utf-8");
|
|
699
|
+
const loadStylesheet = async (id, base) => {
|
|
700
|
+
if (id === "tailwindcss") {
|
|
701
|
+
const nodeModules = resolve2(cwd, "node_modules");
|
|
702
|
+
const tailwindCssPath = resolve2(nodeModules, "tailwindcss", "index.css");
|
|
703
|
+
if (!existsSync2(tailwindCssPath)) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const content = readFileSync2(tailwindCssPath, "utf-8");
|
|
709
|
+
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
710
|
+
}
|
|
711
|
+
const full = resolve2(base, id);
|
|
712
|
+
if (existsSync2(full)) {
|
|
713
|
+
const content = readFileSync2(full, "utf-8");
|
|
714
|
+
return { path: full, base: resolve2(full, ".."), content };
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
717
|
+
};
|
|
718
|
+
const result = await compile(entryContent, {
|
|
719
|
+
base: cwd,
|
|
720
|
+
from: entryPath,
|
|
721
|
+
loadStylesheet
|
|
722
|
+
});
|
|
723
|
+
const build2 = result.build.bind(result);
|
|
724
|
+
compilerCache = { cwd, build: build2 };
|
|
725
|
+
return build2;
|
|
726
|
+
}
|
|
727
|
+
async function getCompiledCssForClasses(cwd, classes) {
|
|
728
|
+
const build2 = await getTailwindCompiler(cwd);
|
|
729
|
+
if (build2 === null) return null;
|
|
730
|
+
const deduped = [...new Set(classes)].filter(Boolean);
|
|
731
|
+
if (deduped.length === 0) return null;
|
|
732
|
+
return build2(deduped);
|
|
733
|
+
}
|
|
734
|
+
|
|
611
735
|
// src/render-commands.ts
|
|
612
736
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
613
737
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
@@ -661,6 +785,20 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
661
785
|
if (renderError !== null) {
|
|
662
786
|
throw new Error(`Component render error: ${renderError}`);
|
|
663
787
|
}
|
|
788
|
+
const rootDir = process.cwd();
|
|
789
|
+
const classes = await page.evaluate(() => {
|
|
790
|
+
const set = /* @__PURE__ */ new Set();
|
|
791
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
792
|
+
for (const c of el.className.split(/\s+/)) {
|
|
793
|
+
if (c) set.add(c);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
return [...set];
|
|
797
|
+
});
|
|
798
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
799
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
800
|
+
await page.addStyleTag({ content: projectCss });
|
|
801
|
+
}
|
|
664
802
|
const renderTimeMs = performance.now() - startMs;
|
|
665
803
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
666
804
|
const boundingBox = await rootLocator.boundingBox();
|
|
@@ -669,13 +807,19 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
669
807
|
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
670
808
|
);
|
|
671
809
|
}
|
|
810
|
+
const PAD = 24;
|
|
811
|
+
const MIN_W = 320;
|
|
812
|
+
const MIN_H = 200;
|
|
813
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
814
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
815
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
816
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
817
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
818
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
819
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
820
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
672
821
|
const screenshot = await page.screenshot({
|
|
673
|
-
clip: {
|
|
674
|
-
x: boundingBox.x,
|
|
675
|
-
y: boundingBox.y,
|
|
676
|
-
width: boundingBox.width,
|
|
677
|
-
height: boundingBox.height
|
|
678
|
-
},
|
|
822
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
679
823
|
type: "png"
|
|
680
824
|
});
|
|
681
825
|
const computedStyles = {};
|
|
@@ -702,8 +846,8 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
702
846
|
computedStyles["[data-reactscope-root] > *"] = styles;
|
|
703
847
|
return {
|
|
704
848
|
screenshot,
|
|
705
|
-
width: Math.round(
|
|
706
|
-
height: Math.round(
|
|
849
|
+
width: Math.round(safeW),
|
|
850
|
+
height: Math.round(safeH),
|
|
707
851
|
renderTimeMs,
|
|
708
852
|
computedStyles
|
|
709
853
|
};
|
|
@@ -736,7 +880,7 @@ Available: ${available}`
|
|
|
736
880
|
}
|
|
737
881
|
const { width, height } = parseViewport(opts.viewport);
|
|
738
882
|
const rootDir = process.cwd();
|
|
739
|
-
const filePath =
|
|
883
|
+
const filePath = resolve3(rootDir, descriptor.filePath);
|
|
740
884
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
741
885
|
process.stderr.write(
|
|
742
886
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
@@ -766,7 +910,7 @@ Available: ${available}`
|
|
|
766
910
|
}
|
|
767
911
|
const result = outcome.result;
|
|
768
912
|
if (opts.output !== void 0) {
|
|
769
|
-
const outPath =
|
|
913
|
+
const outPath = resolve3(process.cwd(), opts.output);
|
|
770
914
|
writeFileSync3(outPath, result.screenshot);
|
|
771
915
|
process.stdout.write(
|
|
772
916
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -780,9 +924,9 @@ Available: ${available}`
|
|
|
780
924
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
781
925
|
`);
|
|
782
926
|
} else if (fmt === "file") {
|
|
783
|
-
const dir =
|
|
927
|
+
const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
784
928
|
mkdirSync2(dir, { recursive: true });
|
|
785
|
-
const outPath =
|
|
929
|
+
const outPath = resolve3(dir, `${componentName}.png`);
|
|
786
930
|
writeFileSync3(outPath, result.screenshot);
|
|
787
931
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
788
932
|
process.stdout.write(
|
|
@@ -790,9 +934,9 @@ Available: ${available}`
|
|
|
790
934
|
`
|
|
791
935
|
);
|
|
792
936
|
} else {
|
|
793
|
-
const dir =
|
|
937
|
+
const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
794
938
|
mkdirSync2(dir, { recursive: true });
|
|
795
|
-
const outPath =
|
|
939
|
+
const outPath = resolve3(dir, `${componentName}.png`);
|
|
796
940
|
writeFileSync3(outPath, result.screenshot);
|
|
797
941
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
798
942
|
process.stdout.write(
|
|
@@ -828,7 +972,7 @@ Available: ${available}`
|
|
|
828
972
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
829
973
|
const { width, height } = { width: 375, height: 812 };
|
|
830
974
|
const rootDir = process.cwd();
|
|
831
|
-
const filePath =
|
|
975
|
+
const filePath = resolve3(rootDir, descriptor.filePath);
|
|
832
976
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
833
977
|
const axes = [];
|
|
834
978
|
if (opts.axes !== void 0) {
|
|
@@ -895,7 +1039,7 @@ Available: ${available}`
|
|
|
895
1039
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
896
1040
|
const gen = new SpriteSheetGenerator();
|
|
897
1041
|
const sheet = await gen.generate(result);
|
|
898
|
-
const spritePath =
|
|
1042
|
+
const spritePath = resolve3(process.cwd(), opts.sprite);
|
|
899
1043
|
writeFileSync3(spritePath, sheet.png);
|
|
900
1044
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
901
1045
|
`);
|
|
@@ -905,9 +1049,9 @@ Available: ${available}`
|
|
|
905
1049
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
906
1050
|
const gen = new SpriteSheetGenerator();
|
|
907
1051
|
const sheet = await gen.generate(result);
|
|
908
|
-
const dir =
|
|
1052
|
+
const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
909
1053
|
mkdirSync2(dir, { recursive: true });
|
|
910
|
-
const outPath =
|
|
1054
|
+
const outPath = resolve3(dir, `${componentName}-matrix.png`);
|
|
911
1055
|
writeFileSync3(outPath, sheet.png);
|
|
912
1056
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
913
1057
|
process.stdout.write(
|
|
@@ -951,7 +1095,7 @@ function registerRenderAll(renderCmd) {
|
|
|
951
1095
|
return;
|
|
952
1096
|
}
|
|
953
1097
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
954
|
-
const outputDir =
|
|
1098
|
+
const outputDir = resolve3(process.cwd(), opts.outputDir);
|
|
955
1099
|
mkdirSync2(outputDir, { recursive: true });
|
|
956
1100
|
const rootDir = process.cwd();
|
|
957
1101
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
@@ -961,7 +1105,7 @@ function registerRenderAll(renderCmd) {
|
|
|
961
1105
|
const renderOne = async (name) => {
|
|
962
1106
|
const descriptor = manifest.components[name];
|
|
963
1107
|
if (descriptor === void 0) return;
|
|
964
|
-
const filePath =
|
|
1108
|
+
const filePath = resolve3(rootDir, descriptor.filePath);
|
|
965
1109
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
966
1110
|
const outcome = await safeRender(
|
|
967
1111
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
@@ -984,7 +1128,7 @@ function registerRenderAll(renderCmd) {
|
|
|
984
1128
|
success: false,
|
|
985
1129
|
errorMessage: outcome.error.message
|
|
986
1130
|
});
|
|
987
|
-
const errPath =
|
|
1131
|
+
const errPath = resolve3(outputDir, `${name}.error.json`);
|
|
988
1132
|
writeFileSync3(
|
|
989
1133
|
errPath,
|
|
990
1134
|
JSON.stringify(
|
|
@@ -1002,9 +1146,9 @@ function registerRenderAll(renderCmd) {
|
|
|
1002
1146
|
}
|
|
1003
1147
|
const result = outcome.result;
|
|
1004
1148
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
1005
|
-
const pngPath =
|
|
1149
|
+
const pngPath = resolve3(outputDir, `${name}.png`);
|
|
1006
1150
|
writeFileSync3(pngPath, result.screenshot);
|
|
1007
|
-
const jsonPath =
|
|
1151
|
+
const jsonPath = resolve3(outputDir, `${name}.json`);
|
|
1008
1152
|
writeFileSync3(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
1009
1153
|
if (isTTY()) {
|
|
1010
1154
|
process.stdout.write(
|
|
@@ -1430,7 +1574,7 @@ function createProgram(options = {}) {
|
|
|
1430
1574
|
}
|
|
1431
1575
|
);
|
|
1432
1576
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
1433
|
-
const raw =
|
|
1577
|
+
const raw = readFileSync3(tracePath, "utf-8");
|
|
1434
1578
|
const trace = loadTrace(raw);
|
|
1435
1579
|
const source = generateTest(trace, {
|
|
1436
1580
|
description: opts.description,
|