@wix/origami-transforms 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cssPlugins/componentPosition.d.ts +6 -2
- package/dist/cssPlugins/customEnums.d.ts +5 -2
- package/dist/cssPlugins/customMedia.d.ts +7 -2
- package/dist/cssPlugins/fonts.d.ts +6 -2
- package/dist/cssPlugins/index.d.ts +2 -2
- package/dist/cssPlugins/layout/index.d.ts +7 -2
- package/dist/cssPlugins/mixins.d.ts +7 -2
- package/dist/cssPlugins/presets.d.ts +6 -2
- package/dist/cssPlugins/shapeDividers.d.ts +6 -2
- package/dist/cssPlugins/spx/index.d.ts +7 -2
- package/dist/cssPlugins/stackChildren.d.ts +5 -2
- package/dist/cssPlugins/svg.d.ts +4 -1
- package/dist/cssPlugins/wixVectorUrls.d.ts +6 -2
- package/dist/jsxPlugins/index.d.ts +2 -2
- package/dist/jsxPlugins/mediaValidation/index.d.ts +5 -2
- package/dist/jsxPlugins/origamiCompatibility/index.d.ts +6 -2
- package/dist/jsxPlugins/pageSlots.d.ts +7 -6
- package/dist/jsxPlugins/validation/checks.d.ts +7 -7
- package/dist/jsxPlugins/validation/index.d.ts +5 -2
- package/dist/jsxPlugins/validation/rules.d.ts +25 -0
- package/dist/transform/bi.d.ts +13 -0
- package/dist/transform/context.d.ts +1 -0
- package/dist/transform/css/runPlugins.d.ts +4 -1
- package/dist/transform/layoutError.d.ts +8 -4
- package/dist/transform/pipeline.d.ts +7 -9
- package/dist/transform/types.d.ts +16 -13
- package/package.json +6 -4
- package/src/cssPlugins/componentPosition.js +5 -1
- package/src/cssPlugins/customEnums.js +19 -12
- package/src/cssPlugins/customMedia.js +10 -3
- package/src/cssPlugins/fonts.js +5 -1
- package/src/cssPlugins/index.js +1 -1
- package/src/cssPlugins/layout/index.js +8 -1
- package/src/cssPlugins/layout/parser.js +15 -14
- package/src/cssPlugins/mixins.js +13 -6
- package/src/cssPlugins/presets.js +7 -2
- package/src/cssPlugins/shapeDividers.js +6 -2
- package/src/cssPlugins/spx/calls.js +2 -1
- package/src/cssPlugins/spx/cascade.js +3 -2
- package/src/cssPlugins/spx/index.js +8 -1
- package/src/cssPlugins/stackChildren.js +6 -2
- package/src/cssPlugins/svg.js +5 -2
- package/src/cssPlugins/wixVectorUrls.js +6 -2
- package/src/jsxPlugins/index.js +1 -1
- package/src/jsxPlugins/mediaValidation/index.js +14 -5
- package/src/jsxPlugins/origamiCompatibility/index.js +5 -1
- package/src/jsxPlugins/pageSlots.js +6 -1
- package/src/jsxPlugins/validation/checks.js +15 -8
- package/src/jsxPlugins/validation/index.js +9 -3
- package/src/jsxPlugins/validation/rules.js +14 -0
- package/src/transform/bi.js +64 -0
- package/src/transform/context.js +39 -0
- package/src/transform/css/postcssPlugin.js +18 -3
- package/src/transform/css/runPlugins.js +23 -3
- package/src/transform/jsx/runPlugins.js +17 -2
- package/src/transform/layoutError.js +8 -3
- package/src/transform/pipeline.js +25 -5
- package/src/transform/types.js +30 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fail } from './index.js'
|
|
1
2
|
import { basename } from 'node:path'
|
|
2
3
|
import { isValidContainerId } from '@wix/origami-system-components/layout-contract'
|
|
3
4
|
import { relativeToSite } from '../../transform/relativeToSite.js'
|
|
@@ -20,7 +21,7 @@ export const parseTargets = (root, context) => {
|
|
|
20
21
|
const owners = new Map()
|
|
21
22
|
root.walkAtRules('origami-target', target => {
|
|
22
23
|
const name = target.params.trim()
|
|
23
|
-
if (!Object.hasOwn(targets, name)) throw target.error(`Unknown @origami-target ${name}`)
|
|
24
|
+
if (!Object.hasOwn(targets, name)) throw fail('unknown_target', target.error(`Unknown @origami-target ${name}`))
|
|
24
25
|
const owner = target.parent
|
|
25
26
|
// DM writes component rules as .compId; #compId is still accepted.
|
|
26
27
|
if (
|
|
@@ -28,34 +29,34 @@ export const parseTargets = (root, context) => {
|
|
|
28
29
|
!['.', '#'].includes(owner.selector[0]) ||
|
|
29
30
|
!isValidContainerId(owner.selector.slice(1))
|
|
30
31
|
) {
|
|
31
|
-
throw target.error('@origami-target requires a single component ID rule')
|
|
32
|
+
throw fail('target_requires_component_id', target.error('@origami-target requires a single component ID rule'))
|
|
32
33
|
}
|
|
33
34
|
for (let ancestor = owner.parent; ancestor && ancestor.type !== 'root'; ancestor = ancestor.parent) {
|
|
34
35
|
if (
|
|
35
36
|
ancestor.type !== 'atrule' ||
|
|
36
37
|
!['scope', 'media', 'layer', 'supports', 'container'].includes(ancestor.name)
|
|
37
38
|
) {
|
|
38
|
-
throw target.error('@origami-target is only allowed inside layout rule envelopes')
|
|
39
|
+
throw fail('invalid_target_envelope', target.error('@origami-target is only allowed inside layout rule envelopes'))
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
if (!isLayoutSource(target, context))
|
|
42
|
-
throw target.error('@origami-target must originate in site-owned layout.css')
|
|
43
|
+
throw fail('target_not_site_layout', target.error('@origami-target must originate in site-owned layout.css'))
|
|
43
44
|
const siblings = owners.get(owner) ?? []
|
|
44
|
-
if (siblings.some(sibling => sibling.params.trim() === name)) throw target.error(`Duplicate target ${name}`)
|
|
45
|
+
if (siblings.some(sibling => sibling.params.trim() === name)) throw fail('duplicate_target', target.error(`Duplicate target ${name}`))
|
|
45
46
|
const seen = new Set()
|
|
46
47
|
for (const node of target.nodes ?? []) {
|
|
47
48
|
if (node.type === 'comment') continue
|
|
48
|
-
if (node.type !== 'decl') throw node.error('Target bodies must contain declarations only')
|
|
49
|
-
if (seen.has(node.prop)) throw node.error(`Duplicate target property ${node.prop}`)
|
|
49
|
+
if (node.type !== 'decl') throw fail('target_requires_declarations', node.error('Target bodies must contain declarations only'))
|
|
50
|
+
if (seen.has(node.prop)) throw fail('duplicate_target_property', node.error(`Duplicate target property ${node.prop}`))
|
|
50
51
|
seen.add(node.prop)
|
|
51
|
-
if (!targets[name].has(node.prop)) throw node.error(`${node.prop} does not belong to ${name}`)
|
|
52
|
+
if (!targets[name].has(node.prop)) throw fail('property_wrong_target', node.error(`${node.prop} does not belong to ${name}`))
|
|
52
53
|
if (node.prop === 'position' && node.value !== 'relative')
|
|
53
|
-
throw node.error('Content position must be relative')
|
|
54
|
+
throw fail('content_position_not_relative', node.error('Content position must be relative'))
|
|
54
55
|
if (name === OVERFLOW && !['auto', 'scroll', 'hidden', 'visible', 'clip'].includes(node.value)) {
|
|
55
|
-
throw node.error(`Unsupported overflow value ${node.value}`)
|
|
56
|
+
throw fail('unsupported_overflow', node.error(`Unsupported overflow value ${node.value}`))
|
|
56
57
|
}
|
|
57
58
|
if (node.prop === '--content-max-width-align' && !['left', 'center', 'right'].includes(node.value)) {
|
|
58
|
-
throw node.error(`Unsupported content alignment ${node.value}`)
|
|
59
|
+
throw fail('unsupported_content_alignment', node.error(`Unsupported content alignment ${node.value}`))
|
|
59
60
|
}
|
|
60
61
|
if (node.prop === 'display') {
|
|
61
62
|
const type = owner.nodes.find(item => item.type === 'decl' && item.prop === '--container-layout-type')
|
|
@@ -64,7 +65,7 @@ export const parseTargets = (root, context) => {
|
|
|
64
65
|
(type?.type === 'decl' &&
|
|
65
66
|
(!Object.hasOwn(displays, type.value) || displays[type.value] !== node.value))
|
|
66
67
|
) {
|
|
67
|
-
throw node.error('Container type and content display are inconsistent')
|
|
68
|
+
throw fail('inconsistent_container_display', node.error('Container type and content display are inconsistent'))
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -75,10 +76,10 @@ export const parseTargets = (root, context) => {
|
|
|
75
76
|
const seen = new Set()
|
|
76
77
|
for (const node of owner.nodes) {
|
|
77
78
|
if (node.type !== 'decl') continue
|
|
78
|
-
if (seen.has(node.prop)) throw node.error(`Duplicate root property ${node.prop}`)
|
|
79
|
+
if (seen.has(node.prop)) throw fail('duplicate_root_property', node.error(`Duplicate root property ${node.prop}`))
|
|
79
80
|
seen.add(node.prop)
|
|
80
81
|
if ((targets[CONTENT].has(node.prop) && node.prop !== 'position') || targets[OVERFLOW].has(node.prop)) {
|
|
81
|
-
throw node.error(`${node.prop} belongs inside @origami-target`)
|
|
82
|
+
throw fail('property_requires_target', node.error(`${node.prop} belongs inside @origami-target`))
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
}
|
package/src/cssPlugins/mixins.js
CHANGED
|
@@ -7,8 +7,15 @@ const MAX_CLONED_BYTES = 10 * 1024 * 1024
|
|
|
7
7
|
/** @param {import('postcss').AtRule} atRule */
|
|
8
8
|
const mixinName = atRule => atRule.params.trim().replace(/\(\)$/, '')
|
|
9
9
|
|
|
10
|
-
/** @type {
|
|
10
|
+
export const checks = /** @type {const} */ (['missing_mixin', 'cyclic_apply', 'expansion_depth_exceeded', 'cloned_node_budget_exceeded', 'cloned_css_budget_exceeded'])
|
|
11
|
+
/** @typedef {typeof checks[number]} Check */
|
|
12
|
+
|
|
13
|
+
/** @template {Error} E @param {Check} check @param {E} error */
|
|
14
|
+
export const fail = (check, error) => Object.assign(error, { check })
|
|
15
|
+
|
|
16
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
11
17
|
export const mixinsPlugin = {
|
|
18
|
+
checks,
|
|
12
19
|
name: 'mixins',
|
|
13
20
|
order: 10,
|
|
14
21
|
transform(root) {
|
|
@@ -30,16 +37,16 @@ export const mixinsPlugin = {
|
|
|
30
37
|
container.walkAtRules('apply', atRule => {
|
|
31
38
|
const name = mixinName(atRule)
|
|
32
39
|
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(' -> ')}`)
|
|
40
|
+
if (!body) throw fail('missing_mixin', new Error(`@apply ${atRule.params} has no @mixin definition`))
|
|
41
|
+
if (stack.includes(name)) throw fail('cyclic_apply', new Error(`Cyclic @apply: ${[...stack, name].join(' -> ')}`))
|
|
35
42
|
if (stack.length >= MAX_EXPANSION_DEPTH) {
|
|
36
|
-
throw new Error(`@apply ${name} exceeds maximum expansion depth (${MAX_EXPANSION_DEPTH})`)
|
|
43
|
+
throw fail('expansion_depth_exceeded', new Error(`@apply ${name} exceeds maximum expansion depth (${MAX_EXPANSION_DEPTH})`))
|
|
37
44
|
}
|
|
38
45
|
const pending = [...body]
|
|
39
46
|
while (pending.length) {
|
|
40
47
|
const node = pending.pop()
|
|
41
48
|
if (++clonedNodes > MAX_CLONED_NODES) {
|
|
42
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-node budget (${MAX_CLONED_NODES})`)
|
|
49
|
+
throw fail('cloned_node_budget_exceeded', new Error(`@apply ${name} exceeds stylesheet cloned-node budget (${MAX_CLONED_NODES})`))
|
|
43
50
|
}
|
|
44
51
|
if (node && 'nodes' in node && node.nodes) {
|
|
45
52
|
for (const child of node.nodes) pending.push(child)
|
|
@@ -48,7 +55,7 @@ export const mixinsPlugin = {
|
|
|
48
55
|
for (const node of body) {
|
|
49
56
|
clonedBytes += Buffer.byteLength((node.raws.before ?? '') + node.toString(), 'utf8')
|
|
50
57
|
if (clonedBytes > MAX_CLONED_BYTES) {
|
|
51
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-CSS byte budget (${MAX_CLONED_BYTES})`)
|
|
58
|
+
throw fail('cloned_css_budget_exceeded', new Error(`@apply ${name} exceeds stylesheet cloned-CSS byte budget (${MAX_CLONED_BYTES})`))
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
// PostCSS does not revisit nodes inserted in place of the current at-rule.
|
|
@@ -23,8 +23,12 @@ const target = (selector, key) => {
|
|
|
23
23
|
return selector === first ? `.${id}${WRAPPER}` : null
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** @type {
|
|
26
|
+
const checks = /** @type {const} */ ([])
|
|
27
|
+
/** @typedef {typeof checks[number]} Check */
|
|
28
|
+
|
|
29
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
27
30
|
export const presetsPlugin = {
|
|
31
|
+
checks,
|
|
28
32
|
name: 'presets',
|
|
29
33
|
order: 13,
|
|
30
34
|
transform(root, context) {
|
|
@@ -53,7 +57,8 @@ export const presetsPlugin = {
|
|
|
53
57
|
declaration,
|
|
54
58
|
`presets: ${context.relativePath ?? context.filePath}: ` +
|
|
55
59
|
`${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
|
|
60
|
+
`and an inner element's as --preset-{key} on the rule of the element it sits in. Left as authored.`,
|
|
61
|
+
'unmappable_preset'
|
|
57
62
|
)
|
|
58
63
|
continue
|
|
59
64
|
}
|
|
@@ -101,8 +101,12 @@ const layerDeclarations = (compId, vars) => {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** @type {
|
|
104
|
+
const checks = /** @type {const} */ ([])
|
|
105
|
+
/** @typedef {typeof checks[number]} Check */
|
|
106
|
+
|
|
107
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
105
108
|
export const shapeDividersPlugin = {
|
|
109
|
+
checks,
|
|
106
110
|
name: 'shape-dividers',
|
|
107
111
|
// Last, with wix-vector-urls: it only adds rules, which no other plugin needs to see, and it reads only `--height` as
|
|
108
112
|
// `var(--height)`, so spx converts that var's `--spx()` where it is declared.
|
|
@@ -125,7 +129,7 @@ export const shapeDividersPlugin = {
|
|
|
125
129
|
const allVars = { ...baseVars.get(id), ...vars }
|
|
126
130
|
const declarations = layerDeclarations(id, allVars)
|
|
127
131
|
if (declarations === undefined) {
|
|
128
|
-
if (allVars['--svgId']) context.warn?.(rule, `Couldn't draw the shape divider's SVG ${allVars['--svgId']}: it didn't load from Wix's CDN, or has no view box
|
|
132
|
+
if (allVars['--svgId']) context.warn?.(rule, `Couldn't draw the shape divider's SVG ${allVars['--svgId']}: it didn't load from Wix's CDN, or has no view box`, 'svg_not_loaded')
|
|
129
133
|
continue
|
|
130
134
|
}
|
|
131
135
|
const layerRule = postcss.rule({ selector: `${rule.selector} ${LAYER}`, source: rule.source })
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fail } from './index.js'
|
|
1
2
|
const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
2
3
|
|
|
3
4
|
/** Replace literal SPX calls, reporting unsupported arguments at their declaration.
|
|
@@ -8,5 +9,5 @@ const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
|
8
9
|
export const lowerCalls = (declaration, resolve, invalidCall) => {
|
|
9
10
|
if (!declaration.value.includes('--spx(')) return
|
|
10
11
|
declaration.value = declaration.value.replace(CALL, (_, literal) => resolve(Number(literal)))
|
|
11
|
-
if (declaration.value.includes('--spx(')) throw invalidCall()
|
|
12
|
+
if (declaration.value.includes('--spx(')) throw fail('non_literal_spx_argument', invalidCall())
|
|
12
13
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fail } from './index.js'
|
|
1
2
|
import { lowerCalls } from './calls.js'
|
|
2
3
|
|
|
3
4
|
// Keep arithmetic at the call site: an inherited derived custom property would already
|
|
@@ -16,7 +17,7 @@ const UNCAPPED_UNIT = unitCappedAt('99999px')
|
|
|
16
17
|
export const compileCascadeDeclaration = declaration => {
|
|
17
18
|
if (declaration.prop === '--spx-resolver-type') {
|
|
18
19
|
const resolver = declaration.value.trim()
|
|
19
|
-
if (!['scale', 'fixed'].includes(resolver)) throw declaration.error('Unsupported SPX resolver')
|
|
20
|
+
if (!['scale', 'fixed'].includes(resolver)) throw fail('unsupported_spx_resolver', declaration.error('Unsupported SPX resolver'))
|
|
20
21
|
const value = resolver === 'scale' ? '1' : '0'
|
|
21
22
|
const previous = declaration.prev()
|
|
22
23
|
if (
|
|
@@ -32,7 +33,7 @@ export const compileCascadeDeclaration = declaration => {
|
|
|
32
33
|
declaration.prop === '--spx-ref-width' &&
|
|
33
34
|
(!Number.isFinite(Number(declaration.value)) || Number(declaration.value) <= 0)
|
|
34
35
|
) {
|
|
35
|
-
throw declaration.error('SPX reference width must be positive')
|
|
36
|
+
throw fail('invalid_spx_reference_width', declaration.error('SPX reference width must be positive'))
|
|
36
37
|
}
|
|
37
38
|
const unit = declaration.prop === '--content-max-width' ? UNCAPPED_UNIT : UNIT
|
|
38
39
|
lowerCalls(
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { compileCascadeDeclaration } from './cascade.js'
|
|
2
2
|
|
|
3
|
-
/** @type {
|
|
3
|
+
export const checks = /** @type {const} */ (['unsupported_spx_resolver', 'invalid_spx_reference_width', 'non_literal_spx_argument'])
|
|
4
|
+
/** @typedef {typeof checks[number]} Check */
|
|
5
|
+
|
|
6
|
+
/** @template {Error} E @param {Check} check @param {E} error */
|
|
7
|
+
export const fail = (check, error) => Object.assign(error, { check })
|
|
8
|
+
|
|
9
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
4
10
|
export const spxPlugin = {
|
|
11
|
+
checks,
|
|
5
12
|
name: 'spx',
|
|
6
13
|
order: 20,
|
|
7
14
|
transform(root) {
|
|
@@ -157,8 +157,12 @@ const where = declaration => {
|
|
|
157
157
|
|
|
158
158
|
const or = items => (items.length === 1 ? items[0] : `${items.slice(0, -1).join(', ')} or ${items.at(-1)}`)
|
|
159
159
|
|
|
160
|
-
/** @type {
|
|
160
|
+
export const checks = /** @type {const} */ (['child_not_in_stack'])
|
|
161
|
+
/** @typedef {typeof checks[number]} Check */
|
|
162
|
+
|
|
163
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
161
164
|
export const stackChildrenPlugin = {
|
|
165
|
+
checks,
|
|
162
166
|
name: 'stack-children',
|
|
163
167
|
// After mixins copy their bodies in, before spx rewrites --spx() calls away.
|
|
164
168
|
order: 12,
|
|
@@ -199,7 +203,7 @@ export const stackChildrenPlugin = {
|
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
if (violations.size) {
|
|
202
|
-
throw new LayoutError([...violations], {
|
|
206
|
+
throw new LayoutError(stackChildrenPlugin, [...violations].map(message => ({ check: 'child_not_in_stack', message })), {
|
|
203
207
|
file: first?.input.file,
|
|
204
208
|
line: first?.start?.line,
|
|
205
209
|
column: first?.start?.column,
|
package/src/cssPlugins/svg.js
CHANGED
|
@@ -16,13 +16,16 @@ const loading = new Map()
|
|
|
16
16
|
const svgFileName = id => (id.endsWith('.svg') ? id : `${id}.svg`)
|
|
17
17
|
|
|
18
18
|
const load = async id => {
|
|
19
|
+
let ok = false
|
|
19
20
|
try {
|
|
20
21
|
const response = await fetch(SHAPES_URL + svgFileName(id), { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
|
|
21
22
|
const svg = response.ok ? await response.text() : ''
|
|
22
|
-
|
|
23
|
+
ok = SVG_TAG.test(svg)
|
|
24
|
+
loaded.set(id, ok ? svg : null)
|
|
23
25
|
} catch {
|
|
24
26
|
// Not loaded: the plugin warns and keeps the value as written.
|
|
25
27
|
}
|
|
28
|
+
return ok
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/** Loads the SVGs of `ids` that aren't loaded yet. */
|
|
@@ -33,7 +36,7 @@ export const loadSvgs = ids => {
|
|
|
33
36
|
if (!loading.has(id)) loading.set(id, load(id).finally(() => loading.delete(id)))
|
|
34
37
|
return loading.get(id)
|
|
35
38
|
})
|
|
36
|
-
return pending.length ? Promise.all(pending).then((
|
|
39
|
+
return pending.length ? Promise.all(pending).then(results => ({ fetched: results.length, failed: results.filter(ok => !ok).length })) : undefined
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/** The SVG's markup, once `loadSvgs` loaded it. */
|
|
@@ -17,8 +17,12 @@ const idsIn = root => {
|
|
|
17
17
|
return ids
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** @type {
|
|
20
|
+
const checks = /** @type {const} */ ([])
|
|
21
|
+
/** @typedef {typeof checks[number]} Check */
|
|
22
|
+
|
|
23
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
21
24
|
export const wixVectorUrlsPlugin = {
|
|
25
|
+
checks,
|
|
22
26
|
name: 'wix-vector-urls',
|
|
23
27
|
// Last, so a url a mixin or preset copies elsewhere is inlined wherever it ends up.
|
|
24
28
|
order: 25,
|
|
@@ -29,7 +33,7 @@ export const wixVectorUrlsPlugin = {
|
|
|
29
33
|
declaration.value = declaration.value.replace(WIX_VECTOR_URL, (url, _quote, id) => {
|
|
30
34
|
const svg = loadedSvg(id)
|
|
31
35
|
if (svg === undefined) {
|
|
32
|
-
context.warn?.(declaration, `Couldn't load the SVG ${id} from Wix's CDN, so ${url} stays as written
|
|
36
|
+
context.warn?.(declaration, `Couldn't load the SVG ${id} from Wix's CDN, so ${url} stays as written`, 'svg_not_loaded')
|
|
33
37
|
return url
|
|
34
38
|
}
|
|
35
39
|
return svgDataUrl(scaleSvg(svg, svgInfo(svg), 'none'))
|
package/src/jsxPlugins/index.js
CHANGED
|
@@ -7,5 +7,5 @@ import { origamiCompatibilityPlugin } from './origamiCompatibility/index.js'
|
|
|
7
7
|
import { layoutValidationPlugin } from './validation/index.js'
|
|
8
8
|
import { pageSlotsPlugin } from './pageSlots.js'
|
|
9
9
|
|
|
10
|
-
/** @type {readonly import('../transform/types.js').JsxTransformPlugin[]} */
|
|
10
|
+
/** @type {readonly import('../transform/types.js').JsxTransformPlugin<string>[]} */
|
|
11
11
|
export const jsxPlugins = Object.freeze([layoutValidationPlugin, mediaValidationPlugin, origamiCompatibilityPlugin, pageSlotsPlugin])
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warn } from '../../transform/bi.js'
|
|
1
2
|
// Fails the build on an Image whose `uri` is not a Wix media ID, such as a web URL or a local file.
|
|
2
3
|
// The Wix import accepts such a `uri` but the image is broken on the site; `wix origami upload-media`
|
|
3
4
|
// uploads it and points the Image at the uploaded file. `yarn dev` only warns, so the page still
|
|
@@ -12,8 +13,12 @@ const STRUCTURE_FILES = new Set(['structure.tsx', 'structure.jsx'])
|
|
|
12
13
|
// `117a96_1cff23187eeb4050a2a691bcdcad6502~mv2.jpg`: an account prefix, an underscore and a file name.
|
|
13
14
|
const WIX_MEDIA_ID = /^[A-Za-z0-9]+_[A-Za-z0-9_~.-]+\.[A-Za-z0-9]+$/
|
|
14
15
|
|
|
15
|
-
/** @type {
|
|
16
|
+
export const checks = /** @type {const} */ (['media_not_wix'])
|
|
17
|
+
/** @typedef {typeof checks[number]} Check */
|
|
18
|
+
|
|
19
|
+
/** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
|
|
16
20
|
export const mediaValidationPlugin = {
|
|
21
|
+
checks,
|
|
17
22
|
name: 'wix-import-media-validation',
|
|
18
23
|
// Alongside the layout rules: before anything rewrites the tree.
|
|
19
24
|
order: -100,
|
|
@@ -21,6 +26,7 @@ export const mediaValidationPlugin = {
|
|
|
21
26
|
|
|
22
27
|
transform: (program, context) => {
|
|
23
28
|
const file = context.relativePath ?? context.filePath
|
|
29
|
+
/** @type {import('../../transform/types.js').Violation<Check>[]} */
|
|
24
30
|
const violations = []
|
|
25
31
|
let first
|
|
26
32
|
program.traverse({
|
|
@@ -33,17 +39,20 @@ export const mediaValidationPlugin = {
|
|
|
33
39
|
if (uri?.value.type !== 'StringLiteral' || WIX_MEDIA_ID.test(uri.value.value)) return
|
|
34
40
|
const id = attributes.find(attribute => attribute.name.name === 'id')?.value?.value
|
|
35
41
|
const line = uri.loc?.start.line
|
|
36
|
-
violations.push(
|
|
42
|
+
violations.push({ check: 'media_not_wix', message:
|
|
37
43
|
`${file}${line ? `:${line}` : ''} <Image${id ? ` id="${id}"` : ''}> image.uri "${uri.value.value}" is not a Wix media ID. Run \`wix origami upload-media\` to upload it.`
|
|
38
|
-
)
|
|
44
|
+
})
|
|
39
45
|
first ??= { file: context.filePath, line, column: uri.loc?.start.column }
|
|
40
46
|
},
|
|
41
47
|
})
|
|
42
48
|
if (violations.length === 0) return
|
|
43
49
|
if (context.command === 'serve') {
|
|
44
|
-
for (const violation of violations)
|
|
50
|
+
for (const violation of violations) {
|
|
51
|
+
warn('media_not_wix')
|
|
52
|
+
console.warn(`[origami] ${violation.message}`)
|
|
53
|
+
}
|
|
45
54
|
return
|
|
46
55
|
}
|
|
47
|
-
throw new LayoutError(violations, first)
|
|
56
|
+
throw new LayoutError(mediaValidationPlugin, violations, first)
|
|
48
57
|
},
|
|
49
58
|
}
|
|
@@ -22,8 +22,12 @@ const RULES_BY_COMPONENT = new Map(
|
|
|
22
22
|
])
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
/** @type {
|
|
25
|
+
const checks = /** @type {const} */ ([])
|
|
26
|
+
/** @typedef {typeof checks[number]} Check */
|
|
27
|
+
|
|
28
|
+
/** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
|
|
26
29
|
export const origamiCompatibilityPlugin = {
|
|
30
|
+
checks,
|
|
27
31
|
name: 'wix-import-origami-compatibility',
|
|
28
32
|
order: 0,
|
|
29
33
|
appliesTo: context => basename(context.filePath) === STRUCTURE_FILE,
|
|
@@ -32,8 +32,13 @@ const slotFor = component => {
|
|
|
32
32
|
return undefined
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const checks = /** @type {const} */ ([])
|
|
36
|
+
/** @typedef {typeof checks[number]} Check */
|
|
37
|
+
|
|
38
|
+
/** Transitional support for DM's literal Page children alongside authored slot props.
|
|
39
|
+
* @type {import('../transform/types.js').JsxTransformPlugin<Check>} */
|
|
36
40
|
export const pageSlotsPlugin = {
|
|
41
|
+
checks,
|
|
37
42
|
name: 'wix-import-page-slots',
|
|
38
43
|
order: 0,
|
|
39
44
|
appliesTo: context => basename(context.filePath) === 'structure.jsx',
|
|
@@ -75,7 +75,7 @@ const CHECKS = {
|
|
|
75
75
|
export const assertKnownConstraints = rules => {
|
|
76
76
|
for (const rule of rules) {
|
|
77
77
|
for (const key of Object.keys(rule)) {
|
|
78
|
-
if (key !== 'id' && key !== 'components' && !CHECKS[key]) {
|
|
78
|
+
if (key !== 'id' && key !== 'components' && key !== 'checks' && !CHECKS[key]) {
|
|
79
79
|
throw new Error(
|
|
80
80
|
`layout rules: unknown constraint "${key}" on ${rule.id}. Known: ${Object.keys(CHECKS).join(', ')}`
|
|
81
81
|
)
|
|
@@ -86,20 +86,27 @@ export const assertKnownConstraints = rules => {
|
|
|
86
86
|
|
|
87
87
|
export const constrainsChildren = rule => CHILD_CONSTRAINTS.some(key => key in rule)
|
|
88
88
|
|
|
89
|
-
/** Runs every constraint of `rule` on one element; `report` receives each full message.
|
|
89
|
+
/** Runs every constraint of `rule` on one element; `report` receives each full message.
|
|
90
|
+
* @param {import('./rules.js').Rule} rule
|
|
91
|
+
* @param {{ node: any, componentOf: (node: any) => string | undefined, report: (check: import('./index.js').Check, message: string) => void }} input
|
|
92
|
+
*/
|
|
90
93
|
export const checkRule = (rule, { node, componentOf, report }) => {
|
|
91
94
|
const prefix = `${describe(node)} ${expectation(rule)}`
|
|
92
|
-
|
|
95
|
+
/** @param {import('./index.js').Check} check */
|
|
96
|
+
const fail = check => got => report(check, `${prefix}; found ${got}.`)
|
|
93
97
|
|
|
94
98
|
const elements = node.children
|
|
95
99
|
.filter(child => child.type === 'JSXElement')
|
|
96
100
|
.map(element => ({ element, component: componentOf(element) }))
|
|
97
|
-
const context = { node, elements
|
|
101
|
+
const context = { node, elements }
|
|
98
102
|
|
|
99
|
-
if (constrainsChildren(rule)) checkLiteral(context)
|
|
100
|
-
for (const [key, value] of Object.entries(rule)) CHECKS[key]?.(value, context)
|
|
103
|
+
if (constrainsChildren(rule)) checkLiteral({ ...context, fail: fail(rule.checks.literal) })
|
|
104
|
+
for (const [key, value] of Object.entries(rule)) CHECKS[key]?.(value, { ...context, fail: fail(rule.checks[key]) })
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/** `createElement(Section, …)` would hide a constrained component's children from the checks.
|
|
107
|
+
/** `createElement(Section, …)` would hide a constrained component's children from the checks.
|
|
108
|
+
* @param {import('./rules.js').Rule} rule
|
|
109
|
+
* @param {{ node: any, component: string, report: (check: import('./index.js').Check, message: string) => void }} input
|
|
110
|
+
*/
|
|
104
111
|
export const checkCreateElement = (rule, { node, component, report }) =>
|
|
105
|
-
report(`<${component}>${at(node)} ${expectation(rule)}; found createElement(${component}), write it as a JSX tag.`)
|
|
112
|
+
report(rule.checks.createElement, `<${component}>${at(node)} ${expectation(rule)}; found createElement(${component}), write it as a JSX tag.`)
|
|
@@ -26,8 +26,12 @@ const isCreateElement = callee =>
|
|
|
26
26
|
(callee.type === 'Identifier' && callee.name === 'createElement') ||
|
|
27
27
|
(callee.type === 'MemberExpression' && !callee.computed && callee.property.name === 'createElement')
|
|
28
28
|
|
|
29
|
-
/** @type {
|
|
29
|
+
export const checks = /** @type {const} */ (['section_wrong_child', 'section_too_few_children', 'section_too_many_children', 'section_non_literal_children', 'section_create_element'])
|
|
30
|
+
/** @typedef {typeof checks[number]} Check */
|
|
31
|
+
|
|
32
|
+
/** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
|
|
30
33
|
export const layoutValidationPlugin = {
|
|
34
|
+
checks,
|
|
31
35
|
name: 'wix-import-layout-validation',
|
|
32
36
|
// Before any plugin that rewrites the tree, so the rules see it as authored.
|
|
33
37
|
order: -100,
|
|
@@ -40,8 +44,10 @@ export const layoutValidationPlugin = {
|
|
|
40
44
|
return name.type === 'JSXIdentifier' ? name.name : undefined
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/** @type {import('../../transform/types.js').Violation<Check>[]} */
|
|
43
48
|
const violations = []
|
|
44
|
-
|
|
49
|
+
/** @param {Check} check @param {string} message */
|
|
50
|
+
const report = (check, message) => violations.push({ check, message })
|
|
45
51
|
program.traverse({
|
|
46
52
|
JSXElement(path) {
|
|
47
53
|
const component = componentOf(path.node)
|
|
@@ -60,6 +66,6 @@ export const layoutValidationPlugin = {
|
|
|
60
66
|
},
|
|
61
67
|
})
|
|
62
68
|
|
|
63
|
-
if (violations.length) throw new LayoutError(violations)
|
|
69
|
+
if (violations.length) throw new LayoutError(layoutValidationPlugin, violations)
|
|
64
70
|
},
|
|
65
71
|
}
|
|
@@ -13,9 +13,23 @@
|
|
|
13
13
|
|
|
14
14
|
const STANDARD_SECTIONS = ['Section', 'HeaderSection', 'FooterSection']
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} Rule
|
|
18
|
+
* @property {string} id
|
|
19
|
+
* @property {string[]} components
|
|
20
|
+
* @property {string[]} allowedChildren
|
|
21
|
+
* @property {number} minChildren
|
|
22
|
+
* @property {number} maxChildren
|
|
23
|
+
* @property {Record<'allowedChildren' | 'minChildren' | 'maxChildren' | 'literal' | 'createElement', import('./index.js').Check>} checks
|
|
24
|
+
*/
|
|
25
|
+
/** @satisfies {Rule[]} */
|
|
16
26
|
export const RULES = [
|
|
17
27
|
{
|
|
18
28
|
id: 'S2-SECTION-001',
|
|
29
|
+
checks: {
|
|
30
|
+
allowedChildren: 'section_wrong_child', minChildren: 'section_too_few_children',
|
|
31
|
+
maxChildren: 'section_too_many_children', literal: 'section_non_literal_children', createElement: 'section_create_element',
|
|
32
|
+
},
|
|
19
33
|
components: STANDARD_SECTIONS,
|
|
20
34
|
allowedChildren: ['InternalContainer'],
|
|
21
35
|
minChildren: 1,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { currentRow } from '@wix/origami-bi'
|
|
2
|
+
import { LayoutError } from './layoutError.js'
|
|
3
|
+
const summaries = new WeakMap()
|
|
4
|
+
|
|
5
|
+
/** Aggregate completed dev invocations; paths are only in this local set, never in rows. */
|
|
6
|
+
export function countPlugin(context, duration) {
|
|
7
|
+
const parent = currentRow()
|
|
8
|
+
if (!parent || context.command !== 'serve') return
|
|
9
|
+
const { run } = parent
|
|
10
|
+
let summary = summaries.get(run)
|
|
11
|
+
if (!summary) {
|
|
12
|
+
summary = { files: new Set(), plugins: 0, duration_ms: 0 }
|
|
13
|
+
summaries.set(run, summary)
|
|
14
|
+
const timer = setInterval(() => {
|
|
15
|
+
if (!run.commandRow.isOpen) { clearInterval(timer); summaries.delete(run); return }
|
|
16
|
+
if (!summary.plugins) return
|
|
17
|
+
run.commandRow.runInContext(() => run.openRow('step', 'transform_summary', {
|
|
18
|
+
step_details: { files: summary.files.size, plugins: summary.plugins, duration_ms: Math.round(summary.duration_ms) },
|
|
19
|
+
}).end())
|
|
20
|
+
summary.files.clear(); summary.plugins = 0; summary.duration_ms = 0
|
|
21
|
+
}, 60_000)
|
|
22
|
+
timer.unref()
|
|
23
|
+
}
|
|
24
|
+
summary.files.add(context.filePath)
|
|
25
|
+
summary.plugins++
|
|
26
|
+
summary.duration_ms += duration
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded' | 'media_not_wix'} key */
|
|
30
|
+
export function warn(key) {
|
|
31
|
+
const current = currentRow()
|
|
32
|
+
// A passing dev plugin row is discarded; warnings must retain a live parent.
|
|
33
|
+
const parent = current?.step_type === 'build_plugin' ? current.parent : current
|
|
34
|
+
parent?.runInContext(() => parent.run.openRow('warning', key, { error_name: key }).end())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @template {string} Check
|
|
39
|
+
* @param {import('./types.js').TransformPluginBase<Check>} plugin
|
|
40
|
+
* @param {unknown} cause
|
|
41
|
+
* @returns {Check | undefined}
|
|
42
|
+
*/
|
|
43
|
+
export function declaredCheck(plugin, cause) {
|
|
44
|
+
return cause instanceof Error && 'check' in cause ? plugin.checks.find(check => check === cause.check) : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Finish a plugin failure, projecting only its declared keys into check children. */
|
|
48
|
+
export function recordPluginFailure(plugin, cause, pageId) {
|
|
49
|
+
const row = currentRow()
|
|
50
|
+
const check = declaredCheck(plugin, cause)
|
|
51
|
+
const violations = cause instanceof LayoutError ? cause.violations :
|
|
52
|
+
check !== undefined ? [{ check }] : []
|
|
53
|
+
const layout = cause instanceof LayoutError || violations.length > 0
|
|
54
|
+
row?.end(false, { error_name: layout ? 'layout_error' : 'plugin_error' }, {
|
|
55
|
+
outcome: layout ? 'layout_error' : 'plugin_error', violations: violations.length,
|
|
56
|
+
})
|
|
57
|
+
const counts = new Map()
|
|
58
|
+
for (const { check } of violations) {
|
|
59
|
+
if (plugin.checks.includes(check)) counts.set(check, (counts.get(check) ?? 0) + 1)
|
|
60
|
+
}
|
|
61
|
+
for (const [check, violations] of counts) row?.run.recordRow('build_plugin', plugin.name, {
|
|
62
|
+
...(pageId ? { item_type: 'page', item_id: pageId } : {}), step_details: { check, violations },
|
|
63
|
+
}, { success: false, duration_ms: 0 }, row)
|
|
64
|
+
}
|
package/src/transform/context.js
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
|
+
import { currentRow } from '@wix/origami-bi'
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
|
+
import { parse } from '@babel/parser'
|
|
1
5
|
import { relativeToSite } from './relativeToSite.js'
|
|
2
6
|
|
|
7
|
+
const identities = new Map()
|
|
8
|
+
export function pageIdFor(context) {
|
|
9
|
+
if (!currentRow()?.run.enabled || !context.layout?.root) return undefined
|
|
10
|
+
const { root } = context.layout
|
|
11
|
+
const file = context.filePath
|
|
12
|
+
let folder = dirname(file)
|
|
13
|
+
const pages = join(root, 'src/pages')
|
|
14
|
+
while (relative(pages, folder) && !relative(pages, folder).startsWith('..')) {
|
|
15
|
+
const structure = ['structure.jsx', 'structure.tsx'].map(name => join(folder, name)).find(existsSync)
|
|
16
|
+
if (structure) {
|
|
17
|
+
try {
|
|
18
|
+
const modified = statSync(structure).mtimeMs
|
|
19
|
+
if (identities.get(structure)?.modified === modified) return identities.get(structure).id
|
|
20
|
+
const ast = parse(readFileSync(structure, 'utf8'), { sourceType: 'module', plugins: ['jsx', 'typescript'] })
|
|
21
|
+
let id
|
|
22
|
+
const visit = node => {
|
|
23
|
+
if (!node || typeof node !== 'object') return
|
|
24
|
+
if (node.type === 'JSXOpeningElement' && node.name?.name === 'Page') {
|
|
25
|
+
const attribute = node.attributes.find(value => value.type === 'JSXAttribute' && value.name.name === 'id')
|
|
26
|
+
if (attribute?.value?.type === 'StringLiteral') id ??= attribute.value.value
|
|
27
|
+
}
|
|
28
|
+
for (const value of Object.values(node)) {
|
|
29
|
+
if (Array.isArray(value)) value.forEach(visit)
|
|
30
|
+
else if (value && typeof value === 'object') visit(value)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
visit(ast)
|
|
34
|
+
identities.set(structure, { modified, id })
|
|
35
|
+
return id
|
|
36
|
+
} catch { return undefined }
|
|
37
|
+
}
|
|
38
|
+
folder = dirname(folder)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
export const createContextFactory =
|
|
4
43
|
({ layout, command }) =>
|
|
5
44
|
(id, { ssr = false } = {}) =>
|