@wix/zero-config-implementation 1.77.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +4 -0
  2. package/dist/{index-DRWoSsTZ.js → index-CoU1WRYf.js} +28022 -28645
  3. package/dist/{index-CuKmaUAA.js → index-DCJue8M7.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +3 -3
  6. package/src/component-renderer.ts +90 -0
  7. package/src/converters/to-editor-component.ts +2 -2
  8. package/src/extensions/context-providers/context.test.ts +181 -0
  9. package/src/extensions/context-providers/context.ts +83 -0
  10. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  11. package/src/extensions/context-providers/mock-provider.ts +210 -0
  12. package/src/extensions/context-providers/types.ts +22 -0
  13. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  14. package/src/extensions/ref-elements/context.ts +168 -0
  15. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  16. package/src/extensions/shared/ast-scanner.ts +85 -0
  17. package/src/extensions/shared/catalog-loader.ts +64 -0
  18. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  19. package/src/index.ts +108 -67
  20. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  21. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  22. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  23. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  24. package/src/manifest-pipeline.ts +4 -1
  25. package/src/module-loader.test.ts +1 -1
  26. package/src/module-loader.ts +1 -1
  27. package/src/react-runtime-loader.ts +49 -8
  28. package/dist/index-BGHyk7CU.js +0 -708
  29. package/src/ref-elements/context.ts +0 -280
  30. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  31. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  32. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  33. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  34. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  35. /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
@@ -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',
@@ -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 '../information-extractors/ts/types'
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, '../../../ref-element-fixtures')
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, '../__fixtures__/esm-ref-element-entry.mjs'),
43
+ path.resolve(__dirname, '../../__fixtures__/esm-ref-element-entry.mjs'),
44
44
  '@wix/zero-config-ref-element-fixtures/extensions',
45
45
  )
46
46