@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
|
@@ -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',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import type { ComponentInfo } from '../../information-extractors/ts/types'
|
|
3
|
+
import { collectAllPackageImportSpecifiers } from '../shared/ast-scanner'
|
|
4
|
+
import { loadNormalizedCatalogEntries } from '../shared/catalog-loader'
|
|
5
|
+
import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
|
|
6
|
+
import { extractBasePackageName, extractModuleLeafExportName } from '../shared/package-specifier'
|
|
7
|
+
import { extractEligibleRefElementPaths } from './eligible-paths'
|
|
8
|
+
import type { RefElementContext, RefElementModule } from './types'
|
|
9
|
+
|
|
10
|
+
interface NormalizedRefElementCatalogEntry {
|
|
11
|
+
moduleSpecifier: string
|
|
12
|
+
refComponentType: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function buildRefElementContext(
|
|
16
|
+
program: ts.Program,
|
|
17
|
+
sourceFilePath: string,
|
|
18
|
+
componentInfo: ComponentInfo,
|
|
19
|
+
compiledEntryPath: string,
|
|
20
|
+
): Promise<RefElementContext> {
|
|
21
|
+
const importedModuleSpecifiers = collectAllPackageImportSpecifiers(
|
|
22
|
+
program,
|
|
23
|
+
sourceFilePath,
|
|
24
|
+
referencesRefElementModule,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
eligiblePaths: extractEligibleRefElementPaths(componentInfo),
|
|
29
|
+
runtimeModules: await resolveRuntimeModules(importedModuleSpecifiers, compiledEntryPath),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function resolveRuntimeModules(
|
|
34
|
+
importedModuleSpecifiers: string[],
|
|
35
|
+
compiledEntryPath: string,
|
|
36
|
+
): Promise<RefElementModule[]> {
|
|
37
|
+
if (importedModuleSpecifiers.length === 0) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const importedModuleSpecifierSet = new Set(importedModuleSpecifiers)
|
|
42
|
+
const catalogEntries = await loadNormalizedCatalogEntries(
|
|
43
|
+
importedModuleSpecifiers,
|
|
44
|
+
compiledEntryPath,
|
|
45
|
+
normalizeRefElementExtensionEntry,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const runtimeModulesBySpecifier = new Map<string, Set<string>>()
|
|
49
|
+
|
|
50
|
+
for (const catalogEntry of catalogEntries) {
|
|
51
|
+
if (!importedModuleSpecifierSet.has(catalogEntry.moduleSpecifier)) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const refComponentTypes = runtimeModulesBySpecifier.get(catalogEntry.moduleSpecifier) ?? new Set<string>()
|
|
56
|
+
refComponentTypes.add(catalogEntry.refComponentType)
|
|
57
|
+
runtimeModulesBySpecifier.set(catalogEntry.moduleSpecifier, refComponentTypes)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const runtimeModules: RefElementModule[] = []
|
|
61
|
+
|
|
62
|
+
for (const [moduleSpecifier, refComponentTypes] of runtimeModulesBySpecifier.entries()) {
|
|
63
|
+
if (refComponentTypes.size !== 1) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const refComponentType = [...refComponentTypes][0]
|
|
68
|
+
if (!refComponentType) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolvedModuleUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, moduleSpecifier)
|
|
73
|
+
if (!resolvedModuleUrl) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
runtimeModules.push({
|
|
78
|
+
exportNames: buildRefElementExportNames(moduleSpecifier),
|
|
79
|
+
moduleSpecifier,
|
|
80
|
+
resolvedModuleUrl,
|
|
81
|
+
refComponentType,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return runtimeModules
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeRefElementExtensionEntry(rawEntry: unknown): NormalizedRefElementCatalogEntry | undefined {
|
|
89
|
+
const entry = rawEntry as {
|
|
90
|
+
options?: { type?: string; resources?: { client?: { moduleSpecifier?: string } } }
|
|
91
|
+
type?: string
|
|
92
|
+
resources?: { client?: { moduleSpecifier?: string } }
|
|
93
|
+
}
|
|
94
|
+
const normalizedEntry = entry.options ?? entry
|
|
95
|
+
const moduleSpecifier = normalizedEntry.resources?.client?.moduleSpecifier
|
|
96
|
+
const refComponentType = normalizedEntry.type
|
|
97
|
+
|
|
98
|
+
if (!moduleSpecifier || !refComponentType) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
moduleSpecifier,
|
|
104
|
+
refComponentType,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function referencesRefElementModule(statement: ts.Statement, moduleSpecifier: string): boolean {
|
|
109
|
+
const moduleLeafExportName = extractModuleLeafExportName(moduleSpecifier)
|
|
110
|
+
if (!moduleLeafExportName) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (ts.isImportDeclaration(statement)) {
|
|
115
|
+
return importsRefElementModule(statement, moduleLeafExportName)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ts.isExportDeclaration(statement)) {
|
|
119
|
+
return reExportsRefElementModule(statement, moduleLeafExportName)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function importsRefElementModule(importDeclaration: ts.ImportDeclaration, moduleLeafExportName: string): boolean {
|
|
126
|
+
const importClause = importDeclaration.importClause
|
|
127
|
+
if (!importClause) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (importClause.name) {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!importClause.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return importClause.namedBindings.elements.some((importSpecifier) => {
|
|
140
|
+
const importedName = importSpecifier.propertyName?.text ?? importSpecifier.name.text
|
|
141
|
+
return importedName === moduleLeafExportName
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function reExportsRefElementModule(exportDeclaration: ts.ExportDeclaration, moduleLeafExportName: string): boolean {
|
|
146
|
+
if (!exportDeclaration.exportClause || !ts.isNamedExports(exportDeclaration.exportClause)) {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return exportDeclaration.exportClause.elements.some((exportSpecifier) => {
|
|
151
|
+
const exportedSourceName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text
|
|
152
|
+
return exportedSourceName === 'default' || exportedSourceName === moduleLeafExportName
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildRefElementExportNames(moduleSpecifier: string): string[] {
|
|
157
|
+
const exportNames = new Set<string>(['default'])
|
|
158
|
+
if (extractBasePackageName(moduleSpecifier) === moduleSpecifier) {
|
|
159
|
+
return [...exportNames]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const moduleLeafName = extractModuleLeafExportName(moduleSpecifier)
|
|
163
|
+
if (moduleLeafName) {
|
|
164
|
+
exportNames.add(moduleLeafName)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [...exportNames]
|
|
168
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentInfo, ResolvedType } from '
|
|
1
|
+
import type { ComponentInfo, ResolvedType } from '../../information-extractors/ts/types'
|
|
2
2
|
|
|
3
3
|
export function extractEligibleRefElementPaths(componentInfo: ComponentInfo): string[] {
|
|
4
4
|
const elementPropsInfo = componentInfo.props.elementProps
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
export function isPackageModuleSpecifier(moduleSpecifier: string): boolean {
|
|
4
|
+
return !moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/') && !moduleSpecifier.startsWith('file:')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function findSourceFile(program: ts.Program, sourceFilePath: string): ts.SourceFile | undefined {
|
|
8
|
+
return (
|
|
9
|
+
program.getSourceFile(sourceFilePath) ??
|
|
10
|
+
program.getSourceFiles().find((sourceFile) => sourceFile.fileName === sourceFilePath)
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveImportedSourceFile(
|
|
15
|
+
program: ts.Program,
|
|
16
|
+
importingFilePath: string,
|
|
17
|
+
moduleSpecifier: string,
|
|
18
|
+
): ts.SourceFile | undefined {
|
|
19
|
+
const resolvedModule = ts.resolveModuleName(moduleSpecifier, importingFilePath, program.getCompilerOptions(), ts.sys)
|
|
20
|
+
const resolvedFileName = resolvedModule.resolvedModule?.resolvedFileName
|
|
21
|
+
|
|
22
|
+
if (!resolvedFileName) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resolvedSourceFile = findSourceFile(program, resolvedFileName)
|
|
27
|
+
if (!resolvedSourceFile || resolvedSourceFile.isDeclarationFile) {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return resolvedSourceFile
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readReferencedModuleSpecifier(statement: ts.Statement): string | undefined {
|
|
35
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
36
|
+
return statement.moduleSpecifier.text
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
40
|
+
return statement.moduleSpecifier.text
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function collectAllPackageImportSpecifiers(
|
|
47
|
+
program: ts.Program,
|
|
48
|
+
sourceFilePath: string,
|
|
49
|
+
filter?: (statement: ts.Statement, moduleSpecifier: string) => boolean,
|
|
50
|
+
): string[] {
|
|
51
|
+
const entrySourceFile = findSourceFile(program, sourceFilePath)
|
|
52
|
+
if (!entrySourceFile) {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const packageSpecifiers = new Set<string>()
|
|
57
|
+
const visitedSourceFiles = new Set<string>()
|
|
58
|
+
|
|
59
|
+
function visitSourceFile(sourceFile: ts.SourceFile): void {
|
|
60
|
+
if (visitedSourceFiles.has(sourceFile.fileName)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
visitedSourceFiles.add(sourceFile.fileName)
|
|
65
|
+
|
|
66
|
+
for (const statement of sourceFile.statements) {
|
|
67
|
+
const moduleSpecifier = readReferencedModuleSpecifier(statement)
|
|
68
|
+
if (!moduleSpecifier) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isPackageModuleSpecifier(moduleSpecifier) && (!filter || filter(statement, moduleSpecifier))) {
|
|
73
|
+
packageSpecifiers.add(moduleSpecifier)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const importedSourceFile = resolveImportedSourceFile(program, sourceFile.fileName, moduleSpecifier)
|
|
77
|
+
if (importedSourceFile) {
|
|
78
|
+
visitSourceFile(importedSourceFile)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
visitSourceFile(entrySourceFile)
|
|
84
|
+
return [...packageSpecifiers]
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isPackageModuleSpecifier } from './ast-scanner'
|
|
2
|
+
import { resolveModuleUrlFromEntryPath } from './module-resolution'
|
|
3
|
+
import { extractBasePackageName } from './package-specifier'
|
|
4
|
+
|
|
5
|
+
interface RawExtensionModule {
|
|
6
|
+
extensions?: unknown[]
|
|
7
|
+
default?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractRawExtensionEntries(extensionModule: Record<string, unknown>): unknown[] {
|
|
11
|
+
const defaultExport = extensionModule.default
|
|
12
|
+
const extensionEntries = Array.isArray(defaultExport)
|
|
13
|
+
? defaultExport
|
|
14
|
+
: ((defaultExport as { extensions?: unknown[] } | undefined)?.extensions ?? extensionModule.extensions)
|
|
15
|
+
|
|
16
|
+
return Array.isArray(extensionEntries) ? extensionEntries : []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function loadRawExtensionEntries(packageName: string, compiledEntryPath: string): Promise<unknown[]> {
|
|
20
|
+
const resolvedExtensionsUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, `${packageName}/extensions`)
|
|
21
|
+
if (!resolvedExtensionsUrl) {
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const extensionModule = (await import(resolvedExtensionsUrl)) as RawExtensionModule & Record<string, unknown>
|
|
27
|
+
return extractRawExtensionEntries(extensionModule)
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The package declared an /extensions export but the file doesn't exist — skip it.
|
|
30
|
+
if (isModuleNotFoundError(error)) {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isModuleNotFoundError(error: unknown): boolean {
|
|
38
|
+
return (
|
|
39
|
+
error instanceof Error &&
|
|
40
|
+
'code' in error &&
|
|
41
|
+
(error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND')
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* For each package name derived from `importedSpecifiers`, loads its `/extensions` catalog and
|
|
47
|
+
* normalizes every raw entry through `normalizeEntry`. Returns the flat list of non-undefined results.
|
|
48
|
+
*/
|
|
49
|
+
export async function loadNormalizedCatalogEntries<T>(
|
|
50
|
+
importedSpecifiers: string[],
|
|
51
|
+
compiledEntryPath: string,
|
|
52
|
+
normalizeEntry: (rawEntry: unknown) => T | undefined,
|
|
53
|
+
): Promise<T[]> {
|
|
54
|
+
const packageNames = [...new Set(importedSpecifiers.filter(isPackageModuleSpecifier).map(extractBasePackageName))]
|
|
55
|
+
|
|
56
|
+
const entriesPerPackage = await Promise.all(
|
|
57
|
+
packageNames.map(async (packageName) => {
|
|
58
|
+
const rawEntries = await loadRawExtensionEntries(packageName, compiledEntryPath)
|
|
59
|
+
return rawEntries.map(normalizeEntry).filter((entry): entry is T => entry !== undefined)
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return entriesPerPackage.flat()
|
|
64
|
+
}
|
|
@@ -19,7 +19,7 @@ describe('resolveModulePathFromEntryPath', () => {
|
|
|
19
19
|
|
|
20
20
|
const packageRootPath = path.join(temporaryDirectoryPath, 'consumer-package')
|
|
21
21
|
const nodeModulesPath = path.join(packageRootPath, 'node_modules', '@wix')
|
|
22
|
-
const fixturePackagePath = path.resolve(__dirname, '
|
|
22
|
+
const fixturePackagePath = path.resolve(__dirname, '../../../../ref-element-fixtures')
|
|
23
23
|
|
|
24
24
|
fs.mkdirSync(nodeModulesPath, { recursive: true })
|
|
25
25
|
fs.symlinkSync(fixturePackagePath, path.join(nodeModulesPath, 'zero-config-ref-element-fixtures'), 'dir')
|
|
@@ -40,7 +40,7 @@ describe('resolveModulePathFromEntryPath', () => {
|
|
|
40
40
|
describe('resolveModuleUrlFromEntryPath', () => {
|
|
41
41
|
it('returns a file url for the resolved module path', () => {
|
|
42
42
|
const resolvedExtensionsUrl = resolveModuleUrlFromEntryPath(
|
|
43
|
-
path.resolve(__dirname, '
|
|
43
|
+
path.resolve(__dirname, '../../__fixtures__/esm-ref-element-entry.mjs'),
|
|
44
44
|
'@wix/zero-config-ref-element-fixtures/extensions',
|
|
45
45
|
)
|
|
46
46
|
|