@wix/origami-site-runtime 1.0.13 → 1.0.14
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/README.md +1 -1
- package/package.json +8 -8
- package/src/index.js +2 -2
- package/src/routing/siteLayout.js +1 -9
- package/src/siteViteConfig.js +4 -4
- package/src/cssPlugins/customMedia.js +0 -52
- package/src/cssPlugins/fonts.js +0 -93
- package/src/cssPlugins/index.js +0 -32
- package/src/cssPlugins/layout/README.md +0 -77
- package/src/cssPlugins/layout/compiler.js +0 -116
- package/src/cssPlugins/layout/contract.js +0 -35
- package/src/cssPlugins/layout/index.js +0 -12
- package/src/cssPlugins/layout/parser.js +0 -81
- package/src/cssPlugins/mixins.js +0 -63
- package/src/cssPlugins/presets.js +0 -73
- package/src/cssPlugins/spx/README.md +0 -73
- package/src/cssPlugins/spx/calls.js +0 -12
- package/src/cssPlugins/spx/cascade.js +0 -41
- package/src/cssPlugins/spx/index.js +0 -13
- package/src/cssPlugins/stackChildren.js +0 -205
- package/src/jsxPlugins/index.js +0 -10
- package/src/jsxPlugins/mediaValidation/index.js +0 -49
- package/src/jsxPlugins/origamiCompatibility/ast.js +0 -30
- package/src/jsxPlugins/origamiCompatibility/componentBindings.js +0 -40
- package/src/jsxPlugins/origamiCompatibility/index.js +0 -48
- package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +0 -15
- package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +0 -22
- package/src/jsxPlugins/validation/checks.js +0 -105
- package/src/jsxPlugins/validation/index.js +0 -65
- package/src/jsxPlugins/validation/rules.js +0 -24
- package/src/jsxPlugins/validation/tree.js +0 -15
- package/src/transform/context.js +0 -13
- package/src/transform/css/postcssPlugin.js +0 -60
- package/src/transform/css/runPlugins.js +0 -40
- package/src/transform/jsx/babelAdapter.js +0 -37
- package/src/transform/jsx/runPlugins.js +0 -23
- package/src/transform/layoutError.js +0 -19
- package/src/transform/pipeline.js +0 -60
- package/src/transform/registry.js +0 -40
- package/src/transform/types.js +0 -47
package/src/cssPlugins/mixins.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import postcss from 'postcss'
|
|
2
|
-
|
|
3
|
-
const MAX_EXPANSION_DEPTH = 64
|
|
4
|
-
const MAX_CLONED_NODES = 100_000
|
|
5
|
-
const MAX_CLONED_BYTES = 10 * 1024 * 1024
|
|
6
|
-
|
|
7
|
-
/** @param {import('postcss').AtRule} atRule */
|
|
8
|
-
const mixinName = atRule => atRule.params.trim().replace(/\(\)$/, '')
|
|
9
|
-
|
|
10
|
-
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
11
|
-
export const mixinsPlugin = {
|
|
12
|
-
name: 'mixins',
|
|
13
|
-
order: 10,
|
|
14
|
-
transform(root) {
|
|
15
|
-
/** @type {Map<string, import('postcss').ChildNode[]>} */
|
|
16
|
-
const bodies = new Map()
|
|
17
|
-
root.walkAtRules('mixin', atRule => {
|
|
18
|
-
const first = atRule.first
|
|
19
|
-
const body = first?.type === 'atrule' && first.name === 'result' ? first : atRule
|
|
20
|
-
bodies.set(mixinName(atRule), body.nodes ?? [])
|
|
21
|
-
atRule.remove()
|
|
22
|
-
})
|
|
23
|
-
// Share budgets across all call sites, including intermediate bodies that are later replaced.
|
|
24
|
-
// Count descendants too: cloning a rule also clones its entire subtree.
|
|
25
|
-
let clonedNodes = 0
|
|
26
|
-
let clonedBytes = 0
|
|
27
|
-
/** @param {import('postcss').Root} container
|
|
28
|
-
* @param {string[]} stack */
|
|
29
|
-
const expand = (container, stack) => {
|
|
30
|
-
container.walkAtRules('apply', atRule => {
|
|
31
|
-
const name = mixinName(atRule)
|
|
32
|
-
const body = bodies.get(name)
|
|
33
|
-
if (!body) throw new Error(`@apply ${atRule.params} has no @mixin definition`)
|
|
34
|
-
if (stack.includes(name)) throw new Error(`Cyclic @apply: ${[...stack, name].join(' -> ')}`)
|
|
35
|
-
if (stack.length >= MAX_EXPANSION_DEPTH) {
|
|
36
|
-
throw new Error(`@apply ${name} exceeds maximum expansion depth (${MAX_EXPANSION_DEPTH})`)
|
|
37
|
-
}
|
|
38
|
-
const pending = [...body]
|
|
39
|
-
while (pending.length) {
|
|
40
|
-
const node = pending.pop()
|
|
41
|
-
if (++clonedNodes > MAX_CLONED_NODES) {
|
|
42
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-node budget (${MAX_CLONED_NODES})`)
|
|
43
|
-
}
|
|
44
|
-
if (node && 'nodes' in node && node.nodes) {
|
|
45
|
-
for (const child of node.nodes) pending.push(child)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
for (const node of body) {
|
|
49
|
-
clonedBytes += Buffer.byteLength((node.raws.before ?? '') + node.toString(), 'utf8')
|
|
50
|
-
if (clonedBytes > MAX_CLONED_BYTES) {
|
|
51
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-CSS byte budget (${MAX_CLONED_BYTES})`)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// PostCSS does not revisit nodes inserted in place of the current at-rule.
|
|
55
|
-
// Expand a clone first so nested uses resolve without changing the definition.
|
|
56
|
-
const expanded = postcss.root({ nodes: body.map(node => node.clone()) })
|
|
57
|
-
expand(expanded, [...stack, name])
|
|
58
|
-
atRule.replaceWith(...expanded.nodes)
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
expand(root, [])
|
|
62
|
-
},
|
|
63
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// A preset is set on the component's own rule (`#id` or `.id`): `--preset` for the component, `--preset-{key}` for
|
|
2
|
-
// its ref element `key` (as listed in the wrapper's `REF_ELEMENTS`). An element nested deeper is reached the same way,
|
|
3
|
-
// from the rule of the element it sits in: `.id .{parent} { --preset-{key}: … }`. The wrapper reads
|
|
4
|
-
// `--preset` off the presets-wrapper classes it renders (see the origami-component-usage skill), so each declaration
|
|
5
|
-
// moves there, as `--preset` in a rule of its own: `.{id}-presets-wrapper`, or `{selector} .{key}-presets-wrapper` so
|
|
6
|
-
// an inner element's preset reaches that instance only. A rule that does not start at a component is left as authored.
|
|
7
|
-
//
|
|
8
|
-
// A preset on a component selector that can't be mapped is reported as a warning and left as authored rather than failing the
|
|
9
|
-
// build: exported sites still carry presets in the older shape, and they must keep building until they are migrated.
|
|
10
|
-
|
|
11
|
-
import postcss from 'postcss'
|
|
12
|
-
|
|
13
|
-
const PRESET = /^--preset(?:-([\w-]+))?$/
|
|
14
|
-
const COMPONENT = /^[#.]([\w-]+)$/
|
|
15
|
-
const WRAPPER = '-presets-wrapper'
|
|
16
|
-
|
|
17
|
-
/** Where a preset on `selector` goes: undefined when `selector` does not start at a component, null when it can't go. */
|
|
18
|
-
const target = (selector, key) => {
|
|
19
|
-
const [first] = selector.split(/\s+/, 1)
|
|
20
|
-
const id = COMPONENT.exec(first)?.[1]
|
|
21
|
-
if (id === undefined || id.endsWith(WRAPPER)) return undefined
|
|
22
|
-
if (key !== undefined) return `${selector} .${key}${WRAPPER}`
|
|
23
|
-
return selector === first ? `.${id}${WRAPPER}` : null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
27
|
-
export const presetsPlugin = {
|
|
28
|
-
name: 'presets',
|
|
29
|
-
order: 13,
|
|
30
|
-
transform(root, context) {
|
|
31
|
-
/** @type {import('postcss').Declaration[]} */
|
|
32
|
-
const declarations = []
|
|
33
|
-
root.walkDecls(PRESET, declaration => {
|
|
34
|
-
if (declaration.parent?.type === 'rule') declarations.push(declaration)
|
|
35
|
-
})
|
|
36
|
-
// Last first: each moved declaration lands right after its outermost rule, so source order is kept.
|
|
37
|
-
for (const declaration of declarations.reverse()) {
|
|
38
|
-
const rule = /** @type {import('postcss').Rule} */ (declaration.parent)
|
|
39
|
-
// The outermost rule, and the full selectors resolved as CSS Nesting does.
|
|
40
|
-
let outer = rule
|
|
41
|
-
let selectors = rule.selectors
|
|
42
|
-
while (outer.parent?.type === 'rule') {
|
|
43
|
-
outer = /** @type {import('postcss').Rule} */ (outer.parent)
|
|
44
|
-
const inner = selectors
|
|
45
|
-
selectors = outer.selectors.flatMap(parent =>
|
|
46
|
-
inner.map(selector => (selector.includes('&') ? selector.replaceAll('&', parent) : `${parent} ${selector}`))
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
const targets = selectors.map(selector => target(selector, PRESET.exec(declaration.prop)?.[1]))
|
|
50
|
-
if (targets.every(selector => selector === undefined)) continue
|
|
51
|
-
if (!targets.every(Boolean)) {
|
|
52
|
-
context.warn?.(
|
|
53
|
-
declaration,
|
|
54
|
-
`presets: ${context.relativePath ?? context.filePath}: ` +
|
|
55
|
-
`${declaration.prop} on "${selectors.join(', ')}": set --preset on #id or .id alone, ` +
|
|
56
|
-
`and an inner element's as --preset-{key} on the rule of the element it sits in. Left as authored.`
|
|
57
|
-
)
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
declaration.prop = '--preset'
|
|
61
|
-
const presetRule = postcss.rule({ selector: targets.join(', ') })
|
|
62
|
-
if (rule.source) presetRule.source = rule.source
|
|
63
|
-
outer.after(presetRule.append(declaration))
|
|
64
|
-
// Drop the rules the move left empty, from the declaration's own up to the outermost.
|
|
65
|
-
for (let node = rule; !node.nodes.length; ) {
|
|
66
|
-
const parent = node.parent
|
|
67
|
-
node.remove()
|
|
68
|
-
if (node === outer) break
|
|
69
|
-
node = /** @type {import('postcss').Rule} */ (parent)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# SPX follows the active CSS settings
|
|
2
|
-
|
|
3
|
-
A component can declare a size once and change its scaling settings at a breakpoint:
|
|
4
|
-
|
|
5
|
-
```css
|
|
6
|
-
#button {
|
|
7
|
-
--spx-ref-width: 1280;
|
|
8
|
-
--spx-resolver-type: scale;
|
|
9
|
-
border-width: --spx(4);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
@media (max-width: 600px) {
|
|
13
|
-
#button {
|
|
14
|
-
--spx-ref-width: 390;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
The desktop border declaration still applies on mobile. At a 390px viewport, it should
|
|
20
|
-
be 4px again because its reference width is now 390. If the build replaces `4 / 1280`
|
|
21
|
-
with a constant, the mobile reference-width override can no longer affect that border.
|
|
22
|
-
|
|
23
|
-
This applies to regular component styles as well as container layouts. There is one
|
|
24
|
-
SPX conversion path for all declarations. Layout first moves content and overflow
|
|
25
|
-
styles onto their runtime wrappers; this plugin then converts every `--spx(number)`.
|
|
26
|
-
|
|
27
|
-
## What the browser calculates
|
|
28
|
-
|
|
29
|
-
In scale mode, the size is approximately:
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
number × (scaling-factor − scrollbar-width) / spx-ref-width
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Nonzero scaled values have a minimum magnitude of 0.5px, preserving their sign.
|
|
36
|
-
In fixed mode, the number is a pixel value, including fractional pixels. Zero stays zero.
|
|
37
|
-
|
|
38
|
-
The plugin keeps reference width, scaling factor and scrollbar width as `var()` reads
|
|
39
|
-
inside each expression. It translates `--spx-resolver-type: scale` to a private numeric
|
|
40
|
-
switch of 1, and `fixed` to 0. That switch selects scaled lengths or fixed pixels using
|
|
41
|
-
ordinary CSS arithmetic, without requiring CSS custom functions or style queries.
|
|
42
|
-
|
|
43
|
-
Settings stay in their authored rules, including their media queries, scopes, layers,
|
|
44
|
-
source order and `!important`. Even a stylesheet that contains only settings must be
|
|
45
|
-
processed, because it can affect calls declared in another stylesheet. The original
|
|
46
|
-
resolver declaration is retained alongside its numeric switch.
|
|
47
|
-
|
|
48
|
-
We inline the calculation instead of inheriting a precomputed `--origami-spx-unit`.
|
|
49
|
-
An inherited derived custom property has already been resolved on its parent; changing
|
|
50
|
-
reference width or scaling factor on a child would not recompute it there. Reading the
|
|
51
|
-
settings in each expression lets ordinary components and internal wrappers use their
|
|
52
|
-
own effective settings. No per-container SPX reset is needed.
|
|
53
|
-
|
|
54
|
-
Existing preamble declarations remain in CSS and participate in the normal cascade.
|
|
55
|
-
When settings are absent, expressions fall back to reference width 1280, scale mode,
|
|
56
|
-
viewport width 100vw and scrollbar width 0px. Nested components inherit settings unless
|
|
57
|
-
they override them. As with any CSS custom property, an authored token containing an
|
|
58
|
-
SPX expression resolves where the token is declared; inheriting the token does not
|
|
59
|
-
re-evaluate it using a child's settings.
|
|
60
|
-
|
|
61
|
-
## Files and boundaries
|
|
62
|
-
|
|
63
|
-
- `index.js` removes the SPX function definition and visits declarations.
|
|
64
|
-
- `cascade.js` translates resolver settings and generates browser-evaluated expressions.
|
|
65
|
-
- `calls.js` parses literal numeric SPX calls and rejects unsupported arguments.
|
|
66
|
-
|
|
67
|
-
Reference widths must be positive numbers and resolver modes must be `scale` or `fixed`.
|
|
68
|
-
SPX stopper clamping is still outside this plugin: it uses the effective
|
|
69
|
-
`--scaling-factor` supplied by CSS and does not generate Thunderbolt's clamp formula.
|
|
70
|
-
|
|
71
|
-
`test/spx.browser.mjs` checks computed styles for the button example, nested overrides,
|
|
72
|
-
settings in separate stylesheets, cascade priority, fixed fractions, signs and zero.
|
|
73
|
-
Run it together with the container browser checks using the package's `test:browser` script.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
2
|
-
|
|
3
|
-
/** Replace literal SPX calls, reporting unsupported arguments at their declaration.
|
|
4
|
-
* @param {import('postcss').Declaration} declaration
|
|
5
|
-
* @param {(value: number) => string} resolve
|
|
6
|
-
* @param {() => Error} invalidCall
|
|
7
|
-
*/
|
|
8
|
-
export const lowerCalls = (declaration, resolve, invalidCall) => {
|
|
9
|
-
if (!declaration.value.includes('--spx(')) return
|
|
10
|
-
declaration.value = declaration.value.replace(CALL, (_, literal) => resolve(Number(literal)))
|
|
11
|
-
if (declaration.value.includes('--spx(')) throw invalidCall()
|
|
12
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { lowerCalls } from './calls.js'
|
|
2
|
-
|
|
3
|
-
// Keep arithmetic at the call site: an inherited derived custom property would already
|
|
4
|
-
// be computed against its parent's settings. Only the numeric resolver switch inherits.
|
|
5
|
-
const SCALE = 'var(--origami-spx-scale, 1)'
|
|
6
|
-
const UNIT = `(${SCALE} * (var(--scaling-factor, 100vw) - var(--scrollbar-width, 0px)) / var(--spx-ref-width, 1280) + (1 - ${SCALE}) * 1px)`
|
|
7
|
-
const FLOOR = `calc(0.5px * ${SCALE})`
|
|
8
|
-
|
|
9
|
-
/** Compile settings and calls in place, retaining their cascade and source locations.
|
|
10
|
-
* @param {import('postcss').Declaration} declaration
|
|
11
|
-
*/
|
|
12
|
-
export const compileCascadeDeclaration = declaration => {
|
|
13
|
-
if (declaration.prop === '--spx-resolver-type') {
|
|
14
|
-
const resolver = declaration.value.trim()
|
|
15
|
-
if (!['scale', 'fixed'].includes(resolver)) throw declaration.error('Unsupported SPX resolver')
|
|
16
|
-
const value = resolver === 'scale' ? '1' : '0'
|
|
17
|
-
const previous = declaration.prev()
|
|
18
|
-
if (
|
|
19
|
-
previous?.type !== 'decl' ||
|
|
20
|
-
previous.prop !== '--origami-spx-scale' ||
|
|
21
|
-
previous.value !== value ||
|
|
22
|
-
previous.important !== declaration.important
|
|
23
|
-
) {
|
|
24
|
-
declaration.before(declaration.clone({ prop: '--origami-spx-scale', value }))
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (
|
|
28
|
-
declaration.prop === '--spx-ref-width' &&
|
|
29
|
-
(!Number.isFinite(Number(declaration.value)) || Number(declaration.value) <= 0)
|
|
30
|
-
) {
|
|
31
|
-
throw declaration.error('SPX reference width must be positive')
|
|
32
|
-
}
|
|
33
|
-
lowerCalls(
|
|
34
|
-
declaration,
|
|
35
|
-
number => {
|
|
36
|
-
if (number === 0) return '0px'
|
|
37
|
-
return number < 0 ? `min(calc(-1 * ${FLOOR}), ${number} * ${UNIT})` : `max(${FLOOR}, ${number} * ${UNIT})`
|
|
38
|
-
},
|
|
39
|
-
() => declaration.error('SPX calls --spx() with a non-literal argument')
|
|
40
|
-
)
|
|
41
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { compileCascadeDeclaration } from './cascade.js'
|
|
2
|
-
|
|
3
|
-
/** @type {import('../../transform/types.js').CssTransformPlugin} */
|
|
4
|
-
export const spxPlugin = {
|
|
5
|
-
name: 'spx',
|
|
6
|
-
order: 20,
|
|
7
|
-
transform(root) {
|
|
8
|
-
root.walkAtRules('function', atRule => {
|
|
9
|
-
if (atRule.params.trimStart().startsWith('--spx(')) atRule.remove()
|
|
10
|
-
})
|
|
11
|
-
root.walkDecls(compileCascadeDeclaration)
|
|
12
|
-
},
|
|
13
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// Checks the CSS of a stack's direct children (rule S2-STACK-001). It changes nothing: every violation in the
|
|
2
|
-
// stylesheet is collected, and the build fails once with all of them.
|
|
3
|
-
//
|
|
4
|
-
// A stack is a parent whose rule declares `--container-layout-type: StackContainerLayout`; its
|
|
5
|
-
// `flex-direction` says which way it stacks (column → vertical, anything else → horizontal, as in
|
|
6
|
-
// CSS). Parent and child are the JSX tree's, so the tree comes from the item's `structure.tsx` (or `.jsx`): next
|
|
7
|
-
// to the stylesheet, or one folder up when the stylesheet is the generated one in `_generated/`. Elements and
|
|
8
|
-
// rules meet on `id` and `#id`. The stylesheet is the page's, with its
|
|
9
|
-
// carriers already inlined (see ../transform/css/postcssPlugin.js), so every rule is in view.
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
12
|
-
import { basename, dirname, join } from 'node:path'
|
|
13
|
-
import { parseSync } from '@babel/core'
|
|
14
|
-
import { LayoutError } from '../transform/layoutError.js'
|
|
15
|
-
|
|
16
|
-
const STRUCTURE_FILES = ['structure.tsx', 'structure.jsx']
|
|
17
|
-
const GENERATED = '_generated'
|
|
18
|
-
const STACK_CONTAINER = 'StackContainerLayout'
|
|
19
|
-
const BASE = ''
|
|
20
|
-
|
|
21
|
-
// What a stack child may write, per direction. `0` is allowed wherever a length is.
|
|
22
|
-
const ALLOWED = {
|
|
23
|
-
vertical: { width: ['spx', 'px', 'vw', 'vh', '%', 'calc'], margin: ['spx', 'px', 'vw', 'vh', '%'] },
|
|
24
|
-
horizontal: { width: ['spx', 'px', 'vw', 'vh', 'calc'], margin: ['spx', 'px', 'vw', 'vh'] },
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const UNIT = /^-?(?:\d+\.?\d*|\.\d+)(px|vw|vh|%)$/i
|
|
28
|
-
const LABELS = { spx: '--spx()', px: 'px', vw: 'vw', vh: 'vh', '%': '%', calc: 'calc()' }
|
|
29
|
-
|
|
30
|
-
/** `width` → 'width', `margin`/`margin-*` → 'margin', anything else → undefined. */
|
|
31
|
-
const kindOf = prop =>
|
|
32
|
-
prop === 'width' ? 'width' : prop === 'margin' || prop.startsWith('margin-') ? 'margin' : undefined
|
|
33
|
-
|
|
34
|
-
/** A value split on top-level whitespace, so `calc(1px + 2%)` stays one token. */
|
|
35
|
-
const tokens = value => {
|
|
36
|
-
const result = []
|
|
37
|
-
let depth = 0
|
|
38
|
-
let current = ''
|
|
39
|
-
for (const char of value.replace(/!important\s*$/i, '').trim()) {
|
|
40
|
-
if (char === '(') depth++
|
|
41
|
-
else if (char === ')') depth--
|
|
42
|
-
if (depth === 0 && /\s/.test(char)) {
|
|
43
|
-
if (current) result.push(current)
|
|
44
|
-
current = ''
|
|
45
|
-
} else current += char
|
|
46
|
-
}
|
|
47
|
-
if (current) result.push(current)
|
|
48
|
-
return result
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The kind of length a token is: 'spx', 'px', 'vw', 'vh', '%', 'calc', 'zero', or undefined. */
|
|
52
|
-
const lengthOf = token => {
|
|
53
|
-
if (/^--spx\(.*\)$/.test(token)) return 'spx'
|
|
54
|
-
if (/^calc\(.*\)$/i.test(token)) return 'calc'
|
|
55
|
-
if (/^-?0*\.?0+$/.test(token)) return 'zero'
|
|
56
|
-
return UNIT.exec(token)?.[1].toLowerCase()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The folder holding the structure file a stylesheet is checked against: its own, or its item's for `_generated/`. */
|
|
60
|
-
const structureFolderOf = stylesheetPath => {
|
|
61
|
-
const folder = dirname(stylesheetPath)
|
|
62
|
-
return basename(folder) === GENERATED ? dirname(folder) : folder
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Child id → its direct parent's id, for elements with a literal `id`. Slot props are not children. */
|
|
66
|
-
const parentsIn = structurePath => {
|
|
67
|
-
const ast = parseSync(readFileSync(structurePath, 'utf8'), {
|
|
68
|
-
babelrc: false,
|
|
69
|
-
configFile: false,
|
|
70
|
-
filename: structurePath,
|
|
71
|
-
parserOpts: { plugins: structurePath.endsWith('.tsx') ? ['jsx', 'typescript'] : ['jsx'] },
|
|
72
|
-
})
|
|
73
|
-
const parents = new Map()
|
|
74
|
-
const idOf = element => {
|
|
75
|
-
const attribute = element.openingElement.attributes.find(
|
|
76
|
-
node => node.type === 'JSXAttribute' && node.name.name === 'id' && node.value?.type === 'StringLiteral'
|
|
77
|
-
)
|
|
78
|
-
return attribute?.value.value
|
|
79
|
-
}
|
|
80
|
-
const directChildren = node =>
|
|
81
|
-
node.children.flatMap(child =>
|
|
82
|
-
child.type === 'JSXElement' ? [child] : child.type === 'JSXFragment' ? directChildren(child) : []
|
|
83
|
-
)
|
|
84
|
-
const visit = node => {
|
|
85
|
-
if (Array.isArray(node)) return node.forEach(visit)
|
|
86
|
-
if (!node || typeof node.type !== 'string') return
|
|
87
|
-
if (node.type === 'JSXElement') {
|
|
88
|
-
const parent = idOf(node)
|
|
89
|
-
for (const child of directChildren(node)) {
|
|
90
|
-
const id = idOf(child)
|
|
91
|
-
if (parent !== undefined && id !== undefined) parents.set(id, parent)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
for (const [key, value] of Object.entries(node)) {
|
|
95
|
-
if (key !== 'loc' && value && typeof value === 'object') visit(value)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
visit(ast?.program)
|
|
99
|
-
return parents
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** The at-rule conditions a rule sits under, e.g. `media (max-width: 750px)`, or BASE. */
|
|
103
|
-
const conditionOf = rule => {
|
|
104
|
-
const conditions = []
|
|
105
|
-
for (let node = rule.parent; node && node.type !== 'root'; node = node.parent) {
|
|
106
|
-
if (node.type === 'atrule') conditions.unshift(`${node.name} ${node.params}`)
|
|
107
|
-
}
|
|
108
|
-
return conditions.join(' / ')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Id → condition → the declarations that rules selecting exactly `#id` make there, in source order. */
|
|
112
|
-
const declarationsById = root => {
|
|
113
|
-
const byId = new Map()
|
|
114
|
-
root.walkRules(rule => {
|
|
115
|
-
const ids = rule.selectors.map(selector => /^#([\w-]+)$/.exec(selector.trim())?.[1]).filter(Boolean)
|
|
116
|
-
if (!ids.length) return
|
|
117
|
-
const condition = conditionOf(rule)
|
|
118
|
-
for (const id of ids) {
|
|
119
|
-
const byCondition = byId.get(id) ?? new Map()
|
|
120
|
-
byId.set(id, byCondition)
|
|
121
|
-
const declarations = byCondition.get(condition) ?? []
|
|
122
|
-
byCondition.set(condition, declarations)
|
|
123
|
-
rule.each(node => {
|
|
124
|
-
if (node.type === 'decl') declarations.push(node)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
return byId
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** The declarations in effect under `condition`: the base ones, then the condition's own. */
|
|
132
|
-
const effective = (byCondition, condition) => {
|
|
133
|
-
const result = new Map()
|
|
134
|
-
for (const key of condition === BASE ? [BASE] : [BASE, condition]) {
|
|
135
|
-
for (const declaration of byCondition?.get(key) ?? []) result.set(declaration.prop, declaration)
|
|
136
|
-
}
|
|
137
|
-
return result
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** 'vertical' or 'horizontal' when these declarations make a stack, else undefined. */
|
|
141
|
-
const directionOf = declarations => {
|
|
142
|
-
if (declarations.get('--container-layout-type')?.value.trim() !== STACK_CONTAINER) return undefined
|
|
143
|
-
return /^column/.test(declarations.get('flex-direction')?.value.trim() ?? '') ? 'vertical' : 'horizontal'
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** ` (layout.css:12)`: the carrier the declaration was written in, which the bundler doesn't name. */
|
|
147
|
-
const where = declaration => {
|
|
148
|
-
const file = declaration.source?.input.file
|
|
149
|
-
const line = declaration.source?.start?.line
|
|
150
|
-
return file ? ` (${basename(file)}${line ? `:${line}` : ''})` : ''
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const or = items => (items.length === 1 ? items[0] : `${items.slice(0, -1).join(', ')} or ${items.at(-1)}`)
|
|
154
|
-
|
|
155
|
-
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
156
|
-
export const stackChildrenPlugin = {
|
|
157
|
-
name: 'stack-children',
|
|
158
|
-
// After mixins copy their bodies in, before spx rewrites --spx() calls away.
|
|
159
|
-
order: 12,
|
|
160
|
-
transform(root, context) {
|
|
161
|
-
const structurePath = STRUCTURE_FILES.map(file => join(structureFolderOf(context.filePath), file)).find(
|
|
162
|
-
existsSync
|
|
163
|
-
)
|
|
164
|
-
if (structurePath === undefined) return
|
|
165
|
-
context.addDependency?.(structurePath)
|
|
166
|
-
const parents = parentsIn(structurePath)
|
|
167
|
-
if (parents.size === 0) return
|
|
168
|
-
const byId = declarationsById(root)
|
|
169
|
-
|
|
170
|
-
const violations = new Set()
|
|
171
|
-
let first
|
|
172
|
-
for (const [child, parent] of parents) {
|
|
173
|
-
const childRules = byId.get(child)
|
|
174
|
-
const parentRules = byId.get(parent)
|
|
175
|
-
if (childRules === undefined || parentRules === undefined) continue
|
|
176
|
-
for (const condition of new Set([BASE, ...parentRules.keys(), ...childRules.keys()])) {
|
|
177
|
-
const direction = directionOf(effective(parentRules, condition))
|
|
178
|
-
if (direction === undefined) continue
|
|
179
|
-
for (const declaration of effective(childRules, condition).values()) {
|
|
180
|
-
const kind = kindOf(declaration.prop)
|
|
181
|
-
if (kind === undefined) continue
|
|
182
|
-
const allowed = ALLOWED[direction][kind]
|
|
183
|
-
const bad = tokens(declaration.value).filter(token => {
|
|
184
|
-
const length = lengthOf(token)
|
|
185
|
-
return length === undefined || (length !== 'zero' && !allowed.includes(length))
|
|
186
|
-
})
|
|
187
|
-
if (!bad.length) continue
|
|
188
|
-
first ??= declaration.source
|
|
189
|
-
violations.add(
|
|
190
|
-
`#${child}${where(declaration)}, a ${direction} stack child: ${declaration.prop} must be ` +
|
|
191
|
-
`${or(allowed.map(unit => LABELS[unit]))}; found ${bad.join(' ')}.`
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (violations.size) {
|
|
198
|
-
throw new LayoutError([...violations], {
|
|
199
|
-
file: first?.input.file,
|
|
200
|
-
line: first?.start?.line,
|
|
201
|
-
column: first?.start?.column,
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
}
|
package/src/jsxPlugins/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// The JSX stage's plugins, in no particular order — the pipeline sorts by each descriptor's
|
|
2
|
-
// `order`. A descriptor needs a `name`, an `order`, and `transform(program, context)`, where
|
|
3
|
-
// `program` is the Babel Program path for the module; see `../transform/types.js`.
|
|
4
|
-
|
|
5
|
-
import { mediaValidationPlugin } from './mediaValidation/index.js'
|
|
6
|
-
import { origamiCompatibilityPlugin } from './origamiCompatibility/index.js'
|
|
7
|
-
import { layoutValidationPlugin } from './validation/index.js'
|
|
8
|
-
|
|
9
|
-
/** @type {readonly import('../transform/types.js').JsxTransformPlugin[]} */
|
|
10
|
-
export const jsxPlugins = Object.freeze([layoutValidationPlugin, mediaValidationPlugin, origamiCompatibilityPlugin])
|
|
@@ -1,49 +0,0 @@
|
|
|
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`
|
|
3
|
-
// uploads it and points the Image at the uploaded file. `yarn dev` only warns, so the page still
|
|
4
|
-
// previews. Like the layout rules, Image is identified by its JSX tag name, and a computed `uri` is
|
|
5
|
-
// not checked.
|
|
6
|
-
|
|
7
|
-
import { basename } from 'node:path'
|
|
8
|
-
import { LayoutError } from '../../transform/layoutError.js'
|
|
9
|
-
|
|
10
|
-
const STRUCTURE_FILES = new Set(['structure.tsx', 'structure.jsx'])
|
|
11
|
-
|
|
12
|
-
// `117a96_1cff23187eeb4050a2a691bcdcad6502~mv2.jpg`: an account prefix, an underscore and a file name.
|
|
13
|
-
const WIX_MEDIA_ID = /^[A-Za-z0-9]+_[A-Za-z0-9_~.-]+\.[A-Za-z0-9]+$/
|
|
14
|
-
|
|
15
|
-
/** @type {import('../../transform/types.js').JsxTransformPlugin} */
|
|
16
|
-
export const mediaValidationPlugin = {
|
|
17
|
-
name: 'wix-import-media-validation',
|
|
18
|
-
// Alongside the layout rules: before anything rewrites the tree.
|
|
19
|
-
order: -100,
|
|
20
|
-
appliesTo: context => STRUCTURE_FILES.has(basename(context.filePath)),
|
|
21
|
-
|
|
22
|
-
transform: (program, context) => {
|
|
23
|
-
const file = context.relativePath ?? context.filePath
|
|
24
|
-
const violations = []
|
|
25
|
-
let first
|
|
26
|
-
program.traverse({
|
|
27
|
-
JSXOpeningElement({ node }) {
|
|
28
|
-
if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'Image') return
|
|
29
|
-
const attributes = node.attributes.filter(attribute => attribute.type === 'JSXAttribute')
|
|
30
|
-
const image = attributes.find(attribute => attribute.name.name === 'image')?.value?.expression
|
|
31
|
-
const uri = image?.type === 'ObjectExpression' &&
|
|
32
|
-
image.properties.find(property => property.key?.name === 'uri' || property.key?.value === 'uri')
|
|
33
|
-
if (uri?.value.type !== 'StringLiteral' || WIX_MEDIA_ID.test(uri.value.value)) return
|
|
34
|
-
const id = attributes.find(attribute => attribute.name.name === 'id')?.value?.value
|
|
35
|
-
const line = uri.loc?.start.line
|
|
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.`
|
|
38
|
-
)
|
|
39
|
-
first ??= { file: context.filePath, line, column: uri.loc?.start.column }
|
|
40
|
-
},
|
|
41
|
-
})
|
|
42
|
-
if (violations.length === 0) return
|
|
43
|
-
if (context.command === 'serve') {
|
|
44
|
-
for (const violation of violations) console.warn(`[origami] ${violation}`)
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
throw new LayoutError(violations, first)
|
|
48
|
-
},
|
|
49
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Shared JSX/object-literal reads used by the compatibility rules.
|
|
2
|
-
|
|
3
|
-
export const findAttribute = (attributes, name) =>
|
|
4
|
-
attributes.find(attribute => attribute.type === 'JSXAttribute' && attribute.name.name === name)
|
|
5
|
-
|
|
6
|
-
const propertyName = property => {
|
|
7
|
-
if (property.computed) return undefined
|
|
8
|
-
if (property.key.type === 'Identifier') return property.key.name
|
|
9
|
-
if (property.key.type === 'StringLiteral') return property.key.value
|
|
10
|
-
return undefined
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Later duplicate keys win, matching how the object literal itself would evaluate.
|
|
14
|
-
export const indexProperties = properties => {
|
|
15
|
-
const fields = new Map()
|
|
16
|
-
for (const property of properties) {
|
|
17
|
-
if (property.type !== 'ObjectProperty') continue
|
|
18
|
-
const name = propertyName(property)
|
|
19
|
-
if (name !== undefined) fields.set(name, property)
|
|
20
|
-
}
|
|
21
|
-
return fields
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The object literal a JSX prop was given, or undefined when it is anything else. */
|
|
25
|
-
export const objectPropValue = (openingElement, name) => {
|
|
26
|
-
const attribute = findAttribute(openingElement.attributes, name)
|
|
27
|
-
if (attribute?.value?.type !== 'JSXExpressionContainer') return undefined
|
|
28
|
-
const { expression } = attribute.value
|
|
29
|
-
return expression.type === 'ObjectExpression' ? expression : undefined
|
|
30
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Resolve Origami component identity from named virtual-module imports or named/default imports
|
|
2
|
-
// from @wix/origami-system-components/<Name>. Local aliases keep that identity; unrelated local
|
|
3
|
-
// components with the same JSX tag name are left alone.
|
|
4
|
-
|
|
5
|
-
const ORIGAMI_MODULES = new Set(['document', 'document/components', 'wixEditorElements'])
|
|
6
|
-
|
|
7
|
-
const importedName = specifier => {
|
|
8
|
-
if (specifier.type !== 'ImportSpecifier') return undefined
|
|
9
|
-
return specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const componentName = (source, specifier) => {
|
|
13
|
-
if (ORIGAMI_MODULES.has(source)) return importedName(specifier)
|
|
14
|
-
|
|
15
|
-
const match = /^@wix\/origami-system-components\/([^/]+)$/u.exec(source)
|
|
16
|
-
if (!match) return undefined
|
|
17
|
-
const name = match[1]
|
|
18
|
-
const imported = importedName(specifier)
|
|
19
|
-
return specifier.type === 'ImportDefaultSpecifier' || imported === name || imported === 'default'
|
|
20
|
-
? name
|
|
21
|
-
: undefined
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {object} program
|
|
26
|
-
* @param {Map<string, readonly object[]>} rulesByComponent
|
|
27
|
-
* @returns {Map<string, readonly object[]>} local binding name -> rules to apply to it
|
|
28
|
-
*/
|
|
29
|
-
export const collectComponentBindings = (program, rulesByComponent) => {
|
|
30
|
-
const bindings = new Map()
|
|
31
|
-
for (const statement of program.body) {
|
|
32
|
-
if (statement.type !== 'ImportDeclaration') continue
|
|
33
|
-
for (const specifier of statement.specifiers) {
|
|
34
|
-
const name = componentName(statement.source.value, specifier)
|
|
35
|
-
const rules = name === undefined ? undefined : rulesByComponent.get(name)
|
|
36
|
-
if (rules !== undefined) bindings.set(specifier.local.name, rules)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return bindings
|
|
40
|
-
}
|