@wix/zero-config-implementation 1.71.0 → 1.73.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 (47) hide show
  1. package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
  2. package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
  3. package/dist/index.d.ts +23 -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.test.ts +88 -0
  14. package/src/converters/to-editor-component.ts +35 -6
  15. package/src/index.ts +121 -54
  16. package/src/information-extractors/css/parse.test.ts +49 -1
  17. package/src/information-extractors/css/parse.ts +167 -3
  18. package/src/information-extractors/css/selector-matcher.ts +3 -1
  19. package/src/information-extractors/css/types.ts +11 -0
  20. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  21. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  22. package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
  23. package/src/information-extractors/react/extractors/css-properties.ts +248 -98
  24. package/src/information-extractors/react/extractors/index.ts +1 -0
  25. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  26. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  27. package/src/information-extractors/react/index.ts +1 -0
  28. package/src/information-extractors/react/types.ts +1 -1
  29. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  30. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  31. package/src/manifest-pipeline.ts +22 -15
  32. package/src/module-loader.test.ts +150 -0
  33. package/src/module-loader.ts +48 -8
  34. package/src/react-runtime-interceptor.ts +64 -0
  35. package/src/react-runtime-loader.ts +656 -0
  36. package/src/ref-elements/component-tag.ts +105 -0
  37. package/src/ref-elements/context.test.ts +179 -0
  38. package/src/ref-elements/context.ts +280 -0
  39. package/src/ref-elements/eligible-paths.ts +45 -0
  40. package/src/ref-elements/module-resolution.test.ts +50 -0
  41. package/src/ref-elements/module-resolution.ts +21 -0
  42. package/src/ref-elements/module-specifier.ts +17 -0
  43. package/src/ref-elements/path-utils.test.ts +14 -0
  44. package/src/ref-elements/path-utils.ts +55 -0
  45. package/src/ref-elements/types.ts +17 -0
  46. package/src/utils/css-class.ts +15 -0
  47. 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
@@ -9,6 +9,7 @@ import {
9
9
  type RunExtractorsOptions,
10
10
  createCssPropertiesExtractor,
11
11
  createPropTrackerExtractor,
12
+ enrichContainerProperties,
12
13
  enrichGapProperties,
13
14
  runExtractors,
14
15
  } from './information-extractors/react'
@@ -17,6 +18,7 @@ import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } fr
17
18
  import { matchCssSelectors, parseCss } from './information-extractors/css'
18
19
  import type { CSSParserAPI } from './information-extractors/css'
19
20
  import { compileSass } from './information-extractors/css/sass-adapter'
21
+ import { extractElementPropsPathFromPropPath, extractParentElementPropsPath } from './ref-elements/path-utils'
20
22
 
21
23
  import type { ComponentType } from 'react'
22
24
 
@@ -72,6 +74,7 @@ export function processComponent(
72
74
  report: (error: ExtractionError) => void,
73
75
  options?: RunExtractorsOptions,
74
76
  compiledEntryPath?: string,
77
+ eligiblePaths?: string[],
75
78
  ): ProcessComponentResult {
76
79
  const { componentName } = componentInfo
77
80
 
@@ -109,7 +112,7 @@ export function processComponent(
109
112
  // Render component with mock props and track prop flow (can fail)
110
113
  if (Component) {
111
114
  try {
112
- const { extractor: propTracker, state } = createPropTrackerExtractor()
115
+ const { extractor: propTracker, state } = createPropTrackerExtractor({ eligiblePaths })
113
116
  const cssExtractor = createCssPropertiesExtractor()
114
117
 
115
118
  const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options, compiledEntryPath)
@@ -161,7 +164,8 @@ export function processComponent(
161
164
  if (html && extractedElements.length > 0 && css.length > 0) {
162
165
  try {
163
166
  const matchResult = matchCssSelectors(html, extractedElements, css)
164
- const gapEnrichedElements = enrichGapProperties(matchResult.elements)
167
+ const containerEnrichedElements = enrichContainerProperties(matchResult.elements)
168
+ const gapEnrichedElements = enrichGapProperties(containerEnrichedElements)
165
169
  enhancedInfo = {
166
170
  ...enhancedInfo,
167
171
  elements: convertElements(gapEnrichedElements),
@@ -196,7 +200,10 @@ export function processComponent(
196
200
  function buildCoupledProps(
197
201
  componentInfo: ComponentInfo,
198
202
  stores: TrackingStores,
199
- ): { props: Record<string, CoupledProp>; innerElementProps: Map<string, Record<string, CoupledProp>> } {
203
+ ): {
204
+ props: Record<string, CoupledProp>
205
+ innerElementProps: Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }>
206
+ } {
200
207
  const result: Record<string, CoupledProp> = {}
201
208
 
202
209
  for (const [name, info] of Object.entries(componentInfo.props)) {
@@ -214,8 +221,6 @@ function buildCoupledProps(
214
221
  return { props: result, innerElementProps }
215
222
  }
216
223
 
217
- const ELEMENT_PROPS_PREFIX = 'props.elementProps.'
218
-
219
224
  /**
220
225
  * Processes stores.propUsages entries with paths starting with "props.elementProps."
221
226
  * to extract inner element prop bindings grouped by elementId (traceId).
@@ -223,34 +228,36 @@ const ELEMENT_PROPS_PREFIX = 'props.elementProps.'
223
228
  function processElementPropsWrites(
224
229
  componentInfo: ComponentInfo,
225
230
  stores: TrackingStores,
226
- ): Map<string, Record<string, CoupledProp>> {
227
- const result = new Map<string, Record<string, CoupledProp>>()
231
+ ): Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }> {
232
+ const result = new Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }>()
228
233
 
229
234
  const elementPropsInfo = componentInfo.props.elementProps
230
235
  if (!elementPropsInfo) return result
231
236
 
232
237
  for (const [path, writeInfo] of stores.propUsages) {
233
- if (!path.startsWith(ELEMENT_PROPS_PREFIX)) continue
238
+ const relativePath = extractElementPropsPathFromPropPath(path)
239
+ if (!relativePath) continue
234
240
 
235
241
  // 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
242
  const propInfo = resolveInnerPropInfo(elementPropsInfo, relativePath)
238
243
  if (!propInfo) continue
239
244
 
245
+ const elementPropsPath = extractParentElementPropsPath(relativePath)
246
+ if (!elementPropsPath) continue
240
247
  const bindings = extractBindings(writeInfo)
241
248
 
242
249
  // Group by elementId from the bindings
243
250
  for (const binding of bindings) {
244
251
  const { elementId } = binding
245
- let propsForElement = result.get(elementId)
246
- if (!propsForElement) {
247
- propsForElement = {}
248
- result.set(elementId, propsForElement)
252
+ let entry = result.get(elementId)
253
+ if (!entry) {
254
+ entry = { elementPropsPath, props: {} }
255
+ result.set(elementId, entry)
249
256
  }
250
257
 
251
258
  const leafName = propInfo.name
252
- if (!propsForElement[leafName]) {
253
- propsForElement[leafName] = {
259
+ if (!entry.props[leafName]) {
260
+ entry.props[leafName] = {
254
261
  ...propInfo,
255
262
  logicOnly: false,
256
263
  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
+ }