@agent-scope/cli 1.6.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 +1046 -218
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +832 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +832 -22
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var playwright$1 = require('playwright');
|
|
|
9
9
|
var render = require('@agent-scope/render');
|
|
10
10
|
var esbuild = require('esbuild');
|
|
11
11
|
var module$1 = require('module');
|
|
12
|
+
var tokens = require('@agent-scope/tokens');
|
|
12
13
|
|
|
13
14
|
function _interopNamespace(e) {
|
|
14
15
|
if (e && e.__esModule) return e;
|
|
@@ -606,6 +607,471 @@ function csvEscape(value) {
|
|
|
606
607
|
}
|
|
607
608
|
return value;
|
|
608
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
|
+
}
|
|
609
1075
|
var CONFIG_FILENAMES = [
|
|
610
1076
|
".reactscope/config.json",
|
|
611
1077
|
".reactscope/config.js",
|
|
@@ -723,24 +1189,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
723
1189
|
}
|
|
724
1190
|
|
|
725
1191
|
// src/render-commands.ts
|
|
726
|
-
var
|
|
1192
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
727
1193
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
728
|
-
var
|
|
729
|
-
async function
|
|
730
|
-
if (
|
|
731
|
-
|
|
1194
|
+
var _pool2 = null;
|
|
1195
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1196
|
+
if (_pool2 === null) {
|
|
1197
|
+
_pool2 = new render.BrowserPool({
|
|
732
1198
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
733
1199
|
viewportWidth,
|
|
734
1200
|
viewportHeight
|
|
735
1201
|
});
|
|
736
|
-
await
|
|
1202
|
+
await _pool2.init();
|
|
737
1203
|
}
|
|
738
|
-
return
|
|
1204
|
+
return _pool2;
|
|
739
1205
|
}
|
|
740
|
-
async function
|
|
741
|
-
if (
|
|
742
|
-
await
|
|
743
|
-
|
|
1206
|
+
async function shutdownPool2() {
|
|
1207
|
+
if (_pool2 !== null) {
|
|
1208
|
+
await _pool2.close();
|
|
1209
|
+
_pool2 = null;
|
|
744
1210
|
}
|
|
745
1211
|
}
|
|
746
1212
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -751,7 +1217,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
751
1217
|
_satori: satori,
|
|
752
1218
|
async renderCell(props, _complexityClass) {
|
|
753
1219
|
const startMs = performance.now();
|
|
754
|
-
const pool = await
|
|
1220
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
755
1221
|
const htmlHarness = await buildComponentHarness(
|
|
756
1222
|
filePath,
|
|
757
1223
|
componentName,
|
|
@@ -848,7 +1314,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
848
1314
|
};
|
|
849
1315
|
}
|
|
850
1316
|
function registerRenderSingle(renderCmd) {
|
|
851
|
-
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(
|
|
852
1318
|
async (componentName, opts) => {
|
|
853
1319
|
try {
|
|
854
1320
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -887,7 +1353,7 @@ Available: ${available}`
|
|
|
887
1353
|
}
|
|
888
1354
|
}
|
|
889
1355
|
);
|
|
890
|
-
await
|
|
1356
|
+
await shutdownPool2();
|
|
891
1357
|
if (outcome.crashed) {
|
|
892
1358
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
893
1359
|
`);
|
|
@@ -935,7 +1401,7 @@ Available: ${available}`
|
|
|
935
1401
|
);
|
|
936
1402
|
}
|
|
937
1403
|
} catch (err) {
|
|
938
|
-
await
|
|
1404
|
+
await shutdownPool2();
|
|
939
1405
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
940
1406
|
`);
|
|
941
1407
|
process.exit(1);
|
|
@@ -947,7 +1413,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
947
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(
|
|
948
1414
|
"--contexts <ids>",
|
|
949
1415
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
950
|
-
).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(
|
|
951
1417
|
async (componentName, opts) => {
|
|
952
1418
|
try {
|
|
953
1419
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1020,7 +1486,7 @@ Available: ${available}`
|
|
|
1020
1486
|
concurrency
|
|
1021
1487
|
});
|
|
1022
1488
|
const result = await matrix.render();
|
|
1023
|
-
await
|
|
1489
|
+
await shutdownPool2();
|
|
1024
1490
|
process.stderr.write(
|
|
1025
1491
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1026
1492
|
`
|
|
@@ -1065,7 +1531,7 @@ Available: ${available}`
|
|
|
1065
1531
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1066
1532
|
}
|
|
1067
1533
|
} catch (err) {
|
|
1068
|
-
await
|
|
1534
|
+
await shutdownPool2();
|
|
1069
1535
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1070
1536
|
`);
|
|
1071
1537
|
process.exit(1);
|
|
@@ -1074,7 +1540,7 @@ Available: ${available}`
|
|
|
1074
1540
|
);
|
|
1075
1541
|
}
|
|
1076
1542
|
function registerRenderAll(renderCmd) {
|
|
1077
|
-
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(
|
|
1078
1544
|
async (opts) => {
|
|
1079
1545
|
try {
|
|
1080
1546
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1162,13 +1628,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1162
1628
|
workers.push(worker());
|
|
1163
1629
|
}
|
|
1164
1630
|
await Promise.all(workers);
|
|
1165
|
-
await
|
|
1631
|
+
await shutdownPool2();
|
|
1166
1632
|
process.stderr.write("\n");
|
|
1167
1633
|
const summary = formatSummaryText(results, outputDir);
|
|
1168
1634
|
process.stderr.write(`${summary}
|
|
1169
1635
|
`);
|
|
1170
1636
|
} catch (err) {
|
|
1171
|
-
await
|
|
1637
|
+
await shutdownPool2();
|
|
1172
1638
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1173
1639
|
`);
|
|
1174
1640
|
process.exit(1);
|
|
@@ -1487,6 +1953,348 @@ function buildStructuredReport(report) {
|
|
|
1487
1953
|
route: report.route?.pattern ?? null
|
|
1488
1954
|
};
|
|
1489
1955
|
}
|
|
1956
|
+
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
1957
|
+
var CONFIG_FILE = "reactscope.config.json";
|
|
1958
|
+
function isTTY2() {
|
|
1959
|
+
return process.stdout.isTTY === true;
|
|
1960
|
+
}
|
|
1961
|
+
function pad3(value, width) {
|
|
1962
|
+
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
1963
|
+
}
|
|
1964
|
+
function buildTable2(headers, rows) {
|
|
1965
|
+
const colWidths = headers.map(
|
|
1966
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
1967
|
+
);
|
|
1968
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
1969
|
+
const headerRow = headers.map((h, i) => pad3(h, colWidths[i] ?? 0)).join(" ");
|
|
1970
|
+
const dataRows = rows.map(
|
|
1971
|
+
(row2) => row2.map((cell, i) => pad3(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
1972
|
+
);
|
|
1973
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
1974
|
+
}
|
|
1975
|
+
function resolveTokenFilePath(fileFlag) {
|
|
1976
|
+
if (fileFlag !== void 0) {
|
|
1977
|
+
return path.resolve(process.cwd(), fileFlag);
|
|
1978
|
+
}
|
|
1979
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
1980
|
+
if (fs.existsSync(configPath)) {
|
|
1981
|
+
try {
|
|
1982
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
1983
|
+
const config = JSON.parse(raw);
|
|
1984
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
1985
|
+
const file = config.tokens.file;
|
|
1986
|
+
return path.resolve(process.cwd(), file);
|
|
1987
|
+
}
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
1992
|
+
}
|
|
1993
|
+
function loadTokens(absPath) {
|
|
1994
|
+
if (!fs.existsSync(absPath)) {
|
|
1995
|
+
throw new Error(
|
|
1996
|
+
`Token file not found at ${absPath}.
|
|
1997
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
2001
|
+
return tokens.parseTokenFileSync(raw);
|
|
2002
|
+
}
|
|
2003
|
+
function getRawValue(node, segments) {
|
|
2004
|
+
const [head, ...rest] = segments;
|
|
2005
|
+
if (head === void 0) return null;
|
|
2006
|
+
const child = node[head];
|
|
2007
|
+
if (child === void 0 || child === null) return null;
|
|
2008
|
+
if (rest.length === 0) {
|
|
2009
|
+
if (typeof child === "object" && !Array.isArray(child) && "value" in child) {
|
|
2010
|
+
const v = child.value;
|
|
2011
|
+
return typeof v === "string" || typeof v === "number" ? v : null;
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
if (typeof child === "object" && !Array.isArray(child)) {
|
|
2016
|
+
return getRawValue(child, rest);
|
|
2017
|
+
}
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
function buildResolutionChain(startPath, rawTokens) {
|
|
2021
|
+
const chain = [];
|
|
2022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2023
|
+
let current = startPath;
|
|
2024
|
+
while (!seen.has(current)) {
|
|
2025
|
+
seen.add(current);
|
|
2026
|
+
const rawValue = getRawValue(rawTokens, current.split("."));
|
|
2027
|
+
if (rawValue === null) break;
|
|
2028
|
+
chain.push({ path: current, rawValue: String(rawValue) });
|
|
2029
|
+
const refMatch = /^\{([^}]+)\}$/.exec(String(rawValue));
|
|
2030
|
+
if (refMatch === null) break;
|
|
2031
|
+
current = refMatch[1] ?? "";
|
|
2032
|
+
}
|
|
2033
|
+
return chain;
|
|
2034
|
+
}
|
|
2035
|
+
function registerGet2(tokensCmd) {
|
|
2036
|
+
tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2037
|
+
try {
|
|
2038
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
2039
|
+
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2040
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2041
|
+
const resolvedValue = resolver.resolve(tokenPath);
|
|
2042
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2043
|
+
if (useJson) {
|
|
2044
|
+
const token = tokens$1.find((t) => t.path === tokenPath);
|
|
2045
|
+
process.stdout.write(
|
|
2046
|
+
`${JSON.stringify({ path: tokenPath, value: token?.value, resolvedValue, type: token?.type }, null, 2)}
|
|
2047
|
+
`
|
|
2048
|
+
);
|
|
2049
|
+
} else {
|
|
2050
|
+
process.stdout.write(`${resolvedValue}
|
|
2051
|
+
`);
|
|
2052
|
+
}
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2055
|
+
`);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function registerList2(tokensCmd) {
|
|
2061
|
+
tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2062
|
+
(category, opts) => {
|
|
2063
|
+
try {
|
|
2064
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
2065
|
+
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2066
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2067
|
+
const filtered = resolver.list(opts.type, category);
|
|
2068
|
+
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2069
|
+
if (useJson) {
|
|
2070
|
+
process.stdout.write(`${JSON.stringify(filtered, null, 2)}
|
|
2071
|
+
`);
|
|
2072
|
+
} else {
|
|
2073
|
+
if (filtered.length === 0) {
|
|
2074
|
+
process.stdout.write("No tokens found.\n");
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const headers = ["PATH", "VALUE", "RESOLVED", "TYPE"];
|
|
2078
|
+
const rows = filtered.map((t) => [t.path, String(t.value), t.resolvedValue, t.type]);
|
|
2079
|
+
process.stdout.write(`${buildTable2(headers, rows)}
|
|
2080
|
+
`);
|
|
2081
|
+
}
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2084
|
+
`);
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
function registerSearch(tokensCmd) {
|
|
2091
|
+
tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2092
|
+
(value, opts) => {
|
|
2093
|
+
try {
|
|
2094
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
2095
|
+
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2096
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2097
|
+
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2098
|
+
const typesToSearch = opts.type ? [opts.type] : [
|
|
2099
|
+
"color",
|
|
2100
|
+
"dimension",
|
|
2101
|
+
"fontFamily",
|
|
2102
|
+
"fontWeight",
|
|
2103
|
+
"number",
|
|
2104
|
+
"shadow",
|
|
2105
|
+
"duration",
|
|
2106
|
+
"cubicBezier"
|
|
2107
|
+
];
|
|
2108
|
+
const exactMatches = [];
|
|
2109
|
+
const nearestMatches = [];
|
|
2110
|
+
for (const type of typesToSearch) {
|
|
2111
|
+
const exact = resolver.match(value, type);
|
|
2112
|
+
if (exact !== null) {
|
|
2113
|
+
exactMatches.push({
|
|
2114
|
+
path: exact.token.path,
|
|
2115
|
+
resolvedValue: exact.token.resolvedValue,
|
|
2116
|
+
type: exact.token.type,
|
|
2117
|
+
exact: true,
|
|
2118
|
+
distance: 0
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (exactMatches.length === 0 && opts.fuzzy) {
|
|
2123
|
+
for (const type of typesToSearch) {
|
|
2124
|
+
const typeTokens = tokens$1.filter((t) => t.type === type);
|
|
2125
|
+
if (typeTokens.length === 0) continue;
|
|
2126
|
+
try {
|
|
2127
|
+
const near = resolver.nearest(value, type);
|
|
2128
|
+
nearestMatches.push({
|
|
2129
|
+
path: near.token.path,
|
|
2130
|
+
resolvedValue: near.token.resolvedValue,
|
|
2131
|
+
type: near.token.type,
|
|
2132
|
+
exact: near.exact,
|
|
2133
|
+
distance: near.distance
|
|
2134
|
+
});
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
nearestMatches.sort((a, b) => a.distance - b.distance);
|
|
2139
|
+
nearestMatches.splice(3);
|
|
2140
|
+
}
|
|
2141
|
+
const results = exactMatches.length > 0 ? exactMatches : nearestMatches;
|
|
2142
|
+
if (useJson) {
|
|
2143
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
2144
|
+
`);
|
|
2145
|
+
} else {
|
|
2146
|
+
if (results.length === 0) {
|
|
2147
|
+
process.stdout.write(
|
|
2148
|
+
`No tokens found matching "${value}".
|
|
2149
|
+
Tip: use --fuzzy for nearest-match search.
|
|
2150
|
+
`
|
|
2151
|
+
);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const headers = ["PATH", "RESOLVED VALUE", "TYPE", "MATCH", "DISTANCE"];
|
|
2155
|
+
const rows = results.map((r) => [
|
|
2156
|
+
r.path,
|
|
2157
|
+
r.resolvedValue,
|
|
2158
|
+
r.type,
|
|
2159
|
+
r.exact ? "exact" : "nearest",
|
|
2160
|
+
r.exact ? "\u2014" : r.distance.toFixed(2)
|
|
2161
|
+
]);
|
|
2162
|
+
process.stdout.write(`${buildTable2(headers, rows)}
|
|
2163
|
+
`);
|
|
2164
|
+
}
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2167
|
+
`);
|
|
2168
|
+
process.exit(1);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
function registerResolve(tokensCmd) {
|
|
2174
|
+
tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2175
|
+
try {
|
|
2176
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
2177
|
+
const absFilePath = filePath;
|
|
2178
|
+
const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
|
|
2179
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2180
|
+
resolver.resolve(tokenPath);
|
|
2181
|
+
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
2182
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2183
|
+
if (useJson) {
|
|
2184
|
+
process.stdout.write(`${JSON.stringify({ path: tokenPath, chain }, null, 2)}
|
|
2185
|
+
`);
|
|
2186
|
+
} else {
|
|
2187
|
+
if (chain.length === 0) {
|
|
2188
|
+
process.stdout.write(`Token "${tokenPath}" not found.
|
|
2189
|
+
`);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
const parts = chain.map((step, i) => {
|
|
2193
|
+
if (i < chain.length - 1) {
|
|
2194
|
+
return `${step.path} \u2192 ${step.rawValue}`;
|
|
2195
|
+
}
|
|
2196
|
+
return step.rawValue;
|
|
2197
|
+
});
|
|
2198
|
+
process.stdout.write(`${parts.join("\n ")}
|
|
2199
|
+
`);
|
|
2200
|
+
}
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2203
|
+
`);
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
function registerValidate(tokensCmd) {
|
|
2209
|
+
tokensCmd.command("validate").description(
|
|
2210
|
+
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2211
|
+
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2212
|
+
try {
|
|
2213
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
2214
|
+
if (!fs.existsSync(filePath)) {
|
|
2215
|
+
throw new Error(
|
|
2216
|
+
`Token file not found at ${filePath}.
|
|
2217
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
2221
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2222
|
+
const errors = [];
|
|
2223
|
+
let parsed;
|
|
2224
|
+
try {
|
|
2225
|
+
parsed = JSON.parse(raw);
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
errors.push({
|
|
2228
|
+
code: "PARSE_ERROR",
|
|
2229
|
+
message: `Failed to parse token file as JSON: ${String(err)}`
|
|
2230
|
+
});
|
|
2231
|
+
outputValidationResult(filePath, errors, useJson);
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
tokens.validateTokenFile(parsed);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
if (err instanceof tokens.TokenValidationError) {
|
|
2238
|
+
for (const e of err.errors) {
|
|
2239
|
+
errors.push({ code: e.code, path: e.path, message: e.message });
|
|
2240
|
+
}
|
|
2241
|
+
outputValidationResult(filePath, errors, useJson);
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
}
|
|
2244
|
+
throw err;
|
|
2245
|
+
}
|
|
2246
|
+
try {
|
|
2247
|
+
tokens.parseTokenFileSync(raw);
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
if (err instanceof tokens.TokenParseError) {
|
|
2250
|
+
errors.push({ code: err.code, path: err.path, message: err.message });
|
|
2251
|
+
} else {
|
|
2252
|
+
errors.push({ code: "UNKNOWN", message: String(err) });
|
|
2253
|
+
}
|
|
2254
|
+
outputValidationResult(filePath, errors, useJson);
|
|
2255
|
+
process.exit(1);
|
|
2256
|
+
}
|
|
2257
|
+
outputValidationResult(filePath, errors, useJson);
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2260
|
+
`);
|
|
2261
|
+
process.exit(1);
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
function outputValidationResult(filePath, errors, useJson) {
|
|
2266
|
+
const valid = errors.length === 0;
|
|
2267
|
+
if (useJson) {
|
|
2268
|
+
process.stdout.write(`${JSON.stringify({ valid, file: filePath, errors }, null, 2)}
|
|
2269
|
+
`);
|
|
2270
|
+
} else {
|
|
2271
|
+
if (valid) {
|
|
2272
|
+
process.stdout.write(`\u2713 Token file is valid: ${filePath}
|
|
2273
|
+
`);
|
|
2274
|
+
} else {
|
|
2275
|
+
process.stderr.write(`\u2717 Token file has ${errors.length} error(s): ${filePath}
|
|
2276
|
+
|
|
2277
|
+
`);
|
|
2278
|
+
for (const e of errors) {
|
|
2279
|
+
const pathPrefix = e.path ? ` [${e.path}]` : "";
|
|
2280
|
+
process.stderr.write(` ${e.code}${pathPrefix}: ${e.message}
|
|
2281
|
+
`);
|
|
2282
|
+
}
|
|
2283
|
+
process.exit(1);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function createTokensCommand() {
|
|
2288
|
+
const tokensCmd = new commander.Command("tokens").description(
|
|
2289
|
+
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
2290
|
+
);
|
|
2291
|
+
registerGet2(tokensCmd);
|
|
2292
|
+
registerList2(tokensCmd);
|
|
2293
|
+
registerSearch(tokensCmd);
|
|
2294
|
+
registerResolve(tokensCmd);
|
|
2295
|
+
registerValidate(tokensCmd);
|
|
2296
|
+
return tokensCmd;
|
|
2297
|
+
}
|
|
1490
2298
|
|
|
1491
2299
|
// src/program.ts
|
|
1492
2300
|
function createProgram(options = {}) {
|
|
@@ -1574,11 +2382,14 @@ function createProgram(options = {}) {
|
|
|
1574
2382
|
});
|
|
1575
2383
|
program.addCommand(createManifestCommand());
|
|
1576
2384
|
program.addCommand(createRenderCommand());
|
|
2385
|
+
program.addCommand(createTokensCommand());
|
|
2386
|
+
program.addCommand(createInstrumentCommand());
|
|
1577
2387
|
return program;
|
|
1578
2388
|
}
|
|
1579
2389
|
|
|
1580
2390
|
exports.createManifestCommand = createManifestCommand;
|
|
1581
2391
|
exports.createProgram = createProgram;
|
|
2392
|
+
exports.createTokensCommand = createTokensCommand;
|
|
1582
2393
|
exports.isTTY = isTTY;
|
|
1583
2394
|
exports.matchGlob = matchGlob;
|
|
1584
2395
|
//# sourceMappingURL=index.cjs.map
|