@wix/zero-config-implementation 1.71.0 → 1.73.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 (47) hide show
  1. package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
  2. package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
  3. package/dist/index.d.ts +23 -8
  4. package/dist/index.js +1 -1
  5. package/package.json +4 -3
  6. package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
  7. package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
  8. package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
  9. package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
  10. package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
  11. package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
  12. package/src/component-renderer.ts +12 -9
  13. package/src/converters/to-editor-component.test.ts +88 -0
  14. package/src/converters/to-editor-component.ts +35 -6
  15. package/src/index.ts +121 -54
  16. package/src/information-extractors/css/parse.test.ts +49 -1
  17. package/src/information-extractors/css/parse.ts +167 -3
  18. package/src/information-extractors/css/selector-matcher.ts +3 -1
  19. package/src/information-extractors/css/types.ts +11 -0
  20. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  21. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  22. package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
  23. package/src/information-extractors/react/extractors/css-properties.ts +248 -98
  24. package/src/information-extractors/react/extractors/index.ts +1 -0
  25. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  26. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  27. package/src/information-extractors/react/index.ts +1 -0
  28. package/src/information-extractors/react/types.ts +1 -1
  29. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  30. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  31. package/src/manifest-pipeline.ts +22 -15
  32. package/src/module-loader.test.ts +150 -0
  33. package/src/module-loader.ts +48 -8
  34. package/src/react-runtime-interceptor.ts +64 -0
  35. package/src/react-runtime-loader.ts +656 -0
  36. package/src/ref-elements/component-tag.ts +105 -0
  37. package/src/ref-elements/context.test.ts +179 -0
  38. package/src/ref-elements/context.ts +280 -0
  39. package/src/ref-elements/eligible-paths.ts +45 -0
  40. package/src/ref-elements/module-resolution.test.ts +50 -0
  41. package/src/ref-elements/module-resolution.ts +21 -0
  42. package/src/ref-elements/module-specifier.ts +17 -0
  43. package/src/ref-elements/path-utils.test.ts +14 -0
  44. package/src/ref-elements/path-utils.ts +55 -0
  45. package/src/ref-elements/types.ts +17 -0
  46. package/src/utils/css-class.ts +15 -0
  47. package/src/jsx-runtime-interceptor.ts +0 -245
@@ -1,4 +1,4 @@
1
- import { g as x } from "./index-C4cP1dwy.js";
1
+ import { g as x } from "./index-K3lIO4FC.js";
2
2
  function h(r, a) {
3
3
  for (var i = 0; i < a.length; i++) {
4
4
  const o = a[i];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ComponentType } from 'react';
2
+ import { CssNode } from 'css-tree';
2
3
  import { default as default_2 } from 'typescript';
3
4
  import { EditorReactComponent } from '@wix/react-component-schema';
4
5
  import { Program } from 'typescript';
@@ -302,7 +303,10 @@ export declare interface CoupledComponentInfo {
302
303
  componentName: string;
303
304
  props: Record<string, CoupledProp>;
304
305
  elements: ExtractedElement[];
305
- innerElementProps?: Map<string, Record<string, CoupledProp>>;
306
+ innerElementProps?: Map<string, {
307
+ elementPropsPath: string;
308
+ props: Record<string, CoupledProp>;
309
+ }>;
306
310
  propUsages: TrackingStores['propUsages'];
307
311
  }
308
312
 
@@ -318,13 +322,12 @@ export declare interface CoupledProp extends PropInfo {
318
322
  */
319
323
  export declare function createCssPropertiesExtractor(): ReactExtractor;
320
324
 
321
- /**
322
- * Event emitted for each DOM element created during render.
323
- */
324
325
  export declare interface CreateElementEvent {
325
- tag: string;
326
+ type: unknown;
327
+ isDomElement: boolean;
328
+ tag?: string;
326
329
  props: Record<string, unknown>;
327
- traceId: string;
330
+ traceId?: string;
328
331
  children: unknown[];
329
332
  store: ExtractorStore;
330
333
  }
@@ -337,13 +340,18 @@ export declare interface CreateElementListener {
337
340
  * Creates a prop tracker extractor that:
338
341
  * 1. Generates spy-instrumented mock props during beforeRender
339
342
  * 2. Detects spy markers in element props during onCreateElement
340
- * 3. propUsages tracking data to the store namespaced by 'prop-tracker'
343
+ * 3. Tracks runtime refElement markers from composite components to rendered DOM
344
+ * 4. Writes prop usage data to the store namespaced by 'prop-tracker'
341
345
  */
342
- export declare function createPropTrackerExtractor(): {
346
+ export declare function createPropTrackerExtractor(options?: CreatePropTrackerExtractorOptions): {
343
347
  extractor: ReactExtractor;
344
348
  state: PropTrackerExtractorState;
345
349
  };
346
350
 
351
+ declare interface CreatePropTrackerExtractorOptions {
352
+ eligiblePaths?: string[];
353
+ }
354
+
347
355
  /**
348
356
  * `ErrorClass.subclass()`
349
357
  *
@@ -412,6 +420,11 @@ export declare interface CSSParserAPI {
412
420
  * @returns DOM-matchable selector string or null
413
421
  */
414
422
  getDomSelector: (selector: string) => string | null;
423
+ /**
424
+ * Gets the selector specificity tuple as [ids, classes/attrs/pseudo-classes, elements/pseudo-elements].
425
+ * Returns null when the selector was not parsed.
426
+ */
427
+ getSelectorSpecificity: (selector: string) => [number, number, number] | null;
415
428
  /**
416
429
  * Determines the CSS property type for a custom property based on how it is used.
417
430
  * If all usages of varName are within the same CSS property, returns that property name.
@@ -437,6 +450,8 @@ export declare interface CssPropertiesData {
437
450
  declare interface CSSProperty {
438
451
  name: string;
439
452
  value: string;
453
+ /** Parsed css-tree AST for the property's value. */
454
+ valueAst?: CssNode;
440
455
  /** CSS variable names referenced via var() in this property's value */
441
456
  varRefs?: string[];
442
457
  }
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-C4cP1dwy.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-K3lIO4FC.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.71.0",
7
+ "version": "1.73.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",
@@ -40,7 +40,8 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@wix/builder-services-wrapper": "^1.42.0",
43
- "@wix/react-component-schema": "1.7.0"
43
+ "@wix/react-component-schema": "1.7.0",
44
+ "import-meta-resolve": "^4.2.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@faker-js/faker": "^10.2.0",
@@ -81,5 +82,5 @@
81
82
  ]
82
83
  }
83
84
  },
84
- "falconPackageHash": "d6c8e03493b96fe9947b6b075e997e8ba6e5fea0f79a417f6e26c201"
85
+ "falconPackageHash": "398dcee4762e04fdaa11211df36bfb4ac7e2ea328d412a68f2e363f8"
85
86
  }
@@ -0,0 +1 @@
1
+ module.exports = require('./cjs-ref-element-target.cjs')
@@ -0,0 +1,3 @@
1
+ exports.loadTargetModule = function loadTargetModule() {
2
+ return require('./cjs-ref-element-target.cjs')
3
+ }
@@ -0,0 +1,8 @@
1
+ globalThis.__zeroConfigRefElementEvaluationOrder = ['entry']
2
+
3
+ const target = require('./cjs-ref-element-order-target.cjs')
4
+
5
+ module.exports = {
6
+ evaluationOrder: globalThis.__zeroConfigRefElementEvaluationOrder,
7
+ target,
8
+ }
@@ -0,0 +1,7 @@
1
+ globalThis.__zeroConfigRefElementEvaluationOrder.push('target')
2
+
3
+ function FixtureRefElementOrderTarget() {
4
+ return null
5
+ }
6
+
7
+ module.exports = FixtureRefElementOrderTarget
@@ -0,0 +1,5 @@
1
+ function FixtureRefElementTarget() {
2
+ return null
3
+ }
4
+
5
+ module.exports = FixtureRefElementTarget
@@ -0,0 +1,3 @@
1
+ import RefElementTarget from '@wix/zero-config-ref-element-fixtures/BareRefButton'
2
+
3
+ export default RefElementTarget
@@ -10,7 +10,7 @@
10
10
  * Four complementary layers ensure interception regardless of how JSX is compiled
11
11
  * AND regardless of how many React module instances are loaded in the process:
12
12
  *
13
- * 1. **Node.js ESM loader hook** — registered by `registerJsxLoaderHook()` in
13
+ * 1. **Node.js ESM loader hook** — registered by `ensureReactRuntimeLoader()` in
14
14
  * `module-loader.ts` before any user bundle is imported. Redirects
15
15
  * `react/jsx-runtime` and `react/jsx-dev-runtime` bare-specifier imports to
16
16
  * an in-memory `data:` URL interceptor that reads mutable state from
@@ -75,7 +75,7 @@ import {
75
75
  getOriginals,
76
76
  setCreateElementInterceptor,
77
77
  setJsxInterceptors,
78
- } from './jsx-runtime-interceptor'
78
+ } from './react-runtime-interceptor'
79
79
 
80
80
  export const TRACE_ATTR = 'data-trace-id'
81
81
 
@@ -104,18 +104,17 @@ function createInterceptor(
104
104
  store: ExtractorStore,
105
105
  ): ElementCreator {
106
106
  return (type, elementProps, ...rest) => {
107
- if (!isDOMTag(type)) {
108
- return original(type, elementProps, ...rest)
109
- }
110
-
111
107
  const props = (elementProps ?? {}) as Record<string, unknown>
112
- const traceId = getNextId()
108
+ const isDomElement = isDOMTag(type)
109
+ const traceId = isDomElement ? getNextId() : undefined
113
110
 
114
111
  // Children location differs: createElement uses rest params, jsx/jsxs uses props.children
115
112
  const children = rest.length > 0 ? rest : props.children ? [props.children] : []
116
113
 
117
114
  const event: CreateElementEvent = {
118
- tag: type,
115
+ type,
116
+ isDomElement,
117
+ tag: isDomElement ? type : undefined,
119
118
  props: { ...props, children },
120
119
  traceId,
121
120
  children: children as unknown[],
@@ -124,6 +123,10 @@ function createInterceptor(
124
123
 
125
124
  listeners.forEach((listener) => listener.onCreateElement(event))
126
125
 
126
+ if (!isDomElement || !traceId) {
127
+ return original(type, elementProps, ...rest)
128
+ }
129
+
127
130
  const stampedProps = { ...props, [TRACE_ATTR]: traceId }
128
131
  return original(type, stampedProps, ...rest)
129
132
  }
@@ -248,7 +251,7 @@ export function renderWithExtractors(
248
251
  try {
249
252
  // @ts-expect-error Monkey-patch createElement for default-import callers
250
253
  React.createElement = interceptedCreateElement
251
- // The data: URL react shim (registered by registerJsxLoaderHook) reads this state on every
254
+ // The data: URL react shim (registered by ensureReactRuntimeLoader) reads this state on every
252
255
  // createElement call, covering Rspack/webpack bundles that use `import * as React from 'react'`.
253
256
  setCreateElementInterceptor(interceptedCreateElement)
254
257
  // Use the interceptor API for jsx-runtime (includes jsxDEV for dev mode)
@@ -0,0 +1,88 @@
1
+ import { CSS_PROPERTIES } from '@wix/react-component-schema'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { parseCss } from '../information-extractors/css/parse'
4
+ import type { MatchedCssData } from '../information-extractors/css/types'
5
+ import type { ExtractedElement } from '../information-extractors/react'
6
+ import type { ComponentInfoWithCss } from '../manifest-pipeline'
7
+ import { toEditorReactComponent } from './to-editor-component'
8
+
9
+ function matcherDataFromCss(declarations: string): MatchedCssData {
10
+ const properties = parseCss(`.test { ${declarations} }`).getPropertiesForSelector('.test')
11
+ const customProperties: Record<string, string> = {}
12
+
13
+ for (const property of properties) {
14
+ if (property.name.startsWith('--')) {
15
+ customProperties[property.name] = property.value
16
+ }
17
+ }
18
+
19
+ return {
20
+ matches: [
21
+ {
22
+ selector: '.test',
23
+ properties: properties.filter((property) => !property.name.startsWith('--')),
24
+ specificity: [0, 1, 0],
25
+ },
26
+ ],
27
+ customProperties,
28
+ }
29
+ }
30
+
31
+ function createElementWithDisplayMatcher(tag: string, matcherData: MatchedCssData): ExtractedElement {
32
+ const extractorData = new Map<string, unknown>()
33
+ extractorData.set('css-matcher', matcherData)
34
+ extractorData.set('css-properties', { relevant: [CSS_PROPERTIES.CSS_PROPERTY_TYPE.display] })
35
+
36
+ return {
37
+ traceId: 'trace-root',
38
+ name: 'root',
39
+ tag,
40
+ attributes: {},
41
+ extractorData,
42
+ children: [],
43
+ }
44
+ }
45
+
46
+ function createComponent(rootElement: ExtractedElement): ComponentInfoWithCss {
47
+ return {
48
+ componentName: 'DisplayComponent',
49
+ props: {},
50
+ elements: [rootElement],
51
+ propUsages: new Map(),
52
+ css: [],
53
+ varUsedByTraceId: new Map(),
54
+ }
55
+ }
56
+
57
+ describe('toEditorReactComponent display conversion', () => {
58
+ it('resolves local display fallback chains before emitting the manifest display value', () => {
59
+ const rootElement = createElementWithDisplayMatcher(
60
+ 'span',
61
+ matcherDataFromCss(
62
+ 'display: var(--component-display, inline); --component-display: var(--theme-display, inline-flex)',
63
+ ),
64
+ )
65
+
66
+ const result = toEditorReactComponent(createComponent(rootElement))
67
+ expect(result.editorElement).toBeDefined()
68
+
69
+ expect(result.editorElement?.cssProperties?.display).toEqual({
70
+ display: {
71
+ displayValues: [CSS_PROPERTIES.DISPLAY_VALUE.none, 'inlineFlex'],
72
+ },
73
+ })
74
+ })
75
+
76
+ it('keeps using the tag default when no display declaration exists', () => {
77
+ const rootElement = createElementWithDisplayMatcher('span', matcherDataFromCss(''))
78
+
79
+ const result = toEditorReactComponent(createComponent(rootElement))
80
+ expect(result.editorElement).toBeDefined()
81
+
82
+ expect(result.editorElement?.cssProperties?.display).toEqual({
83
+ display: {
84
+ displayValues: [CSS_PROPERTIES.DISPLAY_VALUE.none, 'inline'],
85
+ },
86
+ })
87
+ })
88
+ })
@@ -20,7 +20,9 @@ import type {
20
20
  TrackingStores,
21
21
  } from '../information-extractors/react'
22
22
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
23
- import { findPreferredSemanticClass } from '../utils/css-class'
23
+ import { buildRefElementManifestKey } from '../ref-elements/path-utils'
24
+ import type { RefElementMatch } from '../ref-elements/types'
25
+ import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
24
26
  import { resolveLonghand } from './css-longhand-resolver'
25
27
  import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
26
28
  import { buildDataItem } from './data-item-builder'
@@ -102,12 +104,12 @@ function buildEditorElement(
102
104
 
103
105
  function getSemanticBlockName(element: ExtractedElement | undefined): string | undefined {
104
106
  if (!element?.attributes.class) return undefined
105
- return findPreferredSemanticClass(element.attributes.class.split(' '))
107
+ return findPreferredSemanticClass(normalizeClassNames(element.attributes.class))
106
108
  }
107
109
 
108
110
  function buildSelector(rootElement?: ExtractedElement): string {
109
111
  if (rootElement?.attributes.class) {
110
- const semanticClass = findPreferredSemanticClass(rootElement.attributes.class.split(' '))
112
+ const semanticClass = findPreferredSemanticClass(normalizeClassNames(rootElement.attributes.class))
111
113
  if (semanticClass) return `.${semanticClass}`
112
114
  }
113
115
  return rootElement?.tag ?? ''
@@ -141,16 +143,31 @@ function buildElements(
141
143
  customClassTriggers: CustomClassTrigger[],
142
144
  innerElementProps?: CoupledComponentInfo['innerElementProps'],
143
145
  propUsages?: TrackingStores['propUsages'],
146
+ parentElementPropsPath?: string,
144
147
  ): Record<string, ElementItem> {
145
148
  const result: Record<string, ElementItem> = {}
146
149
 
147
150
  for (const element of elements) {
148
- const elementData = innerElementProps?.get(element.traceId)
149
- const data = elementData && propUsages ? buildData(elementData, propUsages) : undefined
151
+ const innerEntry = innerElementProps?.get(element.traceId)
152
+ const refElementMatch = element.extractorData.get('ref-element') as RefElementMatch | undefined
153
+
154
+ if (refElementMatch) {
155
+ result[buildRefElementManifestKey(refElementMatch.elementPropsPath, parentElementPropsPath)] = {
156
+ elementType: ELEMENTS.ELEMENT_TYPE.refElement,
157
+ refElement: {
158
+ selector: refElementMatch.selector,
159
+ type: refElementMatch.refComponentType,
160
+ },
161
+ }
162
+ continue
163
+ }
164
+
165
+ const semanticClass = getSemanticBlockName(element)
166
+
167
+ const data = innerEntry && propUsages ? buildData(innerEntry.props, propUsages) : undefined
150
168
  const cssProps = buildCssProperties(element)
151
169
  const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
152
170
 
153
- const semanticClass = getSemanticBlockName(element)
154
171
  // Inner-element custom states come from custom-class triggers only; prop
155
172
  // triggers (`ElementState<>`) are component-level and attach to the root.
156
173
  const states = mergeStates(
@@ -180,6 +197,7 @@ function buildElements(
180
197
  customClassTriggers,
181
198
  innerElementProps,
182
199
  propUsages,
200
+ innerEntry?.elementPropsPath ?? parentElementPropsPath,
183
201
  )
184
202
  : undefined,
185
203
  },
@@ -418,6 +436,17 @@ function buildCssProperties(element: ExtractedElement | undefined): Record<strin
418
436
  function buildDisplayProperty(element: ExtractedElement): CssPropertyItem {
419
437
  const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
420
438
  const resolvedFromCss = matcherData ? resolveDisplayValue(matcherData) : undefined
439
+ const hasExplicitDisplayDeclaration =
440
+ matcherData?.matches.some((match) => match.properties.some((property) => property.name === 'display')) ?? false
441
+
442
+ if (hasExplicitDisplayDeclaration && resolvedFromCss === undefined) {
443
+ return {
444
+ display: {
445
+ displayValues: [DISPLAY_VALUE.none],
446
+ },
447
+ }
448
+ }
449
+
421
450
  const currentValue = resolvedFromCss ?? getDefaultDisplayForTag(element.tag)
422
451
  const currentEnumValue = toDisplayEnumValue(currentValue)
423
452
 
package/src/index.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import { WixServicesWrapper } from '@wix/builder-services-wrapper'
2
2
  import type { EditorReactComponent } from '@wix/react-component-schema'
3
- import { Result, type ResultAsync, errAsync, okAsync } from 'neverthrow'
3
+ import { Result, ResultAsync, errAsync, okAsync } from 'neverthrow'
4
4
  import React, { type ComponentType } from 'react'
5
5
 
6
6
  import { toEditorReactComponent } from './converters'
7
- import { IoError, type NotFoundError, ParseError } from './errors'
7
+ import { BaseError, IoError, type NotFoundError, ParseError } from './errors'
8
8
  import type { ExtractionError } from './extraction-types'
9
9
  import type { RunExtractorsOptions } from './information-extractors/react'
10
10
  import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
11
+ import type { ComponentInfo } from './information-extractors/ts/types'
11
12
  import { processComponent } from './manifest-pipeline'
12
- import { findComponent, findDefaultComponent, loadModule } from './module-loader'
13
+ import { findComponent, findDefaultComponent, loadModuleForExtraction } from './module-loader'
13
14
  import type { LoadModuleFailure } from './module-loader'
15
+ import { buildRefElementContext } from './ref-elements/context'
16
+ import type { RefElementContext } from './ref-elements/types'
14
17
  import { compileTsFile } from './ts-compiler'
15
18
 
16
19
  const defaultWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => {
@@ -65,7 +68,6 @@ export function extractComponentManifestResult(
65
68
  ...options,
66
69
  wrapper: options?.wrapper ?? defaultWrapper,
67
70
  }
68
-
69
71
  // Step 1: Compile TypeScript (fatal)
70
72
  return compileTsFile(componentPath)
71
73
  .andThen((program) => {
@@ -89,66 +91,131 @@ export function extractComponentManifestResult(
89
91
  .andThen(({ program, componentInfo }) => {
90
92
  const { componentName } = componentInfo
91
93
 
92
- // Step 3: Load the compiled package module (non-fatal) — done after TS extraction
93
- // so componentName is known when reporting failures
94
- return loadModule(compiledEntryPath)
95
- .map((moduleExports) => ({
96
- loadComponent: (name: string) => findComponent(moduleExports, name) ?? findDefaultComponent(moduleExports),
97
- failure: null as LoadModuleFailure | null,
98
- }))
99
- .orElse((failure) => okAsync({ loadComponent: () => null as ComponentType<unknown> | null, failure }))
100
- .andThen(({ loadComponent, failure }) => {
101
- if (failure) {
102
- if (failure.esmError) {
94
+ return ResultAsync.fromPromise(
95
+ buildRefElementContext(program, componentPath, componentInfo, compiledEntryPath),
96
+ (thrown) =>
97
+ new ParseError(
98
+ `Failed to build the ref-element context for "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
99
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'extract' } },
100
+ ),
101
+ ).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
+ }
103
126
  report({
104
127
  componentName,
105
128
  phase: 'loader',
106
- error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
129
+ error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
107
130
  })
108
131
  }
109
- report({
110
- componentName,
111
- phase: 'loader',
112
- error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
113
- })
114
- }
115
132
 
116
- // Step 4: Extract CSS imports (non-fatal)
117
- let cssImportPaths: string[] = []
118
- const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
119
- if (cssResult.isOk()) {
120
- cssImportPaths = cssResult.value
121
- } else {
122
- const thrown = cssResult.error
123
- report({
124
- componentName,
125
- phase: 'css',
126
- error: new ParseError(
127
- `Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
128
- { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
129
- ),
130
- })
131
- }
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
+ })
148
+ }
132
149
 
133
- // Step 5: Process the default-exported component (non-fatal unless render fails after load)
134
- const processResult = processComponent(
135
- componentInfo,
136
- loadComponent,
137
- cssImportPaths,
138
- !!failure,
139
- report,
140
- optionsWithDefaults,
141
- compiledEntryPath,
142
- )
143
- if (!processResult.ok) {
144
- return errAsync(processResult.error)
145
- }
146
- const component = toEditorReactComponent(processResult.component)
147
- return okAsync({ component, errors })
148
- })
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,
161
+ cleanup,
162
+ errors,
163
+ ),
164
+ normalizeComponentExtractionCleanupFailure,
165
+ )
166
+ }),
167
+ )
149
168
  })
150
169
  }
151
170
 
171
+ function normalizeComponentExtractionCleanupFailure(thrown: unknown): InstanceType<typeof BaseError> {
172
+ const normalizedError = BaseError.normalize(thrown)
173
+ if (normalizedError instanceof BaseError) {
174
+ return normalizedError
175
+ }
176
+
177
+ return new IoError('Unexpected failure during component extraction cleanup', {
178
+ cause: normalizedError,
179
+ props: { phase: 'loader' },
180
+ })
181
+ }
182
+
183
+ async function processComponentWithCleanup(
184
+ componentInfo: ComponentInfo,
185
+ loadComponent: (name: string) => ComponentType<unknown> | null,
186
+ cssImportPaths: string[],
187
+ didModuleLoadFail: boolean,
188
+ report: (error: ExtractionError) => void,
189
+ options: ExtractComponentManifestOptions,
190
+ compiledEntryPath: string,
191
+ refElementContext: RefElementContext,
192
+ cleanup: () => Promise<void>,
193
+ errors: ExtractionError[],
194
+ ): Promise<ManifestResult> {
195
+ try {
196
+ const processResult = processComponent(
197
+ componentInfo,
198
+ loadComponent,
199
+ cssImportPaths,
200
+ didModuleLoadFail,
201
+ report,
202
+ options,
203
+ compiledEntryPath,
204
+ refElementContext.eligiblePaths,
205
+ )
206
+ if (!processResult.ok) {
207
+ throw processResult.error
208
+ }
209
+
210
+ return {
211
+ component: toEditorReactComponent(processResult.component),
212
+ errors,
213
+ }
214
+ } finally {
215
+ await cleanup()
216
+ }
217
+ }
218
+
152
219
  export async function extractComponentManifest(
153
220
  componentPath: string,
154
221
  compiledEntryPath: string,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { parseCss } from './parse'
2
+ import { parseCss, resolveCssPropertyValue } from './parse'
3
3
 
4
4
  describe('parseCss — getStateClasses', () => {
5
5
  it('extracts a custom-class modifier with no paired pseudo', () => {
@@ -120,3 +120,51 @@ describe('parseCss — getStateClasses', () => {
120
120
  expect(stateClasses).toEqual([])
121
121
  })
122
122
  })
123
+
124
+ describe('resolveCssPropertyValue', () => {
125
+ it('uses the fallback when a referenced custom property is missing from the property-level map', () => {
126
+ const [displayProperty] = parseCss(`
127
+ .title {
128
+ display: var(--layout-display, inline);
129
+ }
130
+ `).getPropertiesForSelector('.title')
131
+
132
+ expect(resolveCssPropertyValue(displayProperty, {})).toBe('inline')
133
+ })
134
+
135
+ it('uses the outer fallback when a referenced custom property is known locally but resolves cyclically', () => {
136
+ const [displayProperty] = parseCss(`
137
+ .title {
138
+ display: var(--layout-display, inline);
139
+ }
140
+ `).getPropertiesForSelector('.title')
141
+
142
+ expect(resolveCssPropertyValue(displayProperty, { '--layout-display': 'var(--layout-display)' })).toBe('inline')
143
+ })
144
+
145
+ it('does not use an inner fallback from a cyclic custom property definition', () => {
146
+ const [displayProperty] = parseCss(`
147
+ .title {
148
+ display: var(--layout-display, inline);
149
+ }
150
+ `).getPropertiesForSelector('.title')
151
+
152
+ expect(resolveCssPropertyValue(displayProperty, { '--layout-display': 'var(--layout-display, inline-flex)' })).toBe(
153
+ 'inline',
154
+ )
155
+ })
156
+
157
+ it('resolves a local custom property chain through its nested fallback when the missing variable is local to the chain', () => {
158
+ const [displayProperty] = parseCss(`
159
+ .title {
160
+ display: var(--component-display, inline);
161
+ }
162
+ `).getPropertiesForSelector('.title')
163
+
164
+ expect(
165
+ resolveCssPropertyValue(displayProperty, {
166
+ '--component-display': 'var(--theme-display, inline-flex)',
167
+ }),
168
+ ).toBe('inline-flex')
169
+ })
170
+ })