@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.js
CHANGED
|
@@ -585,6 +585,471 @@ function csvEscape(value) {
|
|
|
585
585
|
}
|
|
586
586
|
return value;
|
|
587
587
|
}
|
|
588
|
+
|
|
589
|
+
// src/instrument/renders.ts
|
|
590
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
591
|
+
function determineTrigger(event) {
|
|
592
|
+
if (event.forceUpdate) return "force_update";
|
|
593
|
+
if (event.stateChanged) return "state_change";
|
|
594
|
+
if (event.propsChanged) return "props_change";
|
|
595
|
+
if (event.contextChanged) return "context_change";
|
|
596
|
+
if (event.hookDepsChanged) return "hook_dependency";
|
|
597
|
+
return "parent_rerender";
|
|
598
|
+
}
|
|
599
|
+
function isWastedRender(event) {
|
|
600
|
+
return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
|
|
601
|
+
}
|
|
602
|
+
function buildCausalityChains(rawEvents) {
|
|
603
|
+
const result = [];
|
|
604
|
+
const componentLastRender = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const raw of rawEvents) {
|
|
606
|
+
const trigger = determineTrigger(raw);
|
|
607
|
+
const wasted = isWastedRender(raw);
|
|
608
|
+
const chain = [];
|
|
609
|
+
let current = raw;
|
|
610
|
+
const visited = /* @__PURE__ */ new Set();
|
|
611
|
+
while (true) {
|
|
612
|
+
if (visited.has(current.component)) break;
|
|
613
|
+
visited.add(current.component);
|
|
614
|
+
const currentTrigger = determineTrigger(current);
|
|
615
|
+
chain.unshift({
|
|
616
|
+
component: current.component,
|
|
617
|
+
trigger: currentTrigger,
|
|
618
|
+
propsChanged: current.propsChanged,
|
|
619
|
+
stateChanged: current.stateChanged,
|
|
620
|
+
contextChanged: current.contextChanged
|
|
621
|
+
});
|
|
622
|
+
if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
const parentEvent = componentLastRender.get(current.parentComponent);
|
|
626
|
+
if (parentEvent === void 0) break;
|
|
627
|
+
current = parentEvent;
|
|
628
|
+
}
|
|
629
|
+
const rootCause = chain[0];
|
|
630
|
+
const cascadeRenders = rawEvents.filter((e) => {
|
|
631
|
+
if (rootCause === void 0) return false;
|
|
632
|
+
return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
|
|
633
|
+
});
|
|
634
|
+
result.push({
|
|
635
|
+
component: raw.component,
|
|
636
|
+
renderIndex: raw.renderIndex,
|
|
637
|
+
trigger,
|
|
638
|
+
propsChanged: raw.propsChanged,
|
|
639
|
+
stateChanged: raw.stateChanged,
|
|
640
|
+
contextChanged: raw.contextChanged,
|
|
641
|
+
memoized: raw.memoized,
|
|
642
|
+
wasted,
|
|
643
|
+
chain,
|
|
644
|
+
cascade: {
|
|
645
|
+
totalRendersTriggered: cascadeRenders.length,
|
|
646
|
+
uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
|
|
647
|
+
unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
componentLastRender.set(raw.component, raw);
|
|
651
|
+
}
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
function applyHeuristicFlags(renders) {
|
|
655
|
+
const flags = [];
|
|
656
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const r of renders) {
|
|
658
|
+
if (!byComponent.has(r.component)) byComponent.set(r.component, []);
|
|
659
|
+
byComponent.get(r.component).push(r);
|
|
660
|
+
}
|
|
661
|
+
for (const [component, events] of byComponent) {
|
|
662
|
+
const wastedCount = events.filter((e) => e.wasted).length;
|
|
663
|
+
const totalCount = events.length;
|
|
664
|
+
if (wastedCount > 0) {
|
|
665
|
+
flags.push({
|
|
666
|
+
id: "WASTED_RENDER",
|
|
667
|
+
severity: "warning",
|
|
668
|
+
component,
|
|
669
|
+
detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
|
|
670
|
+
data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
for (const event of events) {
|
|
674
|
+
if (event.cascade.totalRendersTriggered > 50) {
|
|
675
|
+
flags.push({
|
|
676
|
+
id: "RENDER_CASCADE",
|
|
677
|
+
severity: "warning",
|
|
678
|
+
component,
|
|
679
|
+
detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
|
|
680
|
+
data: {
|
|
681
|
+
totalRendersTriggered: event.cascade.totalRendersTriggered,
|
|
682
|
+
uniqueComponents: event.cascade.uniqueComponents,
|
|
683
|
+
unchangedPropRenders: event.cascade.unchangedPropRenders
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return flags;
|
|
691
|
+
}
|
|
692
|
+
function buildInstrumentationScript() {
|
|
693
|
+
return (
|
|
694
|
+
/* js */
|
|
695
|
+
`
|
|
696
|
+
(function installScopeRenderInstrumentation() {
|
|
697
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
698
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
699
|
+
|
|
700
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
701
|
+
if (!hook) return;
|
|
702
|
+
|
|
703
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
704
|
+
var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
|
|
705
|
+
|
|
706
|
+
function extractName(fiber) {
|
|
707
|
+
if (!fiber) return 'Unknown';
|
|
708
|
+
var type = fiber.type;
|
|
709
|
+
if (!type) return 'Unknown';
|
|
710
|
+
if (typeof type === 'string') return type; // host element
|
|
711
|
+
if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
|
|
712
|
+
if (type.displayName) return type.displayName;
|
|
713
|
+
if (type.render && typeof type.render === 'function') {
|
|
714
|
+
return type.render.displayName || type.render.name || 'Anonymous';
|
|
715
|
+
}
|
|
716
|
+
return 'Anonymous';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isMemoized(fiber) {
|
|
720
|
+
// MemoComponent = 14, SimpleMemoComponent = 15
|
|
721
|
+
return fiber.tag === 14 || fiber.tag === 15;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function isComponent(fiber) {
|
|
725
|
+
// FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
|
|
726
|
+
return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function shallowEqual(a, b) {
|
|
730
|
+
if (a === b) return true;
|
|
731
|
+
if (!a || !b) return a === b;
|
|
732
|
+
var keysA = Object.keys(a);
|
|
733
|
+
var keysB = Object.keys(b);
|
|
734
|
+
if (keysA.length !== keysB.length) return false;
|
|
735
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
736
|
+
var k = keysA[i];
|
|
737
|
+
if (k === 'children') continue; // ignore children prop
|
|
738
|
+
if (a[k] !== b[k]) return false;
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function getParentComponentName(fiber) {
|
|
744
|
+
var parent = fiber.return;
|
|
745
|
+
while (parent) {
|
|
746
|
+
if (isComponent(parent)) {
|
|
747
|
+
var name = extractName(parent);
|
|
748
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
|
|
749
|
+
}
|
|
750
|
+
parent = parent.return;
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function walkCommit(fiber) {
|
|
756
|
+
if (!fiber) return;
|
|
757
|
+
|
|
758
|
+
if (isComponent(fiber)) {
|
|
759
|
+
var name = extractName(fiber);
|
|
760
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
|
|
761
|
+
var memoized = isMemoized(fiber);
|
|
762
|
+
var currentProps = fiber.memoizedProps || {};
|
|
763
|
+
var prev = renderedComponents.get(name);
|
|
764
|
+
|
|
765
|
+
var propsChanged = true;
|
|
766
|
+
var stateChanged = false;
|
|
767
|
+
var contextChanged = false;
|
|
768
|
+
var hookDepsChanged = false;
|
|
769
|
+
var forceUpdate = false;
|
|
770
|
+
|
|
771
|
+
if (prev) {
|
|
772
|
+
propsChanged = !shallowEqual(prev.lastProps, currentProps);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// State: check memoizedState chain
|
|
776
|
+
var memoizedState = fiber.memoizedState;
|
|
777
|
+
if (prev && prev.lastStateSerialized !== undefined) {
|
|
778
|
+
try {
|
|
779
|
+
var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
780
|
+
stateChanged = stateSig !== prev.lastStateSerialized;
|
|
781
|
+
} catch (_) {
|
|
782
|
+
stateChanged = false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Context: use _debugHookTypes or check dependencies
|
|
787
|
+
var deps = fiber.dependencies;
|
|
788
|
+
if (deps && deps.firstContext) {
|
|
789
|
+
contextChanged = true; // conservative: context dep present = may have changed
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
var stateSig;
|
|
793
|
+
try {
|
|
794
|
+
stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
795
|
+
} catch (_) {
|
|
796
|
+
stateSig = null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
renderedComponents.set(name, {
|
|
800
|
+
lastProps: currentProps,
|
|
801
|
+
lastStateSerialized: stateSig,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
var parentName = getParentComponentName(fiber);
|
|
805
|
+
|
|
806
|
+
window.__SCOPE_RENDER_EVENTS__.push({
|
|
807
|
+
component: name,
|
|
808
|
+
renderIndex: window.__SCOPE_RENDER_INDEX__++,
|
|
809
|
+
propsChanged: prev ? propsChanged : false,
|
|
810
|
+
stateChanged: stateChanged,
|
|
811
|
+
contextChanged: contextChanged,
|
|
812
|
+
memoized: memoized,
|
|
813
|
+
parentComponent: parentName,
|
|
814
|
+
hookDepsChanged: hookDepsChanged,
|
|
815
|
+
forceUpdate: forceUpdate,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
walkCommit(fiber.child);
|
|
821
|
+
walkCommit(fiber.sibling);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
825
|
+
if (typeof originalOnCommit === 'function') {
|
|
826
|
+
originalOnCommit.call(hook, rendererID, root, priorityLevel);
|
|
827
|
+
}
|
|
828
|
+
var wipRoot = root && root.current && root.current.alternate;
|
|
829
|
+
if (wipRoot) walkCommit(wipRoot);
|
|
830
|
+
};
|
|
831
|
+
})();
|
|
832
|
+
`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
async function replayInteraction(page, steps) {
|
|
836
|
+
for (const step of steps) {
|
|
837
|
+
switch (step.action) {
|
|
838
|
+
case "click":
|
|
839
|
+
if (step.target !== void 0) {
|
|
840
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
case "type":
|
|
845
|
+
if (step.target !== void 0 && step.text !== void 0) {
|
|
846
|
+
await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
|
|
847
|
+
await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
case "hover":
|
|
853
|
+
if (step.target !== void 0) {
|
|
854
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
case "blur":
|
|
859
|
+
if (step.target !== void 0) {
|
|
860
|
+
await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
case "focus":
|
|
865
|
+
if (step.target !== void 0) {
|
|
866
|
+
await page.focus(step.target, { timeout: 5e3 }).catch(() => {
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
case "scroll":
|
|
871
|
+
if (step.target !== void 0) {
|
|
872
|
+
await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
case "wait": {
|
|
877
|
+
const timeout = step.timeout ?? 1e3;
|
|
878
|
+
if (step.condition === "idle") {
|
|
879
|
+
await page.waitForLoadState("networkidle", { timeout }).catch(() => {
|
|
880
|
+
});
|
|
881
|
+
} else {
|
|
882
|
+
await page.waitForTimeout(timeout);
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
var _pool = null;
|
|
890
|
+
async function getPool() {
|
|
891
|
+
if (_pool === null) {
|
|
892
|
+
_pool = new BrowserPool({
|
|
893
|
+
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
894
|
+
viewportWidth: 1280,
|
|
895
|
+
viewportHeight: 800
|
|
896
|
+
});
|
|
897
|
+
await _pool.init();
|
|
898
|
+
}
|
|
899
|
+
return _pool;
|
|
900
|
+
}
|
|
901
|
+
async function shutdownPool() {
|
|
902
|
+
if (_pool !== null) {
|
|
903
|
+
await _pool.close();
|
|
904
|
+
_pool = null;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function analyzeRenders(options) {
|
|
908
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
|
|
909
|
+
const manifest = loadManifest(manifestPath);
|
|
910
|
+
const descriptor = manifest.components[options.componentName];
|
|
911
|
+
if (descriptor === void 0) {
|
|
912
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
913
|
+
throw new Error(
|
|
914
|
+
`Component "${options.componentName}" not found in manifest.
|
|
915
|
+
Available: ${available}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const rootDir = process.cwd();
|
|
919
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
920
|
+
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
921
|
+
const pool = await getPool();
|
|
922
|
+
const slot = await pool.acquire();
|
|
923
|
+
const { page } = slot;
|
|
924
|
+
const startMs = performance.now();
|
|
925
|
+
try {
|
|
926
|
+
await page.addInitScript(buildInstrumentationScript());
|
|
927
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
928
|
+
await page.waitForFunction(
|
|
929
|
+
() => window.__SCOPE_RENDER_COMPLETE__ === true,
|
|
930
|
+
{ timeout: 15e3 }
|
|
931
|
+
);
|
|
932
|
+
await page.waitForTimeout(100);
|
|
933
|
+
await page.evaluate(() => {
|
|
934
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
935
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
936
|
+
});
|
|
937
|
+
await replayInteraction(page, options.interaction);
|
|
938
|
+
await page.waitForTimeout(200);
|
|
939
|
+
const interactionDurationMs = performance.now() - startMs;
|
|
940
|
+
const rawEvents = await page.evaluate(() => {
|
|
941
|
+
return window.__SCOPE_RENDER_EVENTS__ ?? [];
|
|
942
|
+
});
|
|
943
|
+
const renders = buildCausalityChains(rawEvents);
|
|
944
|
+
const flags = applyHeuristicFlags(renders);
|
|
945
|
+
const uniqueComponents = new Set(renders.map((r) => r.component)).size;
|
|
946
|
+
const wastedRenders = renders.filter((r) => r.wasted).length;
|
|
947
|
+
return {
|
|
948
|
+
component: options.componentName,
|
|
949
|
+
interaction: options.interaction,
|
|
950
|
+
summary: {
|
|
951
|
+
totalRenders: renders.length,
|
|
952
|
+
uniqueComponents,
|
|
953
|
+
wastedRenders,
|
|
954
|
+
interactionDurationMs: Math.round(interactionDurationMs)
|
|
955
|
+
},
|
|
956
|
+
renders,
|
|
957
|
+
flags
|
|
958
|
+
};
|
|
959
|
+
} finally {
|
|
960
|
+
pool.release(slot);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function formatRendersTable(result) {
|
|
964
|
+
const lines = [];
|
|
965
|
+
lines.push(`
|
|
966
|
+
\u{1F50D} Re-render Analysis: ${result.component}`);
|
|
967
|
+
lines.push(`${"\u2500".repeat(60)}`);
|
|
968
|
+
lines.push(`Total renders: ${result.summary.totalRenders}`);
|
|
969
|
+
lines.push(`Unique components: ${result.summary.uniqueComponents}`);
|
|
970
|
+
lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
|
|
971
|
+
lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
|
|
972
|
+
lines.push("");
|
|
973
|
+
if (result.renders.length === 0) {
|
|
974
|
+
lines.push("No re-renders captured during interaction.");
|
|
975
|
+
} else {
|
|
976
|
+
lines.push("Re-renders:");
|
|
977
|
+
lines.push(
|
|
978
|
+
`${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
|
|
979
|
+
);
|
|
980
|
+
lines.push("\u2500".repeat(80));
|
|
981
|
+
for (const r of result.renders) {
|
|
982
|
+
const wasted = r.wasted ? "\u26A0 yes" : "no";
|
|
983
|
+
const idx = String(r.renderIndex).padEnd(4);
|
|
984
|
+
const comp = r.component.slice(0, 29).padEnd(30);
|
|
985
|
+
const trig = r.trigger.padEnd(18);
|
|
986
|
+
const w = wasted.padEnd(7);
|
|
987
|
+
const depth = r.chain.length;
|
|
988
|
+
lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (result.flags.length > 0) {
|
|
992
|
+
lines.push("");
|
|
993
|
+
lines.push("Flags:");
|
|
994
|
+
for (const flag of result.flags) {
|
|
995
|
+
const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
|
|
996
|
+
lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return lines.join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
function createInstrumentRendersCommand() {
|
|
1002
|
+
return new 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(
|
|
1003
|
+
"--interaction <json>",
|
|
1004
|
+
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1005
|
+
"[]"
|
|
1006
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
|
|
1007
|
+
async (componentName, opts) => {
|
|
1008
|
+
let interaction = [];
|
|
1009
|
+
try {
|
|
1010
|
+
interaction = JSON.parse(opts.interaction);
|
|
1011
|
+
if (!Array.isArray(interaction)) {
|
|
1012
|
+
throw new Error("Interaction must be a JSON array");
|
|
1013
|
+
}
|
|
1014
|
+
} catch {
|
|
1015
|
+
process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
|
|
1016
|
+
`);
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
process.stderr.write(
|
|
1021
|
+
`Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
|
|
1022
|
+
`
|
|
1023
|
+
);
|
|
1024
|
+
const result = await analyzeRenders({
|
|
1025
|
+
componentName,
|
|
1026
|
+
interaction,
|
|
1027
|
+
manifestPath: opts.manifest
|
|
1028
|
+
});
|
|
1029
|
+
await shutdownPool();
|
|
1030
|
+
if (opts.json || !isTTY()) {
|
|
1031
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1032
|
+
`);
|
|
1033
|
+
} else {
|
|
1034
|
+
process.stdout.write(`${formatRendersTable(result)}
|
|
1035
|
+
`);
|
|
1036
|
+
}
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
await shutdownPool();
|
|
1039
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1040
|
+
`);
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
function createInstrumentCommand() {
|
|
1047
|
+
const instrumentCmd = new Command("instrument").description(
|
|
1048
|
+
"Structured instrumentation commands for React component analysis"
|
|
1049
|
+
);
|
|
1050
|
+
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1051
|
+
return instrumentCmd;
|
|
1052
|
+
}
|
|
588
1053
|
var CONFIG_FILENAMES = [
|
|
589
1054
|
".reactscope/config.json",
|
|
590
1055
|
".reactscope/config.js",
|
|
@@ -702,24 +1167,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
702
1167
|
}
|
|
703
1168
|
|
|
704
1169
|
// src/render-commands.ts
|
|
705
|
-
var
|
|
1170
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
706
1171
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
707
|
-
var
|
|
708
|
-
async function
|
|
709
|
-
if (
|
|
710
|
-
|
|
1172
|
+
var _pool2 = null;
|
|
1173
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1174
|
+
if (_pool2 === null) {
|
|
1175
|
+
_pool2 = new BrowserPool({
|
|
711
1176
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
712
1177
|
viewportWidth,
|
|
713
1178
|
viewportHeight
|
|
714
1179
|
});
|
|
715
|
-
await
|
|
1180
|
+
await _pool2.init();
|
|
716
1181
|
}
|
|
717
|
-
return
|
|
1182
|
+
return _pool2;
|
|
718
1183
|
}
|
|
719
|
-
async function
|
|
720
|
-
if (
|
|
721
|
-
await
|
|
722
|
-
|
|
1184
|
+
async function shutdownPool2() {
|
|
1185
|
+
if (_pool2 !== null) {
|
|
1186
|
+
await _pool2.close();
|
|
1187
|
+
_pool2 = null;
|
|
723
1188
|
}
|
|
724
1189
|
}
|
|
725
1190
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -730,7 +1195,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
730
1195
|
_satori: satori,
|
|
731
1196
|
async renderCell(props, _complexityClass) {
|
|
732
1197
|
const startMs = performance.now();
|
|
733
|
-
const pool = await
|
|
1198
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
734
1199
|
const htmlHarness = await buildComponentHarness(
|
|
735
1200
|
filePath,
|
|
736
1201
|
componentName,
|
|
@@ -827,7 +1292,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
827
1292
|
};
|
|
828
1293
|
}
|
|
829
1294
|
function registerRenderSingle(renderCmd) {
|
|
830
|
-
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",
|
|
1295
|
+
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(
|
|
831
1296
|
async (componentName, opts) => {
|
|
832
1297
|
try {
|
|
833
1298
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -866,7 +1331,7 @@ Available: ${available}`
|
|
|
866
1331
|
}
|
|
867
1332
|
}
|
|
868
1333
|
);
|
|
869
|
-
await
|
|
1334
|
+
await shutdownPool2();
|
|
870
1335
|
if (outcome.crashed) {
|
|
871
1336
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
872
1337
|
`);
|
|
@@ -914,7 +1379,7 @@ Available: ${available}`
|
|
|
914
1379
|
);
|
|
915
1380
|
}
|
|
916
1381
|
} catch (err) {
|
|
917
|
-
await
|
|
1382
|
+
await shutdownPool2();
|
|
918
1383
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
919
1384
|
`);
|
|
920
1385
|
process.exit(1);
|
|
@@ -926,7 +1391,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
926
1391
|
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(
|
|
927
1392
|
"--contexts <ids>",
|
|
928
1393
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
929
|
-
).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",
|
|
1394
|
+
).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(
|
|
930
1395
|
async (componentName, opts) => {
|
|
931
1396
|
try {
|
|
932
1397
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -999,7 +1464,7 @@ Available: ${available}`
|
|
|
999
1464
|
concurrency
|
|
1000
1465
|
});
|
|
1001
1466
|
const result = await matrix.render();
|
|
1002
|
-
await
|
|
1467
|
+
await shutdownPool2();
|
|
1003
1468
|
process.stderr.write(
|
|
1004
1469
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1005
1470
|
`
|
|
@@ -1044,7 +1509,7 @@ Available: ${available}`
|
|
|
1044
1509
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1045
1510
|
}
|
|
1046
1511
|
} catch (err) {
|
|
1047
|
-
await
|
|
1512
|
+
await shutdownPool2();
|
|
1048
1513
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1049
1514
|
`);
|
|
1050
1515
|
process.exit(1);
|
|
@@ -1053,7 +1518,7 @@ Available: ${available}`
|
|
|
1053
1518
|
);
|
|
1054
1519
|
}
|
|
1055
1520
|
function registerRenderAll(renderCmd) {
|
|
1056
|
-
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",
|
|
1521
|
+
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(
|
|
1057
1522
|
async (opts) => {
|
|
1058
1523
|
try {
|
|
1059
1524
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1141,13 +1606,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1141
1606
|
workers.push(worker());
|
|
1142
1607
|
}
|
|
1143
1608
|
await Promise.all(workers);
|
|
1144
|
-
await
|
|
1609
|
+
await shutdownPool2();
|
|
1145
1610
|
process.stderr.write("\n");
|
|
1146
1611
|
const summary = formatSummaryText(results, outputDir);
|
|
1147
1612
|
process.stderr.write(`${summary}
|
|
1148
1613
|
`);
|
|
1149
1614
|
} catch (err) {
|
|
1150
|
-
await
|
|
1615
|
+
await shutdownPool2();
|
|
1151
1616
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1152
1617
|
`);
|
|
1153
1618
|
process.exit(1);
|
|
@@ -1896,6 +2361,7 @@ function createProgram(options = {}) {
|
|
|
1896
2361
|
program.addCommand(createManifestCommand());
|
|
1897
2362
|
program.addCommand(createRenderCommand());
|
|
1898
2363
|
program.addCommand(createTokensCommand());
|
|
2364
|
+
program.addCommand(createInstrumentCommand());
|
|
1899
2365
|
return program;
|
|
1900
2366
|
}
|
|
1901
2367
|
|