@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.
@@ -1,4 +1,4 @@
1
- import { g as x } from "./index-Ps7eUbv5.js";
1
+ import { g as x } from "./index-DIEcXzPZ.js";
2
2
  function h(r, a) {
3
3
  for (var i = 0; i < a.length; i++) {
4
4
  const o = a[i];
package/dist/index.d.ts CHANGED
@@ -2364,6 +2364,7 @@ export declare interface PropInfo {
2364
2364
  resolvedType: ResolvedType;
2365
2365
  description?: string;
2366
2366
  deprecated?: boolean;
2367
+ isStateTrigger?: boolean;
2367
2368
  }
2368
2369
 
2369
2370
  export declare interface PropSpyMeta {
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-Ps7eUbv5.js";
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.67.0",
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": "98b2efba108de51cc5bc3678f7b32253ba1a26e9b875964b6a8e725e"
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
- // Import the interceptor API - use direct path since vite alias doesn't apply to all packages
72
- import { getOriginals, setJsxInterceptors } from './jsx-runtime-interceptor'
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 = React.createElement
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 class trigger targets the element', () => {
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
- * The custom-class trigger feeds this: a `:global(.modifier)` class on the element
5
- * with no paired native pseudo (CSS extractor `getStateClasses`). It emits the
6
- * modifier as the state's editor-trigger `className`.
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({ element, classTriggers }: BuildCustomStatesParams): States | undefined {
37
+ export function buildCustomStatesBlock({
38
+ element,
39
+ propTriggers,
40
+ classTriggers,
41
+ }: BuildCustomStatesParams): States | undefined {
31
42
  const states: States = {}
32
43
 
33
- // Custom-class triggers that target this element.
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
- const stateName = stateNameFromModifier(rule.modifier)
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 `State<>` does not
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({ element: rootElement, classTriggers: customClassTriggers }),
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
- declaredTypeInfo = getDeclaredTypeInfo(propSymbol, decl.getSourceFile(), checker)
215
+ const declSourceFile = decl.getSourceFile()
216
+ declaredTypeInfo = getDeclaredTypeInfo(propSymbol, declSourceFile, checker)
217
+ stateTrigger = detectStateTrigger(propSymbol, declSourceFile, checker)
215
218
  }
216
219
  }
217
220
 
218
- const typeString = declaredTypeInfo?.name ?? propItem.type.name
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({ type: propType, checker, typeString, declaredSymbol: declaredTypeInfo?.symbol })
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` and
93
- * `react/jsx-dev-runtime` imports to an in-memory interceptor built from
94
- * `data:` URLs — no separate files required, safe for bundled consumers.
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 without needing
109
- // createRequire (data: modules have no filesystem context for CJS resolution)
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
  `