@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.
- package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
- package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
- package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
- package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
- package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
- package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
- package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
- package/src/component-renderer.ts +12 -9
- package/src/converters/to-editor-component.test.ts +88 -0
- package/src/converters/to-editor-component.ts +35 -6
- package/src/index.ts +121 -54
- package/src/information-extractors/css/parse.test.ts +49 -1
- package/src/information-extractors/css/parse.ts +167 -3
- package/src/information-extractors/css/selector-matcher.ts +3 -1
- package/src/information-extractors/css/types.ts +11 -0
- package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
- package/src/information-extractors/react/extractors/core/types.ts +4 -5
- package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
- package/src/information-extractors/react/extractors/css-properties.ts +248 -98
- package/src/information-extractors/react/extractors/index.ts +1 -0
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
- package/src/information-extractors/react/index.ts +1 -0
- package/src/information-extractors/react/types.ts +1 -1
- package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
- package/src/information-extractors/react/utils/mock-generator.ts +38 -13
- package/src/manifest-pipeline.ts +22 -15
- package/src/module-loader.test.ts +150 -0
- package/src/module-loader.ts +48 -8
- package/src/react-runtime-interceptor.ts +64 -0
- package/src/react-runtime-loader.ts +656 -0
- package/src/ref-elements/component-tag.ts +105 -0
- package/src/ref-elements/context.test.ts +179 -0
- package/src/ref-elements/context.ts +280 -0
- package/src/ref-elements/eligible-paths.ts +45 -0
- package/src/ref-elements/module-resolution.test.ts +50 -0
- package/src/ref-elements/module-resolution.ts +21 -0
- package/src/ref-elements/module-specifier.ts +17 -0
- package/src/ref-elements/path-utils.test.ts +14 -0
- package/src/ref-elements/path-utils.ts +55 -0
- package/src/ref-elements/types.ts +17 -0
- package/src/utils/css-class.ts +15 -0
- package/src/jsx-runtime-interceptor.ts +0 -245
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,
|
|
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
|
-
|
|
326
|
+
type: unknown;
|
|
327
|
+
isDomElement: boolean;
|
|
328
|
+
tag?: string;
|
|
326
329
|
props: Record<string, unknown>;
|
|
327
|
-
traceId
|
|
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.
|
|
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-
|
|
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.
|
|
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": "
|
|
85
|
+
"falconPackageHash": "398dcee4762e04fdaa11211df36bfb4ac7e2ea328d412a68f2e363f8"
|
|
85
86
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./cjs-ref-element-target.cjs')
|
|
@@ -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 `
|
|
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 './
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
149
|
-
const
|
|
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,
|
|
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,
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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('
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
})
|