@wix/zero-config-implementation 1.89.0 → 1.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +22 -5
- package/dist/index.js +6176 -6112
- package/package.json +2 -2
- package/src/converters/to-editor-component.test.ts +85 -0
- package/src/converters/to-editor-component.ts +39 -0
- package/src/index.ts +11 -3
- package/src/information-extractors/css/parse.ts +18 -2
- package/src/information-extractors/css/types.ts +21 -4
- package/src/validators/editor-element-validator.test.ts +142 -0
- package/src/validators/editor-element-validator.ts +64 -0
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.91.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",
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
]
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"falconPackageHash": "
|
|
109
|
+
"falconPackageHash": "7232f116f870e20fe94caa49ba503a0a945ec4eed14c9f9b55544590"
|
|
110
110
|
}
|
|
@@ -225,6 +225,91 @@ describe('toEditorReactComponent — @property design tokens', () => {
|
|
|
225
225
|
})
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
+
it('carries the --min / --max / --step descriptors of a number @property into number limits', () => {
|
|
229
|
+
const component = createComponentWithCss(`
|
|
230
|
+
@property --speed { syntax: "<number>"; inherits: true; initial-value: 4; --min: 1; --max: 10; --step: 0.5; }
|
|
231
|
+
`)
|
|
232
|
+
|
|
233
|
+
const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
|
|
234
|
+
|
|
235
|
+
expect(customProps?.speed).toEqual({
|
|
236
|
+
displayName: 'Speed',
|
|
237
|
+
defaultValue: '4',
|
|
238
|
+
cssPropertyType: 'number',
|
|
239
|
+
number: { min: '1', max: '10', multiplier: '0.5' },
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('emits only the usable half of a partial range and omits the rest', () => {
|
|
244
|
+
const component = createComponentWithCss(`
|
|
245
|
+
@property --weight { syntax: "<number>"; inherits: true; initial-value: 2; --min: 0; }
|
|
246
|
+
@property --tempo { syntax: "<number>"; inherits: true; initial-value: 3; --max: 9; --step: nonsense; }
|
|
247
|
+
@property --offset { syntax: "<number>"; inherits: true; initial-value: 0; --min: ; --max: 12; }
|
|
248
|
+
`)
|
|
249
|
+
|
|
250
|
+
const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
|
|
251
|
+
|
|
252
|
+
expect(customProps?.weight).toEqual({
|
|
253
|
+
displayName: 'Weight',
|
|
254
|
+
defaultValue: '2',
|
|
255
|
+
cssPropertyType: 'number',
|
|
256
|
+
number: { min: '0' },
|
|
257
|
+
})
|
|
258
|
+
expect(customProps?.tempo).toEqual({
|
|
259
|
+
displayName: 'Tempo',
|
|
260
|
+
defaultValue: '3',
|
|
261
|
+
cssPropertyType: 'number',
|
|
262
|
+
number: { max: '9' },
|
|
263
|
+
})
|
|
264
|
+
expect(customProps?.offset).toEqual({
|
|
265
|
+
displayName: 'Offset',
|
|
266
|
+
defaultValue: '0',
|
|
267
|
+
cssPropertyType: 'number',
|
|
268
|
+
number: { max: '12' },
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('drops an inverted range and a non-positive step, omitting `number` when nothing is usable', () => {
|
|
273
|
+
const component = createComponentWithCss(`
|
|
274
|
+
@property --span { syntax: "<number>"; inherits: true; initial-value: 5; --min: 10; --max: 2; --step: 0; }
|
|
275
|
+
@property --frozen { syntax: "<number>"; inherits: true; initial-value: 5; --min: 5; --max: 5; --step: 1; }
|
|
276
|
+
`)
|
|
277
|
+
|
|
278
|
+
const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
|
|
279
|
+
|
|
280
|
+
expect(customProps?.span).toEqual({
|
|
281
|
+
displayName: 'Span',
|
|
282
|
+
defaultValue: '5',
|
|
283
|
+
cssPropertyType: 'number',
|
|
284
|
+
})
|
|
285
|
+
expect(customProps?.frozen).toEqual({
|
|
286
|
+
displayName: 'Frozen',
|
|
287
|
+
defaultValue: '5',
|
|
288
|
+
cssPropertyType: 'number',
|
|
289
|
+
number: { multiplier: '1' },
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('ignores range descriptors on a non-number @property', () => {
|
|
294
|
+
const component = createComponentWithCss(`
|
|
295
|
+
@property --sdf-safelight-color { syntax: "<color>"; inherits: true; initial-value: #F02011; --min: 1; --max: 10; }
|
|
296
|
+
@property --gap { syntax: "<length>"; inherits: true; initial-value: 4px; --min: 1; --max: 10; --step: 1; }
|
|
297
|
+
`)
|
|
298
|
+
|
|
299
|
+
const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
|
|
300
|
+
|
|
301
|
+
expect(customProps?.['sdf-safelight-color']).toEqual({
|
|
302
|
+
displayName: 'Sdf Safelight Color',
|
|
303
|
+
defaultValue: '#F02011',
|
|
304
|
+
cssPropertyType: 'color',
|
|
305
|
+
})
|
|
306
|
+
expect(customProps?.gap).toEqual({
|
|
307
|
+
displayName: 'Gap',
|
|
308
|
+
defaultValue: '4px',
|
|
309
|
+
cssPropertyType: 'length',
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
228
313
|
it('emits a customEnum dropdown for a pipe-separated ident-list @property', () => {
|
|
229
314
|
const component = createComponentWithCss(`
|
|
230
315
|
@property --sdf-palette { syntax: "warm | cool | neutral"; inherits: false; initial-value: warm; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CssCustomPropertyItem,
|
|
3
|
+
CssNumber,
|
|
3
4
|
CssPropertyItem,
|
|
4
5
|
CustomPropertyEnum,
|
|
5
6
|
DataItem,
|
|
@@ -324,6 +325,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
324
325
|
? registration.cssPropertyType
|
|
325
326
|
: getVarPropertyTypeFromCssInfos(varName, defaultValue ?? '', component.css)
|
|
326
327
|
const customEnum = buildCustomEnum(registration)
|
|
328
|
+
const numberLimits = buildCssNumber(registration, cssPropertyType)
|
|
327
329
|
const cleanVarName = varName.startsWith('--') ? varName.slice(2) : varName
|
|
328
330
|
|
|
329
331
|
const existingProps = nearestCommonAncestorCustomProps.get(nearestCommonAncestorTraceId) ?? {}
|
|
@@ -332,6 +334,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
332
334
|
...(defaultValue !== undefined && { defaultValue }),
|
|
333
335
|
...(cssPropertyType !== undefined && { cssPropertyType }),
|
|
334
336
|
...(customEnum && { customEnum }),
|
|
337
|
+
...(numberLimits && { number: numberLimits }),
|
|
335
338
|
}
|
|
336
339
|
nearestCommonAncestorCustomProps.set(nearestCommonAncestorTraceId, existingProps)
|
|
337
340
|
}
|
|
@@ -368,6 +371,42 @@ function buildCustomEnum(registration: RegisteredCustomProperty | undefined): Cu
|
|
|
368
371
|
}
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
function parseFiniteDescriptor(rawValue: string | undefined): number | undefined {
|
|
375
|
+
const trimmed = rawValue?.trim()
|
|
376
|
+
if (!trimmed) return undefined
|
|
377
|
+
const parsed = Number(trimmed)
|
|
378
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Builds the clamp the panel's number control applies, from an @property's range
|
|
383
|
+
* descriptors. Anything unusable is dropped rather than passed through.
|
|
384
|
+
*/
|
|
385
|
+
function buildCssNumber(
|
|
386
|
+
registration: RegisteredCustomProperty | undefined,
|
|
387
|
+
cssPropertyType: CssCustomPropertyItem['cssPropertyType'],
|
|
388
|
+
): CssNumber | undefined {
|
|
389
|
+
if (!registration?.range || cssPropertyType !== CSS_PROPERTIES.CSS_DATA_TYPE.number) return undefined
|
|
390
|
+
|
|
391
|
+
let min = parseFiniteDescriptor(registration.range.min)
|
|
392
|
+
let max = parseFiniteDescriptor(registration.range.max)
|
|
393
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
394
|
+
min = undefined
|
|
395
|
+
max = undefined
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const step = parseFiniteDescriptor(registration.range.step)
|
|
399
|
+
const multiplier = step !== undefined && step > 0 ? step : undefined
|
|
400
|
+
|
|
401
|
+
if (min === undefined && max === undefined && multiplier === undefined) return undefined
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
...(min !== undefined && { min: String(min) }),
|
|
405
|
+
...(max !== undefined && { max: String(max) }),
|
|
406
|
+
...(multiplier !== undefined && { multiplier: String(multiplier) }),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
371
410
|
/**
|
|
372
411
|
* Queries each CSS parser API for the property type of a variable and returns
|
|
373
412
|
* the first defined result.
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Result, ResultAsync } from 'neverthrow'
|
|
|
3
3
|
import React, { type ComponentType } from 'react'
|
|
4
4
|
|
|
5
5
|
import { toEditorReactComponent } from './converters'
|
|
6
|
-
import { BaseError, IoError, type NotFoundError, ParseError } from './errors'
|
|
6
|
+
import { BaseError, IoError, type NotFoundError, ParseError, type ValidationError } from './errors'
|
|
7
7
|
import { buildContextProviderModules } from './extensions/context-providers/context'
|
|
8
8
|
import { buildContextAwareWrapper, loadMockProviders } from './extensions/context-providers/mock-provider'
|
|
9
9
|
import { buildRefElementContext } from './extensions/ref-elements/context'
|
|
@@ -16,6 +16,7 @@ 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
18
|
import { compileTsFile } from './ts-compiler'
|
|
19
|
+
import { validateEditorElement } from './validators/editor-element-validator'
|
|
19
20
|
|
|
20
21
|
const defaultWrapper = wrapWithWixServices
|
|
21
22
|
|
|
@@ -52,7 +53,10 @@ export function extractComponentManifestResult(
|
|
|
52
53
|
options?: ExtractComponentManifestOptions,
|
|
53
54
|
): ResultAsync<
|
|
54
55
|
ManifestResult,
|
|
55
|
-
|
|
56
|
+
| InstanceType<typeof NotFoundError>
|
|
57
|
+
| InstanceType<typeof ParseError>
|
|
58
|
+
| InstanceType<typeof IoError>
|
|
59
|
+
| InstanceType<typeof ValidationError>
|
|
56
60
|
> {
|
|
57
61
|
// Step 1: Compile TypeScript (fatal)
|
|
58
62
|
return compileTsFile(componentPath)
|
|
@@ -206,7 +210,11 @@ async function processComponentWithCleanup(
|
|
|
206
210
|
throw processResult.error
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
213
|
+
const editorReactComponent = toEditorReactComponent(processResult.component)
|
|
214
|
+
if (editorReactComponent.editorElement) {
|
|
215
|
+
validateEditorElement(editorReactComponent.editorElement, processResult.component.componentName)
|
|
216
|
+
}
|
|
217
|
+
return { component: editorReactComponent, errors: [] }
|
|
210
218
|
} finally {
|
|
211
219
|
await cleanup()
|
|
212
220
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
CssDataType,
|
|
10
10
|
NativePseudoClass,
|
|
11
11
|
RegisteredCustomProperty,
|
|
12
|
+
RegisteredPropertyRange,
|
|
12
13
|
StateClassRule,
|
|
13
14
|
} from './types'
|
|
14
15
|
|
|
@@ -84,14 +85,14 @@ export function parseCss(cssString: string): CSSParserAPI {
|
|
|
84
85
|
return getSelectorSpecificity(parsed)
|
|
85
86
|
},
|
|
86
87
|
|
|
87
|
-
getVarPropertyType(varName: string, defaultValue: string): string
|
|
88
|
+
getVarPropertyType(varName: string, defaultValue: string): string {
|
|
88
89
|
// A `@property` registration is the declared type and wins over usage inference.
|
|
89
90
|
const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
|
|
90
91
|
const registered = registeredProperties.get(normalizedVarName)
|
|
91
92
|
if (registered) return registered.cssPropertyType
|
|
92
93
|
|
|
93
94
|
const usages = this.getVarUsages(varName)
|
|
94
|
-
if (usages.length === 0) return
|
|
95
|
+
if (usages.length === 0) return inferCssDataType(defaultValue)
|
|
95
96
|
const uniqueProperties = [...new Set(usages)]
|
|
96
97
|
if (uniqueProperties.length === 1) {
|
|
97
98
|
const camelCased = camelCase(uniqueProperties[0])
|
|
@@ -501,6 +502,19 @@ function extractAtruleDescriptors(block: CssNode): Map<string, string> {
|
|
|
501
502
|
return descriptors
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
/** Picks the range descriptors out of the block; css-tree collects them like any other declaration. */
|
|
506
|
+
function extractRangeDescriptors(descriptors: Map<string, string>): RegisteredPropertyRange | undefined {
|
|
507
|
+
const min = descriptors.get('--min')
|
|
508
|
+
const max = descriptors.get('--max')
|
|
509
|
+
const step = descriptors.get('--step')
|
|
510
|
+
if (min === undefined && max === undefined && step === undefined) return undefined
|
|
511
|
+
return {
|
|
512
|
+
...(min !== undefined && { min }),
|
|
513
|
+
...(max !== undefined && { max }),
|
|
514
|
+
...(step !== undefined && { step }),
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
504
518
|
/**
|
|
505
519
|
* Walks the stylesheet for `@property` at-rules and builds a map of registered custom
|
|
506
520
|
* properties keyed by the `--`-prefixed variable name.
|
|
@@ -528,11 +542,13 @@ function parseRegisteredProperties(cssString: string): Map<string, RegisteredCus
|
|
|
528
542
|
if (!resolved) return
|
|
529
543
|
const { cssPropertyType, enumOptions } = resolved
|
|
530
544
|
const initialValue = descriptors.get('initial-value')
|
|
545
|
+
const range = extractRangeDescriptors(descriptors)
|
|
531
546
|
|
|
532
547
|
registered.set(varName, {
|
|
533
548
|
cssPropertyType,
|
|
534
549
|
...(enumOptions && { enumOptions }),
|
|
535
550
|
...(initialValue !== undefined && { defaultValue: initialValue }),
|
|
551
|
+
...(range && { range }),
|
|
536
552
|
})
|
|
537
553
|
},
|
|
538
554
|
})
|
|
@@ -47,6 +47,23 @@ export interface RegisteredCustomProperty {
|
|
|
47
47
|
* (`a | b | c`). Present only when `cssPropertyType` is `customEnum`.
|
|
48
48
|
*/
|
|
49
49
|
enumOptions?: string[]
|
|
50
|
+
/**
|
|
51
|
+
* The numeric range for the control, as raw descriptor strings. Validated at
|
|
52
|
+
* conversion time. Meaningful only when `cssPropertyType` is `number`.
|
|
53
|
+
*/
|
|
54
|
+
range?: RegisteredPropertyRange
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `--min` / `--max` / `--step` descriptors an importer may emit inside an `@property`
|
|
59
|
+
* block. The `--` prefix keeps them in the author namespace: browsers ignore descriptors
|
|
60
|
+
* they don't know, so the CSS stays valid and CSS can't standardise these names out from
|
|
61
|
+
* under us.
|
|
62
|
+
*/
|
|
63
|
+
export interface RegisteredPropertyRange {
|
|
64
|
+
min?: string
|
|
65
|
+
max?: string
|
|
66
|
+
step?: string
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
export type NativePseudoClass = 'hover' | 'focus' | 'disabled' | 'invalid'
|
|
@@ -120,13 +137,13 @@ export interface CSSParserAPI {
|
|
|
120
137
|
* Determines the CSS property type for a custom property.
|
|
121
138
|
* A `@property` registration is the top-priority source (declared type). Otherwise
|
|
122
139
|
* falls back to usage: if all usages of varName are within the same CSS property,
|
|
123
|
-
* returns that property name; if usages differ,
|
|
124
|
-
*
|
|
125
|
-
*
|
|
140
|
+
* returns that property name; if usages differ, or if the variable is not used via
|
|
141
|
+
* var() at all, returns the CSS data type inferred from the initial value
|
|
142
|
+
* ('color', 'length', 'number', or 'string').
|
|
126
143
|
* @param varName - The CSS variable name (with or without --)
|
|
127
144
|
* @param defaultValue - The initial value string of the custom property
|
|
128
145
|
*/
|
|
129
|
-
getVarPropertyType: (varName: string, defaultValue: string) => string
|
|
146
|
+
getVarPropertyType: (varName: string, defaultValue: string) => string
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
149
|
* Returns every custom property registered via a `@property` at-rule, keyed by the
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { CssCustomPropertyItem, EditorElement, ElementItem } from '@wix/react-component-schema'
|
|
2
|
+
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { ValidationError } from '../errors'
|
|
6
|
+
import { validateEditorElement } from './editor-element-validator'
|
|
7
|
+
|
|
8
|
+
function buildMinimalEditorElement(overrides: Partial<EditorElement> = {}): EditorElement {
|
|
9
|
+
return {
|
|
10
|
+
selector: '.root',
|
|
11
|
+
displayName: 'Test Component',
|
|
12
|
+
...overrides,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildInlineElementItem(overrides: Partial<ElementItem> = {}): ElementItem {
|
|
17
|
+
return {
|
|
18
|
+
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
19
|
+
inlineElement: {
|
|
20
|
+
selector: '.child',
|
|
21
|
+
displayName: 'Child',
|
|
22
|
+
},
|
|
23
|
+
...overrides,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildCssCustomPropertyItem(cssPropertyType: CssCustomPropertyItem['cssPropertyType']): CssCustomPropertyItem {
|
|
28
|
+
return { cssPropertyType }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('validateEditorElement', () => {
|
|
32
|
+
it('does not throw for a valid minimal editorElement', () => {
|
|
33
|
+
const editorElement = buildMinimalEditorElement()
|
|
34
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).not.toThrow()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not throw when cssCustomProperties have valid property types', () => {
|
|
38
|
+
const editorElement = buildMinimalEditorElement({
|
|
39
|
+
cssCustomProperties: {
|
|
40
|
+
color: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color),
|
|
41
|
+
size: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.length),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).not.toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws ValidationError when a cssCustomProperty has UNKNOWN cssPropertyType', () => {
|
|
48
|
+
const editorElement = buildMinimalEditorElement({
|
|
49
|
+
cssCustomProperties: {
|
|
50
|
+
brokenProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('includes the field path in the error message for UNKNOWN cssPropertyType', () => {
|
|
57
|
+
const editorElement = buildMinimalEditorElement({
|
|
58
|
+
cssCustomProperties: {
|
|
59
|
+
brokenProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
63
|
+
'editorElement.cssCustomProperties.brokenProp',
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('throws ValidationError when a cssCustomProperty is missing cssPropertyType', () => {
|
|
68
|
+
const editorElement = buildMinimalEditorElement({
|
|
69
|
+
cssCustomProperties: {
|
|
70
|
+
missingTypeProp: {} as CssCustomPropertyItem,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('throws ValidationError when an elements entry has UNKNOWN elementType', () => {
|
|
77
|
+
const editorElement = buildMinimalEditorElement({
|
|
78
|
+
elements: {
|
|
79
|
+
child: {
|
|
80
|
+
elementType: ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType,
|
|
81
|
+
inlineElement: { selector: '.child', displayName: 'Child' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('throws ValidationError when an elements entry is missing elementType', () => {
|
|
89
|
+
const editorElement = buildMinimalEditorElement({
|
|
90
|
+
elements: {
|
|
91
|
+
child: buildInlineElementItem({ elementType: undefined }),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('collects all violations and throws once with all of them listed', () => {
|
|
98
|
+
const editorElement = buildMinimalEditorElement({
|
|
99
|
+
cssCustomProperties: {
|
|
100
|
+
badProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
101
|
+
},
|
|
102
|
+
elements: {
|
|
103
|
+
child: {
|
|
104
|
+
elementType: ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType,
|
|
105
|
+
inlineElement: { selector: '.child', displayName: 'Child' },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
110
|
+
/editorElement\.cssCustomProperties\.badProp.*\n.*editorElement\.elements\.child/s,
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('validates nested inline element cssCustomProperties recursively', () => {
|
|
115
|
+
const editorElement = buildMinimalEditorElement({
|
|
116
|
+
elements: {
|
|
117
|
+
child: {
|
|
118
|
+
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
119
|
+
inlineElement: {
|
|
120
|
+
selector: '.child',
|
|
121
|
+
displayName: 'Child',
|
|
122
|
+
cssCustomProperties: {
|
|
123
|
+
nestedBad: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
130
|
+
'editorElement.elements.child.inlineElement.cssCustomProperties.nestedBad',
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('includes the component name in the error message', () => {
|
|
135
|
+
const editorElement = buildMinimalEditorElement({
|
|
136
|
+
cssCustomProperties: {
|
|
137
|
+
badProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
expect(() => validateEditorElement(editorElement, 'MyButton')).toThrow('MyButton')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CssCustomPropertyItem,
|
|
3
|
+
EditorReactComponent,
|
|
4
|
+
ElementItem,
|
|
5
|
+
InlineElement,
|
|
6
|
+
} from '@wix/react-component-schema'
|
|
7
|
+
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
8
|
+
|
|
9
|
+
import { ValidationError } from '../errors'
|
|
10
|
+
|
|
11
|
+
type RawEditorElement = NonNullable<EditorReactComponent['editorElement']>
|
|
12
|
+
type AnyElement = RawEditorElement | InlineElement
|
|
13
|
+
|
|
14
|
+
export function validateEditorElement(editorElement: RawEditorElement, componentName: string): void {
|
|
15
|
+
const violations = collectElementViolations(editorElement, 'editorElement')
|
|
16
|
+
if (violations.length === 0) return
|
|
17
|
+
|
|
18
|
+
const violationList = violations.map((violation) => ` - ${violation}`).join('\n')
|
|
19
|
+
throw new ValidationError(`Component "${componentName}" produced an invalid editorElement:\n${violationList}`, {
|
|
20
|
+
props: { phase: 'conversion' },
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectElementViolations(element: AnyElement, elementPath: string): string[] {
|
|
25
|
+
return [
|
|
26
|
+
...validateCssCustomProperties(element.cssCustomProperties, elementPath),
|
|
27
|
+
...validateElementsMap(element.elements, elementPath),
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateCssCustomProperties(
|
|
32
|
+
cssCustomProperties: Record<string, CssCustomPropertyItem> | undefined,
|
|
33
|
+
elementPath: string,
|
|
34
|
+
): string[] {
|
|
35
|
+
if (!cssCustomProperties) return []
|
|
36
|
+
|
|
37
|
+
const violations: string[] = []
|
|
38
|
+
for (const [propKey, propValue] of Object.entries(cssCustomProperties)) {
|
|
39
|
+
const unknownType = CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType
|
|
40
|
+
if (!propValue.cssPropertyType || propValue.cssPropertyType === unknownType) {
|
|
41
|
+
violations.push(`${elementPath}.cssCustomProperties.${propKey}: cssPropertyType is not set or UNKNOWN`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return violations
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateElementsMap(elements: Record<string, ElementItem> | undefined, elementPath: string): string[] {
|
|
48
|
+
if (!elements) return []
|
|
49
|
+
|
|
50
|
+
const violations: string[] = []
|
|
51
|
+
for (const [elementKey, elementItem] of Object.entries(elements)) {
|
|
52
|
+
const itemPath = `${elementPath}.elements.${elementKey}`
|
|
53
|
+
const unknownType = ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType
|
|
54
|
+
|
|
55
|
+
if (!elementItem.elementType || elementItem.elementType === unknownType) {
|
|
56
|
+
violations.push(`${itemPath}: elementType is not set or UNKNOWN`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (elementItem.inlineElement) {
|
|
60
|
+
violations.push(...collectElementViolations(elementItem.inlineElement, `${itemPath}.inlineElement`))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return violations
|
|
64
|
+
}
|