@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-BpfsusxK.js → index-CQq7GOf9.js} +1 -1
- package/dist/{index-nLqobONA.js → index-DBSnnglR.js} +14476 -14519
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/converters/to-editor-component.ts +11 -5
- package/src/extraction-types.ts +0 -1
- package/src/index.css-imports-fatal.test.ts +33 -0
- package/src/index.ts +31 -72
- package/src/information-extractors/css/parse.ts +110 -106
- package/src/manifest-pipeline.css-match-fatal.test.ts +41 -0
- package/src/manifest-pipeline.ts +88 -130
- package/src/module-loader.ts +0 -4
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} —
|
|
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-
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
153
|
-
|
|
154
|
-
}
|
|
159
|
+
if (result.isErr()) throw result.error
|
|
160
|
+
data[name] = result.value
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
return data
|
package/src/extraction-types.ts
CHANGED
|
@@ -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
|
|
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} —
|
|
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 (
|
|
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
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
+
if (properties.length === 0) return
|
|
147
152
|
|
|
148
|
-
|
|
153
|
+
const selectorStrings: string[] = []
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
if (isInsideKeyframes(this)) {
|
|
156
|
+
const keyframeName = this.atrule?.prelude ? generate(this.atrule.prelude).trim() : ''
|
|
157
|
+
if (!keyframeName) return
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
}
|
|
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
|
+
})
|