@wix/zero-config-implementation 1.82.0 → 1.84.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.
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs'
2
+ import React from 'react'
3
+ import { beforeEach, expect, it, vi } from 'vitest'
4
+ import { processComponent } from './manifest-pipeline'
5
+
6
+ vi.mock('./information-extractors/css', async (importOriginal) => {
7
+ const original = await importOriginal<typeof import('./information-extractors/css')>()
8
+ return {
9
+ ...original,
10
+ matchCssSelectors: vi.fn(),
11
+ }
12
+ })
13
+
14
+ vi.mock('node:fs', async (importOriginal) => {
15
+ const original = await importOriginal<typeof import('node:fs')>()
16
+ return { ...original, default: { ...original, readFileSync: vi.fn() } }
17
+ })
18
+
19
+ beforeEach(() => {
20
+ vi.mocked(fs.readFileSync).mockReturnValue('.root { color: red; }')
21
+ })
22
+
23
+ it('returns ok:false with IoError when matchCssSelectors throws', async () => {
24
+ const { matchCssSelectors } = await import('./information-extractors/css')
25
+ vi.mocked(matchCssSelectors).mockImplementation(() => {
26
+ throw new Error('simulated selector matching failure')
27
+ })
28
+
29
+ const SimpleComponent: React.FC<unknown> = () => React.createElement('div', { className: 'root' }, 'hello')
30
+
31
+ const result = processComponent({ componentName: 'SimpleComponent', props: {} }, () => SimpleComponent, [
32
+ '/fake/style.css',
33
+ ])
34
+
35
+ expect(result.ok).toBe(false)
36
+ if (!result.ok) {
37
+ expect(result.error.name).toBe('IoError')
38
+ expect(result.error.message).toContain('CSS selector matching failed for "SimpleComponent"')
39
+ expect(result.error.message).toContain('simulated selector matching failure')
40
+ }
41
+ })
@@ -26,7 +26,6 @@ import { compileSass } from './information-extractors/css/sass-adapter'
26
26
  import type { ComponentType } from 'react'
27
27
 
28
28
  import { IoError, NotFoundError, ParseError } from './errors'
29
- import type { ExtractionError } from './extraction-types'
30
29
 
31
30
  // ─────────────────────────────────────────────────────────────────────────────
32
31
  // Types
@@ -58,12 +57,9 @@ export type ProcessComponentResult =
58
57
  * Processes a single component through the full manifest pipeline:
59
58
  * loading, rendering with prop tracking, CSS extraction, and selector matching.
60
59
  *
61
- * On success (`ok: true`), returns enriched component info. Non-fatal loader/CSS
62
- * issues are reported via `report` only; when the module did not load, a fallback
63
- * manifest (TS props without DOM coupling) is still returned.
64
- *
65
- * When the component was loaded but render/extractors throw, returns `ok: false`
66
- * with an {@link IoError} — Tier-1 callers should turn that into a failed `Result`.
60
+ * On success (`ok: true`), returns enriched component info.
61
+ * On failure, returns `ok: false` with a typed error Tier-1 callers should
62
+ * turn that into a failed `Result`.
67
63
  *
68
64
  * @param compiledEntryPath - Optional path to the user's compiled JS entry. When
69
65
  * provided, the renderer also patches the React module resolved from the
@@ -73,8 +69,6 @@ export function processComponent(
73
69
  componentInfo: ComponentInfo,
74
70
  loadComponent: (componentName: string) => ComponentType<unknown> | null,
75
71
  cssImportPaths: string[],
76
- loaderHasError: boolean,
77
- report: (error: ExtractionError) => void,
78
72
  options?: RunExtractorsOptions,
79
73
  compiledEntryPath?: string,
80
74
  eligiblePaths?: string[],
@@ -85,82 +79,70 @@ export function processComponent(
85
79
  let Component: ComponentType<unknown> | null = null
86
80
  try {
87
81
  Component = loadComponent(componentName)
88
- if (!Component && !loaderHasError) {
89
- report({
90
- componentName,
91
- phase: 'loader',
92
- error: new NotFoundError(`Component "${componentName}" not found in package exports`, {
93
- props: { phase: 'loader' },
94
- }),
95
- })
82
+ if (!Component) {
83
+ return {
84
+ ok: false,
85
+ error: new NotFoundError(
86
+ `Component "${componentName}" was not found as a named export in the compiled bundle. Ensure "${componentName}" is exported from the package entry point.`,
87
+ { props: { phase: 'loader' } },
88
+ ),
89
+ }
96
90
  }
97
91
  } catch (thrownError) {
98
- if (!loaderHasError) {
99
- report({
100
- componentName,
101
- phase: 'loader',
102
- error: new IoError(
103
- `Failed to load "${componentName}": ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
104
- { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'loader' } },
105
- ),
106
- })
92
+ return {
93
+ ok: false,
94
+ error: new IoError(
95
+ `Failed to access component "${componentName}" from compiled bundle: ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
96
+ { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'loader' } },
97
+ ),
107
98
  }
108
99
  }
109
100
 
110
- let coupledInfo: CoupledComponentInfo | null = null
111
101
  let enhancedInfo: CoupledComponentInfo
112
102
  let html: string | undefined
113
103
  let extractedElements: ExtractedElement[] = []
114
104
 
115
105
  // Render component with mock props and track prop flow (can fail)
116
- if (Component) {
117
- try {
118
- const { extractor: propTracker, state } = createPropTrackerExtractor({ eligiblePaths })
119
- const cssExtractor = createCssPropertiesExtractor()
120
-
121
- const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options, compiledEntryPath)
122
- html = result.html
123
- extractedElements = result.elements
124
-
125
- const { props: coupledProps, innerElementProps } = buildCoupledProps(componentInfo, state.stores)
126
- coupledInfo = {
127
- componentName,
128
- props: coupledProps,
129
- elements: convertElements(extractedElements),
130
- innerElementProps: innerElementProps.size > 0 ? innerElementProps : undefined,
131
- propUsages: state.stores.propUsages,
132
- }
133
- } catch (thrownError) {
134
- const error = new IoError(thrownError instanceof Error ? thrownError.message : String(thrownError), {
135
- cause: thrownError instanceof Error ? thrownError : undefined,
136
- props: { phase: 'render' },
137
- })
138
- report({ componentName, phase: 'render', error })
139
- return { ok: false, error }
140
- }
141
- }
106
+ try {
107
+ const { extractor: propTracker, state } = createPropTrackerExtractor({ eligiblePaths })
108
+ const cssExtractor = createCssPropertiesExtractor()
109
+
110
+ const result = runExtractors(componentInfo, Component, [propTracker, cssExtractor], options, compiledEntryPath)
111
+ html = result.html
112
+ extractedElements = result.elements
142
113
 
143
- // If rendering succeeded, use the coupled info directly
144
- // (CSS properties are now embedded via css-properties-extractor)
145
- if (coupledInfo) {
146
- enhancedInfo = coupledInfo
147
- } else {
148
- // Fallback: create minimal info without DOM coupling
114
+ const { props: coupledProps, innerElementProps } = buildCoupledProps(componentInfo, state.stores)
149
115
  enhancedInfo = {
150
116
  componentName,
151
- props: Object.fromEntries(
152
- Object.entries(componentInfo.props).map(([name, info]) => [
153
- name,
154
- { ...info, logicOnly: false, propPath: `props.${name}` },
155
- ]),
156
- ),
157
- elements: [],
158
- propUsages: new Map(),
117
+ props: coupledProps,
118
+ elements: convertElements(extractedElements),
119
+ innerElementProps: innerElementProps.size > 0 ? innerElementProps : undefined,
120
+ propUsages: state.stores.propUsages,
121
+ }
122
+ } catch (thrownError) {
123
+ return {
124
+ ok: false,
125
+ error: new IoError(thrownError instanceof Error ? thrownError.message : String(thrownError), {
126
+ cause: thrownError instanceof Error ? thrownError : undefined,
127
+ props: { phase: 'render' },
128
+ }),
159
129
  }
160
130
  }
161
131
 
162
- // Read and parse CSS imports
163
- const css = extractCssInfo(cssImportPaths, componentName, report)
132
+ // Read and parse CSS imports (fatal)
133
+ let css: ExtractedCssInfo[]
134
+ try {
135
+ css = extractCssInfo(cssImportPaths, componentName)
136
+ } catch (thrownError) {
137
+ const error =
138
+ thrownError instanceof ParseError
139
+ ? thrownError
140
+ : new ParseError(
141
+ `Failed to parse CSS file for component "${componentName}": ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
142
+ { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'css' } },
143
+ )
144
+ return { ok: false, error }
145
+ }
164
146
 
165
147
  // Match CSS selectors to elements
166
148
  let varUsedByTraceId = new Map<string, Set<string>>()
@@ -175,14 +157,13 @@ export function processComponent(
175
157
  }
176
158
  varUsedByTraceId = matchResult.varUsedByTraceId
177
159
  } catch (thrownError) {
178
- report({
179
- componentName,
180
- phase: 'css',
160
+ return {
161
+ ok: false,
181
162
  error: new IoError(
182
- `CSS selector matching failed: ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
163
+ `CSS selector matching failed for "${componentName}": ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
183
164
  { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'css' } },
184
165
  ),
185
- })
166
+ }
186
167
  }
187
168
  }
188
169
 
@@ -349,70 +330,47 @@ function convertElements(elements: ExtractedElement[]): CoupledComponentInfo['el
349
330
  * Reads and parses CSS files, extracting standard and custom properties.
350
331
  * Non-fatal parse failures are reported via `reportError`.
351
332
  */
352
- function extractCssInfo(
353
- cssImportPaths: string[],
354
- componentName: string,
355
- report: (error: ExtractionError) => void,
356
- ): ExtractedCssInfo[] {
333
+ function extractCssInfo(cssImportPaths: string[], componentName: string): ExtractedCssInfo[] {
357
334
  const cssInfos: ExtractedCssInfo[] = []
358
335
 
359
336
  for (const cssPath of cssImportPaths) {
360
- try {
361
- // Read and compile CSS file
362
- let cssContent: string
363
- if (cssPath.endsWith('.scss') || cssPath.endsWith('.sass')) {
364
- const compiledCssResult = compileSass(cssPath)
365
- if (compiledCssResult.isErr()) {
366
- report({
367
- componentName,
368
- phase: 'css',
369
- error: new ParseError(`Failed to compile ${cssPath}`, {
370
- cause: compiledCssResult.error,
371
- props: { phase: 'css' },
372
- }),
373
- })
374
- continue
375
- }
376
- cssContent = compiledCssResult.value
377
- } else {
378
- cssContent = fs.readFileSync(cssPath, 'utf-8')
337
+ let cssContent: string
338
+ if (cssPath.endsWith('.scss') || cssPath.endsWith('.sass')) {
339
+ const compiledCssResult = compileSass(cssPath)
340
+ if (compiledCssResult.isErr()) {
341
+ throw new ParseError(`Failed to compile "${cssPath}" for component "${componentName}"`, {
342
+ cause: compiledCssResult.error,
343
+ props: { phase: 'css' },
344
+ })
379
345
  }
346
+ cssContent = compiledCssResult.value
347
+ } else {
348
+ cssContent = fs.readFileSync(cssPath, 'utf-8')
349
+ }
350
+
351
+ const api = parseCss(cssContent)
352
+ const allProps = api.getAllProperties()
353
+
354
+ const properties = new Map<string, string>()
355
+ const customProperties = new Map<string, string>()
380
356
 
381
- // Parse CSS
382
- const api = parseCss(cssContent)
383
- const allProps = api.getAllProperties()
384
-
385
- // Extract regular properties and custom properties (CSS variables)
386
- const properties = new Map<string, string>()
387
- const customProperties = new Map<string, string>()
388
-
389
- for (const [, props] of allProps) {
390
- for (const prop of props) {
391
- if (prop.name.startsWith('--')) {
392
- customProperties.set(prop.name, prop.value)
393
- } else {
394
- properties.set(prop.name, prop.value)
395
- }
357
+ for (const [, props] of allProps) {
358
+ for (const prop of props) {
359
+ if (prop.name.startsWith('--')) {
360
+ customProperties.set(prop.name, prop.value)
361
+ } else {
362
+ properties.set(prop.name, prop.value)
396
363
  }
397
364
  }
398
-
399
- cssInfos.push({
400
- filePath: cssPath,
401
- api,
402
- properties,
403
- customProperties,
404
- isCssModule: /\.module\.(css|scss|sass)$/.test(cssPath),
405
- })
406
- } catch (thrownError) {
407
- report({
408
- componentName,
409
- phase: 'css',
410
- error: new ParseError(
411
- `Failed to parse ${cssPath}: ${thrownError instanceof Error ? thrownError.message : String(thrownError)}`,
412
- { cause: thrownError instanceof Error ? thrownError : undefined, props: { phase: 'css' } },
413
- ),
414
- })
415
365
  }
366
+
367
+ cssInfos.push({
368
+ filePath: cssPath,
369
+ api,
370
+ properties,
371
+ customProperties,
372
+ isCssModule: /\.module\.(css|scss|sass)$/.test(cssPath),
373
+ })
416
374
  }
417
375
 
418
376
  return cssInfos
@@ -148,10 +148,6 @@ export function findComponent(moduleExports: Record<string, unknown>, name: stri
148
148
  return null
149
149
  }
150
150
 
151
- /**
152
- * Finds the default-exported React component from a module.
153
- * Returns null when the default export is missing or is not component-like.
154
- */
155
151
  export function findDefaultComponent(moduleExports: Record<string, unknown>): ComponentType<unknown> | null {
156
152
  const defaultExport = moduleExports.default
157
153
  if (isComponent(defaultExport)) {
@@ -1,8 +0,0 @@
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
- }