@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.
@@ -1,3 +1,3 @@
1
- export function removeCustomEnums(root: any): any;
1
+ export function removeCustomEnums(root: any): void;
2
2
  /** @type {import('../transform/types.js').CssTransformPlugin} */
3
3
  export const customEnumsPlugin: import("../transform/types.js").CssTransformPlugin;
@@ -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.11",
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.7.0",
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": "b2340e8ef9e2e52ae323397986026dcf38f3cd9a3a69fdc64a1e6736"
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 = /^@origami-wrappers\/[\w-]+\/[\w-]+$/
21
- // `#id` or `.id` (exports give an instance both), then only what narrows that same element: classes,
22
- // pseudo-classes, attributes. Then any descendants, such as an inline element's own selector. No other combinators,
23
- // and no pseudo-elements, which the component doesn't read vars from.
24
- const INSTANCE = /^[#.]([\w-]+)(?!.*::)(?:[.:[][^\s>+~]*)?((?:\s+[^\s>+~]+)*)$/
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
- * Id → the enums of every wrapper instance; none for one whose component has no custom enums. `enumsOf` gives a
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
- const byId = new Map()
60
- for (const [id, source] of importsById(structurePath)) {
61
- let folder
92
+ return source => {
62
93
  try {
63
- folder = real(dirname(require.resolve(source)))
94
+ return real(dirname(require.resolve(source)))
64
95
  } catch {
65
- folder = undefined
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 each option as the enum's full var
94
- * set; the blocks themselves; and the ones that can't be read.
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 .menu-input` for an inline element. A var an option doesn't
97
- * set has the default option's value, or `inherit` when the default doesn't set it either, which is what an instance
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 [editorSelector, ...inline] = selector.split(/\s+/)
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
- const folder = real(dirname(file))
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
- * An inline element's vars, as a rule of their own after the outermost node, under the same at-rules.
173
- * `:not()` keeps them off the same inline element of an instance nested inside this one. `inserted` maps each
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 inlineRule = (declaration, instance, { inline, editorSelector }, options, inserted) => {
177
- let node = postcss
178
- .rule({
179
- selector: `${instance} ${inline}:not(${instance} ${editorSelector} ${inline})`,
180
- source: declaration.source,
181
- raws: { semicolon: true },
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 enums are the blocks inlined from its folder, or else those in the folder's custom-enums.css.
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 enumsOf = folder => {
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 enums = []
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
- enums = definitions.byFolder.get(folder) ?? []
267
+ definition = definitions.byFolder.get(folder) ?? definition
223
268
  }
224
- readFolders.set(folder, enums)
269
+ readFolders.set(folder, definition)
225
270
  }
226
271
  return readFolders.get(folder)
227
272
  }
228
- const instances = structurePath === undefined ? new Map() : enumsById(structurePath, enumsOf)
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
- * What `declaration` resolves to on the instance or inline element `selector` is, the reason it can't, or null
241
- * when that element has no such enum, so the var is an ordinary one. `onElement` says the selector is the
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 match = INSTANCE.exec(selector)
246
- const enums = match ? instances.get(match[1]) : undefined
247
- if (match === null || enums === undefined) {
248
- return (
249
- `"${selector}" is not a component instance. Set a custom enum on the instance's own #id or .id (with ` +
250
- `a literal id="…" in the structure file), or on its inline element (#id .menu-input), with ` +
251
- `nothing after either but classes or pseudo-classes.`
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
- // An inline element's selector, then only what narrows it, as on the instance.
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 `two of the component's elements have a custom enum ${key}, so it can't say which.`
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 { found, options, onElement: descendants !== '' || found.inline === undefined }
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
- `custom-enums: @custom-enum ${block.params}${where(block)}: can't be read; it needs a --key, a ` +
281
- `default: that is one of its @option blocks, and an element rule around it. Its key is left as authored.`
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
- ? rule.selectors.flatMap(own => {
293
- const resolved = resolve(rule, own)
294
- if (resolved.length !== 1) return ['set a custom enum on one instance rule, not under a parent selector list.']
295
- const result = lookup(declaration, resolved[0])
296
- return typeof result === 'string' ? [result] : []
297
- })
298
- : ['set a custom enum on one instance rule, not outside a rule.']
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 new Set(declarations.map(ruleOf))) {
316
- if (rule === undefined || rule.selectors.length < 2) continue
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 = /** @type {import('postcss').Rule} */ (ruleOf(declaration))
399
+ const rule = ruleOf(declaration)
400
+ if (rule === undefined) continue
327
401
  const [selector] = resolve(rule, rule.selector)
328
- const result = /** @type {Exclude<ReturnType<typeof lookup>, string>} */ (lookup(declaration, selector))
329
- if (result === null) continue
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
- - Set the enum on the instance's own `#id` or `.id`, or on its inline element's selector under it
79
- (`#comp-1 .menu-input`), which is how exports write it. Classes and pseudo-classes after either are kept
80
- (`#comp-1:hover`); a combinator other than a space (`#comp-1 > .menu-input`) or a pseudo-element
81
- (`#comp-1::before`) is an error. A selector list is split into one rule per instance.
82
- - On an inline element, the key is that element's enum, so two elements can share a key. A key on a descendant
83
- with no such enum, such as an element nested in an inline element (`#comp-1 .navbar .dropdown`), is an
84
- ordinary var, left as authored.
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) is a warning, not an error: it
99
- comes from the wrapper package, which the site can't fix. Its key is left as authored, so the instance
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, which the
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()}`