react-cosmos-core 6.0.0-alpha.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/LICENSE +21 -0
- package/dist/fixture/FixtureContext.d.ts +7 -0
- package/dist/fixture/FixtureContext.js +5 -0
- package/dist/fixture/Viewport.d.ts +8 -0
- package/dist/fixture/Viewport.js +19 -0
- package/dist/fixture/types.d.ts +13 -0
- package/dist/fixture/types.js +1 -0
- package/dist/fixture/useSelect/index.d.ts +2 -0
- package/dist/fixture/useSelect/index.js +11 -0
- package/dist/fixture/useSelect/shared.d.ts +6 -0
- package/dist/fixture/useSelect/shared.js +3 -0
- package/dist/fixture/useSelect/useCreateFixtureState.d.ts +2 -0
- package/dist/fixture/useSelect/useCreateFixtureState.js +32 -0
- package/dist/fixture/useSelect/useCurrentValue.d.ts +2 -0
- package/dist/fixture/useSelect/useCurrentValue.js +11 -0
- package/dist/fixture/useSelect/useSetValue.d.ts +2 -0
- package/dist/fixture/useSelect/useSetValue.js +25 -0
- package/dist/fixture/useValue/index.d.ts +7 -0
- package/dist/fixture/useValue/index.js +9 -0
- package/dist/fixture/useValue/shared.d.ts +3 -0
- package/dist/fixture/useValue/shared.js +1 -0
- package/dist/fixture/useValue/useCreateFixtureState.d.ts +2 -0
- package/dist/fixture/useValue/useCreateFixtureState.js +35 -0
- package/dist/fixture/useValue/useCurrentValue.d.ts +2 -0
- package/dist/fixture/useValue/useCurrentValue.js +13 -0
- package/dist/fixture/useValue/useSetValue.d.ts +3 -0
- package/dist/fixture/useValue/useSetValue.js +36 -0
- package/dist/fixtureState/classState.d.ts +22 -0
- package/dist/fixtureState/classState.js +40 -0
- package/dist/fixtureState/controls.d.ts +2 -0
- package/dist/fixtureState/controls.js +4 -0
- package/dist/fixtureState/createValues.d.ts +3 -0
- package/dist/fixtureState/createValues.js +39 -0
- package/dist/fixtureState/extendWithValues.d.ts +3 -0
- package/dist/fixtureState/extendWithValues.js +22 -0
- package/dist/fixtureState/props.d.ts +30 -0
- package/dist/fixtureState/props.js +49 -0
- package/dist/fixtureState/shared.d.ts +9 -0
- package/dist/fixtureState/shared.js +30 -0
- package/dist/fixtureState/types.d.ts +59 -0
- package/dist/fixtureState/types.js +1 -0
- package/dist/fixtureTree/createFixtureTree/collapseFixtureDirs.d.ts +2 -0
- package/dist/fixtureTree/createFixtureTree/collapseFixtureDirs.js +33 -0
- package/dist/fixtureTree/createFixtureTree/collapseIndexes.d.ts +2 -0
- package/dist/fixtureTree/createFixtureTree/collapseIndexes.js +15 -0
- package/dist/fixtureTree/createFixtureTree/collapseNamedIndexes.d.ts +2 -0
- package/dist/fixtureTree/createFixtureTree/collapseNamedIndexes.js +30 -0
- package/dist/fixtureTree/createFixtureTree/collapseOuterDirs.d.ts +2 -0
- package/dist/fixtureTree/createFixtureTree/collapseOuterDirs.js +13 -0
- package/dist/fixtureTree/createFixtureTree/createRawFixtureTree.d.ts +3 -0
- package/dist/fixtureTree/createFixtureTree/createRawFixtureTree.js +66 -0
- package/dist/fixtureTree/createFixtureTree/hideFixtureSuffix.d.ts +2 -0
- package/dist/fixtureTree/createFixtureTree/hideFixtureSuffix.js +24 -0
- package/dist/fixtureTree/createFixtureTree/index.d.ts +7 -0
- package/dist/fixtureTree/createFixtureTree/index.js +16 -0
- package/dist/fixtureTree/flattenFixtureTree.d.ts +10 -0
- package/dist/fixtureTree/flattenFixtureTree.js +28 -0
- package/dist/fixtureTree/types.d.ts +11 -0
- package/dist/fixtureTree/types.js +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/playground/playgroundUrl.d.ts +13 -0
- package/dist/playground/playgroundUrl.js +36 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/decorateFixtureRefs/index.d.ts +5 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/decorateFixtureRefs/index.js +46 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/decorateFixtureRefs/isRefSupported.d.ts +4 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/decorateFixtureRefs/isRefSupported.js +19 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/index.d.ts +3 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/index.js +13 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/shared.d.ts +18 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/shared.js +14 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/useFixtureState.d.ts +4 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/useFixtureState.js +156 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/useReadClassState.d.ts +4 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/classState/useReadClassState.js +60 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/index.d.ts +7 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/index.js +9 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/props/index.d.ts +3 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/props/index.js +77 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/props/useFixtureProps.d.ts +3 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/props/useFixtureProps.js +70 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/findRelevantElementPaths.d.ts +2 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/findRelevantElementPaths.js +23 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/findElementPaths.d.ts +2 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/findElementPaths.js +22 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/getElementAtPath.d.ts +3 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/getElementAtPath.js +26 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/index.d.ts +11 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/index.js +11 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/setElementAtPath.d.ts +2 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/setElementAtPath.js +35 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/shared.d.ts +2 -0
- package/dist/renderer/FixtureLoader/FixtureCapture/shared/nodeTree/shared.js +6 -0
- package/dist/renderer/FixtureLoader/FixtureLoader.d.ts +52 -0
- package/dist/renderer/FixtureLoader/FixtureLoader.js +226 -0
- package/dist/renderer/FixtureLoader/FixtureProvider.d.ts +18 -0
- package/dist/renderer/FixtureLoader/FixtureProvider.js +18 -0
- package/dist/renderer/FixtureLoader/fixtureHelpers.d.ts +2 -0
- package/dist/renderer/FixtureLoader/fixtureHelpers.js +15 -0
- package/dist/renderer/FixtureLoader/getDecoratedFixtureElement/FixtureElement.d.ts +9 -0
- package/dist/renderer/FixtureLoader/getDecoratedFixtureElement/FixtureElement.js +5 -0
- package/dist/renderer/FixtureLoader/getDecoratedFixtureElement/index.d.ts +2 -0
- package/dist/renderer/FixtureLoader/getDecoratedFixtureElement/index.js +15 -0
- package/dist/renderer/FixtureLoader/postMessage.d.ts +2 -0
- package/dist/renderer/FixtureLoader/postMessage.js +22 -0
- package/dist/renderer/FixtureLoader/webSockets.d.ts +2 -0
- package/dist/renderer/FixtureLoader/webSockets.js +32 -0
- package/dist/renderer/getFixtureList.d.ts +8 -0
- package/dist/renderer/getFixtureList.js +19 -0
- package/dist/renderer/getSortedDecoratorsForFixturePath.d.ts +2 -0
- package/dist/renderer/getSortedDecoratorsForFixturePath.js +25 -0
- package/dist/renderer/isMultiFixture.d.ts +2 -0
- package/dist/renderer/isMultiFixture.js +6 -0
- package/dist/renderer/reactTypes.d.ts +28 -0
- package/dist/renderer/reactTypes.js +1 -0
- package/dist/renderer/registerPlaygroundShortcuts.d.ts +1 -0
- package/dist/renderer/registerPlaygroundShortcuts.js +40 -0
- package/dist/renderer/rendererConfig.d.ts +3 -0
- package/dist/renderer/rendererConfig.js +1 -0
- package/dist/renderer/types.d.ts +69 -0
- package/dist/renderer/types.js +1 -0
- package/dist/server/cosmosPluginConfig.d.ts +16 -0
- package/dist/server/cosmosPluginConfig.js +1 -0
- package/dist/server/serverMessage.d.ts +10 -0
- package/dist/server/serverMessage.js +1 -0
- package/dist/server/socketMessage.d.ts +10 -0
- package/dist/server/socketMessage.js +6 -0
- package/dist/utils/array.d.ts +3 -0
- package/dist/utils/array.js +21 -0
- package/dist/utils/keys.d.ts +6 -0
- package/dist/utils/keys.js +6 -0
- package/dist/utils/react/areNodesEqual.d.ts +2 -0
- package/dist/utils/react/areNodesEqual.js +38 -0
- package/dist/utils/react/getComponentName.d.ts +2 -0
- package/dist/utils/react/getComponentName.js +11 -0
- package/dist/utils/react/isReactElement.d.ts +2 -0
- package/dist/utils/react/isReactElement.js +4 -0
- package/dist/utils/string.d.ts +5 -0
- package/dist/utils/string.js +9 -0
- package/dist/utils/tree.d.ts +6 -0
- package/dist/utils/tree.js +21 -0
- package/dist/utils/types.d.ts +1 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/uuid.d.ts +1 -0
- package/dist/utils/uuid.js +6 -0
- package/package.json +15 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { isEqual } from 'lodash-es';
|
|
2
|
+
import { useContext, useEffect, useRef, } from 'react';
|
|
3
|
+
import { FixtureContext } from '../../../../fixture/FixtureContext.js';
|
|
4
|
+
import { createFixtureStateClassState, findFixtureStateClassState, getFixtureStateClassState, removeFixtureStateClassState, } from '../../../../fixtureState/classState.js';
|
|
5
|
+
import { createValues } from '../../../../fixtureState/createValues.js';
|
|
6
|
+
import { extendWithValues } from '../../../../fixtureState/extendWithValues.js';
|
|
7
|
+
import { getComponentName } from '../../../../utils/react/getComponentName.js';
|
|
8
|
+
import { findRelevantElementPaths } from '../shared/findRelevantElementPaths.js';
|
|
9
|
+
import { decorateFixtureRefs } from './decorateFixtureRefs/index.js';
|
|
10
|
+
import { replaceState, } from './shared.js';
|
|
11
|
+
export function useFixtureState(fixture, decoratorId, elRefs) {
|
|
12
|
+
const elPaths = findRelevantElementPaths(fixture);
|
|
13
|
+
const { fixtureState, setFixtureState } = useContext(FixtureContext);
|
|
14
|
+
const lastFixtureState = useFixtureStateRef(fixtureState);
|
|
15
|
+
// Keep a copy of the previous fixture state to observe changes
|
|
16
|
+
const prevFixtureState = useRef(fixtureState);
|
|
17
|
+
// Remember initial state of child components to use as a default when
|
|
18
|
+
// resetting fixture state
|
|
19
|
+
const initialStates = useRef({});
|
|
20
|
+
// Ref handlers are reused because every time we pass a new ref handler to
|
|
21
|
+
// a React element it gets called in the next render loop, even when the
|
|
22
|
+
// associated element instance has been preserved. Having ref handlers fire
|
|
23
|
+
// on every render loop results in unwanted operations and race conditions.
|
|
24
|
+
const cachedRefHandlers = useRef({});
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => {
|
|
27
|
+
initialStates.current = {};
|
|
28
|
+
cachedRefHandlers.current = {};
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Remove fixture state for removed child elements (likely via HMR)
|
|
33
|
+
// FIXME: Also invalidate fixture state at this element path if the
|
|
34
|
+
// component type of the corresponding element changed
|
|
35
|
+
const fsProps = getFixtureStateClassState(fixtureState, decoratorId);
|
|
36
|
+
fsProps.forEach(({ elementId }) => {
|
|
37
|
+
const { elPath } = elementId;
|
|
38
|
+
if (elPaths.indexOf(elementId.elPath) === -1) {
|
|
39
|
+
setFixtureState(prevFs => ({
|
|
40
|
+
...prevFs,
|
|
41
|
+
classState: removeFixtureStateClassState(fixtureState, elementId),
|
|
42
|
+
}));
|
|
43
|
+
if (elRefs.current[elPath]) {
|
|
44
|
+
delete elRefs.current[elPath];
|
|
45
|
+
delete initialStates.current[elPath];
|
|
46
|
+
delete cachedRefHandlers.current[elPath];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
elPaths.forEach(elPath => {
|
|
51
|
+
const elementId = { decoratorId, elPath };
|
|
52
|
+
// Component fixture state can be provided before the fixture mounts (eg.
|
|
53
|
+
// a previous snapshot of a fixture state or the current fixture state
|
|
54
|
+
// from another renderer)
|
|
55
|
+
const fsClassState = findFixtureStateClassState(fixtureState, elementId);
|
|
56
|
+
if (!fsClassState) {
|
|
57
|
+
if (initialStates.current[elPath]) {
|
|
58
|
+
const { state } = initialStates.current[elPath];
|
|
59
|
+
const elRef = elRefs.current[elPath];
|
|
60
|
+
if (!isEqual(elRef.state, state)) {
|
|
61
|
+
replaceState(elRef, state);
|
|
62
|
+
}
|
|
63
|
+
setFixtureState(prevFs => ({
|
|
64
|
+
...prevFs,
|
|
65
|
+
classState: createFixtureStateClassState({
|
|
66
|
+
fixtureState: prevFs,
|
|
67
|
+
elementId,
|
|
68
|
+
values: createValues(state),
|
|
69
|
+
componentName: getComponentName(elRef.constructor),
|
|
70
|
+
}),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const elRef = elRefs.current[elPath];
|
|
76
|
+
// The el ref can be missing for three reasons:
|
|
77
|
+
// 1. Element type is stateless
|
|
78
|
+
// 2. Element type is a class, but doesn't have state. An instance exists
|
|
79
|
+
// but has been discarded because of its lack of state.
|
|
80
|
+
// 3. Element instance unmounted and is about to remount. When this
|
|
81
|
+
// happens, the new instance will be handled when its ref fires again.
|
|
82
|
+
if (!elRef) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// The child's state can be out of sync with the fixture state for two
|
|
86
|
+
// reasons:
|
|
87
|
+
// 1. The child's state changed internally
|
|
88
|
+
// 2. The fixture state changed
|
|
89
|
+
// Here we're interested in the second scenario. In the first scenario
|
|
90
|
+
// we want to let the component state override the fixture state.
|
|
91
|
+
const prevFsClassState = findFixtureStateClassState(prevFixtureState.current, elementId);
|
|
92
|
+
if (prevFsClassState && !isEqual(prevFsClassState, fsClassState)) {
|
|
93
|
+
return replaceState(elRef, extendWithValues(elRef.state, fsClassState.values));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}, [
|
|
98
|
+
decoratorId,
|
|
99
|
+
elPaths,
|
|
100
|
+
elRefs,
|
|
101
|
+
fixtureState,
|
|
102
|
+
fixtureState.classState,
|
|
103
|
+
setFixtureState,
|
|
104
|
+
]);
|
|
105
|
+
// Update prev fixture state ref *after* running effects that reference it
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
prevFixtureState.current = fixtureState;
|
|
108
|
+
});
|
|
109
|
+
return decorateFixtureRefs(fixture, handleRef, cachedRefHandlers.current);
|
|
110
|
+
function handleRef(elPath, elRef) {
|
|
111
|
+
if (!elRef) {
|
|
112
|
+
delete elRefs.current[elPath];
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Only track instances with state
|
|
116
|
+
const { state } = elRef;
|
|
117
|
+
if (!state) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
elRefs.current[elPath] = elRef;
|
|
121
|
+
setInitialState(initialStates.current, elPath, elRef);
|
|
122
|
+
const elementId = { decoratorId, elPath };
|
|
123
|
+
const fsClassState = findFixtureStateClassState(lastFixtureState.current, elementId);
|
|
124
|
+
if (!fsClassState) {
|
|
125
|
+
setFixtureState(prevFs => ({
|
|
126
|
+
...prevFs,
|
|
127
|
+
classState: createFixtureStateClassState({
|
|
128
|
+
fixtureState: prevFs,
|
|
129
|
+
elementId,
|
|
130
|
+
values: createValues(state),
|
|
131
|
+
componentName: getComponentName(elRef.constructor),
|
|
132
|
+
}),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
replaceState(elRef, extendWithValues(state, fsClassState.values));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Make latest fixture state accessible in ref callback
|
|
141
|
+
function useFixtureStateRef(fixtureState) {
|
|
142
|
+
const ref = useRef(fixtureState);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
ref.current = fixtureState;
|
|
145
|
+
});
|
|
146
|
+
return ref;
|
|
147
|
+
}
|
|
148
|
+
function setInitialState(initialStates, elPath, elRef) {
|
|
149
|
+
const found = initialStates[elPath];
|
|
150
|
+
const type = elRef.constructor;
|
|
151
|
+
// Keep the first state recevied for this type
|
|
152
|
+
const initialStateExists = found && found.type === type;
|
|
153
|
+
if (!initialStateExists && elRef.state) {
|
|
154
|
+
initialStates[elPath] = { type, state: elRef.state };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MutableRefObject, ReactNode } from 'react';
|
|
2
|
+
import { FixtureDecoratorId } from '../../../../fixtureState/types.js';
|
|
3
|
+
import { ElRefs } from './shared.js';
|
|
4
|
+
export declare function useReadClassState(fixture: ReactNode, decoratorId: FixtureDecoratorId, elRefs: MutableRefObject<ElRefs>): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isEqual } from 'lodash-es';
|
|
2
|
+
import { useContext, useEffect, useRef, } from 'react';
|
|
3
|
+
import { FixtureContext } from '../../../../fixture/FixtureContext.js';
|
|
4
|
+
import { findFixtureStateClassState, updateFixtureStateClassState, } from '../../../../fixtureState/classState.js';
|
|
5
|
+
import { createValues } from '../../../../fixtureState/createValues.js';
|
|
6
|
+
import { extendWithValues } from '../../../../fixtureState/extendWithValues.js';
|
|
7
|
+
import { findRelevantElementPaths } from '../shared/findRelevantElementPaths.js';
|
|
8
|
+
// How often to check the state of the loaded component and update the fixture
|
|
9
|
+
// state if it changed
|
|
10
|
+
const REFRESH_INTERVAL = 200;
|
|
11
|
+
export function useReadClassState(fixture, decoratorId, elRefs) {
|
|
12
|
+
const elPaths = findRelevantElementPaths(fixture);
|
|
13
|
+
const { fixtureState, setFixtureState } = useContext(FixtureContext);
|
|
14
|
+
const timeoutId = useRef(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// The check should run even if no element paths are found at mount, because
|
|
17
|
+
// the fixture can change during the lifecycle of a FixtureCapture instance
|
|
18
|
+
// and the updated fixture might contain elements of stateful components
|
|
19
|
+
scheduleStateCheck();
|
|
20
|
+
return () => {
|
|
21
|
+
if (timeoutId.current) {
|
|
22
|
+
clearTimeout(timeoutId.current);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
function scheduleStateCheck() {
|
|
27
|
+
// Is there a better way to listen to component state changes?
|
|
28
|
+
timeoutId.current = window.setTimeout(checkState, REFRESH_INTERVAL);
|
|
29
|
+
}
|
|
30
|
+
function checkState() {
|
|
31
|
+
let fixtureStateChangeScheduled = false;
|
|
32
|
+
Object.keys(elRefs.current).map(async (elPath) => {
|
|
33
|
+
if (elPaths.indexOf(elPath) === -1) {
|
|
34
|
+
throw new Error(`[FixtureCapture] Child ref exists for missing element path "${elPath}"`);
|
|
35
|
+
}
|
|
36
|
+
const { state } = elRefs.current[elPath];
|
|
37
|
+
const elementId = { decoratorId, elPath };
|
|
38
|
+
const fsClassState = findFixtureStateClassState(fixtureState, elementId);
|
|
39
|
+
if (fsClassState &&
|
|
40
|
+
state &&
|
|
41
|
+
!doesFixtureStateMatchClassState(fsClassState, state)) {
|
|
42
|
+
fixtureStateChangeScheduled = true;
|
|
43
|
+
setFixtureState(prevFs => ({
|
|
44
|
+
...prevFs,
|
|
45
|
+
classState: updateFixtureStateClassState({
|
|
46
|
+
fixtureState: prevFs,
|
|
47
|
+
elementId,
|
|
48
|
+
values: createValues(state),
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (!fixtureStateChangeScheduled) {
|
|
54
|
+
scheduleStateCheck();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function doesFixtureStateMatchClassState(fsClassState, state) {
|
|
59
|
+
return isEqual(state, extendWithValues(state, fsClassState.values));
|
|
60
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FixtureDecoratorId } from '../../../fixtureState/types.js';
|
|
3
|
+
export type Props = {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
decoratorId: FixtureDecoratorId;
|
|
6
|
+
};
|
|
7
|
+
export declare function FixtureCapture({ children, decoratorId }: Props): JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useClassStateCapture } from './classState/index.js';
|
|
3
|
+
import { usePropsCapture } from './props/index.js';
|
|
4
|
+
export function FixtureCapture({ children, decoratorId }) {
|
|
5
|
+
let fixture = usePropsCapture(children, decoratorId);
|
|
6
|
+
fixture = useClassStateCapture(fixture, decoratorId);
|
|
7
|
+
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
|
|
8
|
+
return React.createElement(React.Fragment, null, fixture);
|
|
9
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import { FixtureContext } from '../../../../fixture/FixtureContext.js';
|
|
3
|
+
import { createValues } from '../../../../fixtureState/createValues.js';
|
|
4
|
+
import { createFixtureStateProps, findFixtureStateProps, getFixtureStateProps, removeFixtureStateProps, updateFixtureStateProps, } from '../../../../fixtureState/props.js';
|
|
5
|
+
import { areNodesEqual } from '../../../../utils/react/areNodesEqual.js';
|
|
6
|
+
import { getComponentName } from '../../../../utils/react/getComponentName.js';
|
|
7
|
+
import { findRelevantElementPaths } from '../shared/findRelevantElementPaths.js';
|
|
8
|
+
import { getElementAtPath, getExpectedElementAtPath, } from '../shared/nodeTree/index.js';
|
|
9
|
+
import { useFixtureProps } from './useFixtureProps.js';
|
|
10
|
+
export function usePropsCapture(fixture, decoratorId) {
|
|
11
|
+
const { fixtureState, setFixtureState } = useContext(FixtureContext);
|
|
12
|
+
const prevFixtureRef = useRef(fixture);
|
|
13
|
+
const elPaths = findRelevantElementPaths(fixture);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Create empty fixture state
|
|
16
|
+
if (!fixtureState.props && elPaths.length === 0) {
|
|
17
|
+
// Make sure not to override any (currently pending) fixture state props
|
|
18
|
+
setFixtureState(prevFs => ({ ...prevFs, props: prevFs.props || [] }));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Remove fixture state for removed child elements (likely via HMR)
|
|
22
|
+
// FIXME: Also invalidate fixture state at this element path if the
|
|
23
|
+
// component type of the corresponding element changed
|
|
24
|
+
const fsProps = getFixtureStateProps(fixtureState, decoratorId);
|
|
25
|
+
fsProps.forEach(({ elementId }) => {
|
|
26
|
+
if (elPaths.indexOf(elementId.elPath) === -1) {
|
|
27
|
+
setFixtureState(prevFs => ({
|
|
28
|
+
...prevFs,
|
|
29
|
+
props: removeFixtureStateProps(fixtureState, elementId),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
elPaths.forEach(elPath => {
|
|
34
|
+
const childEl = getExpectedElementAtPath(fixture, elPath);
|
|
35
|
+
const elementId = { decoratorId, elPath };
|
|
36
|
+
// Component fixture state can be provided before the fixture mounts (eg.
|
|
37
|
+
// a previous snapshot of a fixture state or the current fixture state
|
|
38
|
+
// from another renderer)
|
|
39
|
+
if (!findFixtureStateProps(fixtureState, elementId)) {
|
|
40
|
+
const componentName = getComponentName(childEl.type);
|
|
41
|
+
setFixtureState(prevFs => ({
|
|
42
|
+
...prevFs,
|
|
43
|
+
props: createFixtureStateProps({
|
|
44
|
+
fixtureState: prevFs,
|
|
45
|
+
elementId,
|
|
46
|
+
values: createValues(childEl.props),
|
|
47
|
+
componentName,
|
|
48
|
+
}),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const prevChildEl = getElementAtPath(prevFixtureRef.current, elPath);
|
|
53
|
+
if (!areNodesEqual(prevChildEl, childEl, false)) {
|
|
54
|
+
setFixtureState(prevFs => ({
|
|
55
|
+
...prevFs,
|
|
56
|
+
props: updateFixtureStateProps({
|
|
57
|
+
fixtureState,
|
|
58
|
+
elementId,
|
|
59
|
+
values: createValues(childEl.props),
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}, [
|
|
66
|
+
fixture,
|
|
67
|
+
decoratorId,
|
|
68
|
+
elPaths,
|
|
69
|
+
fixtureState,
|
|
70
|
+
fixtureState.props,
|
|
71
|
+
setFixtureState,
|
|
72
|
+
]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
prevFixtureRef.current = fixture;
|
|
75
|
+
});
|
|
76
|
+
return useFixtureProps(fixture, fixtureState, decoratorId);
|
|
77
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isEqual, mapValues } from 'lodash-es';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { extendWithValues } from '../../../../fixtureState/extendWithValues.js';
|
|
4
|
+
import { DEFAULT_RENDER_KEY, findFixtureStateProps, } from '../../../../fixtureState/props.js';
|
|
5
|
+
import { getComponentName } from '../../../../utils/react/getComponentName.js';
|
|
6
|
+
import { findRelevantElementPaths } from '../shared/findRelevantElementPaths.js';
|
|
7
|
+
import { getChildrenPath, setElementAtPath } from '../shared/nodeTree/index.js';
|
|
8
|
+
export function useFixtureProps(fixture, fixtureState, decoratorId) {
|
|
9
|
+
const propCache = React.useMemo(() => ({}),
|
|
10
|
+
// React.useMemo is used as a cache invalidated by decoratorId
|
|
11
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
|
+
[decoratorId]);
|
|
13
|
+
const elPaths = findRelevantElementPaths(fixture);
|
|
14
|
+
return elPaths.reduce((extendedFixture, elPath) => {
|
|
15
|
+
const elementId = { decoratorId, elPath };
|
|
16
|
+
const fsProps = findFixtureStateProps(fixtureState, elementId);
|
|
17
|
+
return setElementAtPath(extendedFixture, elPath, element => {
|
|
18
|
+
if (!fsProps || componentTypeChanged(fsProps.componentName)) {
|
|
19
|
+
return {
|
|
20
|
+
...element,
|
|
21
|
+
key: getElRenderKey(elPath, DEFAULT_RENDER_KEY),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Prevent overriding child elements with outdated "children" prop values
|
|
25
|
+
// stored in fixture state
|
|
26
|
+
// See https://github.com/react-cosmos/react-cosmos/pull/920 for context
|
|
27
|
+
const originalProps = element.props;
|
|
28
|
+
const extendedProps = extendWithValues(originalProps, fsProps.values);
|
|
29
|
+
// Preserve identity between renders for indentical non-primitive props
|
|
30
|
+
const cachedProps = mapValues(extendedProps, (value, propName) => {
|
|
31
|
+
const key = getPropCacheKey(elPath, propName);
|
|
32
|
+
if (!propCache.hasOwnProperty(key))
|
|
33
|
+
propCache[key] = originalProps[propName];
|
|
34
|
+
if (isEqual(propCache[key], value))
|
|
35
|
+
return propCache[key];
|
|
36
|
+
propCache[key] = value;
|
|
37
|
+
return value;
|
|
38
|
+
});
|
|
39
|
+
// HACK alert: Editing React Element by hand
|
|
40
|
+
// This is blasphemy, but there are two reasons why React.cloneElement
|
|
41
|
+
// isn't ideal:
|
|
42
|
+
// 1. Props need to overridden (not merged)
|
|
43
|
+
// 2. element.key has to be set to control whether the prev instance
|
|
44
|
+
// should be reused or not
|
|
45
|
+
// To be revised in case this approach causes trouble in the future.
|
|
46
|
+
// Useful links:
|
|
47
|
+
// - https://reactjs.org/docs/react-api.html#cloneelement
|
|
48
|
+
// - https://github.com/facebook/react/blob/15a8f031838a553e41c0b66eb1bcf1da8448104d/packages/react/src/ReactElement.js#L293-L362
|
|
49
|
+
return {
|
|
50
|
+
...element,
|
|
51
|
+
props: hasChildElPaths(elPaths, elPath)
|
|
52
|
+
? { ...cachedProps, children: originalProps.children }
|
|
53
|
+
: cachedProps,
|
|
54
|
+
key: getElRenderKey(elPath, fsProps.renderKey),
|
|
55
|
+
};
|
|
56
|
+
function componentTypeChanged(componentName) {
|
|
57
|
+
return componentName !== getComponentName(element.type);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}, fixture);
|
|
61
|
+
}
|
|
62
|
+
function getPropCacheKey(elPath, propName) {
|
|
63
|
+
return elPath ? `${elPath}-${propName}` : propName;
|
|
64
|
+
}
|
|
65
|
+
function getElRenderKey(elPath, renderKey) {
|
|
66
|
+
return `${elPath}-${renderKey}`;
|
|
67
|
+
}
|
|
68
|
+
function hasChildElPaths(elPaths, elPath) {
|
|
69
|
+
return elPaths.some(p => p.indexOf(getChildrenPath(elPath)) === 0);
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { findElementPaths, getExpectedElementAtPath, } from './nodeTree/index.js';
|
|
2
|
+
export function findRelevantElementPaths(node) {
|
|
3
|
+
const elPaths = findElementPaths(node);
|
|
4
|
+
return elPaths.filter(elPath => {
|
|
5
|
+
const { type } = getExpectedElementAtPath(node, elPath);
|
|
6
|
+
// Ignore symbol types, like StrictMode
|
|
7
|
+
// https://github.com/react-cosmos/react-cosmos/issues/1249
|
|
8
|
+
if (typeof type === 'symbol')
|
|
9
|
+
return false;
|
|
10
|
+
if (typeof type === 'string')
|
|
11
|
+
return isInterestingTag(type);
|
|
12
|
+
const classType = type;
|
|
13
|
+
return classType.cosmosCapture !== false && isInterestingClass(classType);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function isInterestingTag(tagName) {
|
|
17
|
+
// TODO: Make this customizable
|
|
18
|
+
return tagName !== 'div' && tagName !== 'span';
|
|
19
|
+
}
|
|
20
|
+
function isInterestingClass(type) {
|
|
21
|
+
// TODO: Make this customizable
|
|
22
|
+
return type.name !== 'StyledComponent';
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { flatten } from 'lodash-es';
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
import { isReactElement } from '../../../../../utils/react/isReactElement.js';
|
|
4
|
+
import { getChildrenPath } from './shared.js';
|
|
5
|
+
export function findElementPaths(node, curPath = '') {
|
|
6
|
+
if (Array.isArray(node)) {
|
|
7
|
+
return flatten(node.map((child, idx) => findElementPaths(child, `${curPath}[${idx}]`)));
|
|
8
|
+
}
|
|
9
|
+
if (!isReactElement(node)) {
|
|
10
|
+
// At this point the node can be null, boolean, string, number, Portal, etc.
|
|
11
|
+
// https://github.com/facebook/flow/blob/172d28f542f49bbc1e765131c9dfb9e31780f3a2/lib/react.js#L13-L20
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const { children } = node.props;
|
|
15
|
+
const childElPaths =
|
|
16
|
+
// Props of elements returned by render functions can't be read here
|
|
17
|
+
typeof children !== 'function'
|
|
18
|
+
? findElementPaths(children, getChildrenPath(curPath))
|
|
19
|
+
: [];
|
|
20
|
+
// Ignore Fragment elements, but include their children
|
|
21
|
+
return node.type === Fragment ? childElPaths : [curPath, ...childElPaths];
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { get } from 'lodash-es';
|
|
2
|
+
import { isReactElement } from '../../../../../utils/react/isReactElement.js';
|
|
3
|
+
import { isRootPath } from './shared.js';
|
|
4
|
+
// Why be silent about trying to fetch a node that isn't an element?
|
|
5
|
+
// Because users of this utility only care about elements. Whether the child
|
|
6
|
+
// node was removed or replaced by a different type of node (eg. string,
|
|
7
|
+
// array of elements, etc.) is irrelevant.
|
|
8
|
+
// NICETOHAVE: Assert child path validity
|
|
9
|
+
export function getElementAtPath(node, elPath) {
|
|
10
|
+
if (!isReactElement(node) && !Array.isArray(node)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const rootNode = node;
|
|
14
|
+
const childNode = isRootPath(elPath) ? rootNode : get(rootNode, elPath);
|
|
15
|
+
if (!isReactElement(childNode)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return childNode;
|
|
19
|
+
}
|
|
20
|
+
export function getExpectedElementAtPath(node, elPath) {
|
|
21
|
+
const el = getElementAtPath(node, elPath);
|
|
22
|
+
if (!el) {
|
|
23
|
+
throw new Error(`Element not found at path: ${elPath}`);
|
|
24
|
+
}
|
|
25
|
+
return el;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for extending React Elements from a tree of React Nodes.
|
|
3
|
+
*
|
|
4
|
+
* The root Node is the `children` prop of a parent Element. Besides the Node
|
|
5
|
+
* type, children can also be a function.
|
|
6
|
+
* https://reactjs.org/docs/jsx-in-depth.html#functions-as-children
|
|
7
|
+
*/
|
|
8
|
+
export { findElementPaths } from './findElementPaths.js';
|
|
9
|
+
export { getElementAtPath, getExpectedElementAtPath, } from './getElementAtPath.js';
|
|
10
|
+
export { setElementAtPath } from './setElementAtPath.js';
|
|
11
|
+
export { getChildrenPath } from './shared.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for extending React Elements from a tree of React Nodes.
|
|
3
|
+
*
|
|
4
|
+
* The root Node is the `children` prop of a parent Element. Besides the Node
|
|
5
|
+
* type, children can also be a function.
|
|
6
|
+
* https://reactjs.org/docs/jsx-in-depth.html#functions-as-children
|
|
7
|
+
*/
|
|
8
|
+
export { findElementPaths } from './findElementPaths.js';
|
|
9
|
+
export { getElementAtPath, getExpectedElementAtPath, } from './getElementAtPath.js';
|
|
10
|
+
export { setElementAtPath } from './setElementAtPath.js';
|
|
11
|
+
export { getChildrenPath } from './shared.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { set } from 'lodash-es';
|
|
2
|
+
import { isReactElement } from '../../../../../utils/react/isReactElement.js';
|
|
3
|
+
import { getExpectedElementAtPath } from './getElementAtPath.js';
|
|
4
|
+
import { isRootPath } from './shared.js';
|
|
5
|
+
export function setElementAtPath(node, elPath, updater) {
|
|
6
|
+
const childEl = getExpectedElementAtPath(node, elPath);
|
|
7
|
+
const newEl = updater(childEl);
|
|
8
|
+
if (isRootPath(elPath)) {
|
|
9
|
+
return newEl;
|
|
10
|
+
}
|
|
11
|
+
// If the root is a non-Array non-Element Node we should be at the root path
|
|
12
|
+
// and returned already
|
|
13
|
+
const clonedRoot = cloneNode(node);
|
|
14
|
+
// _.set also accepts arrays
|
|
15
|
+
// https://github.com/lodash/lodash/blob/6018350ac10d5ce6a5b7db625140b82aeab804df/isObject.js#L15-L16
|
|
16
|
+
return set(clonedRoot, elPath, newEl);
|
|
17
|
+
}
|
|
18
|
+
function cloneNode(value) {
|
|
19
|
+
return Array.isArray(value)
|
|
20
|
+
? value.map(n => cloneNodeItem(n))
|
|
21
|
+
: cloneNodeItem(value);
|
|
22
|
+
}
|
|
23
|
+
function cloneNodeItem(value) {
|
|
24
|
+
return isReactElement(value) ? cloneReactElement(value) : value;
|
|
25
|
+
}
|
|
26
|
+
function cloneReactElement(value) {
|
|
27
|
+
const { children, ...otherProps } = value.props;
|
|
28
|
+
return {
|
|
29
|
+
...value,
|
|
30
|
+
props: {
|
|
31
|
+
...otherProps,
|
|
32
|
+
children: cloneNode(children),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { Component, ReactNode } from 'react';
|
|
2
|
+
import { FixtureId } from '../../fixture/types.js';
|
|
3
|
+
import { FixtureState, SetFixtureState } from '../../fixtureState/types.js';
|
|
4
|
+
import { ReactDecorator, ReactDecorators, ReactFixtureWrappers } from '../reactTypes.js';
|
|
5
|
+
import { RendererConnect, RendererRequest, RendererResponse, SelectFixtureRequest, SetFixtureStateRequest } from '../types.js';
|
|
6
|
+
export type Props = {
|
|
7
|
+
rendererId: string;
|
|
8
|
+
rendererConnect: RendererConnect;
|
|
9
|
+
fixtures: ReactFixtureWrappers;
|
|
10
|
+
initialFixtureId?: FixtureId;
|
|
11
|
+
selectedFixtureId?: null | FixtureId;
|
|
12
|
+
systemDecorators: ReactDecorator[];
|
|
13
|
+
userDecorators: ReactDecorators;
|
|
14
|
+
renderMessage?: (args: {
|
|
15
|
+
msg: string;
|
|
16
|
+
}) => ReactNode;
|
|
17
|
+
onErrorReset?: () => unknown;
|
|
18
|
+
};
|
|
19
|
+
type SelectedFixture = {
|
|
20
|
+
fixtureId: FixtureId;
|
|
21
|
+
fixtureState: FixtureState;
|
|
22
|
+
syncedFixtureState: FixtureState;
|
|
23
|
+
};
|
|
24
|
+
type State = {
|
|
25
|
+
selectedFixture: null | SelectedFixture;
|
|
26
|
+
renderKey: number;
|
|
27
|
+
};
|
|
28
|
+
export declare class FixtureLoader extends Component<Props, State> {
|
|
29
|
+
state: State;
|
|
30
|
+
unsubscribe: null | (() => unknown);
|
|
31
|
+
componentDidMount(): void;
|
|
32
|
+
componentWillUnmount(): void;
|
|
33
|
+
componentDidUpdate(prevProps: Props): void;
|
|
34
|
+
shouldComponentUpdate(prevProps: Props, prevState: State): boolean;
|
|
35
|
+
render(): string | number | boolean | React.ReactFragment | JSX.Element | null | undefined;
|
|
36
|
+
handleRequest: (msg: RendererRequest) => void;
|
|
37
|
+
handleSelectFixtureRequest({ payload }: SelectFixtureRequest): void;
|
|
38
|
+
handleUnselectFixtureRequest(): void;
|
|
39
|
+
handleSetFixtureStateRequest({ payload }: SetFixtureStateRequest): void;
|
|
40
|
+
postReadyState(): void;
|
|
41
|
+
postFixtureListUpdate(): void;
|
|
42
|
+
postFixtureStateChange: (fixtureId: FixtureId, fixtureState: FixtureState) => void;
|
|
43
|
+
setFixtureState: SetFixtureState;
|
|
44
|
+
getFixtureList(): {
|
|
45
|
+
[x: string]: import("../../fixture/types.js").FixtureListItem;
|
|
46
|
+
};
|
|
47
|
+
fireChangeCallback(): void;
|
|
48
|
+
updateSyncedFixtureState(syncedFixtureState: FixtureState): void;
|
|
49
|
+
postMessage(msg: RendererResponse): void;
|
|
50
|
+
renderMessage(msg: string): React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
export {};
|