@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.
Files changed (35) hide show
  1. package/README.md +4 -0
  2. package/dist/{index-DRWoSsTZ.js → index-CoU1WRYf.js} +28022 -28645
  3. package/dist/{index-CuKmaUAA.js → index-DCJue8M7.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +3 -3
  6. package/src/component-renderer.ts +90 -0
  7. package/src/converters/to-editor-component.ts +2 -2
  8. package/src/extensions/context-providers/context.test.ts +181 -0
  9. package/src/extensions/context-providers/context.ts +83 -0
  10. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  11. package/src/extensions/context-providers/mock-provider.ts +210 -0
  12. package/src/extensions/context-providers/types.ts +22 -0
  13. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  14. package/src/extensions/ref-elements/context.ts +168 -0
  15. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  16. package/src/extensions/shared/ast-scanner.ts +85 -0
  17. package/src/extensions/shared/catalog-loader.ts +64 -0
  18. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  19. package/src/index.ts +108 -67
  20. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  21. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  22. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  23. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  24. package/src/manifest-pipeline.ts +4 -1
  25. package/src/module-loader.test.ts +1 -1
  26. package/src/module-loader.ts +1 -1
  27. package/src/react-runtime-loader.ts +49 -8
  28. package/dist/index-BGHyk7CU.js +0 -708
  29. package/src/ref-elements/context.ts +0 -280
  30. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  31. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  32. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  33. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  34. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  35. /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 the ref-element context for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
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 3: Load the compiled package module (non-fatal) — done after TS extraction
103
- // so componentName is known when reporting failures
104
- loadModuleForExtraction(compiledEntryPath, refElementContext.runtimeModules)
105
- .map(({ cleanup, moduleExports }) => ({
106
- loadComponent: (name: string) => findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports),
107
- failure: null as LoadModuleFailure | null,
108
- cleanup,
109
- }))
110
- .orElse((failure) =>
111
- okAsync({
112
- loadComponent: () => null as ComponentType<unknown> | null,
113
- cleanup: async () => {},
114
- failure,
115
- }),
116
- )
117
- .andThen(({ cleanup, failure, loadComponent }) => {
118
- if (failure) {
119
- if (failure.esmError) {
120
- report({
121
- componentName,
122
- phase: 'loader',
123
- error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
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
- // Step 4: Extract CSS imports (non-fatal)
134
- let cssImportPaths: string[] = []
135
- const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
136
- if (cssResult.isOk()) {
137
- cssImportPaths = cssResult.value
138
- } else {
139
- const thrown = cssResult.error
140
- report({
141
- componentName,
142
- phase: 'css',
143
- error: new ParseError(
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: Process the default-exported component (non-fatal unless render fails after load)
151
- return ResultAsync.fromPromise(
152
- processComponentWithCleanup(
153
- componentInfo,
154
- loadComponent,
155
- cssImportPaths,
156
- !!failure,
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
- errors,
163
- ),
164
- normalizeComponentExtractionCleanupFailure,
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 { createRefElementMarker, extractElementPropsPathFromPropPath } from '../../../ref-elements/path-utils'
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
+ }
@@ -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
 
@@ -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>)[Symbol.for(REACT_ORIGINALS_KEY)] = reactModule
158
+ ;(globalThis as Record<PropertyKey, unknown>)[REACT_ORIGINALS_SYMBOL] = reactModule
132
159
 
133
- const reactModuleUrl = `data:text/javascript,${encodeURIComponent(buildReactModuleShimSource(Object.keys(reactModule)))}`
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(reactExportNames: string[]): string {
487
- const reactExportLines = reactExportNames
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 reactModule.createElement.apply(null, arguments);
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 reactModule;
546
+ export default new Proxy({}, { get: function(_, prop) { return globalThis[REACT_KEY][prop]; } });
506
547
  ${reactExportLines}
507
548
  `
508
549
  }