@wix/origami-site-runtime 1.0.13 → 1.0.15

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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/package.json +8 -8
  3. package/src/index.js +2 -2
  4. package/src/routing/siteLayout.js +1 -9
  5. package/src/siteViteConfig.js +4 -4
  6. package/src/cssPlugins/customMedia.js +0 -52
  7. package/src/cssPlugins/fonts.js +0 -93
  8. package/src/cssPlugins/index.js +0 -32
  9. package/src/cssPlugins/layout/README.md +0 -77
  10. package/src/cssPlugins/layout/compiler.js +0 -116
  11. package/src/cssPlugins/layout/contract.js +0 -35
  12. package/src/cssPlugins/layout/index.js +0 -12
  13. package/src/cssPlugins/layout/parser.js +0 -81
  14. package/src/cssPlugins/mixins.js +0 -63
  15. package/src/cssPlugins/presets.js +0 -73
  16. package/src/cssPlugins/spx/README.md +0 -73
  17. package/src/cssPlugins/spx/calls.js +0 -12
  18. package/src/cssPlugins/spx/cascade.js +0 -41
  19. package/src/cssPlugins/spx/index.js +0 -13
  20. package/src/cssPlugins/stackChildren.js +0 -205
  21. package/src/jsxPlugins/index.js +0 -10
  22. package/src/jsxPlugins/mediaValidation/index.js +0 -49
  23. package/src/jsxPlugins/origamiCompatibility/ast.js +0 -30
  24. package/src/jsxPlugins/origamiCompatibility/componentBindings.js +0 -40
  25. package/src/jsxPlugins/origamiCompatibility/index.js +0 -48
  26. package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +0 -15
  27. package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +0 -22
  28. package/src/jsxPlugins/validation/checks.js +0 -105
  29. package/src/jsxPlugins/validation/index.js +0 -65
  30. package/src/jsxPlugins/validation/rules.js +0 -24
  31. package/src/jsxPlugins/validation/tree.js +0 -15
  32. package/src/transform/context.js +0 -13
  33. package/src/transform/css/postcssPlugin.js +0 -60
  34. package/src/transform/css/runPlugins.js +0 -40
  35. package/src/transform/jsx/babelAdapter.js +0 -37
  36. package/src/transform/jsx/runPlugins.js +0 -23
  37. package/src/transform/layoutError.js +0 -19
  38. package/src/transform/pipeline.js +0 -60
  39. package/src/transform/registry.js +0 -40
  40. package/src/transform/types.js +0 -47
@@ -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
- }
@@ -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
- }