@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/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-
|
|
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.
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@wix/builder-services-wrapper": "^1.
|
|
42
|
+
"@wix/builder-services-wrapper": "^1.56.0",
|
|
43
43
|
"@wix/react-component-schema": "1.7.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
]
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
|
-
"falconPackageHash": "
|
|
85
|
+
"falconPackageHash": "95a755e580164c3acabbf27f3813514e4fcdb56b90214e7b830134af"
|
|
86
86
|
}
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
setCreateElementInterceptor,
|
|
77
77
|
setJsxInterceptors,
|
|
78
78
|
} from './react-runtime-interceptor'
|
|
79
|
+
import { getActiveReactModule, setActiveReactModule } from './react-runtime-loader'
|
|
79
80
|
|
|
80
81
|
export const TRACE_ATTR = 'data-trace-id'
|
|
81
82
|
|
|
@@ -132,6 +133,84 @@ function createInterceptor(
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// Dispatcher bridge
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const REACT_18_INTERNALS_KEY = '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED'
|
|
141
|
+
const REACT_19_INTERNALS_KEY = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE'
|
|
142
|
+
|
|
143
|
+
function readReactInternals(reactModule: unknown): Record<string, unknown> | undefined {
|
|
144
|
+
if (typeof reactModule !== 'object' || reactModule === null) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const reactRecord = reactModule as Record<string, unknown>
|
|
149
|
+
const internals = reactRecord[REACT_18_INTERNALS_KEY] ?? reactRecord[REACT_19_INTERNALS_KEY]
|
|
150
|
+
return typeof internals === 'object' && internals !== null ? (internals as Record<string, unknown>) : undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Forwards the host React's hook dispatcher to the user React so host-bound hook
|
|
155
|
+
* callers (e.g. WixServicesWrapper) resolve the dispatcher the user's react-dom
|
|
156
|
+
* installs during SSR. Returns a function that restores the host React.
|
|
157
|
+
*/
|
|
158
|
+
function bridgeHostDispatcherToUserReact(hostReact: unknown, userReact: unknown): () => void {
|
|
159
|
+
const hostInternals = readReactInternals(hostReact)
|
|
160
|
+
const userInternals = readReactInternals(userReact)
|
|
161
|
+
if (!hostInternals || !userInternals) {
|
|
162
|
+
return () => {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// React 18 exposes a `{ current }` holder captured by reference at load time, so
|
|
166
|
+
// forward its `current` to the user holder rather than swapping the holder itself.
|
|
167
|
+
if (hostInternals.ReactCurrentDispatcher && userInternals.ReactCurrentDispatcher) {
|
|
168
|
+
const hostDispatcherHolder = hostInternals.ReactCurrentDispatcher as Record<string, unknown>
|
|
169
|
+
const userDispatcherHolder = userInternals.ReactCurrentDispatcher as Record<string, unknown>
|
|
170
|
+
if (hostDispatcherHolder === userDispatcherHolder) {
|
|
171
|
+
return () => {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const previousDescriptor = Object.getOwnPropertyDescriptor(hostDispatcherHolder, 'current')
|
|
175
|
+
Object.defineProperty(hostDispatcherHolder, 'current', {
|
|
176
|
+
configurable: true,
|
|
177
|
+
get: () => userDispatcherHolder.current,
|
|
178
|
+
set: (dispatcher) => {
|
|
179
|
+
userDispatcherHolder.current = dispatcher
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
return () => {
|
|
183
|
+
Object.defineProperty(
|
|
184
|
+
hostDispatcherHolder,
|
|
185
|
+
'current',
|
|
186
|
+
previousDescriptor ?? { configurable: true, enumerable: true, value: null, writable: true },
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// React 19 keeps the dispatcher directly on the internals object as `H`, so
|
|
192
|
+
// forward that property to the user internals.
|
|
193
|
+
if ('H' in userInternals) {
|
|
194
|
+
const previousDescriptor = Object.getOwnPropertyDescriptor(hostInternals, 'H')
|
|
195
|
+
Object.defineProperty(hostInternals, 'H', {
|
|
196
|
+
configurable: true,
|
|
197
|
+
get: () => userInternals.H,
|
|
198
|
+
set: (dispatcher) => {
|
|
199
|
+
userInternals.H = dispatcher
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
return () => {
|
|
203
|
+
Object.defineProperty(
|
|
204
|
+
hostInternals,
|
|
205
|
+
'H',
|
|
206
|
+
previousDescriptor ?? { configurable: true, enumerable: true, value: undefined, writable: true },
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return () => {}
|
|
212
|
+
}
|
|
213
|
+
|
|
135
214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
215
|
// Renderer
|
|
137
216
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -248,6 +327,12 @@ export function renderWithExtractors(
|
|
|
248
327
|
const interceptedJsxs = createInterceptor(originalJsxs as ElementCreator, listeners, getNextId, store)
|
|
249
328
|
const interceptedJsxDEV = createInterceptor(originalJsxDEV as ElementCreator, listeners, getNextId, store)
|
|
250
329
|
|
|
330
|
+
// The shim reads globalThis[REACT_KEY] on every hook call, so pointing it at
|
|
331
|
+
// userReact ensures ESM hooks use the same dispatcher that SSR drives.
|
|
332
|
+
const previousActiveReactModule = getActiveReactModule()
|
|
333
|
+
// Restores the host React's hook dispatcher, bridged to the user React below.
|
|
334
|
+
let restoreHostDispatcher: () => void = () => {}
|
|
335
|
+
|
|
251
336
|
try {
|
|
252
337
|
// @ts-expect-error Monkey-patch createElement for default-import callers
|
|
253
338
|
React.createElement = interceptedCreateElement
|
|
@@ -268,6 +353,9 @@ export function renderWithExtractors(
|
|
|
268
353
|
// Patch user-bundle React modules when they are distinct instances
|
|
269
354
|
if (userReact) {
|
|
270
355
|
userReact.createElement = interceptedCreateElement
|
|
356
|
+
setActiveReactModule(userReact)
|
|
357
|
+
// Host-bound hook callers (e.g. WixServicesWrapper) need the user React's dispatcher too.
|
|
358
|
+
restoreHostDispatcher = bridgeHostDispatcherToUserReact(React, userReact)
|
|
271
359
|
}
|
|
272
360
|
if (userJsxRuntime) {
|
|
273
361
|
userJsxRuntime.jsx = interceptedJsx
|
|
@@ -281,6 +369,8 @@ export function renderWithExtractors(
|
|
|
281
369
|
const element = renderingReact.createElement(Component, componentProps as Record<string, unknown>)
|
|
282
370
|
return (userRenderToStaticMarkup ?? renderToStaticMarkup)(element)
|
|
283
371
|
} finally {
|
|
372
|
+
restoreHostDispatcher()
|
|
373
|
+
setActiveReactModule(previousActiveReactModule)
|
|
284
374
|
// Restore originals
|
|
285
375
|
// @ts-expect-error Restore monkey-patched createElement
|
|
286
376
|
React.createElement = originalCreateElement
|
|
@@ -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
|
+
})
|