@wix/zero-config-implementation 1.78.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 (33) hide show
  1. package/README.md +4 -0
  2. package/dist/{index--6I20lGB.js → index-CoU1WRYf.js} +25036 -24768
  3. package/dist/{index-DAAmOM1c.js → index-DCJue8M7.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +2 -2
  6. package/src/converters/to-editor-component.ts +2 -2
  7. package/src/extensions/context-providers/context.test.ts +181 -0
  8. package/src/extensions/context-providers/context.ts +83 -0
  9. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  10. package/src/extensions/context-providers/mock-provider.ts +210 -0
  11. package/src/extensions/context-providers/types.ts +22 -0
  12. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  13. package/src/extensions/ref-elements/context.ts +168 -0
  14. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  15. package/src/extensions/shared/ast-scanner.ts +85 -0
  16. package/src/extensions/shared/catalog-loader.ts +64 -0
  17. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  18. package/src/index.ts +108 -67
  19. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  20. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  21. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  22. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  23. package/src/manifest-pipeline.ts +4 -1
  24. package/src/module-loader.test.ts +1 -1
  25. package/src/module-loader.ts +1 -1
  26. package/src/react-runtime-loader.ts +28 -2
  27. package/src/ref-elements/context.ts +0 -280
  28. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  29. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  30. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  31. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  32. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  33. /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { g as x } from "./index--6I20lGB.js";
1
+ import { g as x } from "./index-CoU1WRYf.js";
2
2
  function h(r, a) {
3
3
  for (var i = 0; i < a.length; i++) {
4
4
  const o = a[i];
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index--6I20lGB.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-CoU1WRYf.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.78.0",
7
+ "version": "1.79.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -82,5 +82,5 @@
82
82
  ]
83
83
  }
84
84
  },
85
- "falconPackageHash": "7ea33e17604c8d13038271d43e3350d8bb9d9531ec0110efca2a7144"
85
+ "falconPackageHash": "95a755e580164c3acabbf27f3813514e4fcdb56b90214e7b830134af"
86
86
  }
@@ -11,6 +11,8 @@ import type {
11
11
  } from '@wix/react-component-schema'
12
12
  import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
13
13
  import { camelCase } from 'case-anything'
14
+ import { buildRefElementManifestKey } from '../extensions/ref-elements/path-utils'
15
+ import type { RefElementMatch } from '../extensions/ref-elements/types'
14
16
  import type { ComponentInfoWithCss } from '../index'
15
17
  import type { MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
16
18
  import type {
@@ -21,8 +23,6 @@ import type {
21
23
  TrackingStores,
22
24
  } from '../information-extractors/react'
23
25
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
24
- import { buildRefElementManifestKey } from '../ref-elements/path-utils'
25
- import type { RefElementMatch } from '../ref-elements/types'
26
26
  import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
27
27
  import { resolveLonghand } from './css-longhand-resolver'
28
28
  import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
@@ -0,0 +1,181 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import ts from 'typescript'
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+
7
+ import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
8
+ import { buildContextProviderModules } from './context'
9
+
10
+ const temporaryDirectoryPaths: string[] = []
11
+
12
+ afterEach(() => {
13
+ for (const temporaryDirectoryPath of temporaryDirectoryPaths.splice(0)) {
14
+ fs.rmSync(temporaryDirectoryPath, { force: true, recursive: true })
15
+ }
16
+ })
17
+
18
+ const FORMAT_A_CATALOG_ENTRY = {
19
+ type: 'GenericExtension',
20
+ options: {
21
+ compType: 'EDITOR_CONTEXT_PROVIDER',
22
+ compData: {
23
+ editorContextProvider: {
24
+ context: { items: { title: { dataType: 'text' } } },
25
+ resources: {
26
+ contextSpecifier: {
27
+ hook: 'useEventContext',
28
+ moduleSpecifier: 'my-context-package/context-provider',
29
+ },
30
+ dependencies: { contextDependencies: [] },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ }
36
+
37
+ const FORMAT_B_CATALOG_ENTRY = {
38
+ context: { items: { title: { dataType: 'text' } } },
39
+ resources: {
40
+ contextSpecifier: {
41
+ hook: 'useEventContext',
42
+ moduleSpecifier: 'my-context-package/context-provider',
43
+ },
44
+ dependencies: { contextDependencies: [] },
45
+ },
46
+ }
47
+
48
+ function createPackageSetup(options: {
49
+ extensionsContent: string
50
+ importSpecifier?: string
51
+ }): { compiledEntryPath: string; sourceFilePath: string } {
52
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-context-provider-'))
53
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
54
+
55
+ const sourceFilePath = path.join(temporaryDirectoryPath, 'Component.tsx')
56
+ const compiledEntryPath = path.join(temporaryDirectoryPath, 'dist', 'index.js')
57
+ const packageDirectoryPath = path.join(temporaryDirectoryPath, 'node_modules', 'my-context-package')
58
+
59
+ fs.mkdirSync(path.dirname(compiledEntryPath), { recursive: true })
60
+ fs.mkdirSync(packageDirectoryPath, { recursive: true })
61
+
62
+ fs.writeFileSync(
63
+ sourceFilePath,
64
+ `import { useEventContext } from '${options.importSpecifier ?? 'my-context-package/context-provider'}'\nexport default function Component() { return null }\n`,
65
+ )
66
+ fs.writeFileSync(compiledEntryPath, 'export {}\n')
67
+ fs.writeFileSync(path.join(packageDirectoryPath, 'context-provider.js'), 'export function useEventContext() {}\n')
68
+ fs.writeFileSync(path.join(packageDirectoryPath, 'extensions.js'), options.extensionsContent)
69
+ fs.writeFileSync(
70
+ path.join(packageDirectoryPath, 'package.json'),
71
+ JSON.stringify(
72
+ {
73
+ name: 'my-context-package',
74
+ type: 'module',
75
+ exports: {
76
+ '.': './context-provider.js',
77
+ './context-provider': './context-provider.js',
78
+ './extensions': './extensions.js',
79
+ },
80
+ },
81
+ null,
82
+ 2,
83
+ ),
84
+ )
85
+
86
+ return { compiledEntryPath, sourceFilePath }
87
+ }
88
+
89
+ function createProgram(sourceFilePath: string): ts.Program {
90
+ return ts.createProgram({
91
+ rootNames: [sourceFilePath],
92
+ options: {
93
+ allowJs: true,
94
+ jsx: ts.JsxEmit.ReactJSX,
95
+ },
96
+ })
97
+ }
98
+
99
+ describe('buildContextProviderModules', () => {
100
+ it('returns empty when the import is a bare package with no sub-path', async () => {
101
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
102
+ importSpecifier: 'my-context-package',
103
+ extensionsContent: `export default [${JSON.stringify(FORMAT_A_CATALOG_ENTRY)}]\n`,
104
+ })
105
+ const program = createProgram(sourceFilePath)
106
+
107
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
108
+
109
+ expect(modules).toEqual([])
110
+ })
111
+
112
+ it('resolves a context provider from a Format A catalog entry (manifest nested under options.compData.editorContextProvider)', async () => {
113
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
114
+ extensionsContent: `export default [${JSON.stringify(FORMAT_A_CATALOG_ENTRY)}]\n`,
115
+ })
116
+ const program = createProgram(sourceFilePath)
117
+
118
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
119
+
120
+ expect(modules).toEqual([
121
+ {
122
+ hookName: 'useEventContext',
123
+ moduleSpecifier: 'my-context-package/context-provider',
124
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(compiledEntryPath, 'my-context-package/context-provider'),
125
+ contextSchema: { items: { title: { dataType: 'text' } } },
126
+ contextDependencies: [],
127
+ },
128
+ ])
129
+ })
130
+
131
+ it('resolves a context provider from a Format B catalog entry (manifest at root)', async () => {
132
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
133
+ extensionsContent: `export default [${JSON.stringify(FORMAT_B_CATALOG_ENTRY)}]\n`,
134
+ })
135
+ const program = createProgram(sourceFilePath)
136
+
137
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
138
+
139
+ expect(modules).toEqual([
140
+ {
141
+ hookName: 'useEventContext',
142
+ moduleSpecifier: 'my-context-package/context-provider',
143
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(compiledEntryPath, 'my-context-package/context-provider'),
144
+ contextSchema: { items: { title: { dataType: 'text' } } },
145
+ contextDependencies: [],
146
+ },
147
+ ])
148
+ })
149
+
150
+ it('returns empty when the imported specifier does not match any catalog entry moduleSpecifier', async () => {
151
+ const nonMatchingEntry = {
152
+ ...FORMAT_B_CATALOG_ENTRY,
153
+ resources: {
154
+ ...FORMAT_B_CATALOG_ENTRY.resources,
155
+ contextSpecifier: {
156
+ hook: 'useOtherContext',
157
+ moduleSpecifier: 'my-context-package/other-module',
158
+ },
159
+ },
160
+ }
161
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
162
+ extensionsContent: `export default [${JSON.stringify(nonMatchingEntry)}]\n`,
163
+ })
164
+ const program = createProgram(sourceFilePath)
165
+
166
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
167
+
168
+ expect(modules).toEqual([])
169
+ })
170
+
171
+ it('throws when the extensions catalog throws during loading', async () => {
172
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
173
+ extensionsContent: "throw new Error('broken catalog')\n",
174
+ })
175
+ const program = createProgram(sourceFilePath)
176
+
177
+ await expect(buildContextProviderModules(program, sourceFilePath, compiledEntryPath)).rejects.toThrow(
178
+ 'broken catalog',
179
+ )
180
+ })
181
+ })
@@ -0,0 +1,83 @@
1
+ import type ts from 'typescript'
2
+ import { collectAllPackageImportSpecifiers } from '../shared/ast-scanner'
3
+ import { loadNormalizedCatalogEntries } from '../shared/catalog-loader'
4
+ import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
5
+ import type { ContextProviderModule, ContextSchema } from './types'
6
+
7
+ interface NormalizedContextProviderCatalogEntry {
8
+ hookName: string
9
+ moduleSpecifier: string
10
+ contextSchema: ContextSchema
11
+ contextDependencies: string[]
12
+ }
13
+
14
+ export async function buildContextProviderModules(
15
+ program: ts.Program,
16
+ sourceFilePath: string,
17
+ compiledEntryPath: string,
18
+ ): Promise<ContextProviderModule[]> {
19
+ const allImportedSpecifiers = collectAllPackageImportSpecifiers(program, sourceFilePath)
20
+ if (allImportedSpecifiers.length === 0) {
21
+ return []
22
+ }
23
+
24
+ const catalogEntries = await loadNormalizedCatalogEntries(
25
+ allImportedSpecifiers,
26
+ compiledEntryPath,
27
+ normalizeContextProviderEntry,
28
+ )
29
+
30
+ const importedSpecifierSet = new Set(allImportedSpecifiers)
31
+ const contextProviderModules: ContextProviderModule[] = []
32
+
33
+ for (const catalogEntry of catalogEntries) {
34
+ if (!importedSpecifierSet.has(catalogEntry.moduleSpecifier)) {
35
+ continue
36
+ }
37
+
38
+ const resolvedModuleUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, catalogEntry.moduleSpecifier)
39
+ if (!resolvedModuleUrl) {
40
+ continue
41
+ }
42
+
43
+ contextProviderModules.push({
44
+ hookName: catalogEntry.hookName,
45
+ moduleSpecifier: catalogEntry.moduleSpecifier,
46
+ resolvedModuleUrl,
47
+ contextSchema: catalogEntry.contextSchema,
48
+ contextDependencies: catalogEntry.contextDependencies,
49
+ })
50
+ }
51
+
52
+ return contextProviderModules
53
+ }
54
+
55
+ type UnknownObject = Record<string, unknown> | undefined
56
+
57
+ function normalizeContextProviderEntry(rawEntry: unknown): NormalizedContextProviderCatalogEntry | undefined {
58
+ if (rawEntry === null || typeof rawEntry !== 'object') {
59
+ return undefined
60
+ }
61
+
62
+ const entry = rawEntry as Record<string, unknown>
63
+ // Format A wraps the manifest under options.compData.editorContextProvider; Format B exports it at the root.
64
+ const compData = (entry.options as UnknownObject)?.compData as UnknownObject
65
+ const normalizedEntry = (compData?.editorContextProvider ?? entry) as Record<string, unknown>
66
+
67
+ const resources = normalizedEntry.resources as UnknownObject
68
+ const contextSpecifier = resources?.contextSpecifier as { hook?: string; moduleSpecifier?: string } | undefined
69
+
70
+ if (!contextSpecifier?.hook || !contextSpecifier?.moduleSpecifier) {
71
+ return undefined
72
+ }
73
+
74
+ const dependencies = resources?.dependencies as { contextDependencies?: string[] } | undefined
75
+ return {
76
+ hookName: contextSpecifier.hook,
77
+ moduleSpecifier: contextSpecifier.moduleSpecifier,
78
+ contextSchema: {
79
+ items: (normalizedEntry.context as ContextSchema | undefined)?.items ?? {},
80
+ },
81
+ contextDependencies: dependencies?.contextDependencies ?? [],
82
+ }
83
+ }
@@ -0,0 +1,44 @@
1
+ import React, { type ComponentType } from 'react'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { buildContextAwareWrapper } from './mock-provider'
5
+
6
+ function makeProvider(label: string): ComponentType<{ children?: React.ReactNode }> {
7
+ const Provider = ({ children }: { children?: React.ReactNode }) =>
8
+ React.createElement('div', { 'data-provider': label }, children)
9
+ return Provider
10
+ }
11
+
12
+ describe('buildContextAwareWrapper', () => {
13
+ it('places dependency providers outermost (topological sort)', () => {
14
+ // ticketProvider depends on eventProvider — eventProvider must render outermost
15
+ const loadedProviders = [
16
+ { moduleSpecifier: 'pkg/ticket', contextDependencies: ['pkg/event'], Provider: makeProvider('ticket') },
17
+ { moduleSpecifier: 'pkg/event', contextDependencies: [], Provider: makeProvider('event') },
18
+ ]
19
+
20
+ const identityWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => Component
21
+ const wrap = buildContextAwareWrapper(loadedProviders, identityWrapper, React)
22
+ const Inner: ComponentType<unknown> = () => React.createElement('span', null, 'content')
23
+ const html = renderToStaticMarkup(React.createElement(wrap(Inner), {}))
24
+
25
+ // event (the dependency) must appear before ticket in the HTML — it's the outer wrapper
26
+ expect(html.indexOf('data-provider="event"')).toBeLessThan(html.indexOf('data-provider="ticket"'))
27
+ })
28
+
29
+ it('composes options.wrapper as the outermost layer when provided', () => {
30
+ const customWrapper =
31
+ (Component: ComponentType<unknown>): ComponentType<unknown> =>
32
+ (props) =>
33
+ React.createElement('div', { 'data-wrapper': 'custom' }, React.createElement(Component, props as object))
34
+
35
+ const loadedProviders = [{ moduleSpecifier: 'pkg/event', contextDependencies: [], Provider: makeProvider('event') }]
36
+
37
+ const wrap = buildContextAwareWrapper(loadedProviders, customWrapper, React)
38
+ const Inner: ComponentType<unknown> = () => React.createElement('span', null, 'content')
39
+ const html = renderToStaticMarkup(React.createElement(wrap(Inner), {}))
40
+
41
+ expect(html.indexOf('data-wrapper="custom"')).toBeLessThan(html.indexOf('data-provider="event"'))
42
+ expect(html.indexOf('data-provider="event"')).toBeLessThan(html.indexOf('<span>'))
43
+ })
44
+ })
@@ -0,0 +1,210 @@
1
+ import type React from 'react'
2
+ import type { ComponentType, Context } from 'react'
3
+ import { generateMockContextValues } from '../../information-extractors/react/utils/mock-generator'
4
+ import { computeSignedContextProviderUrl, ensureReactRuntimeLoader } from '../../react-runtime-loader'
5
+ import type { RefElementModule } from '../ref-elements/types'
6
+ import type { ContextProviderModule } from './types'
7
+
8
+ type AnyFunction = (...args: unknown[]) => unknown
9
+
10
+ interface ReactShim {
11
+ useContext(context: unknown): unknown
12
+ [hookName: string]: unknown
13
+ }
14
+
15
+ type GlobalWithReact = Record<symbol, ReactShim>
16
+
17
+ // Hooks whose return value is consumed before useContext is reached — returning undefined would crash before the interceptor fires.
18
+ const RETURN_VALUE_HOOK_STUBS: Record<string, AnyFunction> = {
19
+ useState: (initialState) => [
20
+ typeof initialState === 'function' ? (initialState as () => unknown)() : initialState,
21
+ () => {},
22
+ ],
23
+ useReducer: (_reducer, initialState, init) => [
24
+ typeof init === 'function' ? (init as (state: unknown) => unknown)(initialState) : initialState,
25
+ () => {},
26
+ ],
27
+ useRef: (initialValue) => ({ current: initialValue }),
28
+ useMemo: (factory) => {
29
+ try {
30
+ return (factory as () => unknown)()
31
+ } catch {
32
+ return undefined
33
+ }
34
+ },
35
+ useCallback: (callbackFn) => callbackFn,
36
+ useTransition: () => [false, () => {}],
37
+ useDeferredValue: (value) => value,
38
+ useSyncExternalStore: (_subscribe, getSnapshot) => (getSnapshot as () => unknown)(),
39
+ }
40
+
41
+ const genericHookStub: AnyFunction = () => undefined
42
+
43
+ interface LoadedMockProvider {
44
+ moduleSpecifier: string
45
+ contextDependencies: string[]
46
+ Provider: ComponentType<{ children?: React.ReactNode }>
47
+ }
48
+
49
+ /**
50
+ * Builds a synthetic mock Provider for each context provider module, filled with
51
+ * schema-derived values. Identifies each context by calling the hook and intercepting
52
+ * `useContext` — intercepting `createContext` would require patching before the module
53
+ * loads, which fails when the module is already in the ESM cache.
54
+ *
55
+ * The loop is intentionally serial — each iteration patches a global and restores it in a
56
+ * finally block; concurrent iterations would interleave the patches.
57
+ */
58
+ export async function loadMockProviders(
59
+ contextProviderModules: ContextProviderModule[],
60
+ reactInstance: typeof React,
61
+ refElementModules: RefElementModule[] = [],
62
+ ): Promise<LoadedMockProvider[]> {
63
+ ensureReactRuntimeLoader()
64
+
65
+ const loadedProviders: LoadedMockProvider[] = []
66
+
67
+ for (const contextProviderModule of contextProviderModules) {
68
+ const capturedContext = await captureHookContext(contextProviderModule, refElementModules)
69
+ if (!capturedContext) {
70
+ console.warn(
71
+ `Could not capture context for hook "${contextProviderModule.hookName}" from "${contextProviderModule.moduleSpecifier}" — skipping mock provider.`,
72
+ )
73
+ continue
74
+ }
75
+
76
+ const mockValues = generateMockContextValues(contextProviderModule.contextSchema)
77
+ const Provider = buildSyntheticProvider(capturedContext, mockValues, reactInstance)
78
+ loadedProviders.push({
79
+ moduleSpecifier: contextProviderModule.moduleSpecifier,
80
+ contextDependencies: contextProviderModule.contextDependencies,
81
+ Provider,
82
+ })
83
+ }
84
+
85
+ return loadedProviders
86
+ }
87
+
88
+ /** Providers that others depend on are placed outermost (applied last in the chain). */
89
+ export function buildContextAwareWrapper(
90
+ loadedProviders: LoadedMockProvider[],
91
+ outerWrapper: (Component: ComponentType<unknown>) => ComponentType<unknown>,
92
+ reactInstance: typeof React,
93
+ ): (Component: ComponentType<unknown>) => ComponentType<unknown> {
94
+ const sortedProviders = sortProvidersByDependency(loadedProviders)
95
+
96
+ return (Component: ComponentType<unknown>): ComponentType<unknown> => {
97
+ const WrappedWithProviders: ComponentType<unknown> = (props) => {
98
+ let element = reactInstance.createElement(Component, props as Record<string, unknown>)
99
+
100
+ for (const { Provider } of sortedProviders) {
101
+ element = reactInstance.createElement(Provider, null, element)
102
+ }
103
+
104
+ return element as React.ReactElement
105
+ }
106
+ return outerWrapper(WrappedWithProviders)
107
+ }
108
+ }
109
+
110
+ async function captureHookContext(
111
+ contextProviderModule: ContextProviderModule,
112
+ refElementModules: RefElementModule[],
113
+ ): Promise<Context<unknown> | null> {
114
+ // Must use the same signed URL the statics bundle will use so both paths hit the same ESM cache entry.
115
+ const signedModuleUrl = computeSignedContextProviderUrl(contextProviderModule.resolvedModuleUrl, refElementModules)
116
+ const moduleExports = (await import(signedModuleUrl)) as Record<string, unknown>
117
+ const hookFn = moduleExports[contextProviderModule.hookName]
118
+ if (typeof hookFn !== 'function') {
119
+ return null
120
+ }
121
+
122
+ let capturedContext: Context<unknown> | null = null
123
+
124
+ const useContextInterceptor = (context: Context<unknown>) => {
125
+ // First useContext call wins — hooks in this system are expected to read exactly one context.
126
+ if (!capturedContext) {
127
+ capturedContext = context
128
+ }
129
+ // Return a truthy mock so any "must be used within a Provider" guard doesn't throw
130
+ return {} as unknown
131
+ }
132
+
133
+ const REACT_KEY = Symbol.for('zero-config:react-originals')
134
+ const shimReact = (globalThis as unknown as GlobalWithReact)[REACT_KEY]
135
+
136
+ if (!shimReact) {
137
+ throw new Error(
138
+ `React shim is not registered under Symbol.for('${REACT_KEY.description}') — ensure ensureReactRuntimeLoader() was called before loadMockProviders()`,
139
+ )
140
+ }
141
+
142
+ const originalUseContext = shimReact.useContext
143
+ shimReact.useContext = useContextInterceptor
144
+
145
+ const originalHooks: Record<string, unknown> = {}
146
+ for (const hookName of Object.keys(shimReact)) {
147
+ if (hookName !== 'useContext' && hookName.startsWith('use') && typeof shimReact[hookName] === 'function') {
148
+ originalHooks[hookName] = shimReact[hookName]
149
+ shimReact[hookName] = RETURN_VALUE_HOOK_STUBS[hookName] ?? genericHookStub
150
+ }
151
+ }
152
+
153
+ try {
154
+ ;(hookFn as () => unknown)()
155
+ } catch {
156
+ // Hook may throw after useContext fires (e.g. a "must be used within a Provider" guard) — capturedContext is already set.
157
+ } finally {
158
+ shimReact.useContext = originalUseContext
159
+ for (const [hookName, original] of Object.entries(originalHooks)) {
160
+ shimReact[hookName] = original
161
+ }
162
+ }
163
+
164
+ return capturedContext
165
+ }
166
+
167
+ function buildSyntheticProvider(
168
+ capturedContext: Context<unknown>,
169
+ mockValues: Record<string, unknown>,
170
+ reactInstance: typeof React,
171
+ ): ComponentType<{ children?: React.ReactNode }> {
172
+ return ({ children }: { children?: React.ReactNode }): React.ReactElement => {
173
+ return reactInstance.createElement(capturedContext.Provider, { value: mockValues }, children)
174
+ }
175
+ }
176
+
177
+ // Topological sort: dependency providers come last so they wrap outermost.
178
+ function sortProvidersByDependency(providers: LoadedMockProvider[]): LoadedMockProvider[] {
179
+ const providerBySpecifier = new Map(providers.map((provider) => [provider.moduleSpecifier, provider]))
180
+ const visited = new Set<string>()
181
+ const sorted: LoadedMockProvider[] = []
182
+
183
+ function visit(moduleSpecifier: string): void {
184
+ if (visited.has(moduleSpecifier)) {
185
+ return
186
+ }
187
+ visited.add(moduleSpecifier)
188
+
189
+ const provider = providerBySpecifier.get(moduleSpecifier)
190
+ if (!provider) {
191
+ return
192
+ }
193
+
194
+ for (const dependency of provider.contextDependencies) {
195
+ if (providerBySpecifier.has(dependency)) {
196
+ visit(dependency)
197
+ }
198
+ }
199
+
200
+ sorted.push(provider)
201
+ }
202
+
203
+ for (const provider of providers) {
204
+ visit(provider.moduleSpecifier)
205
+ }
206
+
207
+ // Post-order DFS gives [dependency, dependent]. Reverse so dependencies come last
208
+ // in the loop below — last element applied is outermost in the React tree.
209
+ return sorted.reverse()
210
+ }
@@ -0,0 +1,22 @@
1
+ export interface ContextProviderModule {
2
+ hookName: string
3
+ moduleSpecifier: string
4
+ resolvedModuleUrl: string
5
+ contextSchema: ContextSchema
6
+ contextDependencies: string[]
7
+ }
8
+
9
+ export interface ContextSchema {
10
+ items: Record<string, ContextSchemaItem>
11
+ }
12
+
13
+ export interface ContextSchemaItem {
14
+ dataType: string
15
+ displayName?: string
16
+ data?: ContextSchema
17
+ arrayItems?: ContextArrayItems
18
+ }
19
+
20
+ export interface ContextArrayItems {
21
+ item?: ContextSchemaItem
22
+ }
@@ -4,9 +4,9 @@ import path from 'node:path'
4
4
  import ts from 'typescript'
5
5
  import { afterEach, describe, expect, it } from 'vitest'
6
6
 
7
- import type { ComponentInfo } from '../information-extractors/ts/types'
7
+ import type { ComponentInfo } from '../../information-extractors/ts/types'
8
+ import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
8
9
  import { buildRefElementContext } from './context'
9
- import { resolveModuleUrlFromEntryPath } from './module-resolution'
10
10
 
11
11
  const temporaryDirectoryPaths: string[] = []
12
12
  const compiledEntryPath = path.resolve(__dirname, 'context.test.ts')
@@ -97,7 +97,7 @@ describe('buildRefElementContext', () => {
97
97
  componentName: 'Component',
98
98
  props: {},
99
99
  } satisfies ComponentInfo,
100
- path.resolve(__dirname, '../__fixtures__/esm-ref-element-entry.mjs'),
100
+ path.resolve(__dirname, '../../__fixtures__/esm-ref-element-entry.mjs'),
101
101
  )
102
102
 
103
103
  expect(refElementContext.runtimeModules).toEqual([
@@ -105,7 +105,7 @@ describe('buildRefElementContext', () => {
105
105
  exportNames: ['default', 'BareRefButton'],
106
106
  moduleSpecifier: '@wix/zero-config-ref-element-fixtures/BareRefButton',
107
107
  resolvedModuleUrl: resolveModuleUrlFromEntryPath(
108
- path.resolve(__dirname, '../__fixtures__/esm-ref-element-entry.mjs'),
108
+ path.resolve(__dirname, '../../__fixtures__/esm-ref-element-entry.mjs'),
109
109
  '@wix/zero-config-ref-element-fixtures/BareRefButton',
110
110
  ),
111
111
  refComponentType: 'wixEditorElements.BareRefButton',