@wix/zero-config-implementation 1.78.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +4 -0
  2. package/dist/{index--6I20lGB.js → index-CoU1WRYf.js} +25036 -24768
  3. package/dist/{index-DAAmOM1c.js → index-DCJue8M7.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +2 -2
  6. package/src/converters/to-editor-component.ts +2 -2
  7. package/src/extensions/context-providers/context.test.ts +181 -0
  8. package/src/extensions/context-providers/context.ts +83 -0
  9. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  10. package/src/extensions/context-providers/mock-provider.ts +210 -0
  11. package/src/extensions/context-providers/types.ts +22 -0
  12. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  13. package/src/extensions/ref-elements/context.ts +168 -0
  14. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  15. package/src/extensions/shared/ast-scanner.ts +85 -0
  16. package/src/extensions/shared/catalog-loader.ts +64 -0
  17. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  18. package/src/index.ts +108 -67
  19. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  20. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  21. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  22. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  23. package/src/manifest-pipeline.ts +4 -1
  24. package/src/module-loader.test.ts +1 -1
  25. package/src/module-loader.ts +1 -1
  26. package/src/react-runtime-loader.ts +28 -2
  27. package/src/ref-elements/context.ts +0 -280
  28. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  29. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  30. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  31. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  32. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  33. /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
@@ -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
 
package/src/index.ts CHANGED
@@ -5,6 +5,10 @@ import React, { type ComponentType } from 'react'
5
5
 
6
6
  import { toEditorReactComponent } from './converters'
7
7
  import { BaseError, IoError, type NotFoundError, ParseError } from './errors'
8
+ import { buildContextProviderModules } from './extensions/context-providers/context'
9
+ import { buildContextAwareWrapper, loadMockProviders } from './extensions/context-providers/mock-provider'
10
+ import { buildRefElementContext } from './extensions/ref-elements/context'
11
+ import type { RefElementContext } from './extensions/ref-elements/types'
8
12
  import type { ExtractionError } from './extraction-types'
9
13
  import type { RunExtractorsOptions } from './information-extractors/react'
10
14
  import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
@@ -12,8 +16,6 @@ import type { ComponentInfo } from './information-extractors/ts/types'
12
16
  import { processComponent } from './manifest-pipeline'
13
17
  import { findComponent, findDefaultComponent, loadModuleForExtraction } from './module-loader'
14
18
  import type { LoadModuleFailure } from './module-loader'
15
- import { buildRefElementContext } from './ref-elements/context'
16
- import type { RefElementContext } from './ref-elements/types'
17
19
  import { compileTsFile } from './ts-compiler'
18
20
 
19
21
  const defaultWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => {
@@ -64,10 +66,6 @@ export function extractComponentManifestResult(
64
66
  options?.onError?.(error)
65
67
  }
66
68
 
67
- const optionsWithDefaults: ExtractComponentManifestOptions = {
68
- ...options,
69
- wrapper: options?.wrapper ?? defaultWrapper,
70
- }
71
69
  // Step 1: Compile TypeScript (fatal)
72
70
  return compileTsFile(componentPath)
73
71
  .andThen((program) => {
@@ -91,79 +89,122 @@ export function extractComponentManifestResult(
91
89
  .andThen(({ program, componentInfo }) => {
92
90
  const { componentName } = componentInfo
93
91
 
92
+ // Step 3a: Build ref-element context
94
93
  return ResultAsync.fromPromise(
95
94
  buildRefElementContext(program, componentPath, componentInfo, compiledEntryPath),
96
95
  (thrown) =>
97
96
  new ParseError(
98
- `Failed to build the ref-element context for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
97
+ `Failed to build ref-element context for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
99
98
  { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
100
99
  ),
101
100
  ).andThen((refElementContext) =>
102
- // Step 3: Load the compiled package module (non-fatal) — done after TS extraction
103
- // so componentName is known when reporting failures
104
- loadModuleForExtraction(compiledEntryPath, refElementContext.runtimeModules)
105
- .map(({ cleanup, moduleExports }) => ({
106
- loadComponent: (name: string) => findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports),
107
- failure: null as LoadModuleFailure | null,
108
- cleanup,
109
- }))
110
- .orElse((failure) =>
111
- okAsync({
112
- loadComponent: () => null as ComponentType<unknown> | null,
113
- cleanup: async () => {},
114
- failure,
115
- }),
116
- )
117
- .andThen(({ cleanup, failure, loadComponent }) => {
118
- if (failure) {
119
- if (failure.esmError) {
120
- report({
121
- componentName,
122
- phase: 'loader',
123
- error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
124
- })
125
- }
126
- report({
127
- componentName,
128
- phase: 'loader',
129
- error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
130
- })
101
+ // Step 3b: Build context provider modules
102
+ ResultAsync.fromPromise(
103
+ buildContextProviderModules(program, componentPath, compiledEntryPath),
104
+ (thrown) =>
105
+ new ParseError(
106
+ `Failed to build context provider modules for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
107
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
108
+ ),
109
+ ).andThen((contextProviderModules) => {
110
+ // Step 4: Build context provider wrapper with mock providers
111
+ const buildWrapper = async (): Promise<(Component: ComponentType<unknown>) => ComponentType<unknown>> => {
112
+ const baseWrapper = options?.wrapper ?? defaultWrapper
113
+ if (contextProviderModules.length === 0) {
114
+ return baseWrapper
115
+ }
116
+ const loadedProviders = await loadMockProviders(
117
+ contextProviderModules,
118
+ React,
119
+ refElementContext.runtimeModules,
120
+ )
121
+ if (loadedProviders.length === 0) {
122
+ return baseWrapper
131
123
  }
124
+ return buildContextAwareWrapper(loadedProviders, baseWrapper, React)
125
+ }
132
126
 
133
- // Step 4: Extract CSS imports (non-fatal)
134
- let cssImportPaths: string[] = []
135
- const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
136
- if (cssResult.isOk()) {
137
- cssImportPaths = cssResult.value
138
- } else {
139
- const thrown = cssResult.error
140
- report({
141
- componentName,
142
- phase: 'css',
143
- error: new ParseError(
144
- `Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
145
- { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
146
- ),
147
- })
127
+ return ResultAsync.fromPromise(
128
+ buildWrapper(),
129
+ (thrown) =>
130
+ new ParseError(
131
+ `Failed to build context provider wrapper for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
132
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
133
+ ),
134
+ ).andThen((wrapper) => {
135
+ const optionsWithDefaults: ExtractComponentManifestOptions = {
136
+ ...options,
137
+ wrapper,
148
138
  }
149
139
 
150
- // Step 5: Process the default-exported component (non-fatal unless render fails after load)
151
- return ResultAsync.fromPromise(
152
- processComponentWithCleanup(
153
- componentInfo,
154
- loadComponent,
155
- cssImportPaths,
156
- !!failure,
157
- report,
158
- optionsWithDefaults,
159
- compiledEntryPath,
160
- refElementContext,
140
+ // Step 5: Load the compiled package module (non-fatal) - done after TS extraction
141
+ // so componentName is known when reporting failures
142
+ return loadModuleForExtraction(compiledEntryPath, refElementContext.runtimeModules)
143
+ .map(({ cleanup, moduleExports }) => ({
144
+ loadComponent: (name: string) =>
145
+ findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports),
146
+ failure: null as LoadModuleFailure | null,
161
147
  cleanup,
162
- errors,
163
- ),
164
- normalizeComponentExtractionCleanupFailure,
165
- )
166
- }),
148
+ }))
149
+ .orElse((failure) =>
150
+ okAsync({
151
+ loadComponent: () => null as ComponentType<unknown> | null,
152
+ cleanup: async () => {},
153
+ failure,
154
+ }),
155
+ )
156
+ .andThen(({ cleanup, failure, loadComponent }) => {
157
+ if (failure) {
158
+ if (failure.esmError) {
159
+ report({
160
+ componentName,
161
+ phase: 'loader',
162
+ error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
163
+ })
164
+ }
165
+ report({
166
+ componentName,
167
+ phase: 'loader',
168
+ error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
169
+ })
170
+ }
171
+
172
+ // Step 6: Extract CSS imports (non-fatal)
173
+ let cssImportPaths: string[] = []
174
+ const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
175
+ if (cssResult.isOk()) {
176
+ cssImportPaths = cssResult.value
177
+ } else {
178
+ const thrown = cssResult.error
179
+ report({
180
+ componentName,
181
+ phase: 'css',
182
+ error: new ParseError(
183
+ `Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
184
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
185
+ ),
186
+ })
187
+ }
188
+
189
+ // Step 7: Process the default-exported component
190
+ return ResultAsync.fromPromise(
191
+ processComponentWithCleanup(
192
+ componentInfo,
193
+ loadComponent,
194
+ cssImportPaths,
195
+ !!failure,
196
+ report,
197
+ optionsWithDefaults,
198
+ compiledEntryPath,
199
+ refElementContext,
200
+ cleanup,
201
+ errors,
202
+ ),
203
+ normalizeComponentExtractionCleanupFailure,
204
+ )
205
+ })
206
+ })
207
+ }),
167
208
  )
168
209
  })
169
210
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { createRefElementMarker } from '../../../ref-elements/path-utils'
3
- import type { RefElementMatch } from '../../../ref-elements/types'
2
+ import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
3
+ import type { RefElementMatch } from '../../../extensions/ref-elements/types'
4
4
  import { ExtractorStore } from './core/store'
5
5
  import { createPropTrackerExtractor } from './prop-tracker'
6
6
 
@@ -7,9 +7,9 @@
7
7
 
8
8
  import type { HTMLAttributes } from 'react'
9
9
  import { TRACE_ATTR } from '../../../component-renderer'
10
- import { readRefElementComponentType } from '../../../ref-elements/component-tag'
11
- import { parseRefElementMarker } from '../../../ref-elements/path-utils'
12
- import type { RefElementMatch } from '../../../ref-elements/types'
10
+ import { readRefElementComponentType } from '../../../extensions/ref-elements/component-tag'
11
+ import { parseRefElementMarker } from '../../../extensions/ref-elements/path-utils'
12
+ import type { RefElementMatch } from '../../../extensions/ref-elements/types'
13
13
  import { findPreferredSemanticClass, normalizeClassNames } from '../../../utils/css-class'
14
14
  import type { PropSpyMeta, TrackingStores } from '../types'
15
15
  import { type PropSpyRegistrar, generateMockProps, resetMockCounter } from '../utils/mock-generator'