@wix/zero-config-implementation 1.91.0 → 1.93.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.js +5523 -5516
- package/package.json +2 -2
- package/src/converters/to-editor-component.test.ts +65 -0
- package/src/converters/to-editor-component.ts +9 -3
- package/src/information-extractors/react/extractors/css-properties.test.ts +32 -5
- package/src/information-extractors/react/extractors/css-properties.ts +31 -17
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.93.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": "d7c036bb07aaafe708985efa56e88c2eefb34077d2d892938112978f"
|
|
110
110
|
}
|
|
@@ -166,6 +166,71 @@ describe('toEditorReactComponent display conversion', () => {
|
|
|
166
166
|
})
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
describe('toEditorReactComponent selector conversion', () => {
|
|
170
|
+
it.each([
|
|
171
|
+
['a', 'a___'],
|
|
172
|
+
['div', 'div_'],
|
|
173
|
+
['span', 'span'],
|
|
174
|
+
])('pads the root selector %j to at least four characters', (tag, expectedSelector) => {
|
|
175
|
+
const rootElement = createElementWithDisplayMatcher(tag, matcherDataFromCss(''))
|
|
176
|
+
|
|
177
|
+
const editorElement = toEditorReactComponent(createComponent(rootElement)).editorElement
|
|
178
|
+
|
|
179
|
+
expect(editorElement?.selector).toBe(expectedSelector)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('pads a short semantic class selector', () => {
|
|
183
|
+
const rootElement = createSemanticElement({
|
|
184
|
+
traceId: 'trace-root',
|
|
185
|
+
name: 'root',
|
|
186
|
+
className: 'a',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const editorElement = toEditorReactComponent(createComponent(rootElement)).editorElement
|
|
190
|
+
|
|
191
|
+
expect(editorElement?.selector).toBe('.a__')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('pads short inline and ref element selectors', () => {
|
|
195
|
+
const inlineElement: ExtractedElement = {
|
|
196
|
+
traceId: 'trace-link',
|
|
197
|
+
name: 'link',
|
|
198
|
+
tag: 'a',
|
|
199
|
+
attributes: {},
|
|
200
|
+
extractorData: new Map<string, unknown>(),
|
|
201
|
+
children: [],
|
|
202
|
+
}
|
|
203
|
+
const refElement: ExtractedElement = {
|
|
204
|
+
traceId: 'trace-action-button',
|
|
205
|
+
name: 'actionButton',
|
|
206
|
+
tag: 'div',
|
|
207
|
+
attributes: {},
|
|
208
|
+
extractorData: new Map<string, unknown>([
|
|
209
|
+
[
|
|
210
|
+
'ref-element',
|
|
211
|
+
{
|
|
212
|
+
elementPropsPath: 'actionButton',
|
|
213
|
+
refComponentType: 'test.Button',
|
|
214
|
+
selector: '.a',
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
]),
|
|
218
|
+
children: [],
|
|
219
|
+
}
|
|
220
|
+
const rootElement = createSemanticElement({
|
|
221
|
+
traceId: 'trace-root',
|
|
222
|
+
name: 'root',
|
|
223
|
+
className: 'root',
|
|
224
|
+
children: [inlineElement, refElement],
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const elements = toEditorReactComponent(createComponent(rootElement)).editorElement?.elements
|
|
228
|
+
|
|
229
|
+
expect(elements?.link?.inlineElement?.selector).toBe('a___')
|
|
230
|
+
expect(elements?.actionButton?.refElement?.selector).toBe('.a__')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
169
234
|
function cssInfoFromStylesheet(cssString: string): ExtractedCssInfo {
|
|
170
235
|
const api = parseCss(cssString)
|
|
171
236
|
const properties = new Map<string, string>()
|
|
@@ -34,6 +34,8 @@ import type { StateClassTrigger } from './state-class-trigger'
|
|
|
34
34
|
import { buildStatesBlock } from './states-builder'
|
|
35
35
|
import { formatDisplayName } from './utils'
|
|
36
36
|
|
|
37
|
+
const MINIMUM_SELECTOR_LENGTH = 4
|
|
38
|
+
|
|
37
39
|
/**
|
|
38
40
|
* Merges native and custom state blocks. Native states take precedence on any
|
|
39
41
|
* key collision (e.g. an author wrapping `isDisabled` in `ElementState<>` does not
|
|
@@ -140,9 +142,13 @@ function getSemanticBlockName(element: ExtractedElement | undefined): string | u
|
|
|
140
142
|
function buildSelector(rootElement?: ExtractedElement): string {
|
|
141
143
|
if (rootElement?.attributes.class) {
|
|
142
144
|
const semanticClass = findPreferredSemanticClass(normalizeClassNames(rootElement.attributes.class))
|
|
143
|
-
if (semanticClass) return `.${semanticClass}`
|
|
145
|
+
if (semanticClass) return padSelector(`.${semanticClass}`)
|
|
144
146
|
}
|
|
145
|
-
return rootElement?.tag ?? ''
|
|
147
|
+
return padSelector(rootElement?.tag ?? '')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function padSelector(selector: string): string {
|
|
151
|
+
return selector.padEnd(MINIMUM_SELECTOR_LENGTH, '_')
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
function buildInnerElementDisplayName(element: ExtractedElement, ancestorSemanticClasses: string[]): string {
|
|
@@ -206,7 +212,7 @@ function buildElements(
|
|
|
206
212
|
result[buildRefElementManifestKey(refElementMatch.elementPropsPath, parentElementPropsPath)] = {
|
|
207
213
|
elementType: ELEMENTS.ELEMENT_TYPE.refElement,
|
|
208
214
|
refElement: {
|
|
209
|
-
selector: refElementMatch.selector,
|
|
215
|
+
selector: padSelector(refElementMatch.selector),
|
|
210
216
|
type: refElementMatch.refComponentType,
|
|
211
217
|
},
|
|
212
218
|
}
|
|
@@ -38,7 +38,7 @@ function createElementWithCssProperties(
|
|
|
38
38
|
): ExtractedElement {
|
|
39
39
|
const extractorData = new Map<string, unknown>()
|
|
40
40
|
extractorData.set('css-matcher', matcherDataFromCss(declarations))
|
|
41
|
-
extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes
|
|
41
|
+
extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes) })
|
|
42
42
|
|
|
43
43
|
return {
|
|
44
44
|
traceId: 'trace-1',
|
|
@@ -173,11 +173,38 @@ describe('getCssPropertiesForTag', () => {
|
|
|
173
173
|
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
it('
|
|
177
|
-
const
|
|
176
|
+
it('gives interactive controls both box and text properties', () => {
|
|
177
|
+
for (const tag of ['button', 'select', 'textarea']) {
|
|
178
|
+
const relevantProperties = getCssPropertiesForTag(tag)
|
|
179
|
+
|
|
180
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
181
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
182
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('treats an input with no type as text-bearing', () => {
|
|
187
|
+
const relevantProperties = getCssPropertiesForTag('input')
|
|
178
188
|
|
|
179
189
|
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
180
|
-
expect(relevantProperties).
|
|
190
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('treats text-entry and button-like input types as text-bearing', () => {
|
|
194
|
+
for (const inputType of ['text', 'email', 'password', 'search', 'number', 'submit', 'button', 'reset']) {
|
|
195
|
+
const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
|
|
196
|
+
|
|
197
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('keeps typography off input types that render no text of their own', () => {
|
|
202
|
+
for (const inputType of ['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden']) {
|
|
203
|
+
const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
|
|
204
|
+
|
|
205
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
206
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
207
|
+
}
|
|
181
208
|
})
|
|
182
209
|
|
|
183
210
|
it('keeps media elements out of box properties even when their default display is non-inline', () => {
|
|
@@ -203,7 +230,7 @@ describe('getCssPropertiesForTag', () => {
|
|
|
203
230
|
})
|
|
204
231
|
|
|
205
232
|
it('keeps role-based heading text-only by default', () => {
|
|
206
|
-
const relevantProperties = getCssPropertiesForTag('div', 'heading')
|
|
233
|
+
const relevantProperties = getCssPropertiesForTag('div', { role: 'heading' })
|
|
207
234
|
|
|
208
235
|
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
209
236
|
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
@@ -19,12 +19,18 @@ export interface CssPropertiesData {
|
|
|
19
19
|
relevant: string[]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface SemanticAttributes {
|
|
23
|
+
role?: string
|
|
24
|
+
type?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
28
|
// Semantic Traits
|
|
24
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
|
-
* Text & Typography - elements where readability and hierarchy are primary
|
|
32
|
+
* Text & Typography - elements where readability and hierarchy are primary.
|
|
33
|
+
* Form controls render their own text; `input` varies by type (see below).
|
|
28
34
|
*/
|
|
29
35
|
const TEXT_TAGS = new Set([
|
|
30
36
|
'h1',
|
|
@@ -42,8 +48,14 @@ const TEXT_TAGS = new Set([
|
|
|
42
48
|
'em',
|
|
43
49
|
'b',
|
|
44
50
|
'i',
|
|
51
|
+
'button',
|
|
52
|
+
'select',
|
|
53
|
+
'textarea',
|
|
45
54
|
])
|
|
46
55
|
|
|
56
|
+
/** Anything else - including a missing or invalid `type` - defaults to text. */
|
|
57
|
+
const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden'])
|
|
58
|
+
|
|
47
59
|
const DEFAULT_NON_BOX_TEXT_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
48
60
|
|
|
49
61
|
/**
|
|
@@ -107,13 +119,15 @@ const MEDIA_CSS_PROPERTIES = [
|
|
|
107
119
|
CSS_PROPERTY_TYPE.boxShadow,
|
|
108
120
|
]
|
|
109
121
|
|
|
110
|
-
function hasTextSemantics(tag: string,
|
|
122
|
+
function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
|
|
123
|
+
const normalizedRole = attributes.role?.toLowerCase()
|
|
124
|
+
if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
const normalizedTag = tag.toLowerCase()
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
|
|
115
|
-
return true
|
|
116
|
-
}
|
|
129
|
+
if (normalizedTag === 'input') {
|
|
130
|
+
return !NON_TEXT_INPUT_TYPES.has((attributes.type ?? 'text').toLowerCase())
|
|
117
131
|
}
|
|
118
132
|
return TEXT_TAGS.has(normalizedTag)
|
|
119
133
|
}
|
|
@@ -135,11 +149,11 @@ function hasMediaSemantics(tag: string, role?: string): boolean {
|
|
|
135
149
|
* Note: hasTextContent determination is deferred to tree building since
|
|
136
150
|
* we can't know during createElement if a child will have text content.
|
|
137
151
|
*/
|
|
138
|
-
export function getCssPropertiesForTag(tag: string,
|
|
152
|
+
export function getCssPropertiesForTag(tag: string, attributes: SemanticAttributes = {}): string[] {
|
|
139
153
|
const properties: string[] = []
|
|
140
|
-
const hasTextProperties = hasTextSemantics(tag,
|
|
141
|
-
const hasMediaProperties = hasMediaSemantics(tag, role)
|
|
142
|
-
const hasBoxProperties = hasDefaultBoxProperties(tag, role) && !hasMediaProperties
|
|
154
|
+
const hasTextProperties = hasTextSemantics(tag, attributes)
|
|
155
|
+
const hasMediaProperties = hasMediaSemantics(tag, attributes.role)
|
|
156
|
+
const hasBoxProperties = hasDefaultBoxProperties(tag, attributes.role) && !hasMediaProperties
|
|
143
157
|
|
|
144
158
|
if (hasTextProperties) {
|
|
145
159
|
properties.push(...TEXT_CSS_PROPERTIES)
|
|
@@ -465,12 +479,11 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
|
|
|
465
479
|
*/
|
|
466
480
|
export function enrichContainerProperties(elements: ExtractedElement[]): ExtractedElement[] {
|
|
467
481
|
return elements.map((element) => {
|
|
468
|
-
|
|
469
|
-
if (hasTextSemantics(element.tag, role)) {
|
|
482
|
+
if (hasTextSemantics(element.tag, element.attributes)) {
|
|
470
483
|
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
471
484
|
const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
|
|
472
485
|
|
|
473
|
-
if (cssData && hasEffectiveContainerDisplay(element.tag, role, matcherData)) {
|
|
486
|
+
if (cssData && hasEffectiveContainerDisplay(element.tag, element.attributes.role, matcherData)) {
|
|
474
487
|
element.extractorData.set('css-properties', {
|
|
475
488
|
relevant: addBoxProperties(cssData.relevant),
|
|
476
489
|
})
|
|
@@ -500,9 +513,10 @@ export function createCssPropertiesExtractor(): ReactExtractor {
|
|
|
500
513
|
if (!event.isDomElement || !event.tag || !event.traceId) return
|
|
501
514
|
|
|
502
515
|
const { tag, props, traceId, store } = event
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
516
|
+
const relevant = getCssPropertiesForTag(tag, {
|
|
517
|
+
role: props.role as string | undefined,
|
|
518
|
+
type: props.type as string | undefined,
|
|
519
|
+
})
|
|
506
520
|
const data: CssPropertiesData = { relevant }
|
|
507
521
|
|
|
508
522
|
store.set(traceId, 'css-properties', data)
|