@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.
- package/dist/{index-DT1jTZEG.js → index-CSi4SZpQ.js} +1 -1
- package/dist/{index-C4cP1dwy.js → index-CdHu4-o-.js} +20459 -18722
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
- package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
- package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
- package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
- package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
- package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
- package/src/component-renderer.ts +12 -9
- package/src/converters/to-editor-component.ts +24 -6
- package/src/index.ts +121 -54
- package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
- package/src/information-extractors/react/extractors/core/types.ts +4 -5
- package/src/information-extractors/react/extractors/css-properties.ts +2 -0
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
- package/src/information-extractors/react/types.ts +1 -1
- package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
- package/src/information-extractors/react/utils/mock-generator.ts +38 -13
- package/src/manifest-pipeline.ts +19 -14
- package/src/module-loader.test.ts +150 -0
- package/src/module-loader.ts +48 -8
- package/src/react-runtime-interceptor.ts +64 -0
- package/src/react-runtime-loader.ts +656 -0
- package/src/ref-elements/component-tag.ts +105 -0
- package/src/ref-elements/context.test.ts +179 -0
- package/src/ref-elements/context.ts +280 -0
- package/src/ref-elements/eligible-paths.ts +45 -0
- package/src/ref-elements/module-resolution.test.ts +50 -0
- package/src/ref-elements/module-resolution.ts +21 -0
- package/src/ref-elements/module-specifier.ts +17 -0
- package/src/ref-elements/path-utils.test.ts +14 -0
- package/src/ref-elements/path-utils.ts +55 -0
- package/src/ref-elements/types.ts +17 -0
- package/src/utils/css-class.ts +15 -0
- 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(
|
|
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(
|
|
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
|
package/src/manifest-pipeline.ts
CHANGED
|
@@ -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
|
-
): {
|
|
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
|
-
|
|
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
|
|
246
|
-
if (!
|
|
247
|
-
|
|
248
|
-
result.set(elementId,
|
|
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 (!
|
|
253
|
-
|
|
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
|
+
})
|
package/src/module-loader.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
62
|
-
(
|
|
84
|
+
importModuleForExtraction(entryPath, runtimeModules),
|
|
85
|
+
(esmError): Error => (esmError instanceof Error ? esmError : new Error(String(esmError))),
|
|
63
86
|
).orElse((esmError) => {
|
|
64
87
|
try {
|
|
65
|
-
|
|
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
|
+
}
|