@wix/zero-config-implementation 1.77.0 → 1.79.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/README.md +4 -0
- package/dist/{index-DRWoSsTZ.js → index-CoU1WRYf.js} +28022 -28645
- package/dist/{index-CuKmaUAA.js → index-DCJue8M7.js} +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/component-renderer.ts +90 -0
- package/src/converters/to-editor-component.ts +2 -2
- package/src/extensions/context-providers/context.test.ts +181 -0
- package/src/extensions/context-providers/context.ts +83 -0
- package/src/extensions/context-providers/mock-provider.test.ts +44 -0
- package/src/extensions/context-providers/mock-provider.ts +210 -0
- package/src/extensions/context-providers/types.ts +22 -0
- package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
- package/src/extensions/ref-elements/context.ts +168 -0
- package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
- package/src/extensions/shared/ast-scanner.ts +85 -0
- package/src/extensions/shared/catalog-loader.ts +64 -0
- package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
- package/src/index.ts +108 -67
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
- package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
- package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
- package/src/information-extractors/react/utils/mock-generator.ts +104 -1
- package/src/manifest-pipeline.ts +4 -1
- package/src/module-loader.test.ts +1 -1
- package/src/module-loader.ts +1 -1
- package/src/react-runtime-loader.ts +49 -8
- package/dist/index-BGHyk7CU.js +0 -708
- package/src/ref-elements/context.ts +0 -280
- /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
- /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
- /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
- /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
- /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
- /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ import React, { type ComponentType } from 'react'
|
|
|
5
5
|
|
|
6
6
|
import { toEditorReactComponent } from './converters'
|
|
7
7
|
import { BaseError, IoError, type NotFoundError, ParseError } from './errors'
|
|
8
|
+
import { buildContextProviderModules } from './extensions/context-providers/context'
|
|
9
|
+
import { buildContextAwareWrapper, loadMockProviders } from './extensions/context-providers/mock-provider'
|
|
10
|
+
import { buildRefElementContext } from './extensions/ref-elements/context'
|
|
11
|
+
import type { RefElementContext } from './extensions/ref-elements/types'
|
|
8
12
|
import type { ExtractionError } from './extraction-types'
|
|
9
13
|
import type { RunExtractorsOptions } from './information-extractors/react'
|
|
10
14
|
import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
|
|
@@ -12,8 +16,6 @@ import type { ComponentInfo } from './information-extractors/ts/types'
|
|
|
12
16
|
import { processComponent } from './manifest-pipeline'
|
|
13
17
|
import { findComponent, findDefaultComponent, loadModuleForExtraction } from './module-loader'
|
|
14
18
|
import type { LoadModuleFailure } from './module-loader'
|
|
15
|
-
import { buildRefElementContext } from './ref-elements/context'
|
|
16
|
-
import type { RefElementContext } from './ref-elements/types'
|
|
17
19
|
import { compileTsFile } from './ts-compiler'
|
|
18
20
|
|
|
19
21
|
const defaultWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => {
|
|
@@ -64,10 +66,6 @@ export function extractComponentManifestResult(
|
|
|
64
66
|
options?.onError?.(error)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
const optionsWithDefaults: ExtractComponentManifestOptions = {
|
|
68
|
-
...options,
|
|
69
|
-
wrapper: options?.wrapper ?? defaultWrapper,
|
|
70
|
-
}
|
|
71
69
|
// Step 1: Compile TypeScript (fatal)
|
|
72
70
|
return compileTsFile(componentPath)
|
|
73
71
|
.andThen((program) => {
|
|
@@ -91,79 +89,122 @@ export function extractComponentManifestResult(
|
|
|
91
89
|
.andThen(({ program, componentInfo }) => {
|
|
92
90
|
const { componentName } = componentInfo
|
|
93
91
|
|
|
92
|
+
// Step 3a: Build ref-element context
|
|
94
93
|
return ResultAsync.fromPromise(
|
|
95
94
|
buildRefElementContext(program, componentPath, componentInfo, compiledEntryPath),
|
|
96
95
|
(thrown) =>
|
|
97
96
|
new ParseError(
|
|
98
|
-
`Failed to build
|
|
97
|
+
`Failed to build ref-element context for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
99
98
|
{ cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
|
|
100
99
|
),
|
|
101
100
|
).andThen((refElementContext) =>
|
|
102
|
-
// Step
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
report({
|
|
127
|
-
componentName,
|
|
128
|
-
phase: 'loader',
|
|
129
|
-
error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
|
|
130
|
-
})
|
|
101
|
+
// Step 3b: Build context provider modules
|
|
102
|
+
ResultAsync.fromPromise(
|
|
103
|
+
buildContextProviderModules(program, componentPath, compiledEntryPath),
|
|
104
|
+
(thrown) =>
|
|
105
|
+
new ParseError(
|
|
106
|
+
`Failed to build context provider modules for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
107
|
+
{ cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
|
|
108
|
+
),
|
|
109
|
+
).andThen((contextProviderModules) => {
|
|
110
|
+
// Step 4: Build context provider wrapper with mock providers
|
|
111
|
+
const buildWrapper = async (): Promise<(Component: ComponentType<unknown>) => ComponentType<unknown>> => {
|
|
112
|
+
const baseWrapper = options?.wrapper ?? defaultWrapper
|
|
113
|
+
if (contextProviderModules.length === 0) {
|
|
114
|
+
return baseWrapper
|
|
115
|
+
}
|
|
116
|
+
const loadedProviders = await loadMockProviders(
|
|
117
|
+
contextProviderModules,
|
|
118
|
+
React,
|
|
119
|
+
refElementContext.runtimeModules,
|
|
120
|
+
)
|
|
121
|
+
if (loadedProviders.length === 0) {
|
|
122
|
+
return baseWrapper
|
|
131
123
|
}
|
|
124
|
+
return buildContextAwareWrapper(loadedProviders, baseWrapper, React)
|
|
125
|
+
}
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
`Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
145
|
-
{ cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
|
|
146
|
-
),
|
|
147
|
-
})
|
|
127
|
+
return ResultAsync.fromPromise(
|
|
128
|
+
buildWrapper(),
|
|
129
|
+
(thrown) =>
|
|
130
|
+
new ParseError(
|
|
131
|
+
`Failed to build context provider wrapper for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
132
|
+
{ cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
|
|
133
|
+
),
|
|
134
|
+
).andThen((wrapper) => {
|
|
135
|
+
const optionsWithDefaults: ExtractComponentManifestOptions = {
|
|
136
|
+
...options,
|
|
137
|
+
wrapper,
|
|
148
138
|
}
|
|
149
139
|
|
|
150
|
-
// Step 5:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
loadComponent
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
report,
|
|
158
|
-
optionsWithDefaults,
|
|
159
|
-
compiledEntryPath,
|
|
160
|
-
refElementContext,
|
|
140
|
+
// Step 5: Load the compiled package module (non-fatal) - done after TS extraction
|
|
141
|
+
// so componentName is known when reporting failures
|
|
142
|
+
return loadModuleForExtraction(compiledEntryPath, refElementContext.runtimeModules)
|
|
143
|
+
.map(({ cleanup, moduleExports }) => ({
|
|
144
|
+
loadComponent: (name: string) =>
|
|
145
|
+
findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports),
|
|
146
|
+
failure: null as LoadModuleFailure | null,
|
|
161
147
|
cleanup,
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
}))
|
|
149
|
+
.orElse((failure) =>
|
|
150
|
+
okAsync({
|
|
151
|
+
loadComponent: () => null as ComponentType<unknown> | null,
|
|
152
|
+
cleanup: async () => {},
|
|
153
|
+
failure,
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
.andThen(({ cleanup, failure, loadComponent }) => {
|
|
157
|
+
if (failure) {
|
|
158
|
+
if (failure.esmError) {
|
|
159
|
+
report({
|
|
160
|
+
componentName,
|
|
161
|
+
phase: 'loader',
|
|
162
|
+
error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
report({
|
|
166
|
+
componentName,
|
|
167
|
+
phase: 'loader',
|
|
168
|
+
error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Step 6: Extract CSS imports (non-fatal)
|
|
173
|
+
let cssImportPaths: string[] = []
|
|
174
|
+
const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
|
|
175
|
+
if (cssResult.isOk()) {
|
|
176
|
+
cssImportPaths = cssResult.value
|
|
177
|
+
} else {
|
|
178
|
+
const thrown = cssResult.error
|
|
179
|
+
report({
|
|
180
|
+
componentName,
|
|
181
|
+
phase: 'css',
|
|
182
|
+
error: new ParseError(
|
|
183
|
+
`Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
184
|
+
{ cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
|
|
185
|
+
),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step 7: Process the default-exported component
|
|
190
|
+
return ResultAsync.fromPromise(
|
|
191
|
+
processComponentWithCleanup(
|
|
192
|
+
componentInfo,
|
|
193
|
+
loadComponent,
|
|
194
|
+
cssImportPaths,
|
|
195
|
+
!!failure,
|
|
196
|
+
report,
|
|
197
|
+
optionsWithDefaults,
|
|
198
|
+
compiledEntryPath,
|
|
199
|
+
refElementContext,
|
|
200
|
+
cleanup,
|
|
201
|
+
errors,
|
|
202
|
+
),
|
|
203
|
+
normalizeComponentExtractionCleanupFailure,
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
}),
|
|
167
208
|
)
|
|
168
209
|
})
|
|
169
210
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createRefElementMarker } from '../../../ref-elements/path-utils'
|
|
3
|
-
import type { RefElementMatch } from '../../../ref-elements/types'
|
|
2
|
+
import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
|
|
3
|
+
import type { RefElementMatch } from '../../../extensions/ref-elements/types'
|
|
4
4
|
import { ExtractorStore } from './core/store'
|
|
5
5
|
import { createPropTrackerExtractor } from './prop-tracker'
|
|
6
6
|
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import type { HTMLAttributes } from 'react'
|
|
9
9
|
import { TRACE_ATTR } from '../../../component-renderer'
|
|
10
|
-
import { readRefElementComponentType } from '../../../ref-elements/component-tag'
|
|
11
|
-
import { parseRefElementMarker } from '../../../ref-elements/path-utils'
|
|
12
|
-
import type { RefElementMatch } from '../../../ref-elements/types'
|
|
10
|
+
import { readRefElementComponentType } from '../../../extensions/ref-elements/component-tag'
|
|
11
|
+
import { parseRefElementMarker } from '../../../extensions/ref-elements/path-utils'
|
|
12
|
+
import type { RefElementMatch } from '../../../extensions/ref-elements/types'
|
|
13
13
|
import { findPreferredSemanticClass, normalizeClassNames } from '../../../utils/css-class'
|
|
14
14
|
import type { PropSpyMeta, TrackingStores } from '../types'
|
|
15
15
|
import { type PropSpyRegistrar, generateMockProps, resetMockCounter } from '../utils/mock-generator'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createRefElementMarker, parseRefElementMarker } from '../../../ref-elements/path-utils'
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createRefElementMarker, parseRefElementMarker } from '../../../extensions/ref-elements/path-utils'
|
|
3
3
|
import type { ComponentInfo } from '../../ts/types'
|
|
4
|
-
import { generateMockProps, resetMockCounter } from './mock-generator'
|
|
4
|
+
import { generateMockContextValues, generateMockProps, resetMockCounter } from './mock-generator'
|
|
5
5
|
|
|
6
6
|
describe('generateMockProps', () => {
|
|
7
7
|
it('injects a dedicated ref-element id marker into each eligible elementProps branch', () => {
|
|
@@ -129,3 +129,107 @@ describe('generateMockProps', () => {
|
|
|
129
129
|
expect(parseRefElementMarker(ctaProps.id)).toBe('card.elementProps.cta')
|
|
130
130
|
})
|
|
131
131
|
})
|
|
132
|
+
|
|
133
|
+
describe('generateMockContextValues', () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
resetMockCounter()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('returns an empty object for an empty schema', () => {
|
|
139
|
+
expect(generateMockContextValues({ items: {} })).toEqual({})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('maps text to a string', () => {
|
|
143
|
+
const result = generateMockContextValues({ items: { title: { dataType: 'text' } } })
|
|
144
|
+
expect(typeof result.title).toBe('string')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('maps number to a number', () => {
|
|
148
|
+
const result = generateMockContextValues({ items: { count: { dataType: 'number' } } })
|
|
149
|
+
expect(typeof result.count).toBe('number')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('maps boolean to a boolean', () => {
|
|
153
|
+
const result = generateMockContextValues({ items: { active: { dataType: 'boolean' } } })
|
|
154
|
+
expect(typeof result.active).toBe('boolean')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('maps function to a function', () => {
|
|
158
|
+
const result = generateMockContextValues({ items: { onAction: { dataType: 'function' } } })
|
|
159
|
+
expect(typeof result.onAction).toBe('function')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('maps richText to an HTML paragraph string', () => {
|
|
163
|
+
const result = generateMockContextValues({ items: { body: { dataType: 'richText' } } })
|
|
164
|
+
expect(result.body as string).toMatch(/^<p>/)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('maps image to an image-shaped object', () => {
|
|
168
|
+
const result = generateMockContextValues({ items: { photo: { dataType: 'image' } } })
|
|
169
|
+
expect(result.photo).toMatchObject({
|
|
170
|
+
uri: expect.any(String),
|
|
171
|
+
url: expect.any(String),
|
|
172
|
+
width: expect.any(Number),
|
|
173
|
+
height: expect.any(Number),
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('maps link to a link-shaped object', () => {
|
|
178
|
+
const result = generateMockContextValues({ items: { destination: { dataType: 'link' } } })
|
|
179
|
+
expect(result.destination).toMatchObject({ href: expect.any(String), target: expect.any(String) })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('maps data with a nested schema to a nested object', () => {
|
|
183
|
+
const result = generateMockContextValues({
|
|
184
|
+
items: {
|
|
185
|
+
event: {
|
|
186
|
+
dataType: 'data',
|
|
187
|
+
data: { items: { name: { dataType: 'text' } } },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
expect(result.event).toMatchObject({ name: expect.any(String) })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('maps data with an empty schema to a permissive mock that handles Object.keys safely', () => {
|
|
195
|
+
const result = generateMockContextValues({
|
|
196
|
+
items: { event: { dataType: 'data', data: { items: {} } } },
|
|
197
|
+
})
|
|
198
|
+
expect(() => Object.keys(result.event as object)).not.toThrow()
|
|
199
|
+
expect(Object.keys(result.event as object)).toHaveLength(0)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('returns a permissive fallback for undeclared context fields so deep access chains do not crash', () => {
|
|
203
|
+
const result = generateMockContextValues({ items: {} })
|
|
204
|
+
const undeclared = (result as Record<string, unknown>).anything
|
|
205
|
+
expect(() => Object.keys(undeclared as object)).not.toThrow()
|
|
206
|
+
expect(Object.keys(undeclared as object)).toHaveLength(0)
|
|
207
|
+
const deeplyNested = (undeclared as Record<string, unknown>).deeply
|
|
208
|
+
expect(() => (deeplyNested as Record<string, unknown>)?.nested).not.toThrow()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('maps arrayItems with a nested schema to a single-element array containing the element object', () => {
|
|
212
|
+
const result = generateMockContextValues({
|
|
213
|
+
items: {
|
|
214
|
+
tickets: {
|
|
215
|
+
dataType: 'arrayItems',
|
|
216
|
+
arrayItems: { item: { dataType: 'data', data: { items: { price: { dataType: 'number' } } } } },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
expect(result.tickets).toHaveLength(1)
|
|
221
|
+
expect((result.tickets as Record<string, unknown>[])[0]).toMatchObject({ price: expect.any(Number) })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('maps arrayItems with no item schema to an empty array', () => {
|
|
225
|
+
const result = generateMockContextValues({
|
|
226
|
+
items: { tickets: { dataType: 'arrayItems', arrayItems: {} } },
|
|
227
|
+
})
|
|
228
|
+
expect(result.tickets).toEqual([])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('falls back to a string for unknown dataTypes', () => {
|
|
232
|
+
const result = generateMockContextValues({ items: { something: { dataType: 'unknown-type' } } })
|
|
233
|
+
expect(typeof result.something).toBe('string')
|
|
234
|
+
})
|
|
235
|
+
})
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { faker } from '@faker-js/faker'
|
|
7
|
-
import {
|
|
7
|
+
import type { ContextSchema } from '../../../extensions/context-providers/types'
|
|
8
|
+
import {
|
|
9
|
+
createRefElementMarker,
|
|
10
|
+
extractElementPropsPathFromPropPath,
|
|
11
|
+
} from '../../../extensions/ref-elements/path-utils'
|
|
8
12
|
import type { ComponentInfo, DefaultValue, PropInfo, ResolvedType } from '../../ts/types'
|
|
9
13
|
|
|
10
14
|
export const PRESETS_WRAPPER_CLASS_NAME = 'mock-presets-wrapper-probe'
|
|
@@ -484,3 +488,102 @@ function generateMockMenuItems(): unknown[] {
|
|
|
484
488
|
link: generateMockLink(),
|
|
485
489
|
}))
|
|
486
490
|
}
|
|
491
|
+
|
|
492
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
493
|
+
// Context Schema Mock Generator
|
|
494
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* A recursive Proxy that safely handles any property access, function call, or string coercion.
|
|
498
|
+
* Used as a fallback for context fields not declared in the schema.
|
|
499
|
+
*/
|
|
500
|
+
function buildPermissiveMock(): unknown {
|
|
501
|
+
const mock: unknown = new Proxy((() => {}) as (...args: unknown[]) => unknown, {
|
|
502
|
+
get(_target, prop) {
|
|
503
|
+
if (prop === 'then') return undefined // prevent thenable detection
|
|
504
|
+
if (prop === Symbol.toPrimitive) return (_hint: string) => 'mock'
|
|
505
|
+
if (prop === Symbol.iterator) return function* () {}
|
|
506
|
+
if (prop === Symbol.asyncIterator) return async function* () {}
|
|
507
|
+
if (typeof prop === 'symbol') return undefined
|
|
508
|
+
return mock
|
|
509
|
+
},
|
|
510
|
+
ownKeys() {
|
|
511
|
+
return []
|
|
512
|
+
},
|
|
513
|
+
getOwnPropertyDescriptor() {
|
|
514
|
+
return undefined
|
|
515
|
+
},
|
|
516
|
+
apply() {
|
|
517
|
+
return mock
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
return mock
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Wraps schema-derived values in a Proxy so that any property NOT declared in the schema
|
|
525
|
+
* returns a permissive mock instead of undefined, preventing crashes on undeclared field access.
|
|
526
|
+
*/
|
|
527
|
+
function buildSchemaSeededProxy(schemaValues: Record<string, unknown>): Record<string, unknown> {
|
|
528
|
+
return new Proxy(schemaValues, {
|
|
529
|
+
get(target, prop) {
|
|
530
|
+
if (prop === 'then') return undefined // prevent thenable/Promise detection
|
|
531
|
+
if (typeof prop === 'symbol') return Reflect.get(target, prop)
|
|
532
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop as string]
|
|
533
|
+
return buildPermissiveMock()
|
|
534
|
+
},
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Generate mock context values from a context extension schema. */
|
|
539
|
+
export function generateMockContextValues(schema: ContextSchema): Record<string, unknown> {
|
|
540
|
+
const result: Record<string, unknown> = {}
|
|
541
|
+
for (const [key, item] of Object.entries(schema.items)) {
|
|
542
|
+
result[key] = generateValueFromResolvedType(contextDataTypeToResolvedType(item), key, `context.${key}`)
|
|
543
|
+
}
|
|
544
|
+
return buildSchemaSeededProxy(result)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function contextDataTypeToResolvedType(item: ContextSchema['items'][string]): ResolvedType {
|
|
548
|
+
switch (item.dataType) {
|
|
549
|
+
case 'text':
|
|
550
|
+
return { kind: 'primitive', value: 'string' }
|
|
551
|
+
case 'richText':
|
|
552
|
+
return { kind: 'semantic', value: 'RichText' }
|
|
553
|
+
case 'image':
|
|
554
|
+
return { kind: 'semantic', value: 'Image' }
|
|
555
|
+
case 'video':
|
|
556
|
+
return { kind: 'semantic', value: 'Video' }
|
|
557
|
+
case 'vectorArt':
|
|
558
|
+
return { kind: 'semantic', value: 'VectorArt' }
|
|
559
|
+
case 'link':
|
|
560
|
+
return { kind: 'semantic', value: 'Link' }
|
|
561
|
+
case 'function':
|
|
562
|
+
return { kind: 'function' }
|
|
563
|
+
case 'number':
|
|
564
|
+
return { kind: 'primitive', value: 'number' }
|
|
565
|
+
case 'boolean':
|
|
566
|
+
return { kind: 'primitive', value: 'boolean' }
|
|
567
|
+
case 'arrayItems': {
|
|
568
|
+
const itemEntry = item.arrayItems?.item
|
|
569
|
+
if (itemEntry) {
|
|
570
|
+
const itemValue = generateValueFromResolvedType(
|
|
571
|
+
contextDataTypeToResolvedType(itemEntry),
|
|
572
|
+
'item',
|
|
573
|
+
'context.arrayItem',
|
|
574
|
+
)
|
|
575
|
+
return { kind: 'literal', value: [itemValue] }
|
|
576
|
+
}
|
|
577
|
+
return { kind: 'literal', value: [] }
|
|
578
|
+
}
|
|
579
|
+
case 'data': {
|
|
580
|
+
const nestedSchema = item.data
|
|
581
|
+
if (nestedSchema?.items && Object.keys(nestedSchema.items).length > 0) {
|
|
582
|
+
return { kind: 'literal', value: generateMockContextValues(nestedSchema) }
|
|
583
|
+
}
|
|
584
|
+
return { kind: 'literal', value: buildPermissiveMock() }
|
|
585
|
+
}
|
|
586
|
+
default:
|
|
587
|
+
return { kind: 'primitive', value: 'string' }
|
|
588
|
+
}
|
|
589
|
+
}
|
package/src/manifest-pipeline.ts
CHANGED
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
} from './information-extractors/react'
|
|
16
16
|
import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } from './information-extractors/react'
|
|
17
17
|
|
|
18
|
+
import {
|
|
19
|
+
extractElementPropsPathFromPropPath,
|
|
20
|
+
extractParentElementPropsPath,
|
|
21
|
+
} from './extensions/ref-elements/path-utils'
|
|
18
22
|
import { matchCssSelectors, parseCss } from './information-extractors/css'
|
|
19
23
|
import type { CSSParserAPI } from './information-extractors/css'
|
|
20
24
|
import { compileSass } from './information-extractors/css/sass-adapter'
|
|
21
|
-
import { extractElementPropsPathFromPropPath, extractParentElementPropsPath } from './ref-elements/path-utils'
|
|
22
25
|
|
|
23
26
|
import type { ComponentType } from 'react'
|
|
24
27
|
|
|
@@ -2,9 +2,9 @@ import { createRequire } from 'node:module'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { readRefElementComponentType } from './extensions/ref-elements/component-tag'
|
|
5
6
|
import { loadCjsModule, loadModuleForExtraction } from './module-loader'
|
|
6
7
|
import { readLoaderPortRefStateForTests, readRegisteredRefElementModuleUrlsForTests } from './react-runtime-loader'
|
|
7
|
-
import { readRefElementComponentType } from './ref-elements/component-tag'
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url)
|
|
10
10
|
|
package/src/module-loader.ts
CHANGED
|
@@ -4,8 +4,8 @@ import React from 'react'
|
|
|
4
4
|
import type { ComponentType } from 'react'
|
|
5
5
|
import ReactDOM from 'react-dom'
|
|
6
6
|
|
|
7
|
+
import type { RefElementModule } from './extensions/ref-elements/types'
|
|
7
8
|
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.
|
|
@@ -2,12 +2,13 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { createRequire, register } from 'node:module'
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
4
|
import { MessageChannel, MessagePort } from 'node:worker_threads'
|
|
5
|
-
import { buildRefElementTaggerSource, tagRefElementComponentForCleanup } from './ref-elements/component-tag'
|
|
6
|
-
import type { RefElementModule } from './ref-elements/types'
|
|
5
|
+
import { buildRefElementTaggerSource, tagRefElementComponentForCleanup } from './extensions/ref-elements/component-tag'
|
|
6
|
+
import type { RefElementModule } from './extensions/ref-elements/types'
|
|
7
7
|
|
|
8
8
|
const JSX_INTERCEPTOR_STATE_KEY = 'zero-config:jsx-interceptor'
|
|
9
9
|
const JSX_ORIGINALS_KEY = 'zero-config:jsx-originals'
|
|
10
10
|
const REACT_ORIGINALS_KEY = 'zero-config:react-originals'
|
|
11
|
+
const REACT_ORIGINALS_SYMBOL = Symbol.for(REACT_ORIGINALS_KEY)
|
|
11
12
|
const LOADER_REGISTERED_KEY = Symbol.for('zero-config:jsx-loader-registered')
|
|
12
13
|
const RUNTIME_STATE_KEY = Symbol.for('zero-config:ref-element-runtime-state')
|
|
13
14
|
const REF_ELEMENT_LOADER_REQUEST_TIMEOUT_MS = 1_000
|
|
@@ -67,6 +68,32 @@ interface RefElementRuntimeState {
|
|
|
67
68
|
originalModuleLoad: InternalModuleApi['_load'] | null
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Returns the URL that the ESM loader will assign to a context provider module
|
|
73
|
+
* when it is imported transitively from a ref-entry-signed statics bundle.
|
|
74
|
+
*
|
|
75
|
+
* When runtimeModules is non-empty, the statics bundle entry URL carries a
|
|
76
|
+
* `?zero-config-ref-entry=HASH` signature, and `shouldPropagateRefElementEntrySignature`
|
|
77
|
+
* propagates that hash to every non-node_modules file: import. `captureHookContext`
|
|
78
|
+
* must import the provider at the same signed URL so both paths hit the same Node.js
|
|
79
|
+
* ESM cache entry (and therefore the same `createContext` return value).
|
|
80
|
+
*
|
|
81
|
+
* When runtimeModules is empty, no hash is ever appended, so the plain URL is correct.
|
|
82
|
+
*/
|
|
83
|
+
export function computeSignedContextProviderUrl(resolvedModuleUrl: string, runtimeModules: RefElementModule[]): string {
|
|
84
|
+
if (runtimeModules.length === 0) {
|
|
85
|
+
return resolvedModuleUrl
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const registeredModules = mergeRefElementModules(runtimeModules)
|
|
89
|
+
const signature = buildRefElementEntrySignature(registeredModules)
|
|
90
|
+
const signedUrl = new URL(resolvedModuleUrl)
|
|
91
|
+
if (!signedUrl.searchParams.has(REF_ELEMENT_ENTRY_SIGNATURE_QUERY_PARAM)) {
|
|
92
|
+
signedUrl.searchParams.set(REF_ELEMENT_ENTRY_SIGNATURE_QUERY_PARAM, signature)
|
|
93
|
+
}
|
|
94
|
+
return signedUrl.href
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
export async function prepareRefElementEsmImport(
|
|
71
98
|
entryPath: string,
|
|
72
99
|
runtimeModules: RefElementModule[] = [],
|
|
@@ -128,9 +155,9 @@ export function ensureReactRuntimeLoader(): void {
|
|
|
128
155
|
jsxDEV: reactDevRuntime.jsxDEV,
|
|
129
156
|
jsxs: reactRuntime.jsxs,
|
|
130
157
|
}
|
|
131
|
-
;(globalThis as Record<PropertyKey, unknown>)[
|
|
158
|
+
;(globalThis as Record<PropertyKey, unknown>)[REACT_ORIGINALS_SYMBOL] = reactModule
|
|
132
159
|
|
|
133
|
-
const reactModuleUrl = `data:text/javascript,${encodeURIComponent(buildReactModuleShimSource(
|
|
160
|
+
const reactModuleUrl = `data:text/javascript,${encodeURIComponent(buildReactModuleShimSource(reactModule))}`
|
|
134
161
|
const interceptorModuleUrl = `data:text/javascript,${encodeURIComponent(buildJsxRuntimeInterceptorSource())}`
|
|
135
162
|
const loaderSource = buildReactRuntimeLoaderSource({
|
|
136
163
|
interceptorModuleUrl,
|
|
@@ -151,6 +178,14 @@ export function ensureReactRuntimeLoader(): void {
|
|
|
151
178
|
}
|
|
152
179
|
}
|
|
153
180
|
|
|
181
|
+
export function getActiveReactModule(): unknown {
|
|
182
|
+
return (globalThis as Record<symbol, unknown>)[REACT_ORIGINALS_SYMBOL]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function setActiveReactModule(reactModule: unknown): void {
|
|
186
|
+
;(globalThis as Record<symbol, unknown>)[REACT_ORIGINALS_SYMBOL] = reactModule
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
export function readRegisteredRefElementModuleUrlsForTests(): string[] {
|
|
155
190
|
return [...readRefElementRuntimeState().registeredEsmModulesByResolvedUrl.keys()]
|
|
156
191
|
}
|
|
@@ -483,14 +518,20 @@ function isComponent(value: unknown): boolean {
|
|
|
483
518
|
return typeof value === 'object' && value !== null && '$$typeof' in value
|
|
484
519
|
}
|
|
485
520
|
|
|
486
|
-
function buildReactModuleShimSource(
|
|
487
|
-
const reactExportLines =
|
|
521
|
+
function buildReactModuleShimSource(reactModule: Record<string, unknown>): string {
|
|
522
|
+
const reactExportLines = Object.keys(reactModule)
|
|
488
523
|
.map((exportName) => {
|
|
489
524
|
if (exportName === 'createElement') {
|
|
490
525
|
return `export function createElement() {
|
|
491
526
|
var state = globalThis[STATE_KEY];
|
|
492
527
|
if (state && state.currentCreateElement) return state.currentCreateElement.apply(null, arguments);
|
|
493
|
-
return
|
|
528
|
+
return globalThis[REACT_KEY].createElement.apply(null, arguments);
|
|
529
|
+
}`
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (exportName.startsWith('use') && typeof reactModule[exportName] === 'function') {
|
|
533
|
+
return `export function ${exportName}() {
|
|
534
|
+
return globalThis[REACT_KEY].${exportName}.apply(null, arguments);
|
|
494
535
|
}`
|
|
495
536
|
}
|
|
496
537
|
|
|
@@ -502,7 +543,7 @@ function buildReactModuleShimSource(reactExportNames: string[]): string {
|
|
|
502
543
|
var REACT_KEY = Symbol.for(${JSON.stringify(REACT_ORIGINALS_KEY)});
|
|
503
544
|
var STATE_KEY = Symbol.for(${JSON.stringify(JSX_INTERCEPTOR_STATE_KEY)});
|
|
504
545
|
var reactModule = globalThis[REACT_KEY];
|
|
505
|
-
export default
|
|
546
|
+
export default new Proxy({}, { get: function(_, prop) { return globalThis[REACT_KEY][prop]; } });
|
|
506
547
|
${reactExportLines}
|
|
507
548
|
`
|
|
508
549
|
}
|