@wix/zero-config-implementation 1.83.0 → 1.85.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.
package/dist/index.d.ts CHANGED
@@ -857,11 +857,10 @@ export declare function extractComponentManifest(componentPath: string, compiled
857
857
  *
858
858
  * @param componentPath - Path to the TypeScript source file
859
859
  * @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
860
- * @param options.onError - Called for each non-fatal extraction error as it occurs
861
860
  * @errors
862
861
  * - {@link NotFoundError} — Source file does not exist (phase: `compile`)
863
- * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
864
- * - {@link IoError} — Component loaded but render/extractors failed (phase: `render`)
862
+ * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `css` | `extract`)
863
+ * - {@link IoError} — Module failed to load, export access threw, render failed, or CSS selector matching failed
865
864
  */
866
865
  export declare interface ExtractComponentManifestOptions extends RunExtractorsOptions {
867
866
  onError?: (error: ExtractionError) => void;
@@ -909,7 +908,6 @@ declare type ExtractErrTypes<T extends readonly Result<unknown, unknown>[]> = {
909
908
  [idx in keyof T]: T[idx] extends Result<unknown, infer E> ? E : never;
910
909
  };
911
910
 
912
- /** A non-fatal issue encountered during component extraction. */
913
911
  export declare interface ExtractionError {
914
912
  componentName: string;
915
913
  phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-nLqobONA.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-DBSnnglR.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.83.0",
7
+ "version": "1.85.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",
@@ -82,5 +82,5 @@
82
82
  ]
83
83
  }
84
84
  },
85
- "falconPackageHash": "f80ca8596a9188b6f45fa139c17ea7ed3637fceffaddcb566b22d2db"
85
+ "falconPackageHash": "57c46654a2a7b434be3282e37ce2fff52e061ef822797e00a88ba528"
86
86
  }
@@ -11,6 +11,7 @@ import type {
11
11
  } from '@wix/react-component-schema'
12
12
  import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
13
13
  import { camelCase } from 'case-anything'
14
+ import { IoError } from '../errors'
14
15
  import { buildRefElementManifestKey } from '../extensions/ref-elements/path-utils'
15
16
  import type { RefElementMatch } from '../extensions/ref-elements/types'
16
17
  import type { ComponentInfoWithCss } from '../index'
@@ -66,8 +67,14 @@ function buildEditorElement(
66
67
  nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
67
68
  ): EditorElement {
68
69
  const rootElement = component.elements[0]
69
- const childElements = rootElement?.children ?? []
70
- const rootCustomProps = rootElement ? (nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}) : {}
70
+ if (!rootElement) {
71
+ throw new IoError(
72
+ `Component "${component.componentName}" render produced no root element — null or text-only render`,
73
+ { props: { phase: 'render' } },
74
+ )
75
+ }
76
+ const childElements = rootElement.children
77
+ const rootCustomProps = nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}
71
78
 
72
79
  const customClassTriggers = collectCustomClassTriggers(component)
73
80
 
@@ -149,9 +156,8 @@ function buildData(
149
156
  const defaultValue = prop.defaultValue?.kind !== 'unresolved' ? prop.defaultValue?.value : undefined
150
157
 
151
158
  const result = buildDataItem(prop, defaultValue, propUsages, prop.propPath)
152
- if (result.isOk()) {
153
- data[name] = result.value
154
- }
159
+ if (result.isErr()) throw result.error
160
+ data[name] = result.value
155
161
  }
156
162
 
157
163
  return data
@@ -1,6 +1,5 @@
1
1
  import type { BaseError } from './errors'
2
2
 
3
- /** A non-fatal issue encountered during component extraction. */
4
3
  export interface ExtractionError {
5
4
  componentName: string
6
5
  phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
@@ -0,0 +1,33 @@
1
+ import path from 'node:path'
2
+ import { beforeEach, expect, it, vi } from 'vitest'
3
+ import { extractComponentManifestResult } from './index'
4
+
5
+ vi.mock('./information-extractors/ts', async (importOriginal) => {
6
+ const original = await importOriginal<typeof import('./information-extractors/ts')>()
7
+ return {
8
+ ...original,
9
+ extractCssImports: vi.fn(),
10
+ }
11
+ })
12
+
13
+ const componentPath = path.resolve(
14
+ import.meta.dirname,
15
+ '../../../packages/example-components/src/components/FunctionComponent/component.tsx',
16
+ )
17
+ const compiledEntryPath = path.resolve(import.meta.dirname, '../../../packages/example-components/dist/index.js')
18
+
19
+ it('throws ParseError when extractCssImports throws', async () => {
20
+ const { extractCssImports } = await import('./information-extractors/ts')
21
+ vi.mocked(extractCssImports).mockImplementation(() => {
22
+ throw new Error('simulated extractCssImports failure')
23
+ })
24
+
25
+ const result = await extractComponentManifestResult(componentPath, compiledEntryPath)
26
+
27
+ expect(result.isErr()).toBe(true)
28
+ if (result.isErr()) {
29
+ expect(result.error.name).toBe('ParseError')
30
+ expect(result.error.message).toContain('Failed to extract CSS import paths from')
31
+ expect(result.error.message).toContain('simulated extractCssImports failure')
32
+ }
33
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { WixServicesWrapper } from '@wix/builder-services-wrapper'
2
2
  import type { EditorReactComponent } from '@wix/react-component-schema'
3
- import { Result, ResultAsync, errAsync, okAsync } from 'neverthrow'
3
+ import { Result, ResultAsync } from 'neverthrow'
4
4
  import React, { type ComponentType } from 'react'
5
5
 
6
6
  import { toEditorReactComponent } from './converters'
@@ -15,7 +15,6 @@ import { extractCssImports, extractDefaultComponentInfo } from './information-ex
15
15
  import type { ComponentInfo } from './information-extractors/ts/types'
16
16
  import { processComponent } from './manifest-pipeline'
17
17
  import { findComponent, findDefaultComponent, loadModuleForExtraction } from './module-loader'
18
- import type { LoadModuleFailure } from './module-loader'
19
18
  import { compileTsFile } from './ts-compiler'
20
19
 
21
20
  const defaultWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => {
@@ -42,11 +41,10 @@ export interface ManifestResult {
42
41
  *
43
42
  * @param componentPath - Path to the TypeScript source file
44
43
  * @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
45
- * @param options.onError - Called for each non-fatal extraction error as it occurs
46
44
  * @errors
47
45
  * - {@link NotFoundError} — Source file does not exist (phase: `compile`)
48
- * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
49
- * - {@link IoError} — Component loaded but render/extractors failed (phase: `render`)
46
+ * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `css` | `extract`)
47
+ * - {@link IoError} — Module failed to load, export access threw, render failed, or CSS selector matching failed
50
48
  */
51
49
  export interface ExtractComponentManifestOptions extends RunExtractorsOptions {
52
50
  onError?: (error: ExtractionError) => void
@@ -60,12 +58,6 @@ export function extractComponentManifestResult(
60
58
  ManifestResult,
61
59
  InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError> | InstanceType<typeof IoError>
62
60
  > {
63
- const errors: ExtractionError[] = []
64
- const report = (error: ExtractionError): void => {
65
- errors.push(error)
66
- options?.onError?.(error)
67
- }
68
-
69
61
  // Step 1: Compile TypeScript (fatal)
70
62
  return compileTsFile(componentPath)
71
63
  .andThen((program) => {
@@ -137,68 +129,42 @@ export function extractComponentManifestResult(
137
129
  wrapper,
138
130
  }
139
131
 
140
- // Step 5: Load the compiled package module (non-fatal) - done after TS extraction
141
- // so componentName is known when reporting failures
132
+ // Step 5: Load the compiled package module (fatal)
142
133
  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,
147
- cleanup,
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
- }
134
+ .mapErr((failure) => {
135
+ const esmDetail = failure.esmError ? `ESM: ${failure.esmError.message}` : 'ESM: not attempted'
136
+ return new IoError(
137
+ `Failed to load compiled bundle at "${compiledEntryPath}". ${esmDetail}. CJS: ${failure.cjsError.message}. Ensure the package is built before extraction.`,
138
+ { cause: failure.cjsError, props: { phase: 'loader' } },
139
+ )
140
+ })
141
+ .andThen(({ cleanup, moduleExports }) => {
142
+ const loadComponent = (name: string): ComponentType<unknown> | null =>
143
+ findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports)
171
144
 
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
- })
145
+ const getCssImportPaths = (): string[] => {
146
+ const cssResult = Result.fromThrowable(
147
+ extractCssImports,
148
+ (thrown) =>
149
+ new ParseError(
150
+ `Failed to extract CSS import paths from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
151
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
152
+ ),
153
+ )(program)
154
+ if (cssResult.isErr()) throw cssResult.error
155
+ return cssResult.value
187
156
  }
188
157
 
189
- // Step 7: Process the default-exported component
158
+ // Step 6+7: CSS extraction and component processing — both under the cleanup boundary
190
159
  return ResultAsync.fromPromise(
191
160
  processComponentWithCleanup(
192
161
  componentInfo,
193
162
  loadComponent,
194
- cssImportPaths,
195
- !!failure,
196
- report,
163
+ getCssImportPaths,
197
164
  optionsWithDefaults,
198
165
  compiledEntryPath,
199
166
  refElementContext,
200
167
  cleanup,
201
- errors,
202
168
  ),
203
169
  normalizeComponentExtractionCleanupFailure,
204
170
  )
@@ -224,22 +190,18 @@ function normalizeComponentExtractionCleanupFailure(thrown: unknown): InstanceTy
224
190
  async function processComponentWithCleanup(
225
191
  componentInfo: ComponentInfo,
226
192
  loadComponent: (name: string) => ComponentType<unknown> | null,
227
- cssImportPaths: string[],
228
- didModuleLoadFail: boolean,
229
- report: (error: ExtractionError) => void,
193
+ getCssImportPaths: () => string[],
230
194
  options: ExtractComponentManifestOptions,
231
195
  compiledEntryPath: string,
232
196
  refElementContext: RefElementContext,
233
197
  cleanup: () => Promise<void>,
234
- errors: ExtractionError[],
235
198
  ): Promise<ManifestResult> {
236
199
  try {
200
+ const cssImportPaths = getCssImportPaths()
237
201
  const processResult = processComponent(
238
202
  componentInfo,
239
203
  loadComponent,
240
204
  cssImportPaths,
241
- didModuleLoadFail,
242
- report,
243
205
  options,
244
206
  compiledEntryPath,
245
207
  refElementContext.eligiblePaths,
@@ -248,10 +210,7 @@ async function processComponentWithCleanup(
248
210
  throw processResult.error
249
211
  }
250
212
 
251
- return {
252
- component: toEditorReactComponent(processResult.component),
253
- errors,
254
- }
213
+ return { component: toEditorReactComponent(processResult.component), errors: [] }
255
214
  } finally {
256
215
  await cleanup()
257
216
  }
@@ -292,8 +251,8 @@ export async function extractComponentManifest(
292
251
 
293
252
  // ── Tier 1: High-Level API ──────────────────────────────────────────────────
294
253
  // extractComponentManifest() is exported above as a named function declaration.
295
- export type { ExtractionError } from './extraction-types'
296
254
  export type { EditorReactComponent } from '@wix/react-component-schema'
255
+ export type { ExtractionError } from './extraction-types'
297
256
  export type { ComponentInfoWithCss, ExtractedCssInfo, ProcessComponentResult } from './manifest-pipeline'
298
257
 
299
258
  // ── Tier 2: Pipeline Building Blocks ────────────────────────────────────────
@@ -125,56 +125,58 @@ function parseAllProperties(
125
125
  const propertiesMap = new Map<string, CSSProperty[]>()
126
126
  const varUsagesByProperty = new Map<string, Set<string>>()
127
127
 
128
- try {
129
- const ast = parse(cssString, { parseCustomProperty: true })
130
-
131
- const valueAliases = parseValueAliases(ast)
132
-
133
- walk(ast, {
134
- visit: 'Rule',
135
- enter(this: WalkContext, rule: Rule) {
136
- try {
137
- const properties: CSSProperty[] = []
138
- for (const child of rule.block.children) {
139
- if (child.type !== 'Declaration') continue
140
- const extracted = extractProperty(child, varUsagesByProperty, valueAliases)
141
- if (extracted) {
142
- properties.push(extracted)
143
- }
128
+ const ast = parse(cssString, {
129
+ parseCustomProperty: true,
130
+ parseAtrulePrelude: false,
131
+ onParseError: (error) => {
132
+ throw error
133
+ },
134
+ })
135
+
136
+ const valueAliases = parseValueAliases(ast)
137
+
138
+ walk(ast, {
139
+ visit: 'Rule',
140
+ enter(this: WalkContext, rule: Rule) {
141
+ try {
142
+ const properties: CSSProperty[] = []
143
+ for (const child of rule.block.children) {
144
+ if (child.type !== 'Declaration') continue
145
+ const extracted = extractProperty(child, varUsagesByProperty, valueAliases)
146
+ if (extracted) {
147
+ properties.push(extracted)
144
148
  }
149
+ }
145
150
 
146
- if (properties.length === 0) return
151
+ if (properties.length === 0) return
147
152
 
148
- const selectorStrings: string[] = []
153
+ const selectorStrings: string[] = []
149
154
 
150
- if (isInsideKeyframes(this)) {
151
- const keyframeName = this.atrule?.prelude ? generate(this.atrule.prelude).trim() : ''
152
- if (!keyframeName) return
155
+ if (isInsideKeyframes(this)) {
156
+ const keyframeName = this.atrule?.prelude ? generate(this.atrule.prelude).trim() : ''
157
+ if (!keyframeName) return
153
158
 
154
- const keyframeSelector = generate(rule.prelude).trim()
155
- selectorStrings.push(`@keyframes ${keyframeName} ${keyframeSelector}`)
156
- } else {
157
- if (rule.prelude.type !== 'SelectorList') return
158
- for (const selectorNode of rule.prelude.children) {
159
- if (selectorNode.type !== 'Selector') continue
160
- const selectorString = generate(selectorNode)
161
- parsedSelectors.set(selectorString, selectorNode)
162
- selectorStrings.push(selectorString)
163
- }
159
+ const keyframeSelector = generate(rule.prelude).trim()
160
+ selectorStrings.push(`@keyframes ${keyframeName} ${keyframeSelector}`)
161
+ } else {
162
+ if (rule.prelude.type !== 'SelectorList') return
163
+ for (const selectorNode of rule.prelude.children) {
164
+ if (selectorNode.type !== 'Selector') continue
165
+ const selectorString = generate(selectorNode)
166
+ parsedSelectors.set(selectorString, selectorNode)
167
+ selectorStrings.push(selectorString)
164
168
  }
169
+ }
165
170
 
166
- for (const selectorString of selectorStrings) {
167
- const existing = propertiesMap.get(selectorString) ?? []
168
- propertiesMap.set(selectorString, [...existing, ...properties])
169
- }
170
- } catch {
171
- // Skip rules that can't be processed
171
+ for (const selectorString of selectorStrings) {
172
+ const existing = propertiesMap.get(selectorString) ?? []
173
+ propertiesMap.set(selectorString, [...existing, ...properties])
172
174
  }
173
- },
174
- })
175
- } catch (error) {
176
- console.error('CSS parsing error:', error)
177
- }
175
+ } catch {
176
+ // Skip rules that can't be processed
177
+ }
178
+ },
179
+ })
178
180
 
179
181
  return { propertiesMap, varUsagesByProperty }
180
182
  }
@@ -506,33 +508,34 @@ function extractAtruleDescriptors(block: CssNode): Map<string, string> {
506
508
  function parseRegisteredProperties(cssString: string): Map<string, RegisteredCustomProperty> {
507
509
  const registered = new Map<string, RegisteredCustomProperty>()
508
510
 
509
- try {
510
- const ast = parse(cssString)
511
-
512
- walk(ast, {
513
- visit: 'Atrule',
514
- enter(atrule: Atrule) {
515
- if (atrule.name !== 'property' || !atrule.prelude || !atrule.block) return
516
-
517
- const varName = generate(atrule.prelude).trim()
518
- if (!varName.startsWith('--')) return
519
-
520
- const descriptors = extractAtruleDescriptors(atrule.block)
521
- const resolved = cssPropertyTypeFromSyntax(descriptors.get('syntax') ?? '*')
522
- if (!resolved) return
523
- const { cssPropertyType, enumOptions } = resolved
524
- const initialValue = descriptors.get('initial-value')
525
-
526
- registered.set(varName, {
527
- cssPropertyType,
528
- ...(enumOptions && { enumOptions }),
529
- ...(initialValue !== undefined && { defaultValue: initialValue }),
530
- })
531
- },
532
- })
533
- } catch (error) {
534
- console.error('CSS @property parsing error:', error)
535
- }
511
+ const ast = parse(cssString, {
512
+ parseAtrulePrelude: false,
513
+ onParseError: (error) => {
514
+ throw error
515
+ },
516
+ })
517
+
518
+ walk(ast, {
519
+ visit: 'Atrule',
520
+ enter(atrule: Atrule) {
521
+ if (atrule.name !== 'property' || !atrule.prelude || !atrule.block) return
522
+
523
+ const varName = generate(atrule.prelude).trim()
524
+ if (!varName.startsWith('--')) return
525
+
526
+ const descriptors = extractAtruleDescriptors(atrule.block)
527
+ const resolved = cssPropertyTypeFromSyntax(descriptors.get('syntax') ?? '*')
528
+ if (!resolved) return
529
+ const { cssPropertyType, enumOptions } = resolved
530
+ const initialValue = descriptors.get('initial-value')
531
+
532
+ registered.set(varName, {
533
+ cssPropertyType,
534
+ ...(enumOptions && { enumOptions }),
535
+ ...(initialValue !== undefined && { defaultValue: initialValue }),
536
+ })
537
+ },
538
+ })
536
539
 
537
540
  return registered
538
541
  }
@@ -662,47 +665,48 @@ function analyzeStateSelector(selector: Selector): AnalyzedCompound[] {
662
665
  function parseStateClasses(cssString: string): StateClassRule[] {
663
666
  const results: StateClassRule[] = []
664
667
 
665
- try {
666
- const ast = parse(cssString)
667
-
668
- walk(ast, {
669
- visit: 'Rule',
670
- enter(rule: Rule) {
671
- if (rule.prelude.type !== 'SelectorList') return
672
-
673
- const pseudoByBase = new Map<string, NativePseudoClass>()
674
- const modifiersByBase = new Map<string, Set<string>>()
675
-
676
- for (const selectorNode of rule.prelude.children) {
677
- if (selectorNode.type !== 'Selector') continue
678
- for (const analyzed of analyzeStateSelector(selectorNode)) {
679
- if (analyzed.pseudoClass && !pseudoByBase.has(analyzed.base)) {
680
- pseudoByBase.set(analyzed.base, analyzed.pseudoClass)
681
- }
682
- if (analyzed.modifiers.length > 0) {
683
- const modifiers = modifiersByBase.get(analyzed.base) ?? new Set<string>()
684
- for (const modifier of analyzed.modifiers) modifiers.add(modifier)
685
- modifiersByBase.set(analyzed.base, modifiers)
686
- }
668
+ const ast = parse(cssString, {
669
+ parseAtrulePrelude: false,
670
+ onParseError: (error) => {
671
+ throw error
672
+ },
673
+ })
674
+
675
+ walk(ast, {
676
+ visit: 'Rule',
677
+ enter(rule: Rule) {
678
+ if (rule.prelude.type !== 'SelectorList') return
679
+
680
+ const pseudoByBase = new Map<string, NativePseudoClass>()
681
+ const modifiersByBase = new Map<string, Set<string>>()
682
+
683
+ for (const selectorNode of rule.prelude.children) {
684
+ if (selectorNode.type !== 'Selector') continue
685
+ for (const analyzed of analyzeStateSelector(selectorNode)) {
686
+ if (analyzed.pseudoClass && !pseudoByBase.has(analyzed.base)) {
687
+ pseudoByBase.set(analyzed.base, analyzed.pseudoClass)
688
+ }
689
+ if (analyzed.modifiers.length > 0) {
690
+ const modifiers = modifiersByBase.get(analyzed.base) ?? new Set<string>()
691
+ for (const modifier of analyzed.modifiers) modifiers.add(modifier)
692
+ modifiersByBase.set(analyzed.base, modifiers)
687
693
  }
688
694
  }
695
+ }
689
696
 
690
- for (const [baseSelector, modifiers] of modifiersByBase) {
691
- const basePseudo = pseudoByBase.get(baseSelector)
692
- for (const modifier of modifiers) {
693
- // A modifier is the editor-trigger class for a NATIVE state only when its
694
- // own state name matches the base's pseudo (`x--hover` ↔ `:hover`). A custom
695
- // modifier sharing a base with a native pseudo (`x--selected` next to `:hover`)
696
- // stays custom — it must not inherit the unrelated pseudo.
697
- const pseudoClass = basePseudo && stateNameFromModifier(modifier) === basePseudo ? basePseudo : undefined
698
- results.push({ baseSelector, modifier, pseudoClass })
699
- }
697
+ for (const [baseSelector, modifiers] of modifiersByBase) {
698
+ const basePseudo = pseudoByBase.get(baseSelector)
699
+ for (const modifier of modifiers) {
700
+ // A modifier is the editor-trigger class for a NATIVE state only when its
701
+ // own state name matches the base's pseudo (`x--hover` ↔ `:hover`). A custom
702
+ // modifier sharing a base with a native pseudo (`x--selected` next to `:hover`)
703
+ // stays custom — it must not inherit the unrelated pseudo.
704
+ const pseudoClass = basePseudo && stateNameFromModifier(modifier) === basePseudo ? basePseudo : undefined
705
+ results.push({ baseSelector, modifier, pseudoClass })
700
706
  }
701
- },
702
- })
703
- } catch (error) {
704
- console.error('CSS state-class parsing error:', error)
705
- }
707
+ }
708
+ },
709
+ })
706
710
 
707
711
  return results
708
712
  }
@@ -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
+ })