@wix/origami-transforms 0.1.9 → 0.1.12
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/cssPlugins/customEnums.d.ts +1 -0
- package/dist/wrapperImport.d.ts +7 -0
- package/package.json +6 -2
- package/src/cssPlugins/customEnums.js +192 -112
- package/src/cssPlugins/customEnums.md +50 -19
- package/src/jsxPlugins/mediaValidation/index.js +2 -2
- package/src/transform/css/postcssPlugin.js +7 -1
- package/src/wrapperImport.js +21 -0
|
@@ -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.12",
|
|
4
4
|
"description": "Babel and PostCSS checks and transforms for Wix origami page files.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -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": "5339c279d9f83f77637d6772dcd519d52c576f898a610ca35dcdd0df"
|
|
58
62
|
}
|
|
@@ -1,25 +1,63 @@
|
|
|
1
1
|
// Resolves a component's custom enums at build time. A site sets only the enum's value on an instance,
|
|
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
|
-
// `customEnums.ts`),
|
|
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. 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.
|
|
5
8
|
// Native `if()` can't do it: Firefox doesn't have it, and a var named like its key refers to itself.
|
|
6
9
|
//
|
|
7
10
|
// The instance's component comes from the item's structure file (structureFile.js): the element with
|
|
8
11
|
// `id="comp-1"`, and the `@origami-wrappers/…` import it is rendered from. See customEnums.md.
|
|
9
12
|
|
|
10
|
-
import { realpathSync } from 'node:fs'
|
|
13
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
11
14
|
import { createRequire } from 'node:module'
|
|
12
|
-
import { basename, dirname } from 'node:path'
|
|
15
|
+
import { basename, dirname, join } from 'node:path'
|
|
13
16
|
import postcss from 'postcss'
|
|
14
17
|
import { LayoutError } from '../transform/layoutError.js'
|
|
18
|
+
import { WRAPPERS_SCOPE, wrapperImport } from '../wrapperImport.js'
|
|
15
19
|
import { literalIdOf, parseStructure, structureFileOf, walk } from './structureFile.js'
|
|
16
20
|
|
|
21
|
+
const CUSTOM_ENUMS_CSS = 'custom-enums.css'
|
|
17
22
|
// `@origami-wrappers/<module>/<symbol>`, and nothing that could resolve outside its package.
|
|
18
|
-
const WRAPPER_IMPORT =
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
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
|
+
}
|
|
23
61
|
|
|
24
62
|
/** Id → the wrapper import it is rendered from, for elements with a literal `id` and an imported wrapper tag. */
|
|
25
63
|
const importsById = structurePath => {
|
|
@@ -48,21 +86,27 @@ const real = path => {
|
|
|
48
86
|
}
|
|
49
87
|
}
|
|
50
88
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
* are the `@custom-enum` blocks from its wrapper's folder.
|
|
54
|
-
*/
|
|
55
|
-
const enumsById = (structurePath, byFolder) => {
|
|
89
|
+
/** The folder an `@origami-wrappers/…` import resolves to from the structure file, or undefined when it doesn't. */
|
|
90
|
+
const wrapperFolders = structurePath => {
|
|
56
91
|
const require = createRequire(structurePath)
|
|
57
|
-
|
|
58
|
-
for (const [id, source] of importsById(structurePath)) {
|
|
59
|
-
let folder
|
|
92
|
+
return source => {
|
|
60
93
|
try {
|
|
61
|
-
|
|
94
|
+
return real(dirname(require.resolve(source)))
|
|
62
95
|
} catch {
|
|
63
|
-
|
|
96
|
+
return undefined
|
|
64
97
|
}
|
|
65
|
-
|
|
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)
|
|
109
|
+
byId.set(id, folder === undefined ? [] : enumsOf(folder))
|
|
66
110
|
}
|
|
67
111
|
return byId
|
|
68
112
|
}
|
|
@@ -88,17 +132,39 @@ const resolve = (rule, selector) => {
|
|
|
88
132
|
const fromDependency = declaration => declaration.source?.input.file?.split(/[\\/]/).includes('node_modules') ?? false
|
|
89
133
|
|
|
90
134
|
/**
|
|
91
|
-
* The `@custom-enum` blocks in the stylesheet: wrapper folder → its enums, with
|
|
92
|
-
*
|
|
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.
|
|
93
138
|
*
|
|
94
|
-
* A block sits in its element's rule, `.menu`, or `.menu .
|
|
95
|
-
* set has the default option's value, or `inherit` when the default doesn't set it either, which is
|
|
96
|
-
* 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.
|
|
97
143
|
*/
|
|
98
144
|
const definitionsIn = root => {
|
|
99
145
|
const byFolder = new Map()
|
|
100
146
|
const blocks = []
|
|
101
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
|
+
})
|
|
102
168
|
root.walkAtRules('custom-enum', block => {
|
|
103
169
|
blocks.push(block)
|
|
104
170
|
const keyVar = block.params.trim()
|
|
@@ -123,7 +189,7 @@ const definitionsIn = root => {
|
|
|
123
189
|
}
|
|
124
190
|
|
|
125
191
|
const [selector] = resolve(rule, rule.selector)
|
|
126
|
-
const [
|
|
192
|
+
const [, ...inline] = selector.split(/\s+/)
|
|
127
193
|
const defaults = applied.get(defaultValue)
|
|
128
194
|
const vars = [...new Set([...applied.values()].flatMap(values => Object.keys(values)))]
|
|
129
195
|
const options = Object.fromEntries(
|
|
@@ -132,13 +198,9 @@ const definitionsIn = root => {
|
|
|
132
198
|
Object.fromEntries(vars.map(name => [name, values[name] ?? defaults[name] ?? 'inherit'])),
|
|
133
199
|
])
|
|
134
200
|
)
|
|
135
|
-
|
|
136
|
-
const enums = byFolder.get(folder) ?? []
|
|
137
|
-
byFolder.set(folder, enums)
|
|
138
|
-
enums.push({
|
|
201
|
+
definitionOf(file).enums.push({
|
|
139
202
|
key: keyVar,
|
|
140
203
|
options,
|
|
141
|
-
editorSelector,
|
|
142
204
|
// An inline element's selector inside the editor element's, undefined for the editor element itself.
|
|
143
205
|
inline: inline.length ? inline.join(' ') : undefined,
|
|
144
206
|
})
|
|
@@ -167,25 +229,17 @@ const expansion = (declaration, values) =>
|
|
|
167
229
|
)
|
|
168
230
|
|
|
169
231
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* 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.
|
|
173
234
|
*/
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let outer = declaration.parent
|
|
183
|
-
for (let parent = declaration.parent; parent && parent.type !== 'root'; parent = parent.parent) {
|
|
184
|
-
if (parent.type === 'atrule') node = parent.clone({ nodes: [] }).append(node)
|
|
185
|
-
outer = parent
|
|
186
|
-
}
|
|
187
|
-
;(inserted.get(outer) ?? outer).after(node)
|
|
188
|
-
inserted.set(outer, node)
|
|
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()
|
|
241
|
+
})
|
|
242
|
+
for (const rule of emptied) if (rule?.type === 'rule' && rule.nodes.length === 0) rule.remove()
|
|
189
243
|
}
|
|
190
244
|
|
|
191
245
|
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
@@ -194,11 +248,47 @@ export const customEnumsPlugin = {
|
|
|
194
248
|
// After mixins copy their bodies in, before presets and layout move declarations out of the instance's rule.
|
|
195
249
|
order: 11,
|
|
196
250
|
transform(root, context) {
|
|
197
|
-
const
|
|
198
|
-
if (blocks.length === 0) return
|
|
251
|
+
const inlined = definitionsIn(root)
|
|
199
252
|
const structurePath = structureFileOf(context.filePath)
|
|
253
|
+
if (structurePath === undefined && inlined.blocks.length === 0) return
|
|
200
254
|
if (structurePath !== undefined) context.addDependency?.(structurePath)
|
|
201
|
-
const
|
|
255
|
+
const malformed = [...inlined.malformed]
|
|
256
|
+
// A wrapper's definitions are the ones inlined from its folder, or else those in the folder's custom-enums.css.
|
|
257
|
+
const readFolders = new Map()
|
|
258
|
+
const definitionOf = folder => {
|
|
259
|
+
if (inlined.byFolder.has(folder)) return inlined.byFolder.get(folder)
|
|
260
|
+
if (!readFolders.has(folder)) {
|
|
261
|
+
const file = join(folder, CUSTOM_ENUMS_CSS)
|
|
262
|
+
let definition = { enums: [], refs: [] }
|
|
263
|
+
if (existsSync(file)) {
|
|
264
|
+
context.addDependency?.(file)
|
|
265
|
+
const definitions = definitionsIn(postcss.parse(readFileSync(file, 'utf8'), { from: file }))
|
|
266
|
+
malformed.push(...definitions.malformed)
|
|
267
|
+
definition = definitions.byFolder.get(folder) ?? definition
|
|
268
|
+
}
|
|
269
|
+
readFolders.set(folder, definition)
|
|
270
|
+
}
|
|
271
|
+
return readFolders.get(folder)
|
|
272
|
+
}
|
|
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)
|
|
202
292
|
const keys = new Set([...instances.values()].flatMap(enums => enums.map(({ key }) => key)))
|
|
203
293
|
|
|
204
294
|
/** @type {import('postcss').Declaration[]} */
|
|
@@ -210,39 +300,45 @@ export const customEnumsPlugin = {
|
|
|
210
300
|
collect()
|
|
211
301
|
|
|
212
302
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* 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.
|
|
216
305
|
*/
|
|
217
306
|
const lookup = (declaration, selector) => {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
}
|
|
226
330
|
}
|
|
227
|
-
|
|
228
|
-
const descendants = match[2].trim()
|
|
229
|
-
const onInline = ({ inline }) =>
|
|
230
|
-
inline !== undefined &&
|
|
231
|
-
descendants.startsWith(inline) &&
|
|
232
|
-
/^(?:$|[.:[])/.test(descendants.slice(inline.length))
|
|
233
|
-
const matching = enums.filter(
|
|
234
|
-
item => item.key === declaration.prop && (descendants === '' || onInline(item))
|
|
235
|
-
)
|
|
331
|
+
const matching = enums.filter(item => item.key === declaration.prop && item.inline === nearest)
|
|
236
332
|
if (matching.length === 0) return null
|
|
237
333
|
const key = declaration.prop.slice(2)
|
|
238
|
-
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.`
|
|
239
335
|
const [found] = matching
|
|
240
336
|
const value = declaration.value.trim()
|
|
241
337
|
const allowed = Object.keys(found.options).join(', ')
|
|
242
338
|
if (/\bvar\(/.test(value)) return `a custom enum's value must be literal, one of ${allowed}.`
|
|
243
339
|
const options = found.options[value]
|
|
244
340
|
if (options === undefined) return `"${value}" is not an option of ${key}; use one of ${allowed}.`
|
|
245
|
-
return
|
|
341
|
+
return options
|
|
246
342
|
}
|
|
247
343
|
|
|
248
344
|
// A block comes from the wrapper package, which the site can't fix: its enum is skipped, so an instance that
|
|
@@ -250,8 +346,12 @@ export const customEnumsPlugin = {
|
|
|
250
346
|
for (const block of malformed) {
|
|
251
347
|
context.warn?.(
|
|
252
348
|
block,
|
|
253
|
-
|
|
254
|
-
`
|
|
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.`
|
|
255
355
|
)
|
|
256
356
|
}
|
|
257
357
|
|
|
@@ -259,16 +359,18 @@ export const customEnumsPlugin = {
|
|
|
259
359
|
const violations = new Set()
|
|
260
360
|
/** @type {any} The first violation's source, whose position the bundler prints. */
|
|
261
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()
|
|
262
364
|
for (const declaration of declarations) {
|
|
263
365
|
const rule = ruleOf(declaration)
|
|
264
|
-
const problems = rule
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
})
|
|
272
374
|
for (const problem of problems) {
|
|
273
375
|
first ??= declaration.source
|
|
274
376
|
violations.add(`custom-enums: ${declaration.prop}: ${declaration.value}${where(declaration)}: ${problem}`)
|
|
@@ -282,45 +384,23 @@ export const customEnumsPlugin = {
|
|
|
282
384
|
})
|
|
283
385
|
}
|
|
284
386
|
|
|
285
|
-
|
|
286
|
-
if (block.parent) block.parent.raws.semicolon = true
|
|
287
|
-
block.remove()
|
|
288
|
-
}
|
|
387
|
+
removeCustomEnums(root)
|
|
289
388
|
|
|
290
389
|
// A rule with a selector list is split, one rule per selector, so each instance gets its own option.
|
|
291
|
-
for (const rule of
|
|
292
|
-
if (rule
|
|
390
|
+
for (const rule of resolving) {
|
|
391
|
+
if (rule.selectors.length < 2) continue
|
|
293
392
|
for (const selector of [...rule.selectors].reverse()) rule.after(rule.clone({ selector }))
|
|
294
393
|
rule.remove()
|
|
295
394
|
}
|
|
296
395
|
declarations.length = 0
|
|
297
396
|
collect()
|
|
298
397
|
|
|
299
|
-
const emptied = new Set()
|
|
300
|
-
const inserted = new Map()
|
|
301
398
|
for (const declaration of declarations) {
|
|
302
|
-
const rule =
|
|
399
|
+
const rule = ruleOf(declaration)
|
|
400
|
+
if (rule === undefined) continue
|
|
303
401
|
const [selector] = resolve(rule, rule.selector)
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
const { found, options, onElement } = result
|
|
307
|
-
if (onElement) {
|
|
308
|
-
declaration.replaceWith(expansion(declaration, options))
|
|
309
|
-
} else {
|
|
310
|
-
emptied.add(declaration.parent)
|
|
311
|
-
inlineRule(declaration, selector, found, options, inserted)
|
|
312
|
-
declaration.remove()
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Drop the rules and at-rules the inline moves left empty, up to the outermost.
|
|
317
|
-
for (let nodes = [...emptied]; nodes.length; ) {
|
|
318
|
-
nodes = nodes.flatMap(node => {
|
|
319
|
-
if (node.nodes.length || node.type === 'root') return []
|
|
320
|
-
const parent = node.parent
|
|
321
|
-
node.remove()
|
|
322
|
-
return parent ? [parent] : []
|
|
323
|
-
})
|
|
402
|
+
const options = /** @type {Exclude<ReturnType<typeof lookup>, string>} */ (lookup(declaration, selector))
|
|
403
|
+
if (options !== null) declaration.replaceWith(expansion(declaration, options))
|
|
324
404
|
}
|
|
325
405
|
},
|
|
326
406
|
}
|
|
@@ -57,15 +57,41 @@ the defaults:
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The
|
|
61
|
-
the
|
|
62
|
-
them from the
|
|
63
|
-
|
|
60
|
+
The item's JSX imports each used wrapper's `defaults.css`, which imports this file, so the browser loads
|
|
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, and the ref declarations, from the wrapper's own CSS, which runs no
|
|
63
|
+
other plugin; browsers would ignore the unknown at-rule anyway, and the defaults still apply. A stylesheet
|
|
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`), 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.
|
|
64
90
|
|
|
65
91
|
The item's structure file (next to the stylesheet, or one folder up for `_generated/`) says which
|
|
66
92
|
component each `id` is: the element `<Menu id="comp-1" />` and its `@origami-wrappers/…` import. The
|
|
67
|
-
import resolves to the wrapper's folder, and
|
|
68
|
-
declares the structure file, so editing
|
|
93
|
+
import resolves to the wrapper's folder, and that folder's `custom-enums.css` holds its enums. The plugin
|
|
94
|
+
declares the structure file and each `custom-enums.css` it reads, so editing either re-runs the stylesheet.
|
|
69
95
|
|
|
70
96
|
Each option is written as the full set of the enum's vars: the default's value wherever the option itself is
|
|
71
97
|
silent, or `inherit` when the default is silent too (what an instance that picks no option gets). So a mobile
|
|
@@ -73,26 +99,31 @@ option replaces all of a desktop option's vars, not just the ones it names.
|
|
|
73
99
|
|
|
74
100
|
## The rules
|
|
75
101
|
|
|
76
|
-
-
|
|
77
|
-
(`#comp-1
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
83
117
|
- The vars get the specificity of the rule the enum was set in, like any var the site sets there. An
|
|
84
118
|
active preset (`.w-{id} .menu`) outranks `.comp-1`, so its values win over the option; `#comp-1`
|
|
85
119
|
outranks the preset.
|
|
86
120
|
- A var on an instance whose component has no enum of that name is an ordinary var, left as authored.
|
|
87
121
|
- The vars are written where the enum was, so a var the site sets later in the rule wins.
|
|
88
|
-
- 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,
|
|
89
|
-
`#comp-1 .menu-input:not(#comp-1 .menu .menu-input)`, under the same `@media`, in source order. The `:not()` keeps
|
|
90
|
-
it off the inline element of another instance nested inside this one.
|
|
91
122
|
- The value must be one of the options, written literally. Anything else fails the build with every
|
|
92
123
|
violation in one error, and the stylesheet is left as it was.
|
|
93
124
|
- A stylesheet with no structure file, or whose components have no custom enums, is left as authored.
|
|
94
125
|
- Declarations inlined from `node_modules` are the wrapper's own and are skipped: its enum defaults on
|
|
95
126
|
`.menu`, and the presets that set enum keys.
|
|
96
|
-
- A `@custom-enum` the plugin can't read (no `default:` among its options)
|
|
97
|
-
|
|
98
|
-
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Fails the build on an Image whose `uri` is not a Wix media ID, such as a web URL or a local file.
|
|
2
|
-
// The Wix import accepts such a `uri` but the image is broken on the site; `origami upload-media`
|
|
2
|
+
// The Wix import accepts such a `uri` but the image is broken on the site; `wix origami upload-media`
|
|
3
3
|
// uploads it and points the Image at the uploaded file. `yarn dev` only warns, so the page still
|
|
4
4
|
// previews. Like the layout rules, Image is identified by its JSX tag name, and a computed `uri` is
|
|
5
5
|
// not checked.
|
|
@@ -34,7 +34,7 @@ export const mediaValidationPlugin = {
|
|
|
34
34
|
const id = attributes.find(attribute => attribute.name.name === 'id')?.value?.value
|
|
35
35
|
const line = uri.loc?.start.line
|
|
36
36
|
violations.push(
|
|
37
|
-
`${file}${line ? `:${line}` : ''} <Image${id ? ` id="${id}"` : ''}> image.uri "${uri.value.value}" is not a Wix media ID. Run \`origami upload-media\` to upload it.`
|
|
37
|
+
`${file}${line ? `:${line}` : ''} <Image${id ? ` id="${id}"` : ''}> image.uri "${uri.value.value}" is not a Wix media ID. Run \`wix origami upload-media\` to upload it.`
|
|
38
38
|
)
|
|
39
39
|
first ??= { file: context.filePath, line, column: uri.loc?.start.column }
|
|
40
40
|
},
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// stylesheet the plugins see is the whole page: a rule declared in one carrier and a rule that
|
|
9
9
|
// depends on it in another arrive in the same document.
|
|
10
10
|
|
|
11
|
+
import { removeCustomEnums } from '../../cssPlugins/customEnums.js'
|
|
11
12
|
import { runCssPlugins } from './runPlugins.js'
|
|
12
13
|
|
|
13
14
|
const DEPENDENCY_DIRECTORY = 'node_modules/'
|
|
@@ -34,7 +35,12 @@ export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures })
|
|
|
34
35
|
if (from === undefined) return
|
|
35
36
|
|
|
36
37
|
const fileContext = contextFor(from)
|
|
37
|
-
|
|
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
|
+
if (!isSiteOwned(fileContext.relativePath)) {
|
|
41
|
+
removeCustomEnums(root)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
38
44
|
|
|
39
45
|
const plugins = selectPlugins(fileContext)
|
|
40
46
|
if (!plugins.length) 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()}`
|