@agent-scope/cli 1.7.0 → 1.8.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 +660 -187
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +487 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +487 -21
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -607,6 +607,471 @@ function csvEscape(value) {
|
|
|
607
607
|
}
|
|
608
608
|
return value;
|
|
609
609
|
}
|
|
610
|
+
|
|
611
|
+
// src/instrument/renders.ts
|
|
612
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
613
|
+
function determineTrigger(event) {
|
|
614
|
+
if (event.forceUpdate) return "force_update";
|
|
615
|
+
if (event.stateChanged) return "state_change";
|
|
616
|
+
if (event.propsChanged) return "props_change";
|
|
617
|
+
if (event.contextChanged) return "context_change";
|
|
618
|
+
if (event.hookDepsChanged) return "hook_dependency";
|
|
619
|
+
return "parent_rerender";
|
|
620
|
+
}
|
|
621
|
+
function isWastedRender(event) {
|
|
622
|
+
return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
|
|
623
|
+
}
|
|
624
|
+
function buildCausalityChains(rawEvents) {
|
|
625
|
+
const result = [];
|
|
626
|
+
const componentLastRender = /* @__PURE__ */ new Map();
|
|
627
|
+
for (const raw of rawEvents) {
|
|
628
|
+
const trigger = determineTrigger(raw);
|
|
629
|
+
const wasted = isWastedRender(raw);
|
|
630
|
+
const chain = [];
|
|
631
|
+
let current = raw;
|
|
632
|
+
const visited = /* @__PURE__ */ new Set();
|
|
633
|
+
while (true) {
|
|
634
|
+
if (visited.has(current.component)) break;
|
|
635
|
+
visited.add(current.component);
|
|
636
|
+
const currentTrigger = determineTrigger(current);
|
|
637
|
+
chain.unshift({
|
|
638
|
+
component: current.component,
|
|
639
|
+
trigger: currentTrigger,
|
|
640
|
+
propsChanged: current.propsChanged,
|
|
641
|
+
stateChanged: current.stateChanged,
|
|
642
|
+
contextChanged: current.contextChanged
|
|
643
|
+
});
|
|
644
|
+
if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
const parentEvent = componentLastRender.get(current.parentComponent);
|
|
648
|
+
if (parentEvent === void 0) break;
|
|
649
|
+
current = parentEvent;
|
|
650
|
+
}
|
|
651
|
+
const rootCause = chain[0];
|
|
652
|
+
const cascadeRenders = rawEvents.filter((e) => {
|
|
653
|
+
if (rootCause === void 0) return false;
|
|
654
|
+
return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
|
|
655
|
+
});
|
|
656
|
+
result.push({
|
|
657
|
+
component: raw.component,
|
|
658
|
+
renderIndex: raw.renderIndex,
|
|
659
|
+
trigger,
|
|
660
|
+
propsChanged: raw.propsChanged,
|
|
661
|
+
stateChanged: raw.stateChanged,
|
|
662
|
+
contextChanged: raw.contextChanged,
|
|
663
|
+
memoized: raw.memoized,
|
|
664
|
+
wasted,
|
|
665
|
+
chain,
|
|
666
|
+
cascade: {
|
|
667
|
+
totalRendersTriggered: cascadeRenders.length,
|
|
668
|
+
uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
|
|
669
|
+
unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
componentLastRender.set(raw.component, raw);
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
function applyHeuristicFlags(renders) {
|
|
677
|
+
const flags = [];
|
|
678
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
679
|
+
for (const r of renders) {
|
|
680
|
+
if (!byComponent.has(r.component)) byComponent.set(r.component, []);
|
|
681
|
+
byComponent.get(r.component).push(r);
|
|
682
|
+
}
|
|
683
|
+
for (const [component, events] of byComponent) {
|
|
684
|
+
const wastedCount = events.filter((e) => e.wasted).length;
|
|
685
|
+
const totalCount = events.length;
|
|
686
|
+
if (wastedCount > 0) {
|
|
687
|
+
flags.push({
|
|
688
|
+
id: "WASTED_RENDER",
|
|
689
|
+
severity: "warning",
|
|
690
|
+
component,
|
|
691
|
+
detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
|
|
692
|
+
data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
for (const event of events) {
|
|
696
|
+
if (event.cascade.totalRendersTriggered > 50) {
|
|
697
|
+
flags.push({
|
|
698
|
+
id: "RENDER_CASCADE",
|
|
699
|
+
severity: "warning",
|
|
700
|
+
component,
|
|
701
|
+
detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
|
|
702
|
+
data: {
|
|
703
|
+
totalRendersTriggered: event.cascade.totalRendersTriggered,
|
|
704
|
+
uniqueComponents: event.cascade.uniqueComponents,
|
|
705
|
+
unchangedPropRenders: event.cascade.unchangedPropRenders
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return flags;
|
|
713
|
+
}
|
|
714
|
+
function buildInstrumentationScript() {
|
|
715
|
+
return (
|
|
716
|
+
/* js */
|
|
717
|
+
`
|
|
718
|
+
(function installScopeRenderInstrumentation() {
|
|
719
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
720
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
721
|
+
|
|
722
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
723
|
+
if (!hook) return;
|
|
724
|
+
|
|
725
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
726
|
+
var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
|
|
727
|
+
|
|
728
|
+
function extractName(fiber) {
|
|
729
|
+
if (!fiber) return 'Unknown';
|
|
730
|
+
var type = fiber.type;
|
|
731
|
+
if (!type) return 'Unknown';
|
|
732
|
+
if (typeof type === 'string') return type; // host element
|
|
733
|
+
if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
|
|
734
|
+
if (type.displayName) return type.displayName;
|
|
735
|
+
if (type.render && typeof type.render === 'function') {
|
|
736
|
+
return type.render.displayName || type.render.name || 'Anonymous';
|
|
737
|
+
}
|
|
738
|
+
return 'Anonymous';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function isMemoized(fiber) {
|
|
742
|
+
// MemoComponent = 14, SimpleMemoComponent = 15
|
|
743
|
+
return fiber.tag === 14 || fiber.tag === 15;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function isComponent(fiber) {
|
|
747
|
+
// FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
|
|
748
|
+
return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function shallowEqual(a, b) {
|
|
752
|
+
if (a === b) return true;
|
|
753
|
+
if (!a || !b) return a === b;
|
|
754
|
+
var keysA = Object.keys(a);
|
|
755
|
+
var keysB = Object.keys(b);
|
|
756
|
+
if (keysA.length !== keysB.length) return false;
|
|
757
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
758
|
+
var k = keysA[i];
|
|
759
|
+
if (k === 'children') continue; // ignore children prop
|
|
760
|
+
if (a[k] !== b[k]) return false;
|
|
761
|
+
}
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function getParentComponentName(fiber) {
|
|
766
|
+
var parent = fiber.return;
|
|
767
|
+
while (parent) {
|
|
768
|
+
if (isComponent(parent)) {
|
|
769
|
+
var name = extractName(parent);
|
|
770
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
|
|
771
|
+
}
|
|
772
|
+
parent = parent.return;
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function walkCommit(fiber) {
|
|
778
|
+
if (!fiber) return;
|
|
779
|
+
|
|
780
|
+
if (isComponent(fiber)) {
|
|
781
|
+
var name = extractName(fiber);
|
|
782
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
|
|
783
|
+
var memoized = isMemoized(fiber);
|
|
784
|
+
var currentProps = fiber.memoizedProps || {};
|
|
785
|
+
var prev = renderedComponents.get(name);
|
|
786
|
+
|
|
787
|
+
var propsChanged = true;
|
|
788
|
+
var stateChanged = false;
|
|
789
|
+
var contextChanged = false;
|
|
790
|
+
var hookDepsChanged = false;
|
|
791
|
+
var forceUpdate = false;
|
|
792
|
+
|
|
793
|
+
if (prev) {
|
|
794
|
+
propsChanged = !shallowEqual(prev.lastProps, currentProps);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// State: check memoizedState chain
|
|
798
|
+
var memoizedState = fiber.memoizedState;
|
|
799
|
+
if (prev && prev.lastStateSerialized !== undefined) {
|
|
800
|
+
try {
|
|
801
|
+
var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
802
|
+
stateChanged = stateSig !== prev.lastStateSerialized;
|
|
803
|
+
} catch (_) {
|
|
804
|
+
stateChanged = false;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Context: use _debugHookTypes or check dependencies
|
|
809
|
+
var deps = fiber.dependencies;
|
|
810
|
+
if (deps && deps.firstContext) {
|
|
811
|
+
contextChanged = true; // conservative: context dep present = may have changed
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
var stateSig;
|
|
815
|
+
try {
|
|
816
|
+
stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
817
|
+
} catch (_) {
|
|
818
|
+
stateSig = null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
renderedComponents.set(name, {
|
|
822
|
+
lastProps: currentProps,
|
|
823
|
+
lastStateSerialized: stateSig,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
var parentName = getParentComponentName(fiber);
|
|
827
|
+
|
|
828
|
+
window.__SCOPE_RENDER_EVENTS__.push({
|
|
829
|
+
component: name,
|
|
830
|
+
renderIndex: window.__SCOPE_RENDER_INDEX__++,
|
|
831
|
+
propsChanged: prev ? propsChanged : false,
|
|
832
|
+
stateChanged: stateChanged,
|
|
833
|
+
contextChanged: contextChanged,
|
|
834
|
+
memoized: memoized,
|
|
835
|
+
parentComponent: parentName,
|
|
836
|
+
hookDepsChanged: hookDepsChanged,
|
|
837
|
+
forceUpdate: forceUpdate,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
walkCommit(fiber.child);
|
|
843
|
+
walkCommit(fiber.sibling);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
847
|
+
if (typeof originalOnCommit === 'function') {
|
|
848
|
+
originalOnCommit.call(hook, rendererID, root, priorityLevel);
|
|
849
|
+
}
|
|
850
|
+
var wipRoot = root && root.current && root.current.alternate;
|
|
851
|
+
if (wipRoot) walkCommit(wipRoot);
|
|
852
|
+
};
|
|
853
|
+
})();
|
|
854
|
+
`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
async function replayInteraction(page, steps) {
|
|
858
|
+
for (const step of steps) {
|
|
859
|
+
switch (step.action) {
|
|
860
|
+
case "click":
|
|
861
|
+
if (step.target !== void 0) {
|
|
862
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
866
|
+
case "type":
|
|
867
|
+
if (step.target !== void 0 && step.text !== void 0) {
|
|
868
|
+
await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
|
|
869
|
+
await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
case "hover":
|
|
875
|
+
if (step.target !== void 0) {
|
|
876
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
case "blur":
|
|
881
|
+
if (step.target !== void 0) {
|
|
882
|
+
await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
case "focus":
|
|
887
|
+
if (step.target !== void 0) {
|
|
888
|
+
await page.focus(step.target, { timeout: 5e3 }).catch(() => {
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
case "scroll":
|
|
893
|
+
if (step.target !== void 0) {
|
|
894
|
+
await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
break;
|
|
898
|
+
case "wait": {
|
|
899
|
+
const timeout = step.timeout ?? 1e3;
|
|
900
|
+
if (step.condition === "idle") {
|
|
901
|
+
await page.waitForLoadState("networkidle", { timeout }).catch(() => {
|
|
902
|
+
});
|
|
903
|
+
} else {
|
|
904
|
+
await page.waitForTimeout(timeout);
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
var _pool = null;
|
|
912
|
+
async function getPool() {
|
|
913
|
+
if (_pool === null) {
|
|
914
|
+
_pool = new render.BrowserPool({
|
|
915
|
+
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
916
|
+
viewportWidth: 1280,
|
|
917
|
+
viewportHeight: 800
|
|
918
|
+
});
|
|
919
|
+
await _pool.init();
|
|
920
|
+
}
|
|
921
|
+
return _pool;
|
|
922
|
+
}
|
|
923
|
+
async function shutdownPool() {
|
|
924
|
+
if (_pool !== null) {
|
|
925
|
+
await _pool.close();
|
|
926
|
+
_pool = null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function analyzeRenders(options) {
|
|
930
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
|
|
931
|
+
const manifest = loadManifest(manifestPath);
|
|
932
|
+
const descriptor = manifest.components[options.componentName];
|
|
933
|
+
if (descriptor === void 0) {
|
|
934
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Component "${options.componentName}" not found in manifest.
|
|
937
|
+
Available: ${available}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
const rootDir = process.cwd();
|
|
941
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
942
|
+
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
943
|
+
const pool = await getPool();
|
|
944
|
+
const slot = await pool.acquire();
|
|
945
|
+
const { page } = slot;
|
|
946
|
+
const startMs = performance.now();
|
|
947
|
+
try {
|
|
948
|
+
await page.addInitScript(buildInstrumentationScript());
|
|
949
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
950
|
+
await page.waitForFunction(
|
|
951
|
+
() => window.__SCOPE_RENDER_COMPLETE__ === true,
|
|
952
|
+
{ timeout: 15e3 }
|
|
953
|
+
);
|
|
954
|
+
await page.waitForTimeout(100);
|
|
955
|
+
await page.evaluate(() => {
|
|
956
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
957
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
958
|
+
});
|
|
959
|
+
await replayInteraction(page, options.interaction);
|
|
960
|
+
await page.waitForTimeout(200);
|
|
961
|
+
const interactionDurationMs = performance.now() - startMs;
|
|
962
|
+
const rawEvents = await page.evaluate(() => {
|
|
963
|
+
return window.__SCOPE_RENDER_EVENTS__ ?? [];
|
|
964
|
+
});
|
|
965
|
+
const renders = buildCausalityChains(rawEvents);
|
|
966
|
+
const flags = applyHeuristicFlags(renders);
|
|
967
|
+
const uniqueComponents = new Set(renders.map((r) => r.component)).size;
|
|
968
|
+
const wastedRenders = renders.filter((r) => r.wasted).length;
|
|
969
|
+
return {
|
|
970
|
+
component: options.componentName,
|
|
971
|
+
interaction: options.interaction,
|
|
972
|
+
summary: {
|
|
973
|
+
totalRenders: renders.length,
|
|
974
|
+
uniqueComponents,
|
|
975
|
+
wastedRenders,
|
|
976
|
+
interactionDurationMs: Math.round(interactionDurationMs)
|
|
977
|
+
},
|
|
978
|
+
renders,
|
|
979
|
+
flags
|
|
980
|
+
};
|
|
981
|
+
} finally {
|
|
982
|
+
pool.release(slot);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function formatRendersTable(result) {
|
|
986
|
+
const lines = [];
|
|
987
|
+
lines.push(`
|
|
988
|
+
\u{1F50D} Re-render Analysis: ${result.component}`);
|
|
989
|
+
lines.push(`${"\u2500".repeat(60)}`);
|
|
990
|
+
lines.push(`Total renders: ${result.summary.totalRenders}`);
|
|
991
|
+
lines.push(`Unique components: ${result.summary.uniqueComponents}`);
|
|
992
|
+
lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
|
|
993
|
+
lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
|
|
994
|
+
lines.push("");
|
|
995
|
+
if (result.renders.length === 0) {
|
|
996
|
+
lines.push("No re-renders captured during interaction.");
|
|
997
|
+
} else {
|
|
998
|
+
lines.push("Re-renders:");
|
|
999
|
+
lines.push(
|
|
1000
|
+
`${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
|
|
1001
|
+
);
|
|
1002
|
+
lines.push("\u2500".repeat(80));
|
|
1003
|
+
for (const r of result.renders) {
|
|
1004
|
+
const wasted = r.wasted ? "\u26A0 yes" : "no";
|
|
1005
|
+
const idx = String(r.renderIndex).padEnd(4);
|
|
1006
|
+
const comp = r.component.slice(0, 29).padEnd(30);
|
|
1007
|
+
const trig = r.trigger.padEnd(18);
|
|
1008
|
+
const w = wasted.padEnd(7);
|
|
1009
|
+
const depth = r.chain.length;
|
|
1010
|
+
lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (result.flags.length > 0) {
|
|
1014
|
+
lines.push("");
|
|
1015
|
+
lines.push("Flags:");
|
|
1016
|
+
for (const flag of result.flags) {
|
|
1017
|
+
const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1018
|
+
lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return lines.join("\n");
|
|
1022
|
+
}
|
|
1023
|
+
function createInstrumentRendersCommand() {
|
|
1024
|
+
return new commander.Command("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
1025
|
+
"--interaction <json>",
|
|
1026
|
+
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1027
|
+
"[]"
|
|
1028
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
|
|
1029
|
+
async (componentName, opts) => {
|
|
1030
|
+
let interaction = [];
|
|
1031
|
+
try {
|
|
1032
|
+
interaction = JSON.parse(opts.interaction);
|
|
1033
|
+
if (!Array.isArray(interaction)) {
|
|
1034
|
+
throw new Error("Interaction must be a JSON array");
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
|
|
1038
|
+
`);
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
process.stderr.write(
|
|
1043
|
+
`Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
|
|
1044
|
+
`
|
|
1045
|
+
);
|
|
1046
|
+
const result = await analyzeRenders({
|
|
1047
|
+
componentName,
|
|
1048
|
+
interaction,
|
|
1049
|
+
manifestPath: opts.manifest
|
|
1050
|
+
});
|
|
1051
|
+
await shutdownPool();
|
|
1052
|
+
if (opts.json || !isTTY()) {
|
|
1053
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1054
|
+
`);
|
|
1055
|
+
} else {
|
|
1056
|
+
process.stdout.write(`${formatRendersTable(result)}
|
|
1057
|
+
`);
|
|
1058
|
+
}
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
await shutdownPool();
|
|
1061
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1062
|
+
`);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
function createInstrumentCommand() {
|
|
1069
|
+
const instrumentCmd = new commander.Command("instrument").description(
|
|
1070
|
+
"Structured instrumentation commands for React component analysis"
|
|
1071
|
+
);
|
|
1072
|
+
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1073
|
+
return instrumentCmd;
|
|
1074
|
+
}
|
|
610
1075
|
var CONFIG_FILENAMES = [
|
|
611
1076
|
".reactscope/config.json",
|
|
612
1077
|
".reactscope/config.js",
|
|
@@ -724,24 +1189,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
724
1189
|
}
|
|
725
1190
|
|
|
726
1191
|
// src/render-commands.ts
|
|
727
|
-
var
|
|
1192
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
728
1193
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
729
|
-
var
|
|
730
|
-
async function
|
|
731
|
-
if (
|
|
732
|
-
|
|
1194
|
+
var _pool2 = null;
|
|
1195
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1196
|
+
if (_pool2 === null) {
|
|
1197
|
+
_pool2 = new render.BrowserPool({
|
|
733
1198
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
734
1199
|
viewportWidth,
|
|
735
1200
|
viewportHeight
|
|
736
1201
|
});
|
|
737
|
-
await
|
|
1202
|
+
await _pool2.init();
|
|
738
1203
|
}
|
|
739
|
-
return
|
|
1204
|
+
return _pool2;
|
|
740
1205
|
}
|
|
741
|
-
async function
|
|
742
|
-
if (
|
|
743
|
-
await
|
|
744
|
-
|
|
1206
|
+
async function shutdownPool2() {
|
|
1207
|
+
if (_pool2 !== null) {
|
|
1208
|
+
await _pool2.close();
|
|
1209
|
+
_pool2 = null;
|
|
745
1210
|
}
|
|
746
1211
|
}
|
|
747
1212
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -752,7 +1217,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
752
1217
|
_satori: satori,
|
|
753
1218
|
async renderCell(props, _complexityClass) {
|
|
754
1219
|
const startMs = performance.now();
|
|
755
|
-
const pool = await
|
|
1220
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
756
1221
|
const htmlHarness = await buildComponentHarness(
|
|
757
1222
|
filePath,
|
|
758
1223
|
componentName,
|
|
@@ -849,7 +1314,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
849
1314
|
};
|
|
850
1315
|
}
|
|
851
1316
|
function registerRenderSingle(renderCmd) {
|
|
852
|
-
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json",
|
|
1317
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
|
|
853
1318
|
async (componentName, opts) => {
|
|
854
1319
|
try {
|
|
855
1320
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -888,7 +1353,7 @@ Available: ${available}`
|
|
|
888
1353
|
}
|
|
889
1354
|
}
|
|
890
1355
|
);
|
|
891
|
-
await
|
|
1356
|
+
await shutdownPool2();
|
|
892
1357
|
if (outcome.crashed) {
|
|
893
1358
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
894
1359
|
`);
|
|
@@ -936,7 +1401,7 @@ Available: ${available}`
|
|
|
936
1401
|
);
|
|
937
1402
|
}
|
|
938
1403
|
} catch (err) {
|
|
939
|
-
await
|
|
1404
|
+
await shutdownPool2();
|
|
940
1405
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
941
1406
|
`);
|
|
942
1407
|
process.exit(1);
|
|
@@ -948,7 +1413,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
948
1413
|
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
|
|
949
1414
|
"--contexts <ids>",
|
|
950
1415
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
951
|
-
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json",
|
|
1416
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
|
|
952
1417
|
async (componentName, opts) => {
|
|
953
1418
|
try {
|
|
954
1419
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1021,7 +1486,7 @@ Available: ${available}`
|
|
|
1021
1486
|
concurrency
|
|
1022
1487
|
});
|
|
1023
1488
|
const result = await matrix.render();
|
|
1024
|
-
await
|
|
1489
|
+
await shutdownPool2();
|
|
1025
1490
|
process.stderr.write(
|
|
1026
1491
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1027
1492
|
`
|
|
@@ -1066,7 +1531,7 @@ Available: ${available}`
|
|
|
1066
1531
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1067
1532
|
}
|
|
1068
1533
|
} catch (err) {
|
|
1069
|
-
await
|
|
1534
|
+
await shutdownPool2();
|
|
1070
1535
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1071
1536
|
`);
|
|
1072
1537
|
process.exit(1);
|
|
@@ -1075,7 +1540,7 @@ Available: ${available}`
|
|
|
1075
1540
|
);
|
|
1076
1541
|
}
|
|
1077
1542
|
function registerRenderAll(renderCmd) {
|
|
1078
|
-
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json",
|
|
1543
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1079
1544
|
async (opts) => {
|
|
1080
1545
|
try {
|
|
1081
1546
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1163,13 +1628,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1163
1628
|
workers.push(worker());
|
|
1164
1629
|
}
|
|
1165
1630
|
await Promise.all(workers);
|
|
1166
|
-
await
|
|
1631
|
+
await shutdownPool2();
|
|
1167
1632
|
process.stderr.write("\n");
|
|
1168
1633
|
const summary = formatSummaryText(results, outputDir);
|
|
1169
1634
|
process.stderr.write(`${summary}
|
|
1170
1635
|
`);
|
|
1171
1636
|
} catch (err) {
|
|
1172
|
-
await
|
|
1637
|
+
await shutdownPool2();
|
|
1173
1638
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1174
1639
|
`);
|
|
1175
1640
|
process.exit(1);
|
|
@@ -1918,6 +2383,7 @@ function createProgram(options = {}) {
|
|
|
1918
2383
|
program.addCommand(createManifestCommand());
|
|
1919
2384
|
program.addCommand(createRenderCommand());
|
|
1920
2385
|
program.addCommand(createTokensCommand());
|
|
2386
|
+
program.addCommand(createInstrumentCommand());
|
|
1921
2387
|
return program;
|
|
1922
2388
|
}
|
|
1923
2389
|
|