@wix/zero-config-implementation 1.25.0 → 1.27.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.
@@ -19,23 +19,19 @@ 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
37
32
  properties: Map<string, string>
38
33
  customProperties: Map<string, string>
34
+ isCssModule: boolean
39
35
  }
40
36
 
41
37
  export interface ComponentInfoWithCss extends CoupledComponentInfo {
@@ -46,7 +42,6 @@ export interface ComponentInfoWithCss extends CoupledComponentInfo {
46
42
  /** The result of processing a single component through the manifest pipeline. */
47
43
  export interface ProcessComponentResult {
48
44
  component: ComponentInfoWithCss
49
- warnings: ExtractionWarning[]
50
45
  }
51
46
 
52
47
  // ─────────────────────────────────────────────────────────────────────────────
@@ -65,29 +60,34 @@ export function processComponent(
65
60
  componentInfo: ComponentInfo,
66
61
  loadComponent: (componentName: string) => ComponentType<unknown> | null,
67
62
  cssImportPaths: string[],
68
- loaderHasError?: boolean,
63
+ loaderHasError: boolean,
64
+ report: (error: ExtractionError) => void,
69
65
  options?: RunExtractorsOptions,
70
66
  ): ProcessComponentResult {
71
- const warnings: ExtractionWarning[] = []
67
+ const { componentName } = componentInfo
72
68
 
73
69
  // Load the actual component
74
70
  let Component: ComponentType<unknown> | null = null
75
71
  try {
76
- Component = loadComponent(componentInfo.componentName)
72
+ Component = loadComponent(componentName)
77
73
  if (!Component && !loaderHasError) {
78
- warnings.push({
79
- componentName: componentInfo.componentName,
74
+ report({
75
+ componentName,
80
76
  phase: 'loader',
81
- 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
+ }),
82
80
  })
83
81
  }
84
- } catch (error) {
82
+ } catch (thrownError) {
85
83
  if (!loaderHasError) {
86
- warnings.push({
87
- componentName: componentInfo.componentName,
84
+ report({
85
+ componentName,
88
86
  phase: 'loader',
89
- error: `Failed to load "${componentInfo.componentName}": ${error instanceof Error ? error.message : String(error)}`,
90
- 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
+ ),
91
91
  })
92
92
  }
93
93
  }
@@ -109,18 +109,20 @@ export function processComponent(
109
109
 
110
110
  const { props: coupledProps, innerElementProps } = buildCoupledProps(componentInfo, state.stores)
111
111
  coupledInfo = {
112
- componentName: componentInfo.componentName,
112
+ componentName,
113
113
  props: coupledProps,
114
114
  elements: convertElements(extractedElements),
115
115
  innerElementProps: innerElementProps.size > 0 ? innerElementProps : undefined,
116
116
  propUsages: state.stores.propUsages,
117
117
  }
118
- } catch (error) {
119
- warnings.push({
120
- componentName: componentInfo.componentName,
118
+ } catch (thrownError) {
119
+ report({
120
+ componentName,
121
121
  phase: 'render',
122
- error: error instanceof Error ? error.message : String(error),
123
- 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
+ }),
124
126
  })
125
127
  }
126
128
  }
@@ -132,7 +134,7 @@ export function processComponent(
132
134
  } else {
133
135
  // Fallback: create minimal info without DOM coupling
134
136
  enhancedInfo = {
135
- componentName: componentInfo.componentName,
137
+ componentName,
136
138
  props: Object.fromEntries(
137
139
  Object.entries(componentInfo.props).map(([name, info]) => [
138
140
  name,
@@ -145,8 +147,7 @@ export function processComponent(
145
147
  }
146
148
 
147
149
  // Read and parse CSS imports
148
- const { cssInfos: css, warnings: cssWarnings } = extractCssInfo(cssImportPaths, componentInfo.componentName)
149
- warnings.push(...cssWarnings)
150
+ const css = extractCssInfo(cssImportPaths, componentName, report)
150
151
 
151
152
  // Match CSS selectors to elements
152
153
  let varUsedByTraceId = new Map<string, Set<string>>()
@@ -158,12 +159,14 @@ export function processComponent(
158
159
  elements: convertElements(matchResult.elements),
159
160
  }
160
161
  varUsedByTraceId = matchResult.varUsedByTraceId
161
- } catch (error) {
162
- warnings.push({
163
- componentName: componentInfo.componentName,
162
+ } catch (thrownError) {
163
+ report({
164
+ componentName,
164
165
  phase: 'css',
165
- error: `CSS selector matching failed: ${error instanceof Error ? error.message : String(error)}`,
166
- 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
+ ),
167
170
  })
168
171
  }
169
172
  }
@@ -174,7 +177,6 @@ export function processComponent(
174
177
  css,
175
178
  varUsedByTraceId,
176
179
  },
177
- warnings,
178
180
  }
179
181
  }
180
182
 
@@ -326,14 +328,14 @@ function convertElements(elements: ExtractedElement[]): CoupledComponentInfo['el
326
328
 
327
329
  /**
328
330
  * Reads and parses CSS files, extracting standard and custom properties.
329
- * Returns the parsed CSS info alongside any warnings from files that failed to parse.
331
+ * Non-fatal parse failures are reported via `reportError`.
330
332
  */
331
333
  function extractCssInfo(
332
334
  cssImportPaths: string[],
333
335
  componentName: string,
334
- ): { cssInfos: ExtractedCssInfo[]; warnings: ExtractionWarning[] } {
336
+ report: (error: ExtractionError) => void,
337
+ ): ExtractedCssInfo[] {
335
338
  const cssInfos: ExtractedCssInfo[] = []
336
- const warnings: ExtractionWarning[] = []
337
339
 
338
340
  for (const cssPath of cssImportPaths) {
339
341
  try {
@@ -342,11 +344,13 @@ function extractCssInfo(
342
344
  if (cssPath.endsWith('.scss') || cssPath.endsWith('.sass')) {
343
345
  const compiledCssResult = compileSass(cssPath)
344
346
  if (compiledCssResult.isErr()) {
345
- warnings.push({
347
+ report({
346
348
  componentName,
347
349
  phase: 'css',
348
- error: `Failed to parse ${cssPath}: ${compiledCssResult.error.message}`,
349
- stack: compiledCssResult.error.stack,
350
+ error: new ParseError(`Failed to compile ${cssPath}`, {
351
+ cause: compiledCssResult.error,
352
+ props: { phase: 'css' },
353
+ }),
350
354
  })
351
355
  continue
352
356
  }
@@ -378,16 +382,19 @@ function extractCssInfo(
378
382
  api,
379
383
  properties,
380
384
  customProperties,
385
+ isCssModule: /\.module\.(css|scss|sass)$/.test(cssPath),
381
386
  })
382
- } catch (error) {
383
- warnings.push({
387
+ } catch (thrownError) {
388
+ report({
384
389
  componentName,
385
390
  phase: 'css',
386
- error: `Failed to parse ${cssPath}: ${error instanceof Error ? error.message : String(error)}`,
387
- 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
+ ),
388
395
  })
389
396
  }
390
397
  }
391
398
 
392
- return { cssInfos, warnings }
399
+ return cssInfos
393
400
  }
@@ -1,10 +1,17 @@
1
1
  import { createRequire } from 'node:module'
2
- import { pascalCase } from 'case-anything'
3
2
  import { ResultAsync, errAsync, okAsync } from 'neverthrow'
4
3
  import type { ComponentType } from 'react'
5
- import { IoError } from './errors'
6
4
 
7
- type IoErrorInstance = InstanceType<typeof IoError>
5
+ /**
6
+ * Structured failure from `loadModule` when both ESM import and CJS require fail.
7
+ * Carries each error separately so callers can report them independently.
8
+ */
9
+ export interface LoadModuleFailure {
10
+ /** The ESM import error, or null when no ESM was attempted (e.g. empty path). */
11
+ esmError: Error | null
12
+ /** The CJS require error, or a generic error for the empty-path case. */
13
+ cjsError: Error
14
+ }
8
15
 
9
16
  /**
10
17
  * Attempts to load a module, first via ESM `import()`, then via CJS `require`.
@@ -17,32 +24,30 @@ type IoErrorInstance = InstanceType<typeof IoError>
17
24
  *
18
25
  * @param entryPath - Absolute path to the module entry point.
19
26
  * @returns A `ResultAsync` containing the module exports on success.
20
- * @errors {IoError} When both ESM import and CJS require fail.
27
+ * @errors {LoadModuleFailure} When both ESM import and CJS require fail.
21
28
  */
22
- export function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, IoErrorInstance> {
23
- return ResultAsync.fromPromise(import(entryPath) as Promise<Record<string, unknown>>, () => null).orElse(() => {
29
+ export function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, LoadModuleFailure> {
30
+ if (!entryPath) {
31
+ return errAsync({ esmError: null, cjsError: new Error('No compiled entry path provided') })
32
+ }
33
+
34
+ return ResultAsync.fromPromise(
35
+ import(entryPath) as Promise<Record<string, unknown>>,
36
+ (esmErr): Error => (esmErr instanceof Error ? esmErr : new Error(String(esmErr))),
37
+ ).orElse((esmError) => {
24
38
  try {
25
39
  const require = createRequire(import.meta.url)
26
40
  const exports = require(entryPath)
27
41
  return okAsync(exports as Record<string, unknown>)
28
42
  } catch (requireErr) {
29
- const cause = requireErr instanceof Error ? requireErr : new Error(String(requireErr))
30
- return errAsync(
31
- new IoError(`Failed to load ${entryPath}: ${cause.message}`, {
32
- cause,
33
- props: { phase: 'load' },
34
- }),
35
- )
43
+ const cjsError = requireErr instanceof Error ? requireErr : new Error(String(requireErr))
44
+ return errAsync({ esmError, cjsError })
36
45
  }
37
46
  })
38
47
  }
39
48
 
40
- function isPascalCase(name: string): boolean {
41
- return name.length > 0 && pascalCase(name) === name
42
- }
43
-
44
49
  function isComponent(value: unknown): value is ComponentType<unknown> {
45
- if (typeof value === 'function') return isPascalCase(value.name)
50
+ if (typeof value === 'function') return true
46
51
  // React.memo() and React.forwardRef() return objects, not functions
47
52
  if (typeof value === 'object' && value !== null && '$$typeof' in value) return true
48
53
  return false
@@ -51,7 +56,7 @@ function isComponent(value: unknown): value is ComponentType<unknown> {
51
56
  export function findComponent(moduleExports: Record<string, unknown>, name: string): ComponentType<unknown> | null {
52
57
  // Direct named export
53
58
  const direct = moduleExports[name]
54
- if ((typeof direct === 'function' && isPascalCase(name)) || isComponent(direct)) {
59
+ if (isComponent(direct)) {
55
60
  return direct as ComponentType<unknown>
56
61
  }
57
62