@wix/zero-config-implementation 1.49.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.49.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": "54ff02d476b1c16a5fa7e9f8dbb1eb74a6011ac62dcee23ecc6301be"
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
  }
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) {
@@ -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