@wix/zero-config-implementation 1.67.0 → 1.69.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/{index-Ps7eUbv5.js → index-DIEcXzPZ.js} +8989 -8919
- package/dist/{index-BfHROU60.js → index-YRNaMIzt.js} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/component-renderer.ts +17 -6
- package/src/converters/custom-states-builder.test.ts +66 -1
- package/src/converters/custom-states-builder.ts +53 -11
- package/src/converters/to-editor-component.ts +9 -3
- package/src/information-extractors/ts/components.test.ts +29 -0
- package/src/information-extractors/ts/components.ts +72 -3
- package/src/information-extractors/ts/types.ts +5 -0
- package/src/jsx-runtime-interceptor.ts +83 -5
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-
|
|
1
|
+
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-DIEcXzPZ.js";
|
|
2
2
|
import "react";
|
|
3
3
|
export {
|
|
4
4
|
t as BaseError,
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.69.0",
|
|
8
8
|
"description": "Core library for extracting component manifests from JS and CSS files",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
]
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
-
"falconPackageHash": "
|
|
84
|
+
"falconPackageHash": "8bdff6ff3fe92e412898e57920456ab68a81029f0885fed3b9a41283"
|
|
85
85
|
}
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
*
|
|
20
20
|
* 2. **`React.createElement` monkey-patch** — Classic JSX transform and explicit
|
|
21
21
|
* `React.createElement()` calls are intercepted by temporarily replacing the
|
|
22
|
-
* function on the React module object.
|
|
22
|
+
* function on the React module object. The ESM loader hook (point 1) also
|
|
23
|
+
* intercepts the `react` bare specifier itself, so Rspack/webpack bundles that
|
|
24
|
+
* use `import * as React from 'react'` and call `React.createElement` are also
|
|
25
|
+
* covered via the data: URL shim's state-aware wrapper.
|
|
23
26
|
*
|
|
24
27
|
* 3. **CJS module export patch** — Pre-built CJS bundles that call
|
|
25
28
|
* `require('react/jsx-runtime')` at runtime. We use `createRequire` to obtain
|
|
@@ -67,9 +70,12 @@ import { renderToStaticMarkup } from 'react-dom/server'
|
|
|
67
70
|
|
|
68
71
|
import type { ExtractorStore } from './information-extractors/react/extractors/core/store'
|
|
69
72
|
import type { CreateElementEvent } from './information-extractors/react/extractors/core/types'
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
import {
|
|
74
|
+
getOriginalCreateElement,
|
|
75
|
+
getOriginals,
|
|
76
|
+
setCreateElementInterceptor,
|
|
77
|
+
setJsxInterceptors,
|
|
78
|
+
} from './jsx-runtime-interceptor'
|
|
73
79
|
|
|
74
80
|
export const TRACE_ATTR = 'data-trace-id'
|
|
75
81
|
|
|
@@ -165,7 +171,7 @@ export function renderWithExtractors(
|
|
|
165
171
|
const getNextId = () => `t${++nextId}`
|
|
166
172
|
|
|
167
173
|
// Store originals
|
|
168
|
-
const originalCreateElement =
|
|
174
|
+
const originalCreateElement = getOriginalCreateElement() as ElementCreator
|
|
169
175
|
const { jsx: originalJsx, jsxs: originalJsxs, jsxDEV: originalJsxDEV } = getOriginals()
|
|
170
176
|
|
|
171
177
|
// Obtain real CJS module objects so we can patch them for pre-built CJS consumers
|
|
@@ -240,8 +246,11 @@ export function renderWithExtractors(
|
|
|
240
246
|
const interceptedJsxDEV = createInterceptor(originalJsxDEV as ElementCreator, listeners, getNextId, store)
|
|
241
247
|
|
|
242
248
|
try {
|
|
243
|
-
// @ts-expect-error Monkey-patch createElement
|
|
249
|
+
// @ts-expect-error Monkey-patch createElement for default-import callers
|
|
244
250
|
React.createElement = interceptedCreateElement
|
|
251
|
+
// The data: URL react shim (registered by registerJsxLoaderHook) reads this state on every
|
|
252
|
+
// createElement call, covering Rspack/webpack bundles that use `import * as React from 'react'`.
|
|
253
|
+
setCreateElementInterceptor(interceptedCreateElement)
|
|
245
254
|
// Use the interceptor API for jsx-runtime (includes jsxDEV for dev mode)
|
|
246
255
|
setJsxInterceptors(
|
|
247
256
|
interceptedJsx as typeof originalJsx,
|
|
@@ -270,7 +279,9 @@ export function renderWithExtractors(
|
|
|
270
279
|
return (userRenderToStaticMarkup ?? renderToStaticMarkup)(element)
|
|
271
280
|
} finally {
|
|
272
281
|
// Restore originals
|
|
282
|
+
// @ts-expect-error Restore monkey-patched createElement
|
|
273
283
|
React.createElement = originalCreateElement
|
|
284
|
+
setCreateElementInterceptor(null)
|
|
274
285
|
setJsxInterceptors() // Restores to originals
|
|
275
286
|
// Restore CJS module exports
|
|
276
287
|
cjsRuntime.jsx = origCjsJsx
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { ExtractedElement } from '../information-extractors/react'
|
|
3
|
+
import type { CoupledProp } from '../information-extractors/react/types'
|
|
3
4
|
import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
|
|
4
5
|
|
|
5
6
|
function elementWithClass(classAttribute: string): ExtractedElement {
|
|
6
7
|
return { attributes: { class: classAttribute } } as unknown as ExtractedElement
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
function booleanStateProp(name: string): CoupledProp {
|
|
11
|
+
return { name, type: 'boolean', isStateTrigger: true } as unknown as CoupledProp
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
function classTrigger(baseSelector: string, modifier: string, isCssModule = false): CustomClassTrigger {
|
|
10
15
|
return { baseSelector, modifier, isCssModule }
|
|
11
16
|
}
|
|
@@ -14,6 +19,7 @@ describe('buildCustomStatesBlock', () => {
|
|
|
14
19
|
it('emits a custom-class state with className only (no props, no pseudoClass)', () => {
|
|
15
20
|
const states = buildCustomStatesBlock({
|
|
16
21
|
element: elementWithClass('custom-states'),
|
|
22
|
+
propTriggers: [],
|
|
17
23
|
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
18
24
|
})
|
|
19
25
|
|
|
@@ -23,6 +29,7 @@ describe('buildCustomStatesBlock', () => {
|
|
|
23
29
|
it('derives a multi-word state name from the modifier and capital-cases the displayName', () => {
|
|
24
30
|
const states = buildCustomStatesBlock({
|
|
25
31
|
element: elementWithClass('widget'),
|
|
32
|
+
propTriggers: [],
|
|
26
33
|
classTriggers: [classTrigger('.widget', 'widget--in-progress')],
|
|
27
34
|
})
|
|
28
35
|
|
|
@@ -35,6 +42,7 @@ describe('buildCustomStatesBlock', () => {
|
|
|
35
42
|
it('does not apply a block-base class trigger to a BEM child element (plain CSS, exact match)', () => {
|
|
36
43
|
const states = buildCustomStatesBlock({
|
|
37
44
|
element: elementWithClass('custom-states__label'),
|
|
45
|
+
propTriggers: [],
|
|
38
46
|
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
39
47
|
})
|
|
40
48
|
|
|
@@ -46,6 +54,7 @@ describe('buildCustomStatesBlock', () => {
|
|
|
46
54
|
// base element — not a BEM child. (Regression: this used to be dropped as a child.)
|
|
47
55
|
const states = buildCustomStatesBlock({
|
|
48
56
|
element: elementWithClass('root__IWjkn wixui-test-comp-card test-comp-card'),
|
|
57
|
+
propTriggers: [],
|
|
49
58
|
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
50
59
|
})
|
|
51
60
|
|
|
@@ -55,15 +64,71 @@ describe('buildCustomStatesBlock', () => {
|
|
|
55
64
|
it('still excludes a genuine BEM child (`root__label`) from a block-base trigger in a module', () => {
|
|
56
65
|
const states = buildCustomStatesBlock({
|
|
57
66
|
element: elementWithClass('root__label'),
|
|
67
|
+
propTriggers: [],
|
|
58
68
|
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
59
69
|
})
|
|
60
70
|
|
|
61
71
|
expect(states).toBeUndefined()
|
|
62
72
|
})
|
|
63
73
|
|
|
64
|
-
it('returns undefined when no
|
|
74
|
+
it('returns undefined when no trigger targets the element', () => {
|
|
75
|
+
const states = buildCustomStatesBlock({
|
|
76
|
+
element: elementWithClass('custom-states'),
|
|
77
|
+
propTriggers: [],
|
|
78
|
+
classTriggers: [],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(states).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('emits a props-only state for a prop trigger with no matching class (no className synthesized)', () => {
|
|
85
|
+
const states = buildCustomStatesBlock({
|
|
86
|
+
element: elementWithClass('panel'),
|
|
87
|
+
propTriggers: [booleanStateProp('isExpanded')],
|
|
88
|
+
classTriggers: [],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(states).toEqual({
|
|
92
|
+
'is-expanded': { displayName: 'Is Expanded', props: { isExpanded: true } },
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('keeps the full prop name in the state key and displayName (no is/has stripping)', () => {
|
|
97
|
+
const states = buildCustomStatesBlock({
|
|
98
|
+
element: elementWithClass('widget'),
|
|
99
|
+
propTriggers: [booleanStateProp('isLoading')],
|
|
100
|
+
classTriggers: [],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(states?.['is-loading']).toEqual({
|
|
104
|
+
displayName: 'Is Loading',
|
|
105
|
+
props: { isLoading: true },
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('merges a prop trigger with a class trigger that resolves to the same state name', () => {
|
|
110
|
+
const states = buildCustomStatesBlock({
|
|
111
|
+
element: elementWithClass('prop-states'),
|
|
112
|
+
propTriggers: [booleanStateProp('isLoading')],
|
|
113
|
+
classTriggers: [classTrigger('.prop-states', 'prop-states--is-loading')],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(states).toEqual({
|
|
117
|
+
'is-loading': {
|
|
118
|
+
displayName: 'Is Loading',
|
|
119
|
+
className: 'prop-states--is-loading',
|
|
120
|
+
props: { isLoading: true },
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('ignores plain (non-ElementState) props and non-boolean state triggers', () => {
|
|
65
126
|
const states = buildCustomStatesBlock({
|
|
66
127
|
element: elementWithClass('custom-states'),
|
|
128
|
+
propTriggers: [
|
|
129
|
+
{ name: 'showLabel', type: 'boolean' } as unknown as CoupledProp,
|
|
130
|
+
{ name: 'variant', type: "'a' | 'b'", isStateTrigger: true } as unknown as CoupledProp,
|
|
131
|
+
],
|
|
67
132
|
classTriggers: [],
|
|
68
133
|
})
|
|
69
134
|
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Builds the custom (non-native) `states` entries for an element.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two triggers feed this, per the agreed design — props are looked at first:
|
|
5
|
+
* - Prop — a boolean prop wrapped in `ElementState<>` (TS extractor sets
|
|
6
|
+
* `isStateTrigger`) IS a custom state, emitting
|
|
7
|
+
* `props: { <prop>: true }`. The CSS is then searched for a
|
|
8
|
+
* class trigger matching that state's name — when found it
|
|
9
|
+
* supplies the editor-trigger `className`; otherwise the entry
|
|
10
|
+
* is props-only (`className` is never synthesized).
|
|
11
|
+
* - Custom class — a `:global(.modifier)` class on the element with no paired
|
|
12
|
+
* native pseudo (CSS extractor `getStateClasses`) and not
|
|
13
|
+
* claimed by a prop trigger. Emits the modifier as a
|
|
14
|
+
* className-only state.
|
|
7
15
|
*
|
|
8
16
|
* Native states are built separately and take precedence on any key collision.
|
|
9
|
-
*
|
|
10
|
-
* Note: the prop trigger (`State<>` markers → `props` overrides) is intentionally
|
|
11
|
-
* not handled here yet — it is deferred to a follow-up PR, gated on the upstream
|
|
12
|
-
* marker-type agreement.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import type { States } from '@wix/react-component-schema'
|
|
20
|
+
import { kebabCase } from 'case-anything'
|
|
16
21
|
import type { StateClassRule } from '../information-extractors/css/types'
|
|
17
|
-
import type { ExtractedElement } from '../information-extractors/react'
|
|
22
|
+
import type { CoupledProp, ExtractedElement } from '../information-extractors/react'
|
|
18
23
|
import { isGlobalSemanticClass, stateNameFromModifier } from '../utils/css-class'
|
|
19
24
|
import { formatDisplayName } from './utils'
|
|
20
25
|
|
|
@@ -23,17 +28,44 @@ export type CustomClassTrigger = StateClassRule & { isCssModule: boolean }
|
|
|
23
28
|
|
|
24
29
|
interface BuildCustomStatesParams {
|
|
25
30
|
element: ExtractedElement
|
|
31
|
+
/** Prop triggers in scope for this element (root-level `ElementState<>` props; `[]` for inner elements). */
|
|
32
|
+
propTriggers: CoupledProp[]
|
|
26
33
|
/** Custom-class triggers (modifiers with no paired native pseudo) across all CSS files. */
|
|
27
34
|
classTriggers: CustomClassTrigger[]
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export function buildCustomStatesBlock({
|
|
37
|
+
export function buildCustomStatesBlock({
|
|
38
|
+
element,
|
|
39
|
+
propTriggers,
|
|
40
|
+
classTriggers,
|
|
41
|
+
}: BuildCustomStatesParams): States | undefined {
|
|
31
42
|
const states: States = {}
|
|
32
43
|
|
|
33
|
-
//
|
|
44
|
+
// Class triggers that target this element, keyed by their derived state name,
|
|
45
|
+
// so prop triggers can look up their matching class.
|
|
46
|
+
const elementClassTriggers = new Map<string, CustomClassTrigger>()
|
|
34
47
|
for (const rule of classTriggers) {
|
|
35
48
|
if (!elementMatchesBase(element, rule.baseSelector, rule.isCssModule)) continue
|
|
36
|
-
|
|
49
|
+
elementClassTriggers.set(stateNameFromModifier(rule.modifier), rule)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Prop triggers first: each boolean `ElementState<>` prop is a custom state.
|
|
53
|
+
// Then search the CSS for the class trigger matching that state's name — when
|
|
54
|
+
// found it supplies the editor-trigger className; otherwise props-only.
|
|
55
|
+
for (const prop of propTriggers) {
|
|
56
|
+
if (!prop.isStateTrigger || prop.type !== 'boolean') continue
|
|
57
|
+
const stateName = stateNameFromProp(prop.name)
|
|
58
|
+
const matchingClassTrigger = elementClassTriggers.get(stateName)
|
|
59
|
+
states[stateName] = {
|
|
60
|
+
displayName: formatDisplayName(stateName),
|
|
61
|
+
...(matchingClassTrigger && { className: matchingClassTrigger.modifier }),
|
|
62
|
+
props: { [prop.name]: true },
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remaining class triggers — those not claimed by a prop — are className-only states.
|
|
67
|
+
for (const [stateName, rule] of elementClassTriggers) {
|
|
68
|
+
if (states[stateName] !== undefined) continue
|
|
37
69
|
states[stateName] = {
|
|
38
70
|
displayName: formatDisplayName(stateName),
|
|
39
71
|
className: rule.modifier,
|
|
@@ -43,6 +75,16 @@ export function buildCustomStatesBlock({ element, classTriggers }: BuildCustomSt
|
|
|
43
75
|
return Object.keys(states).length > 0 ? states : undefined
|
|
44
76
|
}
|
|
45
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Derives the state name from a boolean prop by kebab-casing the full prop name
|
|
80
|
+
* (`isLoading` → `is-loading`, so the displayName reads "Is Loading"). The prop
|
|
81
|
+
* name is kept whole — no `is`/`has` prefix stripping — so the state key mirrors
|
|
82
|
+
* the prop that triggers it.
|
|
83
|
+
*/
|
|
84
|
+
function stateNameFromProp(propName: string): string {
|
|
85
|
+
return kebabCase(propName)
|
|
86
|
+
}
|
|
87
|
+
|
|
46
88
|
/**
|
|
47
89
|
* Checks whether an element's rendered class list matches a simple single-class
|
|
48
90
|
* base selector (e.g. `.root`, `.custom-states`).
|
|
@@ -29,7 +29,7 @@ import { formatDisplayName } from './utils'
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Merges native and custom state blocks. Native states take precedence on any
|
|
32
|
-
* key collision (e.g. an author wrapping `isDisabled` in `
|
|
32
|
+
* key collision (e.g. an author wrapping `isDisabled` in `ElementState<>` does not
|
|
33
33
|
* shadow the inferred native `disabled` state).
|
|
34
34
|
*/
|
|
35
35
|
function mergeStates(native: States | undefined, custom: States | undefined): States | undefined {
|
|
@@ -75,7 +75,11 @@ function buildEditorElement(
|
|
|
75
75
|
const rootStates = rootElement
|
|
76
76
|
? mergeStates(
|
|
77
77
|
rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined,
|
|
78
|
-
buildCustomStatesBlock({
|
|
78
|
+
buildCustomStatesBlock({
|
|
79
|
+
element: rootElement,
|
|
80
|
+
propTriggers: Object.values(component.props).filter((prop) => prop.isStateTrigger),
|
|
81
|
+
classTriggers: customClassTriggers,
|
|
82
|
+
}),
|
|
79
83
|
)
|
|
80
84
|
: undefined
|
|
81
85
|
|
|
@@ -147,9 +151,11 @@ function buildElements(
|
|
|
147
151
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
148
152
|
|
|
149
153
|
const semanticClass = getSemanticBlockName(element)
|
|
154
|
+
// Inner-element custom states come from custom-class triggers only; prop
|
|
155
|
+
// triggers (`ElementState<>`) are component-level and attach to the root.
|
|
150
156
|
const states = mergeStates(
|
|
151
157
|
semanticClass ? buildStatesBlock(element, semanticClass) : undefined,
|
|
152
|
-
buildCustomStatesBlock({ element, classTriggers: customClassTriggers }),
|
|
158
|
+
buildCustomStatesBlock({ element, propTriggers: [], classTriggers: customClassTriggers }),
|
|
153
159
|
)
|
|
154
160
|
|
|
155
161
|
result[element.name] = {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { compileTsFile } from '../../ts-compiler'
|
|
4
|
+
import { extractAllComponentInfo } from './components'
|
|
5
|
+
|
|
6
|
+
const propTriggeredFixture = path.resolve(
|
|
7
|
+
__dirname,
|
|
8
|
+
'../../../../example-components/src/components/DesignStates/Custom/PropTriggered/WithMatchingClass/component.tsx',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
describe('TS extractor — ElementState<> trigger detection', () => {
|
|
12
|
+
it('flags an ElementState<>-wrapped prop and unwraps its inner type', async () => {
|
|
13
|
+
const programResult = await compileTsFile(propTriggeredFixture)
|
|
14
|
+
expect(programResult.isOk()).toBe(true)
|
|
15
|
+
const program = programResult._unsafeUnwrap()
|
|
16
|
+
|
|
17
|
+
const components = extractAllComponentInfo(program, propTriggeredFixture)
|
|
18
|
+
const component = components.find((candidate) => candidate.componentName === 'PropStates')
|
|
19
|
+
expect(component).toBeDefined()
|
|
20
|
+
|
|
21
|
+
const isLoading = component!.props.isLoading
|
|
22
|
+
expect(isLoading.isStateTrigger).toBe(true)
|
|
23
|
+
// The `ElementState<boolean>` wrapper is unwrapped — the prop behaves as a plain boolean.
|
|
24
|
+
expect(isLoading.type).toBe('boolean')
|
|
25
|
+
|
|
26
|
+
// Plain booleans are never flagged as state triggers.
|
|
27
|
+
expect(component!.props.showLabel.isStateTrigger).toBeUndefined()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -208,16 +208,27 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
|
|
|
208
208
|
|
|
209
209
|
// Resolve the type deeply
|
|
210
210
|
let declaredTypeInfo: DeclaredTypeInfo | undefined
|
|
211
|
+
let stateTrigger: StateTriggerInfo | undefined
|
|
211
212
|
if (propSymbol) {
|
|
212
213
|
const decl = propSymbol.getDeclarations()?.[0]
|
|
213
214
|
if (decl) {
|
|
214
|
-
|
|
215
|
+
const declSourceFile = decl.getSourceFile()
|
|
216
|
+
declaredTypeInfo = getDeclaredTypeInfo(propSymbol, declSourceFile, checker)
|
|
217
|
+
stateTrigger = detectStateTrigger(propSymbol, declSourceFile, checker)
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
|
|
221
|
+
// An `ElementState<Inner>` prop should behave as its inner type everywhere except
|
|
222
|
+
// for the `isStateTrigger` flag, so unwrap the inner type for `type` and resolution
|
|
223
|
+
// (and drop the `ElementState` declared symbol so it isn't mistaken for a semantic type).
|
|
224
|
+
const typeString = stateTrigger?.innerTypeName ?? declaredTypeInfo?.name ?? propItem.type.name
|
|
219
225
|
const resolvedType = propType
|
|
220
|
-
? resolveType({
|
|
226
|
+
? resolveType({
|
|
227
|
+
type: propType,
|
|
228
|
+
checker,
|
|
229
|
+
typeString,
|
|
230
|
+
declaredSymbol: stateTrigger ? undefined : declaredTypeInfo?.symbol,
|
|
231
|
+
})
|
|
221
232
|
: { kind: 'primitive' as const, value: propItem.type.name }
|
|
222
233
|
|
|
223
234
|
// Convert default value
|
|
@@ -239,6 +250,7 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
|
|
|
239
250
|
resolvedType,
|
|
240
251
|
description: propItem.description || undefined,
|
|
241
252
|
deprecated: tags?.deprecated !== undefined ? true : undefined,
|
|
253
|
+
isStateTrigger: stateTrigger ? true : undefined,
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
|
|
@@ -477,6 +489,63 @@ interface DeclaredTypeInfo {
|
|
|
477
489
|
symbol?: ts.Symbol
|
|
478
490
|
}
|
|
479
491
|
|
|
492
|
+
interface StateTriggerInfo {
|
|
493
|
+
/** Text of the inner type argument, e.g. `'boolean'`. */
|
|
494
|
+
innerTypeName: string
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Detects when a prop is declared with the `ElementState<>` marker type from
|
|
499
|
+
* `@wix/react-component-utils` (e.g. `isLoading?: ElementState<boolean>`), which opts
|
|
500
|
+
* the prop in as the trigger for a custom design state. Returns the inner type
|
|
501
|
+
* argument so callers can treat the prop as its underlying type. Returns
|
|
502
|
+
* `undefined` for any other declaration.
|
|
503
|
+
*/
|
|
504
|
+
function detectStateTrigger(
|
|
505
|
+
prop: ts.Symbol,
|
|
506
|
+
sourceFile: ts.SourceFile,
|
|
507
|
+
checker: ts.TypeChecker,
|
|
508
|
+
): StateTriggerInfo | undefined {
|
|
509
|
+
const declaration = prop.getDeclarations()?.[0]
|
|
510
|
+
if (!declaration) return undefined
|
|
511
|
+
|
|
512
|
+
if (
|
|
513
|
+
!(ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) ||
|
|
514
|
+
!declaration.type ||
|
|
515
|
+
!ts.isTypeReferenceNode(declaration.type) ||
|
|
516
|
+
!declaration.type.typeArguments ||
|
|
517
|
+
declaration.type.typeArguments.length !== 1
|
|
518
|
+
) {
|
|
519
|
+
return undefined
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const typeName = declaration.type.typeName
|
|
523
|
+
const identifier = ts.isIdentifier(typeName) ? typeName : typeName.right
|
|
524
|
+
if (identifier.text !== 'ElementState') return undefined
|
|
525
|
+
if (!isElementStateAliasFromUtils(identifier, checker)) return undefined
|
|
526
|
+
|
|
527
|
+
return { innerTypeName: declaration.type.typeArguments[0].getText(sourceFile) }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Confirms an identifier referencing `ElementState` resolves to the marker type
|
|
532
|
+
* alias exported by `@wix/react-component-utils`, rather than an unrelated local
|
|
533
|
+
* type that happens to be named `ElementState`.
|
|
534
|
+
*/
|
|
535
|
+
function isElementStateAliasFromUtils(identifier: ts.Identifier, checker: ts.TypeChecker): boolean {
|
|
536
|
+
let symbol = checker.getSymbolAtLocation(identifier)
|
|
537
|
+
if (!symbol) return false
|
|
538
|
+
if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) {
|
|
539
|
+
symbol = checker.getAliasedSymbol(symbol)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const declaration = symbol.getDeclarations()?.[0]
|
|
543
|
+
if (!declaration || !ts.isTypeAliasDeclaration(declaration)) return false
|
|
544
|
+
if (declaration.name.text !== 'ElementState') return false
|
|
545
|
+
|
|
546
|
+
return declaration.getSourceFile().fileName.includes('react-component-utils')
|
|
547
|
+
}
|
|
548
|
+
|
|
480
549
|
function getDeclaredTypeInfo(
|
|
481
550
|
prop: ts.Symbol,
|
|
482
551
|
sourceFile: ts.SourceFile,
|
|
@@ -51,6 +51,11 @@ export interface PropInfo {
|
|
|
51
51
|
description?: string
|
|
52
52
|
// Whether the prop is marked as deprecated via @deprecated JSDoc tag
|
|
53
53
|
deprecated?: boolean
|
|
54
|
+
// True when the prop is wrapped in the `ElementState<>` marker type from
|
|
55
|
+
// @wix/react-component-utils, declaring it as the trigger for a custom design
|
|
56
|
+
// state. `type` and `resolvedType` reflect the unwrapped inner type, so the prop
|
|
57
|
+
// still behaves as a normal data prop everywhere else.
|
|
58
|
+
isStateTrigger?: boolean
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export interface ComponentInfo {
|
|
@@ -19,6 +19,14 @@ const originalDevRuntime = require('react/jsx-dev-runtime') as typeof import('re
|
|
|
19
19
|
// Type for jsxDEV function
|
|
20
20
|
type JsxDevFn = typeof originalDevRuntime.jsxDEV
|
|
21
21
|
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: broad function type matching React.createElement's signature
|
|
23
|
+
type CreateElementFn = (...args: any[]) => unknown
|
|
24
|
+
|
|
25
|
+
// Captured at module-init time for getOriginalCreateElement(); also reused in
|
|
26
|
+
// registerJsxLoaderHook() to stash the full exports for the react data: URL shim.
|
|
27
|
+
const cjsReact = require('react') as { createElement: CreateElementFn }
|
|
28
|
+
const originalCreateElement: CreateElementFn = cjsReact.createElement
|
|
29
|
+
|
|
22
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
31
|
// Shared state via globalThis
|
|
24
32
|
//
|
|
@@ -33,6 +41,8 @@ interface JsxInterceptorState {
|
|
|
33
41
|
currentJsxs: typeof originalRuntime.jsxs
|
|
34
42
|
currentJsxDEV: JsxDevFn
|
|
35
43
|
isInsideOriginal: boolean
|
|
44
|
+
/** Interceptor for React.createElement set during render; null when not intercepting. */
|
|
45
|
+
currentCreateElement: CreateElementFn | null
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function getState(): JsxInterceptorState {
|
|
@@ -43,6 +53,7 @@ function getState(): JsxInterceptorState {
|
|
|
43
53
|
currentJsxs: originalRuntime.jsxs,
|
|
44
54
|
currentJsxDEV: originalDevRuntime.jsxDEV,
|
|
45
55
|
isInsideOriginal: false,
|
|
56
|
+
currentCreateElement: null,
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
return g[STATE_KEY]!
|
|
@@ -74,6 +85,27 @@ export function getOriginals() {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Sets (or clears) the React.createElement interceptor used by the wrapper
|
|
90
|
+
* installed on the CJS react module by registerJsxLoaderHook.
|
|
91
|
+
*
|
|
92
|
+
* This handles ESM namespace imports (`import * as React from 'react'`) which
|
|
93
|
+
* snapshot the createElement reference at module-load time. Those snapshots
|
|
94
|
+
* point to the wrapper, which reads from this state on every call.
|
|
95
|
+
*
|
|
96
|
+
* Pass null to restore pass-through behaviour (wrapper delegates to original).
|
|
97
|
+
*/
|
|
98
|
+
export function setCreateElementInterceptor(fn: CreateElementFn | null): void {
|
|
99
|
+
getState().currentCreateElement = fn
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns the pre-wrapper React.createElement captured at module-init time.
|
|
104
|
+
*/
|
|
105
|
+
export function getOriginalCreateElement(): CreateElementFn {
|
|
106
|
+
return originalCreateElement
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
110
|
// Hook Registration
|
|
79
111
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -88,10 +120,22 @@ interface JsxOriginals {
|
|
|
88
120
|
jsxDEV: JsxDevFn
|
|
89
121
|
}
|
|
90
122
|
|
|
123
|
+
const REACT_ORIGINALS_KEY = Symbol.for('zero-config:react-originals')
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
|
-
* Registers a Node.js ESM loader hook that redirects `react/jsx-runtime
|
|
93
|
-
* `react/jsx-dev-runtime` imports to
|
|
94
|
-
*
|
|
126
|
+
* Registers a Node.js ESM loader hook that redirects `react/jsx-runtime`,
|
|
127
|
+
* `react/jsx-dev-runtime`, and `react` imports to in-memory `data:` URL
|
|
128
|
+
* interceptors — no separate files required, safe for bundled consumers.
|
|
129
|
+
*
|
|
130
|
+
* Intercepting `react` itself is necessary for bundled components (e.g. from
|
|
131
|
+
* @wix/site-ui) that use `import * as React from 'react'` (Rspack/webpack style)
|
|
132
|
+
* and call `React.createElement` through the namespace object. Node.js snapshots
|
|
133
|
+
* named CJS exports into the namespace at first-load time; since Vitest loads
|
|
134
|
+
* react before our module runs, the snapshot always contains the unwrapped
|
|
135
|
+
* original. The loader hook fires for NEWLY-loaded modules (i.e., the user bundle
|
|
136
|
+
* and its dependencies), redirecting `react` to a data: shim that wraps
|
|
137
|
+
* `createElement` with our state-based interceptor while delegating everything
|
|
138
|
+
* else to the real react instance.
|
|
95
139
|
*
|
|
96
140
|
* Must be called before any dynamic `import()` of user components.
|
|
97
141
|
* Safe to call multiple times; subsequent calls are no-ops.
|
|
@@ -105,8 +149,8 @@ export function registerJsxLoaderHook(): void {
|
|
|
105
149
|
const realRuntime = cjsRequire('react/jsx-runtime') as typeof originalRuntime
|
|
106
150
|
const realDevRuntime = cjsRequire('react/jsx-dev-runtime') as typeof originalDevRuntime
|
|
107
151
|
|
|
108
|
-
// Stash originals so the data: interceptor module can read them
|
|
109
|
-
// createRequire (data: modules have no filesystem context
|
|
152
|
+
// Stash jsx-runtime originals so the data: interceptor module can read them
|
|
153
|
+
// without needing createRequire (data: modules have no filesystem context)
|
|
110
154
|
globalRecord[ORIGINALS_KEY] = {
|
|
111
155
|
Fragment: realRuntime.Fragment,
|
|
112
156
|
jsx: realRuntime.jsx,
|
|
@@ -114,6 +158,36 @@ export function registerJsxLoaderHook(): void {
|
|
|
114
158
|
jsxDEV: realDevRuntime.jsxDEV,
|
|
115
159
|
} satisfies JsxOriginals
|
|
116
160
|
|
|
161
|
+
// Stash the full react module so the react data: shim can re-export everything
|
|
162
|
+
const cjsReactExports = cjsRequire('react') as Record<string, unknown>
|
|
163
|
+
globalRecord[REACT_ORIGINALS_KEY] = cjsReactExports
|
|
164
|
+
|
|
165
|
+
// Generate named re-export lines for every enumerable property of react.
|
|
166
|
+
// `createElement` gets a state-aware wrapper; everything else is a direct
|
|
167
|
+
// delegation to the real react so hooks and other internals work correctly.
|
|
168
|
+
const reactExportLines = Object.keys(cjsReactExports)
|
|
169
|
+
.map((key) => {
|
|
170
|
+
if (key === 'createElement') {
|
|
171
|
+
return `export function createElement() {
|
|
172
|
+
var state = globalThis[STATE_KEY];
|
|
173
|
+
if (state && state.currentCreateElement) return state.currentCreateElement.apply(null, arguments);
|
|
174
|
+
return r.createElement.apply(null, arguments);
|
|
175
|
+
}`
|
|
176
|
+
}
|
|
177
|
+
return `export var ${key} = r.${key};`
|
|
178
|
+
})
|
|
179
|
+
.join('\n')
|
|
180
|
+
|
|
181
|
+
const reactShimSource = `
|
|
182
|
+
var REACT_KEY = Symbol.for('zero-config:react-originals');
|
|
183
|
+
var STATE_KEY = Symbol.for('zero-config:jsx-interceptor');
|
|
184
|
+
var r = globalThis[REACT_KEY];
|
|
185
|
+
export default r;
|
|
186
|
+
${reactExportLines}
|
|
187
|
+
`
|
|
188
|
+
|
|
189
|
+
const reactShimUrl = `data:text/javascript,${encodeURIComponent(reactShimSource)}`
|
|
190
|
+
|
|
117
191
|
const interceptorSource = `
|
|
118
192
|
const ORIGINALS_KEY = Symbol.for('zero-config:jsx-originals');
|
|
119
193
|
const STATE_KEY = Symbol.for('zero-config:jsx-interceptor');
|
|
@@ -155,10 +229,14 @@ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
|
155
229
|
|
|
156
230
|
const loaderSource = `
|
|
157
231
|
const INTERCEPTOR_URL = ${JSON.stringify(interceptorDataUrl)};
|
|
232
|
+
const REACT_SHIM_URL = ${JSON.stringify(reactShimUrl)};
|
|
158
233
|
export async function resolve(specifier, context, nextResolve) {
|
|
159
234
|
if (specifier === 'react/jsx-runtime' || specifier === 'react/jsx-dev-runtime') {
|
|
160
235
|
return { shortCircuit: true, url: INTERCEPTOR_URL };
|
|
161
236
|
}
|
|
237
|
+
if (specifier === 'react') {
|
|
238
|
+
return { shortCircuit: true, url: REACT_SHIM_URL };
|
|
239
|
+
}
|
|
162
240
|
return nextResolve(specifier, context);
|
|
163
241
|
}
|
|
164
242
|
`
|