@wix/zero-config-implementation 1.71.0 → 1.72.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.
Files changed (39) hide show
  1. package/dist/{index-DT1jTZEG.js → index-CSi4SZpQ.js} +1 -1
  2. package/dist/{index-C4cP1dwy.js → index-CdHu4-o-.js} +20459 -18722
  3. package/dist/index.d.ts +15 -8
  4. package/dist/index.js +1 -1
  5. package/package.json +4 -3
  6. package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
  7. package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
  8. package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
  9. package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
  10. package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
  11. package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
  12. package/src/component-renderer.ts +12 -9
  13. package/src/converters/to-editor-component.ts +24 -6
  14. package/src/index.ts +121 -54
  15. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  16. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  17. package/src/information-extractors/react/extractors/css-properties.ts +2 -0
  18. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  19. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  20. package/src/information-extractors/react/types.ts +1 -1
  21. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  22. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  23. package/src/manifest-pipeline.ts +19 -14
  24. package/src/module-loader.test.ts +150 -0
  25. package/src/module-loader.ts +48 -8
  26. package/src/react-runtime-interceptor.ts +64 -0
  27. package/src/react-runtime-loader.ts +656 -0
  28. package/src/ref-elements/component-tag.ts +105 -0
  29. package/src/ref-elements/context.test.ts +179 -0
  30. package/src/ref-elements/context.ts +280 -0
  31. package/src/ref-elements/eligible-paths.ts +45 -0
  32. package/src/ref-elements/module-resolution.test.ts +50 -0
  33. package/src/ref-elements/module-resolution.ts +21 -0
  34. package/src/ref-elements/module-specifier.ts +17 -0
  35. package/src/ref-elements/path-utils.test.ts +14 -0
  36. package/src/ref-elements/path-utils.ts +55 -0
  37. package/src/ref-elements/types.ts +17 -0
  38. package/src/utils/css-class.ts +15 -0
  39. package/src/jsx-runtime-interceptor.ts +0 -245
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { faker } from '@faker-js/faker'
7
+ import { createRefElementMarker, extractElementPropsPathFromPropPath } from '../../../ref-elements/path-utils'
7
8
  import type { ComponentInfo, DefaultValue, PropInfo, ResolvedType } from '../../ts/types'
8
9
 
9
10
  export const PRESETS_WRAPPER_CLASS_NAME = 'mock-presets-wrapper-probe'
@@ -41,16 +42,24 @@ export interface PropSpyRegistrar {
41
42
  registerFunction(path: string, propName: string, value: (...args: unknown[]) => unknown): void
42
43
  }
43
44
 
45
+ interface GenerateMockPropsOptions {
46
+ eligibleRefElementPaths?: Set<string>
47
+ }
48
+
44
49
  /**
45
50
  * Generate mock props object from ComponentInfo.
46
51
  * When a registrar is provided, string and number values are spy-instrumented
47
52
  * for DOM binding detection.
48
53
  */
49
- export function generateMockProps(componentInfo: ComponentInfo, registrar?: PropSpyRegistrar): Record<string, unknown> {
54
+ export function generateMockProps(
55
+ componentInfo: ComponentInfo,
56
+ registrar?: PropSpyRegistrar,
57
+ options?: GenerateMockPropsOptions,
58
+ ): Record<string, unknown> {
50
59
  const mockProps: Record<string, unknown> = {}
51
60
 
52
61
  for (const [propName, propInfo] of Object.entries(componentInfo.props)) {
53
- mockProps[propName] = generateMockValue(propInfo, propName, `props.${propName}`, registrar)
62
+ mockProps[propName] = generateMockValue(propInfo, propName, `props.${propName}`, registrar, options)
54
63
  }
55
64
 
56
65
  // Always inject wix renderer object; components that don't use it will ignore it
@@ -65,13 +74,19 @@ export function generateMockProps(componentInfo: ComponentInfo, registrar?: Prop
65
74
  /**
66
75
  * Generate a mock value based on PropInfo
67
76
  */
68
- function generateMockValue(propInfo: PropInfo, propName: string, path: string, registrar?: PropSpyRegistrar): unknown {
77
+ function generateMockValue(
78
+ propInfo: PropInfo,
79
+ propName: string,
80
+ path: string,
81
+ registrar?: PropSpyRegistrar,
82
+ options?: GenerateMockPropsOptions,
83
+ ): unknown {
69
84
  // In plain mode (no registrar), honour default values for realistic rendering
70
85
  if (!registrar && propInfo.defaultValue !== undefined) {
71
86
  return extractDefaultValueValue(propInfo.defaultValue)
72
87
  }
73
88
 
74
- return generateValueFromResolvedType(propInfo.resolvedType, propName, path, registrar)
89
+ return generateValueFromResolvedType(propInfo.resolvedType, propName, path, registrar, options)
75
90
  }
76
91
 
77
92
  function extractDefaultValueValue(defaultValue: DefaultValue): unknown {
@@ -86,6 +101,7 @@ function generateValueFromResolvedType(
86
101
  propName: string,
87
102
  path: string,
88
103
  registrar?: PropSpyRegistrar,
104
+ options?: GenerateMockPropsOptions,
89
105
  ): unknown {
90
106
  const kind = resolvedType.kind
91
107
 
@@ -103,16 +119,16 @@ function generateValueFromResolvedType(
103
119
  return resolvedType.value
104
120
 
105
121
  case 'union':
106
- return generateUnionValue(resolvedType, propName, path, registrar)
122
+ return generateUnionValue(resolvedType, propName, path, registrar, options)
107
123
 
108
124
  case 'intersection':
109
- return generateIntersectionValue(resolvedType, propName, path, registrar)
125
+ return generateIntersectionValue(resolvedType, propName, path, registrar, options)
110
126
 
111
127
  case 'array':
112
- return generateArrayValue(resolvedType, propName, path, registrar)
128
+ return generateArrayValue(resolvedType, propName, path, registrar, options)
113
129
 
114
130
  case 'object':
115
- return generateObjectValue(resolvedType, propName, path, registrar)
131
+ return generateObjectValue(resolvedType, propName, path, registrar, options)
116
132
 
117
133
  case 'enum':
118
134
  return generateEnumValue(resolvedType)
@@ -225,6 +241,7 @@ function generateUnionValue(
225
241
  propName: string,
226
242
  path: string,
227
243
  registrar?: PropSpyRegistrar,
244
+ options?: GenerateMockPropsOptions,
228
245
  ): unknown {
229
246
  const types = resolvedType.types ?? []
230
247
 
@@ -242,7 +259,7 @@ function generateUnionValue(
242
259
  candidateType.kind === 'primitive' && (candidateType.value === 'undefined' || candidateType.value === 'null')
243
260
  const representative = types.find((candidateType) => !isNullish(candidateType)) ?? types[0]
244
261
  if (representative) {
245
- return generateValueFromResolvedType(representative, propName, path, registrar)
262
+ return generateValueFromResolvedType(representative, propName, path, registrar, options)
246
263
  }
247
264
 
248
265
  return `mock_${propName}_${faker.string.alphanumeric(6)}`
@@ -256,6 +273,7 @@ function generateIntersectionValue(
256
273
  propName: string,
257
274
  path: string,
258
275
  registrar?: PropSpyRegistrar,
276
+ options?: GenerateMockPropsOptions,
259
277
  ): unknown {
260
278
  const types = resolvedType.types ?? []
261
279
  const merged: Record<string, unknown> = {}
@@ -263,7 +281,7 @@ function generateIntersectionValue(
263
281
  for (const type of types) {
264
282
  if (type.kind === 'object' && type.properties) {
265
283
  for (const [key, propInfo] of Object.entries(type.properties)) {
266
- merged[key] = generateMockValue(propInfo, `${propName}.${key}`, `${path}.${key}`, registrar)
284
+ merged[key] = generateMockValue(propInfo, `${propName}.${key}`, `${path}.${key}`, registrar, options)
267
285
  }
268
286
  }
269
287
  }
@@ -279,6 +297,7 @@ function generateArrayValue(
279
297
  propName: string,
280
298
  path: string,
281
299
  registrar?: PropSpyRegistrar,
300
+ options?: GenerateMockPropsOptions,
282
301
  ): unknown[] {
283
302
  const elementType = resolvedType.elementType
284
303
 
@@ -287,8 +306,8 @@ function generateArrayValue(
287
306
  }
288
307
 
289
308
  return [
290
- generateValueFromResolvedType(elementType, `${propName}[0]`, `${path}[0]`, registrar),
291
- generateValueFromResolvedType(elementType, `${propName}[1]`, `${path}[1]`, registrar),
309
+ generateValueFromResolvedType(elementType, `${propName}[0]`, `${path}[0]`, registrar, options),
310
+ generateValueFromResolvedType(elementType, `${propName}[1]`, `${path}[1]`, registrar, options),
292
311
  ]
293
312
  }
294
313
 
@@ -300,12 +319,18 @@ function generateObjectValue(
300
319
  propName: string,
301
320
  path: string,
302
321
  registrar?: PropSpyRegistrar,
322
+ options?: GenerateMockPropsOptions,
303
323
  ): Record<string, unknown> {
304
324
  const properties = resolvedType.properties ?? {}
305
325
  const obj: Record<string, unknown> = {}
306
326
 
307
327
  for (const [key, propInfo] of Object.entries(properties)) {
308
- obj[key] = generateMockValue(propInfo, key, `${path}.${key}`, registrar)
328
+ obj[key] = generateMockValue(propInfo, key, `${path}.${key}`, registrar, options)
329
+ }
330
+
331
+ const elementPropsPath = extractElementPropsPathFromPropPath(path)
332
+ if (elementPropsPath && options?.eligibleRefElementPaths?.has(elementPropsPath)) {
333
+ obj.id = createRefElementMarker(elementPropsPath)
309
334
  }
310
335
 
311
336
  return obj
@@ -17,6 +17,7 @@ import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } fr
17
17
  import { matchCssSelectors, parseCss } from './information-extractors/css'
18
18
  import type { CSSParserAPI } from './information-extractors/css'
19
19
  import { compileSass } from './information-extractors/css/sass-adapter'
20
+ import { extractElementPropsPathFromPropPath, extractParentElementPropsPath } from './ref-elements/path-utils'
20
21
 
21
22
  import type { ComponentType } from 'react'
22
23
 
@@ -72,6 +73,7 @@ export function processComponent(
72
73
  report: (error: ExtractionError) => void,
73
74
  options?: RunExtractorsOptions,
74
75
  compiledEntryPath?: string,
76
+ eligiblePaths?: string[],
75
77
  ): ProcessComponentResult {
76
78
  const { componentName } = componentInfo
77
79
 
@@ -109,7 +111,7 @@ export function processComponent(
109
111
  // Render component with mock props and track prop flow (can fail)
110
112
  if (Component) {
111
113
  try {
112
- const { extractor: propTracker, state } = createPropTrackerExtractor()
114
+ const { extractor: propTracker, state } = createPropTrackerExtractor({ eligiblePaths })
113
115
  const cssExtractor = createCssPropertiesExtractor()
114
116
 
115
117
  const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options, compiledEntryPath)
@@ -196,7 +198,10 @@ export function processComponent(
196
198
  function buildCoupledProps(
197
199
  componentInfo: ComponentInfo,
198
200
  stores: TrackingStores,
199
- ): { props: Record<string, CoupledProp>; innerElementProps: Map<string, Record<string, CoupledProp>> } {
201
+ ): {
202
+ props: Record<string, CoupledProp>
203
+ innerElementProps: Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }>
204
+ } {
200
205
  const result: Record<string, CoupledProp> = {}
201
206
 
202
207
  for (const [name, info] of Object.entries(componentInfo.props)) {
@@ -214,8 +219,6 @@ function buildCoupledProps(
214
219
  return { props: result, innerElementProps }
215
220
  }
216
221
 
217
- const ELEMENT_PROPS_PREFIX = 'props.elementProps.'
218
-
219
222
  /**
220
223
  * Processes stores.propUsages entries with paths starting with "props.elementProps."
221
224
  * to extract inner element prop bindings grouped by elementId (traceId).
@@ -223,34 +226,36 @@ const ELEMENT_PROPS_PREFIX = 'props.elementProps.'
223
226
  function processElementPropsWrites(
224
227
  componentInfo: ComponentInfo,
225
228
  stores: TrackingStores,
226
- ): Map<string, Record<string, CoupledProp>> {
227
- const result = new Map<string, Record<string, CoupledProp>>()
229
+ ): Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }> {
230
+ const result = new Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }>()
228
231
 
229
232
  const elementPropsInfo = componentInfo.props.elementProps
230
233
  if (!elementPropsInfo) return result
231
234
 
232
235
  for (const [path, writeInfo] of stores.propUsages) {
233
- if (!path.startsWith(ELEMENT_PROPS_PREFIX)) continue
236
+ const relativePath = extractElementPropsPathFromPropPath(path)
237
+ if (!relativePath) continue
234
238
 
235
239
  // Resolve the PropInfo for this leaf prop by walking the type tree
236
- const relativePath = path.slice(ELEMENT_PROPS_PREFIX.length) // e.g. "navbar.items"
237
240
  const propInfo = resolveInnerPropInfo(elementPropsInfo, relativePath)
238
241
  if (!propInfo) continue
239
242
 
243
+ const elementPropsPath = extractParentElementPropsPath(relativePath)
244
+ if (!elementPropsPath) continue
240
245
  const bindings = extractBindings(writeInfo)
241
246
 
242
247
  // Group by elementId from the bindings
243
248
  for (const binding of bindings) {
244
249
  const { elementId } = binding
245
- let propsForElement = result.get(elementId)
246
- if (!propsForElement) {
247
- propsForElement = {}
248
- result.set(elementId, propsForElement)
250
+ let entry = result.get(elementId)
251
+ if (!entry) {
252
+ entry = { elementPropsPath, props: {} }
253
+ result.set(elementId, entry)
249
254
  }
250
255
 
251
256
  const leafName = propInfo.name
252
- if (!propsForElement[leafName]) {
253
- propsForElement[leafName] = {
257
+ if (!entry.props[leafName]) {
258
+ entry.props[leafName] = {
254
259
  ...propInfo,
255
260
  logicOnly: false,
256
261
  propPath: path,
@@ -0,0 +1,150 @@
1
+ import { createRequire } from 'node:module'
2
+ import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { loadCjsModule, loadModuleForExtraction } from './module-loader'
6
+ import { readLoaderPortRefStateForTests, readRegisteredRefElementModuleUrlsForTests } from './react-runtime-loader'
7
+ import { readRefElementComponentType } from './ref-elements/component-tag'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ describe('module loading', () => {
12
+ it('temporarily tags ref-element modules in the CJS fallback and cleans them up afterwards', async () => {
13
+ const entryPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-entry.cjs')
14
+ const targetPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-target.cjs')
15
+ const targetModuleExports = require(targetPath) as Record<PropertyKey, unknown>
16
+
17
+ delete targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]
18
+
19
+ const loadedModuleResult = loadCjsModule(entryPath, [
20
+ {
21
+ exportNames: ['default'],
22
+ moduleSpecifier: './cjs-ref-element-target.cjs',
23
+ resolvedModuleUrl: pathToFileURL(targetPath).href,
24
+ refComponentType: 'wixEditorElements.Button',
25
+ },
26
+ ])
27
+
28
+ expect(
29
+ (loadedModuleResult.moduleExports as Record<PropertyKey, unknown>)[
30
+ Symbol.for('zero-config:ref-element-component-type')
31
+ ],
32
+ ).toBe('wixEditorElements.Button')
33
+ expect(targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]).toBe('wixEditorElements.Button')
34
+
35
+ await loadedModuleResult.cleanup()
36
+
37
+ expect(targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]).toBeUndefined()
38
+ })
39
+
40
+ it('does not preload tagged CJS dependencies before the entry bundle runs', async () => {
41
+ const entryPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-order-entry.cjs')
42
+ const targetPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-order-target.cjs')
43
+
44
+ const loadedModuleResult = loadCjsModule(entryPath, [
45
+ {
46
+ exportNames: ['default'],
47
+ moduleSpecifier: './cjs-ref-element-order-target.cjs',
48
+ resolvedModuleUrl: pathToFileURL(targetPath).href,
49
+ refComponentType: 'wixEditorElements.Button',
50
+ },
51
+ ])
52
+
53
+ expect((loadedModuleResult.moduleExports as { evaluationOrder: string[] }).evaluationOrder).toEqual([
54
+ 'entry',
55
+ 'target',
56
+ ])
57
+ await loadedModuleResult.cleanup()
58
+ })
59
+
60
+ it('reloads the ESM entry with a stable ref-element signature while leaving the real module exports untagged', async () => {
61
+ const entryPath = path.resolve(__dirname, '__fixtures__/esm-ref-element-entry.mjs')
62
+ const moduleSpecifier = '@wix/zero-config-ref-element-fixtures/BareRefButton'
63
+ const resolvedModuleUrl = pathToFileURL(
64
+ path.resolve(__dirname, '../../ref-element-fixtures/dist/BareRefButton.js'),
65
+ ).href
66
+ const runtimeModules = [
67
+ {
68
+ exportNames: ['default'],
69
+ moduleSpecifier,
70
+ resolvedModuleUrl,
71
+ refComponentType: 'wixEditorElements.BareRefButton',
72
+ },
73
+ ]
74
+
75
+ const untaggedModuleBeforeExtraction = await import(moduleSpecifier)
76
+ expect(readRefElementComponentType(untaggedModuleBeforeExtraction.default)).toBeUndefined()
77
+
78
+ const initialLoadResult = (await loadModuleForExtraction(entryPath))._unsafeUnwrap()
79
+ await initialLoadResult.cleanup()
80
+
81
+ const loadedModuleResult = (await loadModuleForExtraction(entryPath, runtimeModules))._unsafeUnwrap()
82
+ expect(loadedModuleResult.moduleExports.default).toBeDefined()
83
+ await loadedModuleResult.cleanup()
84
+
85
+ const untaggedModuleAfterExtraction = await import(moduleSpecifier)
86
+ expect(readRefElementComponentType(untaggedModuleAfterExtraction.default)).toBeUndefined()
87
+ expect(readRegisteredRefElementModuleUrlsForTests()).toContain(resolvedModuleUrl)
88
+ expect(readLoaderPortRefStateForTests()).toBe(false)
89
+ })
90
+
91
+ it('tags lazy CJS ref-element loads while the extraction is active', async () => {
92
+ const entryPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-lazy-entry.cjs')
93
+ const targetPath = path.resolve(__dirname, '__fixtures__/cjs-ref-element-target.cjs')
94
+ const targetModuleExports = require(targetPath) as Record<PropertyKey, unknown>
95
+
96
+ delete targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]
97
+
98
+ const loadedModuleResult = loadCjsModule(entryPath, [
99
+ {
100
+ exportNames: ['default'],
101
+ moduleSpecifier: './cjs-ref-element-target.cjs',
102
+ resolvedModuleUrl: pathToFileURL(targetPath).href,
103
+ refComponentType: 'wixEditorElements.Button',
104
+ },
105
+ ])
106
+
107
+ const lazyTargetModule = (
108
+ loadedModuleResult.moduleExports as { loadTargetModule: () => Record<PropertyKey, unknown> }
109
+ ).loadTargetModule()
110
+
111
+ expect(lazyTargetModule[Symbol.for('zero-config:ref-element-component-type')]).toBe('wixEditorElements.Button')
112
+ expect(targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]).toBe('wixEditorElements.Button')
113
+
114
+ await loadedModuleResult.cleanup()
115
+
116
+ expect(targetModuleExports[Symbol.for('zero-config:ref-element-component-type')]).toBeUndefined()
117
+ })
118
+
119
+ it('falls back to CJS when ESM ref-element setup rejects', async () => {
120
+ const entryPath = path.resolve(__dirname, '__fixtures__/esm-ref-element-entry.mjs')
121
+ const resolvedModuleUrl = pathToFileURL(
122
+ path.resolve(__dirname, '../../ref-element-fixtures/dist/BareRefButton.js'),
123
+ ).href
124
+
125
+ const firstLoadResult = await loadModuleForExtraction(entryPath, [
126
+ {
127
+ exportNames: ['default'],
128
+ moduleSpecifier: '@wix/zero-config-ref-element-fixtures/BareRefButton',
129
+ resolvedModuleUrl,
130
+ refComponentType: 'wixEditorElements.BareRefButton',
131
+ },
132
+ ])
133
+ await firstLoadResult._unsafeUnwrap().cleanup()
134
+
135
+ const conflictingLoadResult = await loadModuleForExtraction(entryPath, [
136
+ {
137
+ exportNames: ['default'],
138
+ moduleSpecifier: '@wix/zero-config-ref-element-fixtures/BareRefButton',
139
+ resolvedModuleUrl,
140
+ refComponentType: 'wixEditorElements.ConflictingButton',
141
+ },
142
+ ])
143
+
144
+ expect(conflictingLoadResult.isOk()).toBe(true)
145
+ if (conflictingLoadResult.isOk()) {
146
+ expect(conflictingLoadResult.value.moduleExports.default).toBeDefined()
147
+ await conflictingLoadResult.value.cleanup()
148
+ }
149
+ })
150
+ })
@@ -1,11 +1,11 @@
1
- import { createRequire } from 'node:module'
2
1
  import { Window } from 'happy-dom'
3
2
  import { ResultAsync, errAsync, okAsync } from 'neverthrow'
4
3
  import React from 'react'
5
4
  import type { ComponentType } from 'react'
6
5
  import ReactDOM from 'react-dom'
7
6
 
8
- import { registerJsxLoaderHook } from './jsx-runtime-interceptor'
7
+ import { ensureReactRuntimeLoader, loadTaggedCjsModule, prepareRefElementEsmImport } from './react-runtime-loader'
8
+ import type { RefElementModule } from './ref-elements/types'
9
9
 
10
10
  /**
11
11
  * Structured failure from `loadModule` when both ESM import and CJS require fail.
@@ -18,6 +18,20 @@ export interface LoadModuleFailure {
18
18
  cjsError: Error
19
19
  }
20
20
 
21
+ export interface LoadedModule {
22
+ cleanup: () => Promise<void>
23
+ moduleExports: Record<string, unknown>
24
+ }
25
+
26
+ class NoopIntersectionObserver {
27
+ disconnect(): void {}
28
+ observe(): void {}
29
+ takeRecords(): unknown[] {
30
+ return []
31
+ }
32
+ unobserve(): void {}
33
+ }
34
+
21
35
  function setupWindowGlobals(): void {
22
36
  const globals = globalThis as Record<string, unknown>
23
37
 
@@ -30,6 +44,9 @@ function setupWindowGlobals(): void {
30
44
  }
31
45
 
32
46
  const windowObj = globals.window as Record<string, unknown>
47
+ const intersectionObserver = windowObj.IntersectionObserver ?? NoopIntersectionObserver
48
+ if (globals.IntersectionObserver === undefined) globals.IntersectionObserver = intersectionObserver
49
+ if (windowObj.IntersectionObserver === undefined) windowObj.IntersectionObserver = intersectionObserver
33
50
  if (globals.React === undefined) globals.React = React
34
51
  if (globals.ReactDOM === undefined) globals.ReactDOM = ReactDOM
35
52
  if (windowObj.React === undefined) windowObj.React = React
@@ -50,21 +67,25 @@ function setupWindowGlobals(): void {
50
67
  * @errors {LoadModuleFailure} When both ESM import and CJS require fail.
51
68
  */
52
69
  export function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, LoadModuleFailure> {
70
+ return loadModuleForExtraction(entryPath).map((loadedModule) => loadedModule.moduleExports)
71
+ }
72
+
73
+ export function loadModuleForExtraction(
74
+ entryPath: string,
75
+ runtimeModules: RefElementModule[] = [],
76
+ ): ResultAsync<LoadedModule, LoadModuleFailure> {
53
77
  if (!entryPath) {
54
78
  return errAsync({ esmError: null, cjsError: new Error('No compiled entry path provided') })
55
79
  }
56
80
 
57
81
  setupWindowGlobals()
58
- registerJsxLoaderHook()
59
82
 
60
83
  return ResultAsync.fromPromise(
61
- import(entryPath) as Promise<Record<string, unknown>>,
62
- (esmErr): Error => (esmErr instanceof Error ? esmErr : new Error(String(esmErr))),
84
+ importModuleForExtraction(entryPath, runtimeModules),
85
+ (esmError): Error => (esmError instanceof Error ? esmError : new Error(String(esmError))),
63
86
  ).orElse((esmError) => {
64
87
  try {
65
- const require = createRequire(import.meta.url)
66
- const exports = require(entryPath)
67
- return okAsync(exports as Record<string, unknown>)
88
+ return okAsync(loadTaggedCjsModule(entryPath, runtimeModules))
68
89
  } catch (requireErr) {
69
90
  const cjsError = requireErr instanceof Error ? requireErr : new Error(String(requireErr))
70
91
  return errAsync({ esmError, cjsError })
@@ -72,6 +93,25 @@ export function loadModule(entryPath: string): ResultAsync<Record<string, unknow
72
93
  })
73
94
  }
74
95
 
96
+ async function importModuleWithCleanup(importUrl: string, cleanup: () => Promise<void>): Promise<LoadedModule> {
97
+ try {
98
+ const moduleExports = (await import(importUrl)) as Record<string, unknown>
99
+ return { cleanup, moduleExports }
100
+ } catch (thrown) {
101
+ await cleanup()
102
+ throw thrown
103
+ }
104
+ }
105
+
106
+ async function importModuleForExtraction(entryPath: string, runtimeModules: RefElementModule[]): Promise<LoadedModule> {
107
+ const runtimeSession = await prepareRefElementEsmImport(entryPath, runtimeModules)
108
+ return importModuleWithCleanup(runtimeSession.importUrl, runtimeSession.cleanup)
109
+ }
110
+
111
+ export function loadCjsModule(entryPath: string, refElementModules: RefElementModule[] = []): LoadedModule {
112
+ return loadTaggedCjsModule(entryPath, refElementModules)
113
+ }
114
+
75
115
  function isComponent(value: unknown): value is ComponentType<unknown> {
76
116
  if (typeof value === 'function') return true
77
117
  // React.memo() and React.forwardRef() return objects, not functions
@@ -0,0 +1,64 @@
1
+ import { createRequire } from 'node:module'
2
+
3
+ const require = createRequire(import.meta.url)
4
+ const originalRuntime = require('react/jsx-runtime') as typeof import('react/jsx-runtime')
5
+ const originalDevRuntime = require('react/jsx-dev-runtime') as typeof import('react/jsx-dev-runtime')
6
+
7
+ type JsxDevFunction = typeof originalDevRuntime.jsxDEV
8
+
9
+ // biome-ignore lint/suspicious/noExplicitAny: broad function type matching React.createElement's signature
10
+ type CreateElementFunction = (...args: any[]) => unknown
11
+
12
+ const cjsReact = require('react') as { createElement: CreateElementFunction }
13
+ const originalCreateElement: CreateElementFunction = cjsReact.createElement
14
+ const INTERCEPTOR_STATE_KEY = Symbol.for('zero-config:jsx-interceptor')
15
+
16
+ interface JsxInterceptorState {
17
+ currentCreateElement: CreateElementFunction | null
18
+ currentJsx: typeof originalRuntime.jsx
19
+ currentJsxDEV: JsxDevFunction
20
+ currentJsxs: typeof originalRuntime.jsxs
21
+ isInsideOriginal: boolean
22
+ }
23
+
24
+ function readJsxInterceptorState(): JsxInterceptorState {
25
+ const globalRecord = globalThis as Record<symbol, JsxInterceptorState | undefined>
26
+ if (!globalRecord[INTERCEPTOR_STATE_KEY]) {
27
+ globalRecord[INTERCEPTOR_STATE_KEY] = {
28
+ currentCreateElement: null,
29
+ currentJsx: originalRuntime.jsx,
30
+ currentJsxDEV: originalDevRuntime.jsxDEV,
31
+ currentJsxs: originalRuntime.jsxs,
32
+ isInsideOriginal: false,
33
+ }
34
+ }
35
+
36
+ return globalRecord[INTERCEPTOR_STATE_KEY]
37
+ }
38
+
39
+ export function setJsxInterceptors(
40
+ jsx?: typeof originalRuntime.jsx,
41
+ jsxs?: typeof originalRuntime.jsxs,
42
+ jsxDEV?: JsxDevFunction,
43
+ ): void {
44
+ const interceptorState = readJsxInterceptorState()
45
+ interceptorState.currentJsx = jsx ?? originalRuntime.jsx
46
+ interceptorState.currentJsxs = jsxs ?? originalRuntime.jsxs
47
+ interceptorState.currentJsxDEV = jsxDEV ?? originalDevRuntime.jsxDEV
48
+ }
49
+
50
+ export function getOriginals() {
51
+ return {
52
+ jsx: originalRuntime.jsx,
53
+ jsxs: originalRuntime.jsxs,
54
+ jsxDEV: originalDevRuntime.jsxDEV,
55
+ }
56
+ }
57
+
58
+ export function setCreateElementInterceptor(interceptor: CreateElementFunction | null): void {
59
+ readJsxInterceptorState().currentCreateElement = interceptor
60
+ }
61
+
62
+ export function getOriginalCreateElement(): CreateElementFunction {
63
+ return originalCreateElement
64
+ }