@wix/origami-transforms 0.1.11 → 0.1.13
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.
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** component-wrapper-server's `PACKAGE_SCOPE` (`src/wrapper/packageTarball.ts`). */
|
|
2
|
+
export const WRAPPERS_SCOPE: "@origami-wrappers";
|
|
3
|
+
export function wrapperPackageName(module: string): string;
|
|
4
|
+
export function wrapperImport({ module, symbol }: {
|
|
5
|
+
readonly module: string;
|
|
6
|
+
readonly symbol: string;
|
|
7
|
+
}): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-transforms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Babel and PostCSS checks and transforms for Wix origami page files.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/core": "^7.26.0",
|
|
18
18
|
"@wix/origami-fonts": "1.0.1",
|
|
19
|
-
"@wix/origami-system-components": "1.
|
|
19
|
+
"@wix/origami-system-components": "1.8.0",
|
|
20
20
|
"postcss": "^8.5.3",
|
|
21
21
|
"postcss-custom-media": "^12.0.1"
|
|
22
22
|
},
|
|
@@ -50,9 +50,13 @@
|
|
|
50
50
|
".": {
|
|
51
51
|
"types": "./dist/index.d.ts",
|
|
52
52
|
"default": "./src/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./wrapper-import": {
|
|
55
|
+
"types": "./dist/wrapperImport.d.ts",
|
|
56
|
+
"default": "./src/wrapperImport.js"
|
|
53
57
|
}
|
|
54
58
|
},
|
|
55
59
|
"type": "module",
|
|
56
60
|
"types": "./dist/index.d.ts",
|
|
57
|
-
"falconPackageHash": "
|
|
61
|
+
"falconPackageHash": "97d30a95762f9e320a620f5de79aa530f7b592e354a3d6776219c7fa"
|
|
58
62
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// `#comp-1 { --labelAlign: center }`, and this writes the vars that option applies in its place, from the
|
|
3
3
|
// `@custom-enum` blocks in the component wrapper's `custom-enums.css` (see component-wrapper-server's
|
|
4
4
|
// `customEnums.ts`), read from the wrapper's folder: the item's JSX imports the wrapper's CSS, so the blocks aren't in
|
|
5
|
-
// the stylesheet. A stylesheet that `@import`s the wrapper's CSS has them inlined; those are used and removed.
|
|
5
|
+
// the stylesheet. A stylesheet that `@import`s the wrapper's CSS has them inlined; those are used and removed. A ref
|
|
6
|
+
// element's enums are its type's: the wrapper names the type with `@custom-enum-ref`, and they come from that type's
|
|
7
|
+
// wrapper.
|
|
6
8
|
// Native `if()` can't do it: Firefox doesn't have it, and a var named like its key refers to itself.
|
|
7
9
|
//
|
|
8
10
|
// The instance's component comes from the item's structure file (structureFile.js): the element with
|
|
@@ -13,15 +15,49 @@ import { createRequire } from 'node:module'
|
|
|
13
15
|
import { basename, dirname, join } from 'node:path'
|
|
14
16
|
import postcss from 'postcss'
|
|
15
17
|
import { LayoutError } from '../transform/layoutError.js'
|
|
18
|
+
import { WRAPPERS_SCOPE, wrapperImport } from '../wrapperImport.js'
|
|
16
19
|
import { literalIdOf, parseStructure, structureFileOf, walk } from './structureFile.js'
|
|
17
20
|
|
|
18
21
|
const CUSTOM_ENUMS_CSS = 'custom-enums.css'
|
|
19
22
|
// `@origami-wrappers/<module>/<symbol>`, and nothing that could resolve outside its package.
|
|
20
|
-
const WRAPPER_IMPORT =
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
const WRAPPER_IMPORT = new RegExp(`^${WRAPPERS_SCOPE}/[\\w-]+/[\\w-]+$`)
|
|
24
|
+
// A component type, `wixEditorElements.Button`: its module and symbol.
|
|
25
|
+
const COMPONENT_TYPE = /^([\w-]+)\.([\w-]+)$/
|
|
26
|
+
/** The selector's compounds, split at its combinators, outside parentheses and brackets. */
|
|
27
|
+
const compoundsOf = selector => {
|
|
28
|
+
const compounds = ['']
|
|
29
|
+
let depth = 0
|
|
30
|
+
for (const char of selector.trim()) {
|
|
31
|
+
if (char === '(' || char === '[') depth++
|
|
32
|
+
if (char === ')' || char === ']') depth--
|
|
33
|
+
if (depth === 0 && /[\s>+~]/.test(char)) {
|
|
34
|
+
if (compounds.at(-1) !== '') compounds.push('')
|
|
35
|
+
} else compounds[compounds.length - 1] += char
|
|
36
|
+
}
|
|
37
|
+
return compounds.filter(Boolean)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A compound's own ids and classes, `#comp-1` and `.button`, not those in its `:not(…)` or attributes. */
|
|
41
|
+
const namesOf = compound => {
|
|
42
|
+
let own = compound.replace(/\[[^\]]*\]/g, '')
|
|
43
|
+
while (/\([^()]*\)/.test(own)) own = own.replace(/\([^()]*\)/g, '')
|
|
44
|
+
return own.match(/[.#][\w-]+/g) ?? []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The last of `compounds` that `element`'s compounds match, each a descendant of the one before, or -1: an element
|
|
49
|
+
* matches a compound that has all its names, `.button.hamburger-open-button` matching `.button.hamburger-open-button.x`.
|
|
50
|
+
*/
|
|
51
|
+
const lastMatch = (element, compounds) => {
|
|
52
|
+
const covers = (names, wanted) => wanted.every(name => names.includes(name))
|
|
53
|
+
for (let end = compounds.length - 1; end >= 0; end--) {
|
|
54
|
+
if (!covers(compounds[end], element.at(-1))) continue
|
|
55
|
+
let matched = 0
|
|
56
|
+
for (let at = 0; at < end && matched < element.length - 1; at++) if (covers(compounds[at], element[matched])) matched++
|
|
57
|
+
if (matched === element.length - 1) return end
|
|
58
|
+
}
|
|
59
|
+
return -1
|
|
60
|
+
}
|
|
25
61
|
|
|
26
62
|
/** Id → the wrapper import it is rendered from, for elements with a literal `id` and an imported wrapper tag. */
|
|
27
63
|
const importsById = structurePath => {
|
|
@@ -50,20 +86,26 @@ const real = path => {
|
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
* wrapper folder's enums.
|
|
56
|
-
*/
|
|
57
|
-
const enumsById = (structurePath, enumsOf) => {
|
|
89
|
+
/** The folder an `@origami-wrappers/…` import resolves to from the structure file, or undefined when it doesn't. */
|
|
90
|
+
const wrapperFolders = structurePath => {
|
|
58
91
|
const require = createRequire(structurePath)
|
|
59
|
-
|
|
60
|
-
for (const [id, source] of importsById(structurePath)) {
|
|
61
|
-
let folder
|
|
92
|
+
return source => {
|
|
62
93
|
try {
|
|
63
|
-
|
|
94
|
+
return real(dirname(require.resolve(source)))
|
|
64
95
|
} catch {
|
|
65
|
-
|
|
96
|
+
return undefined
|
|
66
97
|
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Id → the enums of every wrapper instance; none for one whose component has no custom enums. `folderOf` gives an
|
|
103
|
+
* import's wrapper folder, and `enumsOf` a wrapper folder's enums.
|
|
104
|
+
*/
|
|
105
|
+
const enumsById = (structurePath, folderOf, enumsOf) => {
|
|
106
|
+
const byId = new Map()
|
|
107
|
+
for (const [id, source] of importsById(structurePath)) {
|
|
108
|
+
const folder = folderOf(source)
|
|
67
109
|
byId.set(id, folder === undefined ? [] : enumsOf(folder))
|
|
68
110
|
}
|
|
69
111
|
return byId
|
|
@@ -90,17 +132,39 @@ const resolve = (rule, selector) => {
|
|
|
90
132
|
const fromDependency = declaration => declaration.source?.input.file?.split(/[\\/]/).includes('node_modules') ?? false
|
|
91
133
|
|
|
92
134
|
/**
|
|
93
|
-
* The `@custom-enum` blocks in the stylesheet: wrapper folder → its enums, with
|
|
94
|
-
* set; the
|
|
135
|
+
* The `@custom-enum` blocks and `@custom-enum-ref` declarations in the stylesheet: wrapper folder → its enums, with
|
|
136
|
+
* each option as the enum's full var set, and its ref elements; the at-rules themselves; and the ones that can't be
|
|
137
|
+
* read.
|
|
95
138
|
*
|
|
96
|
-
* A block sits in its element's rule, `.menu`, or `.menu .
|
|
97
|
-
* set has the default option's value, or `inherit` when the default doesn't set it either, which is
|
|
98
|
-
* that picks no option gets. An option that doesn't set its own key still says which option it is.
|
|
139
|
+
* A block sits in its element's rule, `.menu`, or `.menu .dropdown` for an inline element at any depth. A var an
|
|
140
|
+
* option doesn't set has the default option's value, or `inherit` when the default doesn't set it either, which is
|
|
141
|
+
* what an instance that picks no option gets. An option that doesn't set its own key still says which option it is.
|
|
142
|
+
* A ref declaration sits in its element's rule, `.menu .button.hamburger-open-button`, and names the element's type.
|
|
99
143
|
*/
|
|
100
144
|
const definitionsIn = root => {
|
|
101
145
|
const byFolder = new Map()
|
|
102
146
|
const blocks = []
|
|
103
147
|
const malformed = []
|
|
148
|
+
const definitionOf = file => {
|
|
149
|
+
const folder = real(dirname(file))
|
|
150
|
+
if (!byFolder.has(folder)) byFolder.set(folder, { enums: [], refs: [] })
|
|
151
|
+
return byFolder.get(folder)
|
|
152
|
+
}
|
|
153
|
+
root.walkAtRules('custom-enum-ref', declaration => {
|
|
154
|
+
blocks.push(declaration)
|
|
155
|
+
const type = COMPONENT_TYPE.exec(declaration.params.trim())
|
|
156
|
+
const file = declaration.source?.input.file
|
|
157
|
+
const rule = declaration.parent?.type === 'rule' ? /** @type {import('postcss').Rule} */ (declaration.parent) : undefined
|
|
158
|
+
const [, ...element] = rule ? resolve(rule, rule.selector)[0].split(/\s+/) : []
|
|
159
|
+
if (type === null || file === undefined || element.length === 0) {
|
|
160
|
+
malformed.push(declaration)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
definitionOf(file).refs.push({
|
|
164
|
+
selector: element.join(' '),
|
|
165
|
+
source: wrapperImport({ module: type[1], symbol: type[2] }),
|
|
166
|
+
})
|
|
167
|
+
})
|
|
104
168
|
root.walkAtRules('custom-enum', block => {
|
|
105
169
|
blocks.push(block)
|
|
106
170
|
const keyVar = block.params.trim()
|
|
@@ -125,7 +189,7 @@ const definitionsIn = root => {
|
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
const [selector] = resolve(rule, rule.selector)
|
|
128
|
-
const [
|
|
192
|
+
const [, ...inline] = selector.split(/\s+/)
|
|
129
193
|
const defaults = applied.get(defaultValue)
|
|
130
194
|
const vars = [...new Set([...applied.values()].flatMap(values => Object.keys(values)))]
|
|
131
195
|
const options = Object.fromEntries(
|
|
@@ -134,13 +198,9 @@ const definitionsIn = root => {
|
|
|
134
198
|
Object.fromEntries(vars.map(name => [name, values[name] ?? defaults[name] ?? 'inherit'])),
|
|
135
199
|
])
|
|
136
200
|
)
|
|
137
|
-
|
|
138
|
-
const enums = byFolder.get(folder) ?? []
|
|
139
|
-
byFolder.set(folder, enums)
|
|
140
|
-
enums.push({
|
|
201
|
+
definitionOf(file).enums.push({
|
|
141
202
|
key: keyVar,
|
|
142
203
|
options,
|
|
143
|
-
editorSelector,
|
|
144
204
|
// An inline element's selector inside the editor element's, undefined for the editor element itself.
|
|
145
205
|
inline: inline.length ? inline.join(' ') : undefined,
|
|
146
206
|
})
|
|
@@ -169,33 +229,18 @@ const expansion = (declaration, values) =>
|
|
|
169
229
|
)
|
|
170
230
|
|
|
171
231
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* outermost node to the last rule put after it, so the rules keep their declarations' source order.
|
|
232
|
+
* Removes the stylesheet's `@custom-enum` blocks and `@custom-enum-ref` declarations, which only this plugin reads,
|
|
233
|
+
* and a rule that held nothing else.
|
|
175
234
|
*/
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
})
|
|
183
|
-
.append(expansion(declaration, options))
|
|
184
|
-
let outer = declaration.parent
|
|
185
|
-
for (let parent = declaration.parent; parent && parent.type !== 'root'; parent = parent.parent) {
|
|
186
|
-
if (parent.type === 'atrule') node = parent.clone({ nodes: [] }).append(node)
|
|
187
|
-
outer = parent
|
|
188
|
-
}
|
|
189
|
-
;(inserted.get(outer) ?? outer).after(node)
|
|
190
|
-
inserted.set(outer, node)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Removes the stylesheet's `@custom-enum` blocks, which only this plugin reads. */
|
|
194
|
-
export const removeCustomEnums = root =>
|
|
195
|
-
root.walkAtRules('custom-enum', block => {
|
|
196
|
-
if (block.parent) block.parent.raws.semicolon = true
|
|
197
|
-
block.remove()
|
|
235
|
+
export const removeCustomEnums = root => {
|
|
236
|
+
const emptied = new Set()
|
|
237
|
+
root.walkAtRules(/^custom-enum(?:-ref)?$/, node => {
|
|
238
|
+
if (node.parent) node.parent.raws.semicolon = true
|
|
239
|
+
emptied.add(node.parent)
|
|
240
|
+
node.remove()
|
|
198
241
|
})
|
|
242
|
+
for (const rule of emptied) if (rule?.type === 'rule' && rule.nodes.length === 0) rule.remove()
|
|
243
|
+
}
|
|
199
244
|
|
|
200
245
|
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
201
246
|
export const customEnumsPlugin = {
|
|
@@ -208,24 +253,42 @@ export const customEnumsPlugin = {
|
|
|
208
253
|
if (structurePath === undefined && inlined.blocks.length === 0) return
|
|
209
254
|
if (structurePath !== undefined) context.addDependency?.(structurePath)
|
|
210
255
|
const malformed = [...inlined.malformed]
|
|
211
|
-
// A wrapper's
|
|
256
|
+
// A wrapper's definitions are the ones inlined from its folder, or else those in the folder's custom-enums.css.
|
|
212
257
|
const readFolders = new Map()
|
|
213
|
-
const
|
|
258
|
+
const definitionOf = folder => {
|
|
214
259
|
if (inlined.byFolder.has(folder)) return inlined.byFolder.get(folder)
|
|
215
260
|
if (!readFolders.has(folder)) {
|
|
216
261
|
const file = join(folder, CUSTOM_ENUMS_CSS)
|
|
217
|
-
let
|
|
262
|
+
let definition = { enums: [], refs: [] }
|
|
218
263
|
if (existsSync(file)) {
|
|
219
264
|
context.addDependency?.(file)
|
|
220
265
|
const definitions = definitionsIn(postcss.parse(readFileSync(file, 'utf8'), { from: file }))
|
|
221
266
|
malformed.push(...definitions.malformed)
|
|
222
|
-
|
|
267
|
+
definition = definitions.byFolder.get(folder) ?? definition
|
|
223
268
|
}
|
|
224
|
-
readFolders.set(folder,
|
|
269
|
+
readFolders.set(folder, definition)
|
|
225
270
|
}
|
|
226
271
|
return readFolders.get(folder)
|
|
227
272
|
}
|
|
228
|
-
const
|
|
273
|
+
const folderOf = structurePath === undefined ? () => undefined : wrapperFolders(structurePath)
|
|
274
|
+
// A wrapper's enums, and those of each ref element's type as if the ref were their inline element:
|
|
275
|
+
// `.button.hamburger-open-button` gets the Button's `.button` enums, and `.button.hamburger-open-button .label`
|
|
276
|
+
// its `.button .label` ones. `seen` stops a type that refers back to itself.
|
|
277
|
+
const enumsOf = (folder, seen = new Set()) => {
|
|
278
|
+
if (seen.has(folder)) return []
|
|
279
|
+
const { enums, refs } = definitionOf(folder)
|
|
280
|
+
const refEnums = refs.flatMap(({ selector, source }) => {
|
|
281
|
+
const refFolder = folderOf(source)
|
|
282
|
+
return refFolder === undefined
|
|
283
|
+
? []
|
|
284
|
+
: enumsOf(refFolder, new Set([...seen, folder])).map(item => ({
|
|
285
|
+
...item,
|
|
286
|
+
inline: item.inline === undefined ? selector : `${selector} ${item.inline}`,
|
|
287
|
+
}))
|
|
288
|
+
})
|
|
289
|
+
return [...enums, ...refEnums]
|
|
290
|
+
}
|
|
291
|
+
const instances = structurePath === undefined ? new Map() : enumsById(structurePath, folderOf, enumsOf)
|
|
229
292
|
const keys = new Set([...instances.values()].flatMap(enums => enums.map(({ key }) => key)))
|
|
230
293
|
|
|
231
294
|
/** @type {import('postcss').Declaration[]} */
|
|
@@ -237,39 +300,45 @@ export const customEnumsPlugin = {
|
|
|
237
300
|
collect()
|
|
238
301
|
|
|
239
302
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* element the enum is on, so its vars go in place.
|
|
303
|
+
* The vars `declaration` resolves to on the element `selector` is nearest to, the reason it can't, or null when
|
|
304
|
+
* that element has no such enum, or `selector` is in no instance, so the var is an ordinary one.
|
|
243
305
|
*/
|
|
244
306
|
const lookup = (declaration, selector) => {
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
const compounds = compoundsOf(selector).map(namesOf)
|
|
308
|
+
const isInstance = name => instances.has(name.slice(1))
|
|
309
|
+
// The last instance in the selector, `#comp-1` or `.comp-1`; what's before it, such as a variant's class,
|
|
310
|
+
// doesn't matter.
|
|
311
|
+
let at = compounds.length - 1
|
|
312
|
+
while (at >= 0 && !compounds[at].some(isInstance)) at--
|
|
313
|
+
if (at === -1) return null
|
|
314
|
+
const enums = instances.get(compounds[at].filter(isInstance).at(-1).slice(1))
|
|
315
|
+
const after = compounds.slice(at + 1)
|
|
316
|
+
// The element nearest the selector's end: the inline or ref element matched last, its longer selector on a
|
|
317
|
+
// tie, or the editor element when none is. Its enums alone resolve, so a key on a variant of the element
|
|
318
|
+
// (`#comp-1 .dropdown .variant-1`) resolves by it, and two elements can share a key.
|
|
319
|
+
let nearest
|
|
320
|
+
let end = -1
|
|
321
|
+
let length = 0
|
|
322
|
+
for (const inline of new Set(enums.flatMap(({ inline }) => (inline === undefined ? [] : [inline])))) {
|
|
323
|
+
const element = compoundsOf(inline).map(namesOf)
|
|
324
|
+
const found = lastMatch(element, after)
|
|
325
|
+
if (found > end || (found !== -1 && found === end && element.length > length)) {
|
|
326
|
+
nearest = inline
|
|
327
|
+
end = found
|
|
328
|
+
length = element.length
|
|
329
|
+
}
|
|
253
330
|
}
|
|
254
|
-
|
|
255
|
-
const descendants = match[2].trim()
|
|
256
|
-
const onInline = ({ inline }) =>
|
|
257
|
-
inline !== undefined &&
|
|
258
|
-
descendants.startsWith(inline) &&
|
|
259
|
-
/^(?:$|[.:[])/.test(descendants.slice(inline.length))
|
|
260
|
-
const matching = enums.filter(
|
|
261
|
-
item => item.key === declaration.prop && (descendants === '' || onInline(item))
|
|
262
|
-
)
|
|
331
|
+
const matching = enums.filter(item => item.key === declaration.prop && item.inline === nearest)
|
|
263
332
|
if (matching.length === 0) return null
|
|
264
333
|
const key = declaration.prop.slice(2)
|
|
265
|
-
if (matching.length > 1) return `
|
|
334
|
+
if (matching.length > 1) return `the element has custom enum ${key} more than once, so it can't say which.`
|
|
266
335
|
const [found] = matching
|
|
267
336
|
const value = declaration.value.trim()
|
|
268
337
|
const allowed = Object.keys(found.options).join(', ')
|
|
269
338
|
if (/\bvar\(/.test(value)) return `a custom enum's value must be literal, one of ${allowed}.`
|
|
270
339
|
const options = found.options[value]
|
|
271
340
|
if (options === undefined) return `"${value}" is not an option of ${key}; use one of ${allowed}.`
|
|
272
|
-
return
|
|
341
|
+
return options
|
|
273
342
|
}
|
|
274
343
|
|
|
275
344
|
// A block comes from the wrapper package, which the site can't fix: its enum is skipped, so an instance that
|
|
@@ -277,8 +346,12 @@ export const customEnumsPlugin = {
|
|
|
277
346
|
for (const block of malformed) {
|
|
278
347
|
context.warn?.(
|
|
279
348
|
block,
|
|
280
|
-
|
|
281
|
-
`
|
|
349
|
+
block.name === 'custom-enum-ref'
|
|
350
|
+
? `custom-enums: @custom-enum-ref ${block.params}${where(block)}: can't be read; it needs a component ` +
|
|
351
|
+
`type, such as wixEditorElements.Button, in an element rule under the editor element's. That ` +
|
|
352
|
+
`element's enums are left as authored.`
|
|
353
|
+
: `custom-enums: @custom-enum ${block.params}${where(block)}: can't be read; it needs a --key, a ` +
|
|
354
|
+
`default: that is one of its @option blocks, and an element rule around it. Its key is left as authored.`
|
|
282
355
|
)
|
|
283
356
|
}
|
|
284
357
|
|
|
@@ -286,16 +359,18 @@ export const customEnumsPlugin = {
|
|
|
286
359
|
const violations = new Set()
|
|
287
360
|
/** @type {any} The first violation's source, whose position the bundler prints. */
|
|
288
361
|
let first
|
|
362
|
+
// The rules with an enum that resolves, the only ones a selector list is split in.
|
|
363
|
+
const resolving = new Set()
|
|
289
364
|
for (const declaration of declarations) {
|
|
290
365
|
const rule = ruleOf(declaration)
|
|
291
|
-
const problems = rule
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
366
|
+
const problems = (rule?.selectors ?? []).flatMap(own => {
|
|
367
|
+
const results = resolve(rule, own).map(selector => lookup(declaration, selector))
|
|
368
|
+
if (results.some(result => result !== null)) resolving.add(rule)
|
|
369
|
+
if (results.length > 1 && results.some(result => result !== null)) {
|
|
370
|
+
return ['set a custom enum on one instance rule, not under a parent selector list.']
|
|
371
|
+
}
|
|
372
|
+
return results.filter(result => typeof result === 'string')
|
|
373
|
+
})
|
|
299
374
|
for (const problem of problems) {
|
|
300
375
|
first ??= declaration.source
|
|
301
376
|
violations.add(`custom-enums: ${declaration.prop}: ${declaration.value}${where(declaration)}: ${problem}`)
|
|
@@ -312,39 +387,20 @@ export const customEnumsPlugin = {
|
|
|
312
387
|
removeCustomEnums(root)
|
|
313
388
|
|
|
314
389
|
// A rule with a selector list is split, one rule per selector, so each instance gets its own option.
|
|
315
|
-
for (const rule of
|
|
316
|
-
if (rule
|
|
390
|
+
for (const rule of resolving) {
|
|
391
|
+
if (rule.selectors.length < 2) continue
|
|
317
392
|
for (const selector of [...rule.selectors].reverse()) rule.after(rule.clone({ selector }))
|
|
318
393
|
rule.remove()
|
|
319
394
|
}
|
|
320
395
|
declarations.length = 0
|
|
321
396
|
collect()
|
|
322
397
|
|
|
323
|
-
const emptied = new Set()
|
|
324
|
-
const inserted = new Map()
|
|
325
398
|
for (const declaration of declarations) {
|
|
326
|
-
const rule =
|
|
399
|
+
const rule = ruleOf(declaration)
|
|
400
|
+
if (rule === undefined) continue
|
|
327
401
|
const [selector] = resolve(rule, rule.selector)
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
const { found, options, onElement } = result
|
|
331
|
-
if (onElement) {
|
|
332
|
-
declaration.replaceWith(expansion(declaration, options))
|
|
333
|
-
} else {
|
|
334
|
-
emptied.add(declaration.parent)
|
|
335
|
-
inlineRule(declaration, selector, found, options, inserted)
|
|
336
|
-
declaration.remove()
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Drop the rules and at-rules the inline moves left empty, up to the outermost.
|
|
341
|
-
for (let nodes = [...emptied]; nodes.length; ) {
|
|
342
|
-
nodes = nodes.flatMap(node => {
|
|
343
|
-
if (node.nodes.length || node.type === 'root') return []
|
|
344
|
-
const parent = node.parent
|
|
345
|
-
node.remove()
|
|
346
|
-
return parent ? [parent] : []
|
|
347
|
-
})
|
|
402
|
+
const options = /** @type {Exclude<ReturnType<typeof lookup>, string>} */ (lookup(declaration, selector))
|
|
403
|
+
if (options !== null) declaration.replaceWith(expansion(declaration, options))
|
|
348
404
|
}
|
|
349
405
|
},
|
|
350
406
|
}
|
|
@@ -59,10 +59,34 @@ the defaults:
|
|
|
59
59
|
|
|
60
60
|
The item's JSX imports each used wrapper's `defaults.css`, which imports this file, so the browser loads
|
|
61
61
|
it ahead of the site's stylesheets, and the blocks are not in the document the plugin gets. The plugin reads
|
|
62
|
-
them from the wrapper's folder instead. The CSS stage removes them from the wrapper's own CSS, which runs no
|
|
62
|
+
them from the wrapper's folder instead. The CSS stage removes them, and the ref declarations, from the wrapper's own CSS, which runs no
|
|
63
63
|
other plugin; browsers would ignore the unknown at-rule anyway, and the defaults still apply. A stylesheet
|
|
64
64
|
that `@import`s `defaults.css` has the blocks inlined: the plugin reads those and removes them from the output.
|
|
65
|
-
An inline element's block sits in `{editor selector} {inline selector}` (`.menu .menu-input`)
|
|
65
|
+
An inline element's block sits in `{editor selector} {inline selector}` (`.menu .menu-input`), flat at any depth: the
|
|
66
|
+
Menu's dropdown, which sits in its navbar, is `.menu .dropdown`, as exports address it (`.comp-1 .dropdown`).
|
|
67
|
+
|
|
68
|
+
## Ref elements
|
|
69
|
+
|
|
70
|
+
A ref element is another component inside this one, such as the Menu's hamburger buttons, which are Buttons. Its
|
|
71
|
+
enums are its type's, so the wrapper doesn't copy them; it names the type in the ref's rule:
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
.menu .button.hamburger-open-button {
|
|
75
|
+
@custom-enum-ref wixEditorElements.Button;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The plugin resolves the type's wrapper from the structure file, through `wrapperImport` (`src/wrapperImport.js`), the
|
|
80
|
+
same name origami-site-generator imports wrappers by (`@origami-wrappers/wixEditorElements/button`), and reads that wrapper's `custom-enums.css`. The type's own enums
|
|
81
|
+
(`.button`) apply on the ref's selector, and its inline elements' (`.button .label`) on the ref's selector followed by
|
|
82
|
+
theirs:
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
.comp-1 .button.hamburger-open-button { --icon-position: row-reverse; }
|
|
86
|
+
.comp-1 .button.hamburger-open-button .label { --label-wrap: nowrap; }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A ref inside the type's wrapper is followed the same way. A type with no wrapper in the site gives the ref no enums.
|
|
66
90
|
|
|
67
91
|
The item's structure file (next to the stylesheet, or one folder up for `_generated/`) says which
|
|
68
92
|
component each `id` is: the element `<Menu id="comp-1" />` and its `@origami-wrappers/…` import. The
|
|
@@ -75,26 +99,31 @@ option replaces all of a desktop option's vars, not just the ones it names.
|
|
|
75
99
|
|
|
76
100
|
## The rules
|
|
77
101
|
|
|
78
|
-
-
|
|
79
|
-
(`#comp-1
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
- A key resolves by the component element nearest the selector's end, inside the selector's last instance
|
|
103
|
+
(`#comp-1` or `.comp-1`): the inline or ref element matched last, or the editor element when none is. What isn't
|
|
104
|
+
an element, such as a variant's class, a state or a combinator, doesn't change which element that is:
|
|
105
|
+
|
|
106
|
+
| selector | resolves by |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `#comp-1`, `#comp-1:hover`, `#comp-1 .variant-1`, `.variant-1 #comp-1` | the Menu |
|
|
109
|
+
| `#comp-1 .dropdown`, `#comp-1 .navbar .dropdown .variant-1` | the dropdown |
|
|
110
|
+
| `#comp-1 .button.hamburger-open-button .variant-1` | the Button's enums |
|
|
111
|
+
| `.button.hamburger-open-button .variant-1`, `.card` | nothing: no instance |
|
|
112
|
+
|
|
113
|
+
Only that element's enums resolve, so two elements can share a key (the navbar's and the dropdown's
|
|
114
|
+
`items-width`). A key that element has no enum of, such as the dropdown's on the instance (`.comp-1 {
|
|
115
|
+
--items-width: fillMenu }`), or a selector in no instance, is an ordinary var, left as authored: not every var
|
|
116
|
+
with an enum's name is that enum. A selector list is split into one rule per instance.
|
|
85
117
|
- The vars get the specificity of the rule the enum was set in, like any var the site sets there. An
|
|
86
118
|
active preset (`.w-{id} .menu`) outranks `.comp-1`, so its values win over the option; `#comp-1`
|
|
87
119
|
outranks the preset.
|
|
88
120
|
- A var on an instance whose component has no enum of that name is an ordinary var, left as authored.
|
|
89
121
|
- The vars are written where the enum was, so a var the site sets later in the rule wins.
|
|
90
|
-
- An inline element's enum set on its selector is written in place. Set on the instance itself, it gets a rule of its own,
|
|
91
|
-
`#comp-1 .menu-input:not(#comp-1 .menu .menu-input)`, under the same `@media`, in source order. The `:not()` keeps
|
|
92
|
-
it off the inline element of another instance nested inside this one.
|
|
93
122
|
- The value must be one of the options, written literally. Anything else fails the build with every
|
|
94
123
|
violation in one error, and the stylesheet is left as it was.
|
|
95
124
|
- A stylesheet with no structure file, or whose components have no custom enums, is left as authored.
|
|
96
125
|
- Declarations inlined from `node_modules` are the wrapper's own and are skipped: its enum defaults on
|
|
97
126
|
`.menu`, and the presets that set enum keys.
|
|
98
|
-
- A `@custom-enum` the plugin can't read (no `default:` among its options)
|
|
99
|
-
|
|
100
|
-
gets the wrapper's default.
|
|
127
|
+
- A `@custom-enum` the plugin can't read (no `default:` among its options), or a `@custom-enum-ref` (no
|
|
128
|
+
`module.Component` type, or no element rule around it), is a warning, not an error: it comes from the wrapper
|
|
129
|
+
package, which the site can't fix. Its keys are left as authored, so the instance gets the wrapper's default.
|
|
@@ -35,8 +35,8 @@ export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures })
|
|
|
35
35
|
if (from === undefined) return
|
|
36
36
|
|
|
37
37
|
const fileContext = contextFor(from)
|
|
38
|
-
// A dependency's stylesheet runs no plugins. It only loses a wrapper's `@custom-enum` blocks
|
|
39
|
-
// custom-enums plugin reads from the wrapper's folder and browsers would skip.
|
|
38
|
+
// A dependency's stylesheet runs no plugins. It only loses a wrapper's `@custom-enum` blocks and `@custom-enum-ref`
|
|
39
|
+
// declarations, which the custom-enums plugin reads from the wrapper's folder and browsers would skip.
|
|
40
40
|
if (!isSiteOwned(fileContext.relativePath)) {
|
|
41
41
|
removeCustomEnums(root)
|
|
42
42
|
return
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// How a site imports a component's wrapper. origami-site-generator writes these imports, and the custom-enums CSS
|
|
2
|
+
// plugin finds a ref element's type's wrapper through the same names. Kept free of dependencies, so the generator's
|
|
3
|
+
// bundle doesn't take the transforms with it.
|
|
4
|
+
|
|
5
|
+
/** component-wrapper-server's `PACKAGE_SCOPE` (`src/wrapper/packageTarball.ts`). */
|
|
6
|
+
export const WRAPPERS_SCOPE = '@origami-wrappers'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The name the site depends on a module's wrapper package under. It keeps the module as is, so an
|
|
10
|
+
* import of it maps back to the module; the package's own name is lowercased, which yarn does not check.
|
|
11
|
+
* @param {string} module
|
|
12
|
+
*/
|
|
13
|
+
export const wrapperPackageName = module => `${WRAPPERS_SCOPE}/${module}`
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where a component's wrapper is imported from: its folder in the module's package, which
|
|
17
|
+
* component-wrapper-server names by the symbol lowercased, so `wixEditorElements.WRichText` is
|
|
18
|
+
* `@origami-wrappers/wixEditorElements/wrichtext`.
|
|
19
|
+
* @param {{ readonly module: string, readonly symbol: string }} component
|
|
20
|
+
*/
|
|
21
|
+
export const wrapperImport = ({ module, symbol }) => `${wrapperPackageName(module)}/${symbol.toLowerCase()}`
|