@wix/zero-config-implementation 1.78.0 → 1.80.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--6I20lGB.js → index-B8oF7hWg.js} +15405 -15073
- package/dist/{index-DAAmOM1c.js → index-CGnbp9jn.js} +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- 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/css/parse.test.ts +58 -0
- package/src/information-extractors/css/parse.ts +54 -7
- package/src/information-extractors/css/sass-adapter.test.ts +137 -0
- package/src/information-extractors/css/sass-adapter.ts +59 -1
- 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 +28 -2
- 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-B8oF7hWg.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.80.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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@wix/builder-services-wrapper": "^1.56.0",
|
|
43
|
-
"@wix/react-component-schema": "1.
|
|
43
|
+
"@wix/react-component-schema": "1.8.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@faker-js/faker": "^10.2.0",
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
]
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
|
-
"falconPackageHash": "
|
|
85
|
+
"falconPackageHash": "9e634a5f6a97f802dcc3b2dbc92f56c3c9623790346ee5278ca65f03"
|
|
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 '
|
|
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, '
|
|
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, '
|
|
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',
|