@wix/zero-config-implementation 1.89.0 → 1.90.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.89.0",
7
+ "version": "1.90.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": "c9dcb49c34e33e60436cbc45d7c84a7433975392a119386339f5495f"
109
+ "falconPackageHash": "3d380033b2cb76ee95040a71fda50b5bbee8d8cb70460bfb8c8a5313"
110
110
  }
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
- InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError> | InstanceType<typeof IoError>
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
- return { component: toEditorReactComponent(processResult.component), errors: [] }
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
  }
@@ -84,14 +84,14 @@ export function parseCss(cssString: string): CSSParserAPI {
84
84
  return getSelectorSpecificity(parsed)
85
85
  },
86
86
 
87
- getVarPropertyType(varName: string, defaultValue: string): string | undefined {
87
+ getVarPropertyType(varName: string, defaultValue: string): string {
88
88
  // A `@property` registration is the declared type and wins over usage inference.
89
89
  const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
90
90
  const registered = registeredProperties.get(normalizedVarName)
91
91
  if (registered) return registered.cssPropertyType
92
92
 
93
93
  const usages = this.getVarUsages(varName)
94
- if (usages.length === 0) return undefined
94
+ if (usages.length === 0) return inferCssDataType(defaultValue)
95
95
  const uniqueProperties = [...new Set(usages)]
96
96
  if (uniqueProperties.length === 1) {
97
97
  const camelCased = camelCase(uniqueProperties[0])
@@ -120,13 +120,13 @@ export interface CSSParserAPI {
120
120
  * Determines the CSS property type for a custom property.
121
121
  * A `@property` registration is the top-priority source (declared type). Otherwise
122
122
  * falls back to usage: if all usages of varName are within the same CSS property,
123
- * returns that property name; if usages differ, returns the CSS data type inferred
124
- * from the initial value ('color', 'length', 'number', or 'string').
125
- * Returns undefined if the variable is neither registered nor used via var().
123
+ * returns that property name; if usages differ, or if the variable is not used via
124
+ * var() at all, returns the CSS data type inferred from the initial value
125
+ * ('color', 'length', 'number', or 'string').
126
126
  * @param varName - The CSS variable name (with or without --)
127
127
  * @param defaultValue - The initial value string of the custom property
128
128
  */
129
- getVarPropertyType: (varName: string, defaultValue: string) => string | undefined
129
+ getVarPropertyType: (varName: string, defaultValue: string) => string
130
130
 
131
131
  /**
132
132
  * 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
+ }