@wix/zero-config-implementation 1.5.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/README.md +72 -0
- package/dist/component-loader.d.ts +42 -0
- package/dist/component-renderer.d.ts +31 -0
- package/dist/converters/data-item-builder.d.ts +15 -0
- package/dist/converters/index.d.ts +1 -0
- package/dist/converters/to-editor-component.d.ts +3 -0
- package/dist/converters/utils.d.ts +16 -0
- package/dist/errors.d.ts +230 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +51978 -0
- package/dist/information-extractors/css/index.d.ts +3 -0
- package/dist/information-extractors/css/parse.d.ts +7 -0
- package/dist/information-extractors/css/selector-matcher.d.ts +3 -0
- package/dist/information-extractors/css/types.d.ts +49 -0
- package/dist/information-extractors/react/extractors/core/index.d.ts +6 -0
- package/dist/information-extractors/react/extractors/core/runner.d.ts +19 -0
- package/dist/information-extractors/react/extractors/core/store.d.ts +17 -0
- package/dist/information-extractors/react/extractors/core/tree-builder.d.ts +15 -0
- package/dist/information-extractors/react/extractors/core/types.d.ts +40 -0
- package/dist/information-extractors/react/extractors/css-properties.d.ts +20 -0
- package/dist/information-extractors/react/extractors/index.d.ts +11 -0
- package/dist/information-extractors/react/extractors/prop-tracker.d.ts +24 -0
- package/dist/information-extractors/react/index.d.ts +9 -0
- package/dist/information-extractors/react/types.d.ts +51 -0
- package/dist/information-extractors/react/utils/mock-generator.d.ts +9 -0
- package/dist/information-extractors/react/utils/prop-spy.d.ts +10 -0
- package/dist/information-extractors/ts/components.d.ts +9 -0
- package/dist/information-extractors/ts/css-imports.d.ts +2 -0
- package/dist/information-extractors/ts/index.d.ts +3 -0
- package/dist/information-extractors/ts/types.d.ts +47 -0
- package/dist/information-extractors/ts/utils/semantic-type-resolver.d.ts +3 -0
- package/dist/jsx-runtime-interceptor.d.ts +42 -0
- package/dist/jsx-runtime-interceptor.js +63 -0
- package/dist/jsx-runtime-loader.d.ts +23 -0
- package/dist/jsx-runtime-loader.js +7 -0
- package/dist/manifest-pipeline.d.ts +33 -0
- package/dist/schema.d.ts +167 -0
- package/dist/ts-compiler.d.ts +13 -0
- package/package.json +81 -0
- package/src/component-loader.test.ts +277 -0
- package/src/component-loader.ts +256 -0
- package/src/component-renderer.ts +192 -0
- package/src/converters/data-item-builder.ts +354 -0
- package/src/converters/index.ts +1 -0
- package/src/converters/to-editor-component.ts +167 -0
- package/src/converters/utils.ts +21 -0
- package/src/errors.ts +103 -0
- package/src/index.ts +223 -0
- package/src/information-extractors/css/README.md +3 -0
- package/src/information-extractors/css/index.ts +3 -0
- package/src/information-extractors/css/parse.ts +450 -0
- package/src/information-extractors/css/selector-matcher.ts +88 -0
- package/src/information-extractors/css/types.ts +56 -0
- package/src/information-extractors/react/extractors/core/index.ts +6 -0
- package/src/information-extractors/react/extractors/core/runner.ts +89 -0
- package/src/information-extractors/react/extractors/core/store.ts +36 -0
- package/src/information-extractors/react/extractors/core/tree-builder.ts +273 -0
- package/src/information-extractors/react/extractors/core/types.ts +48 -0
- package/src/information-extractors/react/extractors/css-properties.ts +214 -0
- package/src/information-extractors/react/extractors/index.ts +27 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +132 -0
- package/src/information-extractors/react/index.ts +53 -0
- package/src/information-extractors/react/types.ts +70 -0
- package/src/information-extractors/react/utils/mock-generator.ts +331 -0
- package/src/information-extractors/react/utils/prop-spy.ts +168 -0
- package/src/information-extractors/ts/components.ts +300 -0
- package/src/information-extractors/ts/css-imports.ts +26 -0
- package/src/information-extractors/ts/index.ts +3 -0
- package/src/information-extractors/ts/types.ts +56 -0
- package/src/information-extractors/ts/utils/semantic-type-resolver.ts +377 -0
- package/src/jsx-runtime-interceptor.ts +146 -0
- package/src/jsx-runtime-loader.ts +38 -0
- package/src/manifest-pipeline.ts +362 -0
- package/src/schema.ts +174 -0
- package/src/ts-compiler.ts +41 -0
- package/tsconfig.json +17 -0
- package/typedoc.json +18 -0
- package/vite.config.ts +45 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Result, ResultAsync } from 'neverthrow'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import type { EditorReactComponent } from './schema'
|
|
4
|
+
|
|
5
|
+
import { extractAllComponentInfo, extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
|
|
6
|
+
import type { ComponentInfo } from './information-extractors/ts'
|
|
7
|
+
// TypeScript extraction
|
|
8
|
+
import { compileTsFile } from './ts-compiler'
|
|
9
|
+
|
|
10
|
+
// Error classes
|
|
11
|
+
import { type NotFoundError, ParseError } from './errors'
|
|
12
|
+
|
|
13
|
+
// Component loader helpers
|
|
14
|
+
import { findComponent, loadModule } from './component-loader'
|
|
15
|
+
|
|
16
|
+
// Pipeline orchestration
|
|
17
|
+
import { processComponent } from './manifest-pipeline'
|
|
18
|
+
export type {
|
|
19
|
+
ComponentInfoWithCss,
|
|
20
|
+
ExtractedCssInfo,
|
|
21
|
+
ExtractionWarning,
|
|
22
|
+
ProcessComponentResult,
|
|
23
|
+
} from './manifest-pipeline'
|
|
24
|
+
|
|
25
|
+
// Converter
|
|
26
|
+
import { toEditorReactComponent } from './converters'
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Types
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface ManifestResult {
|
|
33
|
+
component: EditorReactComponent
|
|
34
|
+
errors: ExtractionError[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExtractionError {
|
|
38
|
+
componentName: string
|
|
39
|
+
phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
|
|
40
|
+
error: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Main API
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract component manifest from a TypeScript source file.
|
|
49
|
+
*
|
|
50
|
+
* @param componentPath - Path to the TypeScript source file
|
|
51
|
+
* @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
|
|
52
|
+
* @returns The manifest result containing components and non-fatal warnings
|
|
53
|
+
* @errors
|
|
54
|
+
* - {@link NotFoundError} — Source file does not exist (phase: `compile`)
|
|
55
|
+
* - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
|
|
56
|
+
*/
|
|
57
|
+
export function extractComponentManifest(
|
|
58
|
+
componentPath: string,
|
|
59
|
+
compiledEntryPath: string,
|
|
60
|
+
): ResultAsync<ManifestResult, InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError>> {
|
|
61
|
+
// Step 1: Load the compiled package module (non-fatal)
|
|
62
|
+
return loadModule(compiledEntryPath)
|
|
63
|
+
.map((moduleExports) => ({ moduleExports, loaderError: null as string | null }))
|
|
64
|
+
.orElse((err) =>
|
|
65
|
+
ResultAsync.fromSafePromise(
|
|
66
|
+
Promise.resolve({ moduleExports: null as Record<string, unknown> | null, loaderError: err.message }),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
.andThen(({ moduleExports, loaderError }) => {
|
|
70
|
+
const loadComponent: (componentName: string) => ComponentType<unknown> | null = moduleExports
|
|
71
|
+
? (name) => findComponent(moduleExports, name)
|
|
72
|
+
: () => null
|
|
73
|
+
|
|
74
|
+
// Step 2: Compile TypeScript (fatal)
|
|
75
|
+
return compileTsFile(componentPath)
|
|
76
|
+
.andThen((program) => {
|
|
77
|
+
// Step 3: Extract default-exported component types (fatal)
|
|
78
|
+
const safeExtract = Result.fromThrowable(
|
|
79
|
+
(prog: typeof program) => {
|
|
80
|
+
const componentInfo = extractDefaultComponentInfo(prog, componentPath)
|
|
81
|
+
if (!componentInfo) {
|
|
82
|
+
throw new Error(`No default export found in "${componentPath}"`)
|
|
83
|
+
}
|
|
84
|
+
return componentInfo
|
|
85
|
+
},
|
|
86
|
+
(thrown) =>
|
|
87
|
+
new ParseError(
|
|
88
|
+
`Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
|
|
89
|
+
{
|
|
90
|
+
cause: thrown as Error,
|
|
91
|
+
props: { phase: 'extract' },
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
const componentInfoResult = safeExtract(program)
|
|
96
|
+
|
|
97
|
+
return componentInfoResult.map((componentInfo) => ({ program, componentInfo }))
|
|
98
|
+
})
|
|
99
|
+
.map(({ program, componentInfo }) => {
|
|
100
|
+
const errors: ExtractionError[] = []
|
|
101
|
+
|
|
102
|
+
// Surface loader error as a non-fatal error
|
|
103
|
+
if (loaderError) {
|
|
104
|
+
errors.push({
|
|
105
|
+
componentName: compiledEntryPath,
|
|
106
|
+
phase: 'loader',
|
|
107
|
+
error: loaderError,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 4: Extract CSS imports (non-fatal)
|
|
112
|
+
let cssImportPaths: string[] = []
|
|
113
|
+
const safeCssImports = Result.fromThrowable(extractCssImports, (thrown) => thrown)
|
|
114
|
+
const cssResult = safeCssImports(program)
|
|
115
|
+
if (cssResult.isOk()) {
|
|
116
|
+
cssImportPaths = cssResult.value
|
|
117
|
+
} else {
|
|
118
|
+
const thrown = cssResult.error
|
|
119
|
+
errors.push({
|
|
120
|
+
componentName: componentPath,
|
|
121
|
+
phase: 'css',
|
|
122
|
+
error: `Failed to extract CSS imports: ${thrown instanceof Error ? (thrown as Error).message : String(thrown)}`,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Step 5: Process the default-exported component (non-fatal)
|
|
127
|
+
const processResult = processComponent(componentInfo, loadComponent, cssImportPaths, !!loaderError)
|
|
128
|
+
errors.push(...processResult.warnings)
|
|
129
|
+
const component = toEditorReactComponent(processResult.component)
|
|
130
|
+
|
|
131
|
+
return { component, errors }
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// Public API Exports
|
|
138
|
+
//
|
|
139
|
+
// The API is organized into three tiers:
|
|
140
|
+
//
|
|
141
|
+
// Tier 1 — High-Level API
|
|
142
|
+
// One-call manifest extraction. Pass a file path, get back a manifest.
|
|
143
|
+
// Start here if you want the default pipeline with no customization.
|
|
144
|
+
//
|
|
145
|
+
// Tier 2 — Pipeline Building Blocks
|
|
146
|
+
// Individual steps of the extraction pipeline. Use these to compose a
|
|
147
|
+
// custom pipeline, swap out extractors, or process intermediate results.
|
|
148
|
+
//
|
|
149
|
+
// Tier 3 — Low-Level Renderer
|
|
150
|
+
// Direct React.createElement interception. Use this only if you need
|
|
151
|
+
// full control over the rendering and listener lifecycle.
|
|
152
|
+
//
|
|
153
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
// ── Tier 1: High-Level API ──────────────────────────────────────────────────
|
|
156
|
+
// extractComponentManifest() is exported above as a named function declaration.
|
|
157
|
+
|
|
158
|
+
// ── Tier 2: Pipeline Building Blocks ────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/** TypeScript compilation & static analysis */
|
|
161
|
+
export { compileTsFile } from './ts-compiler'
|
|
162
|
+
export { extractAllComponentInfo, extractDefaultComponentInfo } from './information-extractors/ts/components'
|
|
163
|
+
export { extractCssImports } from './information-extractors/ts/css-imports'
|
|
164
|
+
export type {
|
|
165
|
+
ComponentInfo,
|
|
166
|
+
PropInfo,
|
|
167
|
+
ResolvedType,
|
|
168
|
+
ResolvedKind,
|
|
169
|
+
DefaultValue,
|
|
170
|
+
} from './information-extractors/ts/types'
|
|
171
|
+
|
|
172
|
+
/** React render-time extraction */
|
|
173
|
+
export {
|
|
174
|
+
runExtractors,
|
|
175
|
+
ExtractorStore,
|
|
176
|
+
buildElementTree,
|
|
177
|
+
createPropTrackerExtractor,
|
|
178
|
+
createCssPropertiesExtractor,
|
|
179
|
+
} from './information-extractors/react'
|
|
180
|
+
export type {
|
|
181
|
+
ExtractionResult,
|
|
182
|
+
ReactExtractor,
|
|
183
|
+
RenderContext,
|
|
184
|
+
CreateElementEvent,
|
|
185
|
+
RenderCompleteEvent,
|
|
186
|
+
ExtractedElement,
|
|
187
|
+
PropTrackerData,
|
|
188
|
+
PropTrackerExtractorState,
|
|
189
|
+
CssPropertiesData,
|
|
190
|
+
PropSpyContext,
|
|
191
|
+
} from './information-extractors/react'
|
|
192
|
+
export type {
|
|
193
|
+
CoupledComponentInfo,
|
|
194
|
+
CoupledProp,
|
|
195
|
+
DOMBinding,
|
|
196
|
+
TrackingStores,
|
|
197
|
+
PropReadInfo,
|
|
198
|
+
PropWriteInfo,
|
|
199
|
+
PropSpyMeta,
|
|
200
|
+
} from './information-extractors/react'
|
|
201
|
+
|
|
202
|
+
/** CSS parsing & selector matching */
|
|
203
|
+
export { parseCss } from './information-extractors/css'
|
|
204
|
+
export type { CSSParserAPI } from './information-extractors/css'
|
|
205
|
+
|
|
206
|
+
/** Error classes */
|
|
207
|
+
export {
|
|
208
|
+
BaseError,
|
|
209
|
+
NotFoundError,
|
|
210
|
+
ParseError,
|
|
211
|
+
ValidationError,
|
|
212
|
+
IoError,
|
|
213
|
+
DefectError,
|
|
214
|
+
withDefectBoundary,
|
|
215
|
+
} from './errors'
|
|
216
|
+
|
|
217
|
+
/** Component loader */
|
|
218
|
+
export { createComponentLoader } from './component-loader'
|
|
219
|
+
|
|
220
|
+
// ── Tier 3: Low-Level Renderer ──────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export { renderWithExtractors } from './component-renderer'
|
|
223
|
+
export type { CreateElementListener } from './component-renderer'
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { transform } from 'lightningcss'
|
|
2
|
+
import type { CSSParserAPI, CSSProperty } from './types'
|
|
3
|
+
// Store parsed selectors for later DOM selector computation
|
|
4
|
+
const parsedSelectors = new Map<string, unknown[]>()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Factory function that parses CSS and returns an API to query the parsed result
|
|
8
|
+
* @param cssString - The CSS file content as a string
|
|
9
|
+
* @returns API object with methods to query the parsed CSS
|
|
10
|
+
*/
|
|
11
|
+
export function parseCss(cssString: string): CSSParserAPI {
|
|
12
|
+
// Clear previous selectors for this parse
|
|
13
|
+
parsedSelectors.clear()
|
|
14
|
+
|
|
15
|
+
// Parse all properties eagerly at initialization using lightningcss
|
|
16
|
+
const allProperties = parseAllProperties(cssString)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
getPropertiesForSelector(selector: string): CSSProperty[] {
|
|
20
|
+
return allProperties.get(selector) ?? []
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
getAllProperties(): Map<string, CSSProperty[]> {
|
|
24
|
+
return allProperties
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
getVarUsages(varName: string): string[] {
|
|
28
|
+
// Normalize variable name to include --
|
|
29
|
+
const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
|
|
30
|
+
|
|
31
|
+
const propertyNames = new Set<string>()
|
|
32
|
+
|
|
33
|
+
// Search through all properties for var() usage
|
|
34
|
+
for (const properties of allProperties.values()) {
|
|
35
|
+
for (const property of properties) {
|
|
36
|
+
// Check if the value contains var(--varName)
|
|
37
|
+
const varPattern = new RegExp(`var\\(\\s*${escapeRegex(normalizedVarName)}\\s*[,)]`, 'i')
|
|
38
|
+
|
|
39
|
+
if (varPattern.test(property.value)) {
|
|
40
|
+
// Ignore shorthand properties (values with multiple space-separated values)
|
|
41
|
+
if (!isShorthandValue(property.value)) {
|
|
42
|
+
propertyNames.add(property.name)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Array.from(propertyNames)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
getUniqueProperties(selectors: string[]): Map<string, string> {
|
|
52
|
+
const mergedProperties = new Map<string, string>()
|
|
53
|
+
|
|
54
|
+
for (const selector of selectors) {
|
|
55
|
+
const properties = this.getPropertiesForSelector(selector)
|
|
56
|
+
|
|
57
|
+
// Within a selector, last value wins (CSS behavior)
|
|
58
|
+
const selectorProperties = new Map<string, string>()
|
|
59
|
+
for (const prop of properties) {
|
|
60
|
+
selectorProperties.set(prop.name, prop.value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Merge into final map - first selector wins
|
|
64
|
+
for (const [name, value] of selectorProperties) {
|
|
65
|
+
if (!mergedProperties.has(name)) {
|
|
66
|
+
mergedProperties.set(name, value)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return mergedProperties
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getDomSelector(selector: string): string | null {
|
|
75
|
+
const parsed = parsedSelectors.get(selector)
|
|
76
|
+
if (!parsed) return null
|
|
77
|
+
return buildDomSelector(parsed)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parses all selectors and their properties from the CSS using lightningcss
|
|
84
|
+
* Supports @media rules, @keyframes, and nested selectors
|
|
85
|
+
* @param cssString - CSS string to parse
|
|
86
|
+
* @returns Map from selector to its CSS properties
|
|
87
|
+
*/
|
|
88
|
+
function parseAllProperties(cssString: string): Map<string, CSSProperty[]> {
|
|
89
|
+
const propertiesMap = new Map<string, CSSProperty[]>()
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Use lightningcss transform with visitor to collect properties
|
|
93
|
+
// Note: We don't return the rule from visitors - just collect data
|
|
94
|
+
transform({
|
|
95
|
+
filename: 'input.css',
|
|
96
|
+
code: Buffer.from(cssString),
|
|
97
|
+
minify: false,
|
|
98
|
+
visitor: {
|
|
99
|
+
Rule: {
|
|
100
|
+
style(rule) {
|
|
101
|
+
try {
|
|
102
|
+
// Extract selector string from the rule
|
|
103
|
+
const selectors = rule.value.selectors
|
|
104
|
+
if (!selectors?.length) return undefined
|
|
105
|
+
|
|
106
|
+
// Convert selector AST to string representation and store parsed selectors
|
|
107
|
+
const selectorStrings = selectors.map((sel: unknown[]) => {
|
|
108
|
+
const str = selectorToString(sel)
|
|
109
|
+
parsedSelectors.set(str, sel)
|
|
110
|
+
return str
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Extract properties from declarations
|
|
114
|
+
const properties: CSSProperty[] = []
|
|
115
|
+
for (const decl of rule.value.declarations?.declarations ?? []) {
|
|
116
|
+
const extracted = extractPropertyNameAndValue(decl)
|
|
117
|
+
if (extracted) {
|
|
118
|
+
properties.push(extracted)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Store properties for each selector
|
|
123
|
+
if (properties.length > 0) {
|
|
124
|
+
for (const selector of selectorStrings) {
|
|
125
|
+
const existing = propertiesMap.get(selector) ?? []
|
|
126
|
+
propertiesMap.set(selector, [...existing, ...properties])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Skip rules that can't be processed
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return undefined
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
keyframes(rule) {
|
|
137
|
+
try {
|
|
138
|
+
// Extract @keyframes name and its keyframe selectors
|
|
139
|
+
const keyframeName = rule.value.name.type === 'ident' ? rule.value.name.value : rule.value.name.value
|
|
140
|
+
|
|
141
|
+
for (const keyframe of rule.value.keyframes) {
|
|
142
|
+
// Each keyframe has selectors like 'from', 'to', '50%'
|
|
143
|
+
const keyframeSelectors = keyframe.selectors.map((s) => {
|
|
144
|
+
if (typeof s === 'string') return s
|
|
145
|
+
if ('percentage' in s) return `${s.percentage}%`
|
|
146
|
+
return String(s)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const properties: CSSProperty[] = []
|
|
150
|
+
for (const decl of keyframe.declarations?.declarations ?? []) {
|
|
151
|
+
const extracted = extractPropertyNameAndValue(decl)
|
|
152
|
+
if (extracted) {
|
|
153
|
+
properties.push(extracted)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (properties.length > 0) {
|
|
158
|
+
for (const selector of keyframeSelectors) {
|
|
159
|
+
const fullSelector = `@keyframes ${keyframeName} ${selector}`
|
|
160
|
+
const existing = propertiesMap.get(fullSelector) ?? []
|
|
161
|
+
propertiesMap.set(fullSelector, [...existing, ...properties])
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Skip keyframes that can't be processed
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return undefined
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// If parsing fails, return empty map
|
|
176
|
+
console.error('CSS parsing error:', error)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return propertiesMap
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Converts a single selector component to a string
|
|
184
|
+
*/
|
|
185
|
+
function selectorComponentToString(component: unknown): string {
|
|
186
|
+
if (!component || typeof component !== 'object') return ''
|
|
187
|
+
const comp = component as Record<string, unknown>
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
if (comp.type === 'class') {
|
|
191
|
+
return `.${comp.name}`
|
|
192
|
+
}
|
|
193
|
+
if (comp.type === 'id') {
|
|
194
|
+
return `#${comp.name}`
|
|
195
|
+
}
|
|
196
|
+
if (comp.type === 'type') {
|
|
197
|
+
return comp.name as string
|
|
198
|
+
}
|
|
199
|
+
if (comp.type === 'universal') {
|
|
200
|
+
return '*'
|
|
201
|
+
}
|
|
202
|
+
if (comp.type === 'attribute') {
|
|
203
|
+
const attr = comp as { name: string; operation?: { operator: string; value: string } }
|
|
204
|
+
if (attr.operation) {
|
|
205
|
+
return `[${attr.name}${attr.operation.operator}"${attr.operation.value}"]`
|
|
206
|
+
}
|
|
207
|
+
return `[${attr.name}]`
|
|
208
|
+
}
|
|
209
|
+
if (comp.type === 'pseudo-class') {
|
|
210
|
+
const pseudo = comp as { kind: string; name?: string }
|
|
211
|
+
return `:${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
|
|
212
|
+
}
|
|
213
|
+
if (comp.type === 'pseudo-element') {
|
|
214
|
+
const pseudo = comp as { kind: string; name?: string }
|
|
215
|
+
return `::${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
|
|
216
|
+
}
|
|
217
|
+
if (comp.type === 'combinator') {
|
|
218
|
+
const combinator = comp.value as string
|
|
219
|
+
if (combinator === 'descendant') return ' '
|
|
220
|
+
if (combinator === 'child') return ' > '
|
|
221
|
+
if (combinator === 'next-sibling') return ' + '
|
|
222
|
+
if (combinator === 'later-sibling') return ' ~ '
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Skip components that can't be processed
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ''
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Converts a selector AST node to a string
|
|
233
|
+
*/
|
|
234
|
+
function selectorToString(selector: unknown[]): string {
|
|
235
|
+
return selector.map(selectorComponentToString).join('')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Builds a DOM-queryable selector by filtering out pseudo-classes and pseudo-elements.
|
|
240
|
+
* Returns null if the selector contains pseudo-elements (unmatchable against real DOM).
|
|
241
|
+
*/
|
|
242
|
+
function buildDomSelector(selector: unknown[]): string | null {
|
|
243
|
+
const parts: string[] = []
|
|
244
|
+
|
|
245
|
+
for (const component of selector) {
|
|
246
|
+
if (!component || typeof component !== 'object') continue
|
|
247
|
+
const comp = component as Record<string, unknown>
|
|
248
|
+
|
|
249
|
+
// Pseudo-elements cannot be matched against real DOM
|
|
250
|
+
if (comp.type === 'pseudo-element') {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Skip pseudo-classes (they apply to states, not static DOM)
|
|
255
|
+
if (comp.type === 'pseudo-class') {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
parts.push(selectorComponentToString(component))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = parts.join('')
|
|
263
|
+
return result || null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Type for lightningcss declaration with custom property support
|
|
267
|
+
interface LightningDecl {
|
|
268
|
+
property: string
|
|
269
|
+
value: unknown
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface CustomPropertyValue {
|
|
273
|
+
name: string
|
|
274
|
+
value: unknown[]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extracts the property name and value from a lightningcss declaration.
|
|
279
|
+
* Handles special cases like custom properties (CSS variables) and unparsed values.
|
|
280
|
+
*/
|
|
281
|
+
function extractPropertyNameAndValue(decl: LightningDecl): CSSProperty | null {
|
|
282
|
+
try {
|
|
283
|
+
// Handle CSS custom properties (variables)
|
|
284
|
+
if (decl.property === 'custom') {
|
|
285
|
+
const customValue = decl.value as CustomPropertyValue
|
|
286
|
+
const name = customValue.name // e.g., "--primary-color"
|
|
287
|
+
const value = serializeCustomPropertyValue(customValue.value)
|
|
288
|
+
if (name && value) {
|
|
289
|
+
return { name, value }
|
|
290
|
+
}
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle unparsed properties (e.g., properties using var())
|
|
295
|
+
if (decl.property === 'unparsed') {
|
|
296
|
+
const unparsedValue = decl.value as { propertyId: { property: string }; value: unknown[] }
|
|
297
|
+
const name = unparsedValue.propertyId?.property
|
|
298
|
+
if (name) {
|
|
299
|
+
const value = propertyValueToString(decl)
|
|
300
|
+
if (value) {
|
|
301
|
+
return { name, value }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Standard property
|
|
308
|
+
const name = propertyNameToString(decl.property)
|
|
309
|
+
const value = propertyValueToString(decl)
|
|
310
|
+
if (name && value) {
|
|
311
|
+
return { name, value }
|
|
312
|
+
}
|
|
313
|
+
return null
|
|
314
|
+
} catch {
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Serializes a custom property value array to a CSS string
|
|
321
|
+
*/
|
|
322
|
+
function serializeCustomPropertyValue(valueArray: unknown[]): string {
|
|
323
|
+
const parts: string[] = []
|
|
324
|
+
|
|
325
|
+
for (const token of valueArray) {
|
|
326
|
+
if (!token || typeof token !== 'object') continue
|
|
327
|
+
const t = token as Record<string, unknown>
|
|
328
|
+
|
|
329
|
+
if (t.type === 'color') {
|
|
330
|
+
const color = t.value as Record<string, unknown>
|
|
331
|
+
if (color.type === 'rgb') {
|
|
332
|
+
const r = color.r as number
|
|
333
|
+
const g = color.g as number
|
|
334
|
+
const b = color.b as number
|
|
335
|
+
const a = color.alpha as number
|
|
336
|
+
if (a === 1) {
|
|
337
|
+
parts.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`)
|
|
338
|
+
} else {
|
|
339
|
+
parts.push(`rgba(${r}, ${g}, ${b}, ${a})`)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else if (t.type === 'length' || t.type === 'dimension') {
|
|
343
|
+
const dim = (t.value as Record<string, unknown>) ?? t
|
|
344
|
+
const unit = dim.unit as string
|
|
345
|
+
const val = dim.value as number
|
|
346
|
+
parts.push(`${val}${unit}`)
|
|
347
|
+
} else if (t.type === 'number') {
|
|
348
|
+
parts.push(String(t.value))
|
|
349
|
+
} else if (t.type === 'token') {
|
|
350
|
+
const tokenValue = t.value as Record<string, unknown>
|
|
351
|
+
if (tokenValue.type === 'ident') {
|
|
352
|
+
parts.push(tokenValue.value as string)
|
|
353
|
+
} else if (tokenValue.type === 'string') {
|
|
354
|
+
parts.push(`"${tokenValue.value}"`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return parts.join(' ')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function toHex(n: number): string {
|
|
363
|
+
return Math.round(n).toString(16).padStart(2, '0')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Converts a property name enum to a CSS property string
|
|
368
|
+
*/
|
|
369
|
+
function propertyNameToString(property: string): string {
|
|
370
|
+
// lightningcss provides property names in kebab-case format
|
|
371
|
+
// Handle both string names and enum values
|
|
372
|
+
if (typeof property === 'string') {
|
|
373
|
+
// Convert camelCase to kebab-case if needed
|
|
374
|
+
return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
|
375
|
+
}
|
|
376
|
+
return String(property)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Converts a declaration AST back to a CSS string using lightningcss's own serializer.
|
|
381
|
+
* This approach uses lightningcss transform with a visitor to inject the AST node
|
|
382
|
+
* into a dummy rule, then extracts the serialized value from the output.
|
|
383
|
+
*/
|
|
384
|
+
function propertyValueToString(decl: { value: unknown; property: string }): string {
|
|
385
|
+
try {
|
|
386
|
+
// Create a dummy CSS rule with a placeholder value
|
|
387
|
+
const dummyCss = `.x { ${decl.property}: 0; }`
|
|
388
|
+
|
|
389
|
+
const result = transform({
|
|
390
|
+
filename: 'input.css',
|
|
391
|
+
code: Buffer.from(dummyCss),
|
|
392
|
+
minify: false,
|
|
393
|
+
visitor: {
|
|
394
|
+
// Use property-specific visitor by creating an object with the property name as key
|
|
395
|
+
Declaration: {
|
|
396
|
+
[decl.property]: () => decl,
|
|
397
|
+
} as Record<string, () => typeof decl>,
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// Extract just the property value from the output
|
|
402
|
+
const output = result.code.toString()
|
|
403
|
+
const match = output.match(/\.x\s*\{\s*([^}]+)\s*\}/)
|
|
404
|
+
|
|
405
|
+
if (match) {
|
|
406
|
+
// Parse the property: value pair and extract just the value
|
|
407
|
+
const declaration = match[1].trim()
|
|
408
|
+
const colonIndex = declaration.indexOf(':')
|
|
409
|
+
if (colonIndex !== -1) {
|
|
410
|
+
return declaration
|
|
411
|
+
.slice(colonIndex + 1)
|
|
412
|
+
.trim()
|
|
413
|
+
.replace(/;$/, '')
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return ''
|
|
418
|
+
} catch {
|
|
419
|
+
// Fallback for any errors
|
|
420
|
+
return ''
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Checks if a CSS property value is a shorthand (has multiple space-separated values)
|
|
426
|
+
* Ignores spaces inside parentheses (like in var() or calc())
|
|
427
|
+
*/
|
|
428
|
+
function isShorthandValue(value: string): boolean {
|
|
429
|
+
let depth = 0
|
|
430
|
+
let result = ''
|
|
431
|
+
|
|
432
|
+
for (const char of value) {
|
|
433
|
+
if (char === '(') {
|
|
434
|
+
depth++
|
|
435
|
+
} else if (char === ')') {
|
|
436
|
+
depth--
|
|
437
|
+
} else if (depth === 0) {
|
|
438
|
+
result += char
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return result.trim().includes(' ')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Escapes special regex characters in a string to use it as a literal pattern
|
|
447
|
+
*/
|
|
448
|
+
function escapeRegex(str: string): string {
|
|
449
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
450
|
+
}
|