@wix/zero-config-implementation 1.26.0 → 1.28.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.
@@ -2,13 +2,7 @@ import { ComponentInfo } from './information-extractors/ts';
2
2
  import { RunExtractorsOptions, CoupledComponentInfo } from './information-extractors/react';
3
3
  import { CSSParserAPI } from './information-extractors/css';
4
4
  import { ComponentType } from 'react';
5
- /** A non-fatal issue encountered during component processing. */
6
- export interface ExtractionWarning {
7
- componentName: string;
8
- phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion';
9
- error: string;
10
- stack?: string;
11
- }
5
+ import { ExtractionError } from './extraction-types';
12
6
  export interface ExtractedCssInfo {
13
7
  filePath: string;
14
8
  api: CSSParserAPI;
@@ -23,7 +17,6 @@ export interface ComponentInfoWithCss extends CoupledComponentInfo {
23
17
  /** The result of processing a single component through the manifest pipeline. */
24
18
  export interface ProcessComponentResult {
25
19
  component: ComponentInfoWithCss;
26
- warnings: ExtractionWarning[];
27
20
  }
28
21
  /**
29
22
  * Processes a single component through the full manifest pipeline:
@@ -33,4 +26,4 @@ export interface ProcessComponentResult {
33
26
  * encountered during processing. Always returns a component, falling back
34
27
  * to minimal info (without DOM coupling) when rendering fails.
35
28
  */
36
- export declare function processComponent(componentInfo: ComponentInfo, loadComponent: (componentName: string) => ComponentType<unknown> | null, cssImportPaths: string[], loaderHasError?: boolean, options?: RunExtractorsOptions): ProcessComponentResult;
29
+ export declare function processComponent(componentInfo: ComponentInfo, loadComponent: (componentName: string) => ComponentType<unknown> | null, cssImportPaths: string[], loaderHasError: boolean, report: (error: ExtractionError) => void, options?: RunExtractorsOptions): ProcessComponentResult;
@@ -1,7 +1,15 @@
1
1
  import { ResultAsync } from 'neverthrow';
2
2
  import { ComponentType } from 'react';
3
- import { IoError } from './errors';
4
- type IoErrorInstance = InstanceType<typeof IoError>;
3
+ /**
4
+ * Structured failure from `loadModule` when both ESM import and CJS require fail.
5
+ * Carries each error separately so callers can report them independently.
6
+ */
7
+ export interface LoadModuleFailure {
8
+ /** The ESM import error, or null when no ESM was attempted (e.g. empty path). */
9
+ esmError: Error | null;
10
+ /** The CJS require error, or a generic error for the empty-path case. */
11
+ cjsError: Error;
12
+ }
5
13
  /**
6
14
  * Attempts to load a module, first via ESM `import()`, then via CJS `require`.
7
15
  *
@@ -13,8 +21,7 @@ type IoErrorInstance = InstanceType<typeof IoError>;
13
21
  *
14
22
  * @param entryPath - Absolute path to the module entry point.
15
23
  * @returns A `ResultAsync` containing the module exports on success.
16
- * @errors {IoError} When both ESM import and CJS require fail.
24
+ * @errors {LoadModuleFailure} When both ESM import and CJS require fail.
17
25
  */
18
- export declare function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, IoErrorInstance>;
26
+ export declare function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, LoadModuleFailure>;
19
27
  export declare function findComponent(moduleExports: Record<string, unknown>, name: string): ComponentType<unknown> | null;
20
- export {};
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.26.0",
7
+ "version": "1.28.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",
@@ -83,5 +83,5 @@
83
83
  ]
84
84
  }
85
85
  },
86
- "falconPackageHash": "3dd0aab1018f95841806376f73376b94c1dcf15d586caa70806a01f8"
86
+ "falconPackageHash": "b9b7aa47b7618d7b031628208f9fb26661e34ddb5fe300679306084e"
87
87
  }
@@ -0,0 +1,8 @@
1
+ import type { BaseError } from './errors'
2
+
3
+ /** A non-fatal issue encountered during component extraction. */
4
+ export interface ExtractionError {
5
+ componentName: string
6
+ phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
7
+ error: InstanceType<typeof BaseError>
8
+ }
package/src/index.ts CHANGED
@@ -1,32 +1,16 @@
1
1
  import type { EditorReactComponent } from '@wix/zero-config-schema'
2
- import { Result, ResultAsync } from 'neverthrow'
2
+ import { Result, type ResultAsync, okAsync } from 'neverthrow'
3
3
  import type { ComponentType } from 'react'
4
4
 
5
- import { extractAllComponentInfo, extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
6
- import type { ComponentInfo } from './information-extractors/ts'
7
- // TypeScript extraction
8
- import { compileTsFile } from './ts-compiler'
9
-
10
- // Error classes
11
- import { type NotFoundError, ParseError } from './errors'
12
-
13
- // Component loader helpers
14
- import { findComponent, loadModule } from './module-loader'
15
-
5
+ import { toEditorReactComponent } from './converters'
6
+ import { IoError, type NotFoundError, ParseError } from './errors'
7
+ import type { ExtractionError } from './extraction-types'
16
8
  import type { RunExtractorsOptions } from './information-extractors/react'
17
- // Pipeline orchestration
9
+ import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
18
10
  import { processComponent } from './manifest-pipeline'
19
- export type {
20
- ComponentInfoWithCss,
21
- ExtractedCssInfo,
22
- ExtractionWarning,
23
- ProcessComponentResult,
24
- } from './manifest-pipeline'
25
-
26
- export type { EditorReactComponent } from '@wix/zero-config-schema'
27
-
28
- // Converter
29
- import { toEditorReactComponent } from './converters'
11
+ import { findComponent, loadModule } from './module-loader'
12
+ import type { LoadModuleFailure } from './module-loader'
13
+ import { compileTsFile } from './ts-compiler'
30
14
 
31
15
  // ─────────────────────────────────────────────────────────────────────────────
32
16
  // Types
@@ -37,15 +21,6 @@ export interface ManifestResult {
37
21
  errors: ExtractionError[]
38
22
  }
39
23
 
40
- export type { RunExtractorsOptions } from './information-extractors/react'
41
-
42
- export interface ExtractionError {
43
- componentName: string
44
- phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
45
- error: string
46
- stack?: string
47
- }
48
-
49
24
  // ─────────────────────────────────────────────────────────────────────────────
50
25
  // Main API
51
26
  // ─────────────────────────────────────────────────────────────────────────────
@@ -55,96 +30,100 @@ export interface ExtractionError {
55
30
  *
56
31
  * @param componentPath - Path to the TypeScript source file
57
32
  * @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
58
- * @returns The manifest result containing components and non-fatal warnings
33
+ * @param options.onError - Called for each non-fatal extraction error as it occurs
59
34
  * @errors
60
35
  * - {@link NotFoundError} — Source file does not exist (phase: `compile`)
61
36
  * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
62
37
  */
38
+ export interface ExtractComponentManifestOptions extends RunExtractorsOptions {
39
+ onError?: (error: ExtractionError) => void
40
+ }
41
+
63
42
  export function extractComponentManifest(
64
43
  componentPath: string,
65
44
  compiledEntryPath: string,
66
- options?: RunExtractorsOptions,
45
+ options?: ExtractComponentManifestOptions,
67
46
  ): ResultAsync<ManifestResult, InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError>> {
68
- // Step 1: Load the compiled package module (non-fatal)
69
- return loadModule(compiledEntryPath)
70
- .map((moduleExports) => ({
71
- moduleExports,
72
- loaderError: null as string | null,
73
- loaderStack: undefined as string | undefined,
74
- }))
75
- .orElse((err) =>
76
- ResultAsync.fromSafePromise(
77
- Promise.resolve({
78
- moduleExports: null as Record<string, unknown> | null,
79
- loaderError: err.message,
80
- loaderStack: err.stack,
81
- }),
82
- ),
83
- )
84
- .andThen(({ moduleExports, loaderError, loaderStack }) => {
85
- const loadComponent: (componentName: string) => ComponentType<unknown> | null = moduleExports
86
- ? (name) => findComponent(moduleExports, name)
87
- : () => null
88
-
89
- // Step 2: Compile TypeScript (fatal)
90
- return compileTsFile(componentPath)
91
- .andThen((program) => {
92
- // Step 3: Extract default-exported component types (fatal)
93
- const safeExtract = Result.fromThrowable(
94
- (prog: typeof program) => {
95
- const componentInfo = extractDefaultComponentInfo(prog, componentPath)
96
- if (!componentInfo) {
97
- throw new Error(`No default export found in "${componentPath}"`)
98
- }
99
- return componentInfo
100
- },
101
- (thrown) =>
102
- new ParseError(
103
- `Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
104
- {
105
- cause: thrown as Error,
106
- props: { phase: 'extract' },
107
- },
108
- ),
109
- )
110
- const componentInfoResult = safeExtract(program)
111
-
112
- return componentInfoResult.map((componentInfo) => ({ program, componentInfo }))
113
- })
114
- .map(({ program, componentInfo }) => {
115
- const errors: ExtractionError[] = []
116
-
117
- // Surface loader error as a non-fatal error
118
- if (loaderError) {
119
- errors.push({
120
- componentName: componentInfo.componentName,
47
+ const errors: ExtractionError[] = []
48
+ const report = (error: ExtractionError): void => {
49
+ errors.push(error)
50
+ options?.onError?.(error)
51
+ }
52
+
53
+ // Step 1: Compile TypeScript (fatal)
54
+ return compileTsFile(componentPath)
55
+ .andThen((program) => {
56
+ // Step 2: Extract default-exported component types (fatal)
57
+ const safeExtract = Result.fromThrowable(
58
+ (prog: typeof program) => {
59
+ const componentInfo = extractDefaultComponentInfo(prog, componentPath)
60
+ if (!componentInfo) {
61
+ throw new Error(`No default export found in "${componentPath}"`)
62
+ }
63
+ return componentInfo
64
+ },
65
+ (thrown) =>
66
+ new ParseError(
67
+ `Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
68
+ { cause: thrown as Error, props: { phase: 'extract' } },
69
+ ),
70
+ )
71
+ return safeExtract(program).map((componentInfo) => ({ program, componentInfo }))
72
+ })
73
+ .andThen(({ program, componentInfo }) => {
74
+ const { componentName } = componentInfo
75
+
76
+ // Step 3: Load the compiled package module (non-fatal) done after TS extraction
77
+ // so componentName is known when reporting failures
78
+ return loadModule(compiledEntryPath)
79
+ .map((moduleExports) => ({
80
+ loadComponent: (name: string) => findComponent(moduleExports, name),
81
+ failure: null as LoadModuleFailure | null,
82
+ }))
83
+ .orElse((failure) => okAsync({ loadComponent: () => null as ComponentType<unknown> | null, failure }))
84
+ .map(({ loadComponent, failure }) => {
85
+ if (failure) {
86
+ if (failure.esmError) {
87
+ report({
88
+ componentName,
89
+ phase: 'loader',
90
+ error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
91
+ })
92
+ }
93
+ report({
94
+ componentName,
121
95
  phase: 'loader',
122
- error: loaderError,
123
- stack: loaderStack,
96
+ error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
124
97
  })
125
98
  }
126
99
 
127
100
  // Step 4: Extract CSS imports (non-fatal)
128
101
  let cssImportPaths: string[] = []
129
- const safeCssImports = Result.fromThrowable(extractCssImports, (thrown) => thrown)
130
- const cssResult = safeCssImports(program)
102
+ const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
131
103
  if (cssResult.isOk()) {
132
104
  cssImportPaths = cssResult.value
133
105
  } else {
134
106
  const thrown = cssResult.error
135
- errors.push({
136
- componentName: componentPath,
107
+ report({
108
+ componentName,
137
109
  phase: 'css',
138
- error: `Failed to extract CSS imports: ${thrown instanceof Error ? (thrown as Error).message : String(thrown)}`,
139
- stack: thrown instanceof Error ? (thrown as Error).stack : undefined,
110
+ error: new ParseError(
111
+ `Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
112
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
113
+ ),
140
114
  })
141
115
  }
142
116
 
143
117
  // Step 5: Process the default-exported component (non-fatal)
144
- const processResult = processComponent(componentInfo, loadComponent, cssImportPaths, !!loaderError, options)
145
- errors.push(...processResult.warnings)
118
+ const processResult = processComponent(
119
+ componentInfo,
120
+ loadComponent,
121
+ cssImportPaths,
122
+ !!failure,
123
+ report,
124
+ options,
125
+ )
146
126
  const component = toEditorReactComponent(processResult.component)
147
-
148
127
  return { component, errors }
149
128
  })
150
129
  })
@@ -171,6 +150,9 @@ export function extractComponentManifest(
171
150
 
172
151
  // ── Tier 1: High-Level API ──────────────────────────────────────────────────
173
152
  // extractComponentManifest() is exported above as a named function declaration.
153
+ export type { ExtractionError } from './extraction-types'
154
+ export type { EditorReactComponent } from '@wix/zero-config-schema'
155
+ export type { ComponentInfoWithCss, ExtractedCssInfo, ProcessComponentResult } from './manifest-pipeline'
174
156
 
175
157
  // ── Tier 2: Pipeline Building Blocks ────────────────────────────────────────
176
158
 
@@ -187,6 +169,7 @@ export type {
187
169
  } from './information-extractors/ts/types'
188
170
 
189
171
  /** React render-time extraction */
172
+ export type { RunExtractorsOptions } from './information-extractors/react'
190
173
  export {
191
174
  runExtractors,
192
175
  ExtractorStore,
@@ -204,8 +187,6 @@ export type {
204
187
  PropTrackerData,
205
188
  PropTrackerExtractorState,
206
189
  CssPropertiesData,
207
- } from './information-extractors/react'
208
- export type {
209
190
  CoupledComponentInfo,
210
191
  CoupledProp,
211
192
  DOMBinding,
@@ -231,6 +212,7 @@ export {
231
212
 
232
213
  /** Module loader primitives */
233
214
  export { loadModule, findComponent } from './module-loader'
215
+ export type { LoadModuleFailure } from './module-loader'
234
216
 
235
217
  // ── Tier 3: Low-Level Renderer ──────────────────────────────────────────────
236
218
 
@@ -12,6 +12,7 @@ import { findPreferredSemanticClass } from '../../../../utils/css-class'
12
12
  import { PRESETS_WRAPPER_CLASS_NAME } from '../../utils/mock-generator'
13
13
  import type { CssPropertiesData } from '../css-properties'
14
14
  import { addTextProperties } from '../css-properties'
15
+ import type { PropTrackerData } from '../prop-tracker'
15
16
  import type { ExtractorStore } from './store'
16
17
 
17
18
  // ─────────────────────────────────────────────────────────────────────────────
@@ -150,7 +151,7 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
150
151
  const id = getAttribute(element, 'id')
151
152
  // Skip spy-instrumented ids (mock_propName_XXXXXX) — they are runtime mock values,
152
153
  // not semantic identifiers, and would produce garbage element names.
153
- if (id && !id.startsWith('mock_')) {
154
+ if (id && !id.includes('mock_')) {
154
155
  return pascalCase(id)
155
156
  }
156
157
 
@@ -179,6 +180,28 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
179
180
  return normalizeTagName(element.tagName)
180
181
  }
181
182
 
183
+ // ─────────────────────────────────────────────────────────────────────────────
184
+ // Element Filtering
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Returns true if the element should be included in the tree:
189
+ * - has a BEM-semantic CSS class, OR
190
+ * - has a className prop propagated from the component's props
191
+ */
192
+ function hasClassNameCriteria(element: Element, extractorData: Map<string, unknown>): boolean {
193
+ const classAttr = getAttribute(element, 'class')
194
+ if (classAttr) {
195
+ const semanticClass = findPreferredSemanticClass(classAttr.split(' '))
196
+ if (semanticClass) return true
197
+ }
198
+
199
+ const propTrackerData = extractorData.get('prop-tracker') as PropTrackerData | undefined
200
+ if (propTrackerData?.boundProps.includes('className')) return true
201
+
202
+ return false
203
+ }
204
+
182
205
  // ─────────────────────────────────────────────────────────────────────────────
183
206
  // Tree Building
184
207
  // ─────────────────────────────────────────────────────────────────────────────
@@ -234,15 +257,20 @@ export function buildElementTree(html: string, store: ExtractorStore): Extracted
234
257
 
235
258
  // If this element has a traceId, it's a traced element
236
259
  if (traceId) {
260
+ // Get extractor data from store
261
+ const extractorData = store.getAll(traceId) ?? new Map()
262
+
263
+ // Filter: only include non-root elements with semantic or propagated className
264
+ if (!isRoot && !hasClassNameCriteria(node, extractorData)) {
265
+ return node.childNodes.flatMap((child) => walkTree(child, ancestorPath, false))
266
+ }
267
+
237
268
  // Compute this element's name part
238
269
  const namePart = isRoot ? 'root' : getElementNamePart(node, getElementById)
239
270
 
240
271
  // Full name is ancestor path + this element's name (no separator)
241
272
  const name = isRoot ? 'root' : ancestorPath + namePart
242
273
 
243
- // Get extractor data from store
244
- const extractorData = store.getAll(traceId) ?? new Map()
245
-
246
274
  // Check for text content
247
275
  const hasText = hasDirectTextContent(node)
248
276
 
@@ -19,18 +19,13 @@ import { compileSass } from './information-extractors/css/sass-adapter'
19
19
 
20
20
  import type { ComponentType } from 'react'
21
21
 
22
+ import { IoError, NotFoundError, ParseError } from './errors'
23
+ import type { ExtractionError } from './extraction-types'
24
+
22
25
  // ─────────────────────────────────────────────────────────────────────────────
23
26
  // Types
24
27
  // ─────────────────────────────────────────────────────────────────────────────
25
28
 
26
- /** A non-fatal issue encountered during component processing. */
27
- export interface ExtractionWarning {
28
- componentName: string
29
- phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
30
- error: string
31
- stack?: string
32
- }
33
-
34
29
  export interface ExtractedCssInfo {
35
30
  filePath: string
36
31
  api: CSSParserAPI
@@ -47,7 +42,6 @@ export interface ComponentInfoWithCss extends CoupledComponentInfo {
47
42
  /** The result of processing a single component through the manifest pipeline. */
48
43
  export interface ProcessComponentResult {
49
44
  component: ComponentInfoWithCss
50
- warnings: ExtractionWarning[]
51
45
  }
52
46
 
53
47
  // ─────────────────────────────────────────────────────────────────────────────
@@ -66,29 +60,34 @@ export function processComponent(
66
60
  componentInfo: ComponentInfo,
67
61
  loadComponent: (componentName: string) => ComponentType<unknown> | null,
68
62
  cssImportPaths: string[],
69
- loaderHasError?: boolean,
63
+ loaderHasError: boolean,
64
+ report: (error: ExtractionError) => void,
70
65
  options?: RunExtractorsOptions,
71
66
  ): ProcessComponentResult {
72
- const warnings: ExtractionWarning[] = []
67
+ const { componentName } = componentInfo
73
68
 
74
69
  // Load the actual component
75
70
  let Component: ComponentType<unknown> | null = null
76
71
  try {
77
- Component = loadComponent(componentInfo.componentName)
72
+ Component = loadComponent(componentName)
78
73
  if (!Component && !loaderHasError) {
79
- warnings.push({
80
- componentName: componentInfo.componentName,
74
+ report({
75
+ componentName,
81
76
  phase: 'loader',
82
- error: `Component "${componentInfo.componentName}" not found in package exports`,
77
+ error: new NotFoundError(`Component "${componentName}" not found in package exports`, {
78
+ props: { phase: 'loader' },
79
+ }),
83
80
  })
84
81
  }
85
- } catch (error) {
82
+ } catch (thrownError) {
86
83
  if (!loaderHasError) {
87
- warnings.push({
88
- componentName: componentInfo.componentName,
84
+ report({
85
+ componentName,
89
86
  phase: 'loader',
90
- error: `Failed to load "${componentInfo.componentName}": ${error instanceof Error ? error.message : String(error)}`,
91
- stack: error instanceof Error ? error.stack : undefined,
87
+ error: new IoError(
88
+ `Failed to load "${componentName}": ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
89
+ { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'loader' } },
90
+ ),
92
91
  })
93
92
  }
94
93
  }
@@ -110,18 +109,20 @@ export function processComponent(
110
109
 
111
110
  const { props: coupledProps, innerElementProps } = buildCoupledProps(componentInfo, state.stores)
112
111
  coupledInfo = {
113
- componentName: componentInfo.componentName,
112
+ componentName,
114
113
  props: coupledProps,
115
114
  elements: convertElements(extractedElements),
116
115
  innerElementProps: innerElementProps.size > 0 ? innerElementProps : undefined,
117
116
  propUsages: state.stores.propUsages,
118
117
  }
119
- } catch (error) {
120
- warnings.push({
121
- componentName: componentInfo.componentName,
118
+ } catch (thrownError) {
119
+ report({
120
+ componentName,
122
121
  phase: 'render',
123
- error: error instanceof Error ? error.message : String(error),
124
- stack: error instanceof Error ? error.stack : undefined,
122
+ error: new IoError(thrownError instanceof Error ? thrownError.message : String(thrownError), {
123
+ cause: thrownError instanceof Error ? thrownError : undefined,
124
+ props: { phase: 'render' },
125
+ }),
125
126
  })
126
127
  }
127
128
  }
@@ -133,7 +134,7 @@ export function processComponent(
133
134
  } else {
134
135
  // Fallback: create minimal info without DOM coupling
135
136
  enhancedInfo = {
136
- componentName: componentInfo.componentName,
137
+ componentName,
137
138
  props: Object.fromEntries(
138
139
  Object.entries(componentInfo.props).map(([name, info]) => [
139
140
  name,
@@ -146,8 +147,7 @@ export function processComponent(
146
147
  }
147
148
 
148
149
  // Read and parse CSS imports
149
- const { cssInfos: css, warnings: cssWarnings } = extractCssInfo(cssImportPaths, componentInfo.componentName)
150
- warnings.push(...cssWarnings)
150
+ const css = extractCssInfo(cssImportPaths, componentName, report)
151
151
 
152
152
  // Match CSS selectors to elements
153
153
  let varUsedByTraceId = new Map<string, Set<string>>()
@@ -159,12 +159,14 @@ export function processComponent(
159
159
  elements: convertElements(matchResult.elements),
160
160
  }
161
161
  varUsedByTraceId = matchResult.varUsedByTraceId
162
- } catch (error) {
163
- warnings.push({
164
- componentName: componentInfo.componentName,
162
+ } catch (thrownError) {
163
+ report({
164
+ componentName,
165
165
  phase: 'css',
166
- error: `CSS selector matching failed: ${error instanceof Error ? error.message : String(error)}`,
167
- stack: error instanceof Error ? error.stack : undefined,
166
+ error: new IoError(
167
+ `CSS selector matching failed: ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
168
+ { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'css' } },
169
+ ),
168
170
  })
169
171
  }
170
172
  }
@@ -175,7 +177,6 @@ export function processComponent(
175
177
  css,
176
178
  varUsedByTraceId,
177
179
  },
178
- warnings,
179
180
  }
180
181
  }
181
182
 
@@ -327,14 +328,14 @@ function convertElements(elements: ExtractedElement[]): CoupledComponentInfo['el
327
328
 
328
329
  /**
329
330
  * Reads and parses CSS files, extracting standard and custom properties.
330
- * Returns the parsed CSS info alongside any warnings from files that failed to parse.
331
+ * Non-fatal parse failures are reported via `reportError`.
331
332
  */
332
333
  function extractCssInfo(
333
334
  cssImportPaths: string[],
334
335
  componentName: string,
335
- ): { cssInfos: ExtractedCssInfo[]; warnings: ExtractionWarning[] } {
336
+ report: (error: ExtractionError) => void,
337
+ ): ExtractedCssInfo[] {
336
338
  const cssInfos: ExtractedCssInfo[] = []
337
- const warnings: ExtractionWarning[] = []
338
339
 
339
340
  for (const cssPath of cssImportPaths) {
340
341
  try {
@@ -343,11 +344,13 @@ function extractCssInfo(
343
344
  if (cssPath.endsWith('.scss') || cssPath.endsWith('.sass')) {
344
345
  const compiledCssResult = compileSass(cssPath)
345
346
  if (compiledCssResult.isErr()) {
346
- warnings.push({
347
+ report({
347
348
  componentName,
348
349
  phase: 'css',
349
- error: `Failed to parse ${cssPath}: ${compiledCssResult.error.message}`,
350
- stack: compiledCssResult.error.stack,
350
+ error: new ParseError(`Failed to compile ${cssPath}`, {
351
+ cause: compiledCssResult.error,
352
+ props: { phase: 'css' },
353
+ }),
351
354
  })
352
355
  continue
353
356
  }
@@ -381,15 +384,17 @@ function extractCssInfo(
381
384
  customProperties,
382
385
  isCssModule: /\.module\.(css|scss|sass)$/.test(cssPath),
383
386
  })
384
- } catch (error) {
385
- warnings.push({
387
+ } catch (thrownError) {
388
+ report({
386
389
  componentName,
387
390
  phase: 'css',
388
- error: `Failed to parse ${cssPath}: ${error instanceof Error ? error.message : String(error)}`,
389
- stack: error instanceof Error ? error.stack : undefined,
391
+ error: new ParseError(
392
+ `Failed to parse ${cssPath}: ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
393
+ { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'css' } },
394
+ ),
390
395
  })
391
396
  }
392
397
  }
393
398
 
394
- return { cssInfos, warnings }
399
+ return cssInfos
395
400
  }