@wix/zero-config-implementation 1.48.0 → 1.50.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.48.0",
7
+ "version": "1.50.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",
@@ -85,5 +85,5 @@
85
85
  ]
86
86
  }
87
87
  },
88
- "falconPackageHash": "8fd0a1265e02ba71d5ed24fddbddbf66183b17ff76ac71ce067af6c6"
88
+ "falconPackageHash": "15350286a134883c84b7fb2738d2553d8eedbec024f0d1cb444a40ca"
89
89
  }
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * ## Interception Strategy
9
9
  *
10
- * Three complementary layers ensure interception regardless of how JSX is compiled:
10
+ * Four complementary layers ensure interception regardless of how JSX is compiled
11
+ * AND regardless of how many React module instances are loaded in the process:
11
12
  *
12
13
  * 1. **Vite alias** — `react/jsx-runtime` is aliased to `jsx-runtime-interceptor.ts`
13
14
  * in vite.config.ts. This handles ESM imports resolved at build time.
@@ -22,12 +23,24 @@
22
23
  * their `jsx`/`jsxs`/`jsxDEV` exports during rendering. This is safe because
23
24
  * CJS module exports are mutable (`writable: true`).
24
25
  *
26
+ * 4. **User-bundle React patch** — When the renderer is hosted inside a process
27
+ * that bundled its own React (e.g. `@wix/cli` ships a baked-in copy of
28
+ * `react.production.min.js`), the user's compiled component bundle still does
29
+ * its own `import j from "react"` and Node resolves that from the bundle's
30
+ * location on disk — a separate React module instance. To cover this case,
31
+ * the renderer also calls `createRequire(compiledEntryPath)` to obtain the
32
+ * user-bundle's React (and `react/jsx-runtime` / `react/jsx-dev-runtime`)
33
+ * and applies the same interceptors to those modules' exports. Identity
34
+ * guards keep the single-React case a no-op.
35
+ *
25
36
  * ## Intercepted Functions
26
37
  *
27
38
  * - `React.createElement` - Classic JSX transform (monkey-patched)
28
39
  * - `jsx` / `jsxs` from `react/jsx-runtime` - New JSX transform via interceptor module
29
40
  * - `jsx` / `jsxs` / `jsxDEV` on the real CJS `react/jsx-runtime` and
30
41
  * `react/jsx-dev-runtime` module objects (monkey-patched for CJS consumers)
42
+ * - The same exports on the React modules resolved from the user component
43
+ * bundle's own location (when a different module instance from the host's)
31
44
  *
32
45
  * ## Known Limitations
33
46
  *
@@ -119,6 +132,10 @@ function createInterceptor(
119
132
  * @param componentProps - Props to pass to the component
120
133
  * @param listeners - Listeners to notify on each DOM element creation
121
134
  * @param store - Shared ExtractorStore, included in each CreateElementEvent
135
+ * @param compiledEntryPath - Optional path to the user's compiled bundle. When
136
+ * provided, the renderer additionally patches the React module instance
137
+ * that the bundle resolves from its own `node_modules` (covers cases
138
+ * where the host bundles its own React separately from the user's).
122
139
  * @returns Static HTML string with trace IDs on DOM elements
123
140
  *
124
141
  * @example
@@ -135,6 +152,7 @@ export function renderWithExtractors(
135
152
  componentProps: unknown,
136
153
  listeners: CreateElementListener[],
137
154
  store: ExtractorStore,
155
+ compiledEntryPath?: string,
138
156
  ): string {
139
157
  let nextId = 0
140
158
  const getNextId = () => `t${++nextId}`
@@ -153,6 +171,43 @@ export function renderWithExtractors(
153
171
  const origCjsJsxs = cjsRuntime.jsxs
154
172
  const origCjsJsxDEV = cjsDevRuntime.jsxDEV
155
173
 
174
+ // Resolve the user-bundle's React (and jsx-runtime variants) from the bundle's
175
+ // own location on disk. When the host (e.g. a CLI) bundled its own React, this
176
+ // resolves to a different module instance and must be patched separately.
177
+ // Identity guards below skip patching when the resolved modules are the same
178
+ // objects already covered by the host-side patches.
179
+ let userReact: { createElement: ElementCreator } | undefined
180
+ let userJsxRuntime: Record<string, unknown> | undefined
181
+ let userJsxDevRuntime: Record<string, unknown> | undefined
182
+ let origUserCreateElement: ElementCreator | undefined
183
+ let origUserJsx: unknown
184
+ let origUserJsxs: unknown
185
+ let origUserJsxDEV: unknown
186
+
187
+ if (compiledEntryPath) {
188
+ try {
189
+ const userRequire = createRequire(compiledEntryPath)
190
+ const candidateReact = userRequire('react') as { createElement: ElementCreator }
191
+ if (candidateReact !== (React as unknown as typeof candidateReact)) {
192
+ userReact = candidateReact
193
+ origUserCreateElement = candidateReact.createElement
194
+ }
195
+ const candidateJsx = userRequire('react/jsx-runtime') as Record<string, unknown>
196
+ if (candidateJsx !== cjsRuntime) {
197
+ userJsxRuntime = candidateJsx
198
+ origUserJsx = candidateJsx.jsx
199
+ origUserJsxs = candidateJsx.jsxs
200
+ }
201
+ const candidateJsxDev = userRequire('react/jsx-dev-runtime') as Record<string, unknown>
202
+ if (candidateJsxDev !== cjsDevRuntime) {
203
+ userJsxDevRuntime = candidateJsxDev
204
+ origUserJsxDEV = candidateJsxDev.jsxDEV
205
+ }
206
+ } catch {
207
+ // user bundle path doesn't resolve react locally — nothing to patch
208
+ }
209
+ }
210
+
156
211
  // Create interceptors
157
212
  const interceptedCreateElement = createInterceptor(
158
213
  originalCreateElement as ElementCreator,
@@ -178,6 +233,18 @@ export function renderWithExtractors(
178
233
  cjsRuntime.jsxs = interceptedJsxs
179
234
  cjsDevRuntime.jsxDEV = interceptedJsxDEV
180
235
 
236
+ // Patch user-bundle React modules when they are distinct instances
237
+ if (userReact) {
238
+ userReact.createElement = interceptedCreateElement
239
+ }
240
+ if (userJsxRuntime) {
241
+ userJsxRuntime.jsx = interceptedJsx
242
+ userJsxRuntime.jsxs = interceptedJsxs
243
+ }
244
+ if (userJsxDevRuntime) {
245
+ userJsxDevRuntime.jsxDEV = interceptedJsxDEV
246
+ }
247
+
181
248
  const element = React.createElement(Component, componentProps as Record<string, unknown>)
182
249
  return renderToStaticMarkup(element)
183
250
  } finally {
@@ -188,5 +255,16 @@ export function renderWithExtractors(
188
255
  cjsRuntime.jsx = origCjsJsx
189
256
  cjsRuntime.jsxs = origCjsJsxs
190
257
  cjsDevRuntime.jsxDEV = origCjsJsxDEV
258
+ // Restore user-bundle React modules if they were patched
259
+ if (userReact && origUserCreateElement) {
260
+ userReact.createElement = origUserCreateElement
261
+ }
262
+ if (userJsxRuntime) {
263
+ userJsxRuntime.jsx = origUserJsx
264
+ userJsxRuntime.jsxs = origUserJsxs
265
+ }
266
+ if (userJsxDevRuntime) {
267
+ userJsxDevRuntime.jsxDEV = origUserJsxDEV
268
+ }
191
269
  }
192
270
  }
@@ -0,0 +1,25 @@
1
+ import { DATA } from '@wix/react-component-schema'
2
+ import type { TrackingStores } from '../information-extractors/react'
3
+
4
+ /**
5
+ * Walks the known A11y attributes (from DATA.A11Y_ATTRIBUTES) in deterministic
6
+ * enum order and returns the subset whose `${propPath}.${attribute}` key was
7
+ * recorded in propUsages by the prop-tracker. When propUsages or propPath is
8
+ * unavailable, returns an empty array — per the schema, an empty list means
9
+ * "all possible A11Y values", which is a safe default.
10
+ */
11
+ export function collectUsedA11yAttributes(
12
+ propUsages: TrackingStores['propUsages'] | undefined,
13
+ propPath: string | undefined,
14
+ ): (typeof DATA.A11Y_ATTRIBUTES)[keyof typeof DATA.A11Y_ATTRIBUTES][] {
15
+ if (!propUsages || !propPath) return []
16
+
17
+ const used: (typeof DATA.A11Y_ATTRIBUTES)[keyof typeof DATA.A11Y_ATTRIBUTES][] = []
18
+ for (const attribute of Object.values(DATA.A11Y_ATTRIBUTES)) {
19
+ if (attribute === DATA.A11Y_ATTRIBUTES.Unknown_AriaAttributes) continue
20
+ if (propUsages.has(`${propPath}.${attribute}`)) {
21
+ used.push(attribute)
22
+ }
23
+ }
24
+ return used
25
+ }
@@ -20,6 +20,7 @@ import { ParseError } from '../errors'
20
20
  import type { DOMBinding, TrackingStores } from '../information-extractors/react'
21
21
  import type { PropInfo, ResolvedType } from '../information-extractors/ts/types'
22
22
  import { WIX_TYPE_TO_DATA_TYPE } from '../wix-type-to-data-type'
23
+ import { collectUsedA11yAttributes } from './a11y-builder'
23
24
  import { formatDisplayName } from './utils'
24
25
 
25
26
  const { DATA_TYPE } = DATA
@@ -120,7 +121,7 @@ function applyResolvedTypeToDataItem(
120
121
  return ok(undefined)
121
122
 
122
123
  case 'semantic':
123
- handleSemanticType(dataItem, resolvedType)
124
+ handleSemanticType({ dataItem, resolvedType, propUsages, propPath })
124
125
  return ok(undefined)
125
126
 
126
127
  default:
@@ -343,7 +344,17 @@ function handleUnionType(
343
344
  * Handles semantic types from React and @wix/public-schemas packages.
344
345
  * Unknown semantic sources or types silently fall back to text.
345
346
  */
346
- function handleSemanticType(dataItem: DataItem, resolvedType: ResolvedType): void {
347
+ function handleSemanticType({
348
+ dataItem,
349
+ resolvedType,
350
+ propUsages,
351
+ propPath,
352
+ }: {
353
+ dataItem: DataItem
354
+ resolvedType: ResolvedType
355
+ propUsages?: TrackingStores['propUsages']
356
+ propPath?: string
357
+ }): void {
347
358
  const semanticValue = resolvedType.value as string
348
359
  const source = resolvedType.source
349
360
 
@@ -363,7 +374,7 @@ function handleSemanticType(dataItem: DataItem, resolvedType: ResolvedType): voi
363
374
  const dataTypeKey = WIX_TYPE_TO_DATA_TYPE[semanticValue]
364
375
  if (dataTypeKey) {
365
376
  dataItem.dataType = DATA_TYPE[dataTypeKey]
366
- applyDataToBuilderType(dataItem, dataTypeKey)
377
+ applyDataToBuilderType({ dataItem, builderType: dataTypeKey, propUsages, propPath })
367
378
  } else {
368
379
  // Unknown Wix semantic type — silently fall back to text
369
380
  dataItem.dataType = DATA_TYPE.text
@@ -414,8 +425,20 @@ function handleFunctionType(dataItem: DataItem, propInfo: PropInfo, bindings?: D
414
425
 
415
426
  /**
416
427
  * Applies special data to builder types if required (e.g., link types, image category).
428
+ * For `a11y`, the list of attributes is built from the prop-tracker's propUsages so
429
+ * only fields actually read in JSX are included.
417
430
  */
418
- function applyDataToBuilderType(dataItem: DataItem, builderType: keyof typeof DATA_TYPE): void {
431
+ function applyDataToBuilderType({
432
+ dataItem,
433
+ builderType,
434
+ propUsages,
435
+ propPath,
436
+ }: {
437
+ dataItem: DataItem
438
+ builderType: keyof typeof DATA_TYPE
439
+ propUsages?: TrackingStores['propUsages']
440
+ propPath?: string
441
+ }): void {
419
442
  switch (builderType) {
420
443
  case 'link':
421
444
  dataItem.dataType = DATA_TYPE.link
@@ -442,5 +465,11 @@ function applyDataToBuilderType(dataItem: DataItem, builderType: keyof typeof DA
442
465
  category: MEDIA.IMAGE_CATEGORY.IMAGE,
443
466
  }
444
467
  break
468
+ case 'a11y':
469
+ dataItem.dataType = DATA_TYPE.a11y
470
+ dataItem.a11y = {
471
+ attributes: collectUsedA11yAttributes(propUsages, propPath),
472
+ }
473
+ break
445
474
  }
446
475
  }
package/src/index.ts CHANGED
@@ -126,6 +126,7 @@ export function extractComponentManifestResult(
126
126
  !!failure,
127
127
  report,
128
128
  options,
129
+ compiledEntryPath,
129
130
  )
130
131
  if (!processResult.ok) {
131
132
  return errAsync(processResult.error)
@@ -41,6 +41,11 @@ export interface RunExtractorsOptions {
41
41
  * @param componentInfo - TypeScript-extracted component information
42
42
  * @param component - The React component to render
43
43
  * @param extractors - Array of extractors to run
44
+ * @param options - Optional caller-provided options (e.g. wrapper HOC)
45
+ * @param compiledEntryPath - Optional path to the user's compiled bundle. When
46
+ * provided, the renderer also patches the React module that the bundle
47
+ * resolves from its own `node_modules`, so duplicate React instances
48
+ * (e.g. when the host bundles its own React) still get intercepted.
44
49
  * @returns Extraction results including HTML, store, and element tree
45
50
  */
46
51
  export function runExtractors(
@@ -48,6 +53,7 @@ export function runExtractors(
48
53
  component: ComponentType<unknown>,
49
54
  extractors: ReactExtractor[],
50
55
  options?: RunExtractorsOptions,
56
+ compiledEntryPath?: string,
51
57
  ): ExtractionResult {
52
58
  // Create shared store
53
59
  const store = new ExtractorStore()
@@ -80,7 +86,7 @@ export function runExtractors(
80
86
  const renderComponent = options?.wrapper ? options.wrapper(context.component) : context.component
81
87
 
82
88
  // Phase 2: Render with element creation interception
83
- const html = renderWithExtractors(renderComponent, context.props, listeners, store)
89
+ const html = renderWithExtractors(renderComponent, context.props, listeners, store, compiledEntryPath)
84
90
 
85
91
  // Phase 3: renderComplete - extractors can post-process
86
92
  for (const ext of extractors) {
@@ -91,7 +91,7 @@ function generateValueFromResolvedType(
91
91
 
92
92
  // Handle semantic types (from React or Wix packages) — returned as plain objects
93
93
  if (kind === 'semantic') {
94
- return generateSemanticValue(resolvedType.value as string, propName)
94
+ return generateSemanticValue(resolvedType.value as string, propName, path, registrar)
95
95
  }
96
96
 
97
97
  // Handle structural types
@@ -129,7 +129,12 @@ function generateValueFromResolvedType(
129
129
  /**
130
130
  * Generate a mock value for semantic types (React or Wix types)
131
131
  */
132
- function generateSemanticValue(semanticType: string, propName: string): unknown {
132
+ function generateSemanticValue(
133
+ semanticType: string,
134
+ propName: string,
135
+ path: string,
136
+ registrar?: PropSpyRegistrar,
137
+ ): unknown {
133
138
  switch (semanticType) {
134
139
  // Wix semantic types
135
140
  case 'Image':
@@ -141,7 +146,7 @@ function generateSemanticValue(semanticType: string, propName: string): unknown
141
146
  case 'Link':
142
147
  return generateMockLink()
143
148
  case 'A11y':
144
- return generateMockA11y()
149
+ return generateMockA11y(path, registrar)
145
150
  case 'VectorArt':
146
151
  return generateMockVectorArt()
147
152
  case 'Audio':
@@ -378,12 +383,57 @@ function generateMockLink(): Record<string, unknown> {
378
383
  }
379
384
  }
380
385
 
381
- function generateMockA11y(): Record<string, unknown> {
382
- return {
383
- ariaLabel: `mock_ariaLabel_${faker.string.alphanumeric(6)}`,
384
- role: faker.helpers.arrayElement(['button', 'link', 'img', 'region']),
385
- tabIndex: faker.helpers.arrayElement([0, -1]),
386
+ // String fields and string-literal-union fields of @wix/public-schemas A11y.
387
+ // These are emitted as unique trackable strings — DOM/React will accept any
388
+ // string for `aria-*` attributes regardless of the TS type union, so the spy
389
+ // markers flow through unchanged and the prop-tracker can detect usage.
390
+ const A11Y_STRING_FIELDS = [
391
+ 'ariaExpanded',
392
+ 'ariaDisabled',
393
+ 'ariaAtomic',
394
+ 'ariaHidden',
395
+ 'ariaBusy',
396
+ 'ariaAutocomplete',
397
+ 'ariaPressed',
398
+ 'ariaHaspopup',
399
+ 'ariaRelevant',
400
+ 'role',
401
+ 'ariaLive',
402
+ 'ariaCurrent',
403
+ 'ariaLabel',
404
+ 'ariaRoledescription',
405
+ 'ariaDescribedby',
406
+ 'ariaLabelledby',
407
+ 'ariaErrormessage',
408
+ 'ariaOwns',
409
+ 'ariaControls',
410
+ 'tag',
411
+ 'ariaMultiline',
412
+ 'ariaInvalid',
413
+ ] as const
414
+
415
+ // Numeric fields of @wix/public-schemas A11y.
416
+ const A11Y_NUMBER_FIELDS = ['tabIndex', 'ariaLevel'] as const
417
+
418
+ // Pure-boolean A11y fields (e.g. `multiline`) are intentionally omitted: the
419
+ // runtime spy tracker only registers strings, numbers and functions, so plain
420
+ // booleans cannot be tied back to a specific prop field.
421
+ function generateMockA11y(path: string, registrar?: PropSpyRegistrar): Record<string, unknown> {
422
+ const a11y: Record<string, unknown> = {}
423
+
424
+ for (const field of A11Y_STRING_FIELDS) {
425
+ const value = `mock_a11y_${field}_${faker.string.alphanumeric(6)}`
426
+ if (registrar) registrar.registerString(`${path}.${field}`, field, value)
427
+ a11y[field] = value
428
+ }
429
+
430
+ for (const field of A11Y_NUMBER_FIELDS) {
431
+ const value = nextTraceableNumber()
432
+ if (registrar) registrar.registerNumber(`${path}.${field}`, field, value)
433
+ a11y[field] = value
386
434
  }
435
+
436
+ return a11y
387
437
  }
388
438
 
389
439
  function generateMockVectorArt(): Record<string, unknown> {
@@ -59,6 +59,10 @@ export type ProcessComponentResult =
59
59
  *
60
60
  * When the component was loaded but render/extractors throw, returns `ok: false`
61
61
  * with an {@link IoError} — Tier-1 callers should turn that into a failed `Result`.
62
+ *
63
+ * @param compiledEntryPath - Optional path to the user's compiled JS entry. When
64
+ * provided, the renderer also patches the React module resolved from the
65
+ * bundle's location on disk to handle duplicate React instances.
62
66
  */
63
67
  export function processComponent(
64
68
  componentInfo: ComponentInfo,
@@ -67,6 +71,7 @@ export function processComponent(
67
71
  loaderHasError: boolean,
68
72
  report: (error: ExtractionError) => void,
69
73
  options?: RunExtractorsOptions,
74
+ compiledEntryPath?: string,
70
75
  ): ProcessComponentResult {
71
76
  const { componentName } = componentInfo
72
77
 
@@ -107,7 +112,7 @@ export function processComponent(
107
112
  const { extractor: propTracker, state } = createPropTrackerExtractor()
108
113
  const cssExtractor = createCssPropertiesExtractor()
109
114
 
110
- const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options)
115
+ const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options, compiledEntryPath)
111
116
  html = result.html
112
117
  extractedElements = result.elements
113
118