@wix/origami-transforms 0.1.28 → 0.1.30
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/calls.d.ts +3 -0
- package/dist/cssPlugins/spx/index.d.ts +7 -2
- package/dist/cssPlugins/spxThemeVariables/index.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/breakpointUtils.d.ts +23 -0
- package/dist/cssPlugins/spxThemeVariables/utils/copyPlanUtils.d.ts +14 -0
- package/dist/cssPlugins/spxThemeVariables/utils/cssTreeUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/restateUtils.d.ts +9 -0
- package/dist/cssPlugins/spxThemeVariables/utils/sectionUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/stylesheetUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/themeUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/writeUtils.d.ts +17 -0
- package/dist/cssPlugins/stackChildren.d.ts +5 -2
- 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 +9 -0
- package/dist/transform/layoutError.d.ts +7 -4
- package/dist/transform/pipeline.d.ts +2 -2
- package/dist/transform/types.d.ts +11 -11
- package/package.json +3 -3
- package/src/cssPlugins/componentPosition.js +5 -1
- package/src/cssPlugins/customEnums.js +17 -11
- package/src/cssPlugins/customMedia.js +10 -3
- package/src/cssPlugins/fonts.js +5 -1
- package/src/cssPlugins/index.js +3 -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 +5 -1
- package/src/cssPlugins/shapeDividers.js +5 -1
- package/src/cssPlugins/spx/README.md +12 -3
- package/src/cssPlugins/spx/calls.js +28 -1
- package/src/cssPlugins/spx/cascade.js +26 -6
- package/src/cssPlugins/spx/index.js +8 -1
- package/src/cssPlugins/spxThemeVariables/README.md +14 -0
- package/src/cssPlugins/spxThemeVariables/index.js +36 -0
- package/src/cssPlugins/spxThemeVariables/utils/breakpointUtils.js +126 -0
- package/src/cssPlugins/spxThemeVariables/utils/copyPlanUtils.js +162 -0
- package/src/cssPlugins/spxThemeVariables/utils/cssTreeUtils.js +58 -0
- package/src/cssPlugins/spxThemeVariables/utils/restateUtils.js +134 -0
- package/src/cssPlugins/spxThemeVariables/utils/sectionUtils.js +32 -0
- package/src/cssPlugins/spxThemeVariables/utils/stylesheetUtils.js +98 -0
- package/src/cssPlugins/spxThemeVariables/utils/themeUtils.js +90 -0
- package/src/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.js +68 -0
- package/src/cssPlugins/spxThemeVariables/utils/writeUtils.js +98 -0
- package/src/cssPlugins/stackChildren.js +6 -2
- package/src/cssPlugins/wixVectorUrls.js +5 -1
- package/src/jsxPlugins/index.js +1 -1
- package/src/jsxPlugins/mediaValidation/index.js +10 -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 +30 -0
- package/src/transform/css/runPlugins.js +5 -7
- package/src/transform/jsx/runPlugins.js +3 -6
- package/src/transform/layoutError.js +7 -3
- package/src/transform/pipeline.js +2 -2
- package/src/transform/types.js +28 -6
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import postcss from 'postcss'
|
|
2
|
+
import { DESKTOP } from './cssTreeUtils.js'
|
|
3
|
+
import { groupSectionsByWrappingAtRules } from './wrappingAtRuleUtils.js'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./copyPlanUtils.js').ThemeCopies} ThemeCopies */
|
|
6
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').Section} Section */
|
|
7
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').SectionGroup} SectionGroup */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} BreakpointCopies
|
|
11
|
+
* @property {string} breakpoint
|
|
12
|
+
* @property {import('postcss').Declaration[]} declarations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const THEME_COPIES_LAYER = 'origami-theme-copies'
|
|
16
|
+
|
|
17
|
+
const LEADING_AT_RULES = new Set(['charset', 'import'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {ThemeCopies} themeCopies
|
|
21
|
+
* @returns {BreakpointCopies[]}
|
|
22
|
+
*/
|
|
23
|
+
const breakpointCopiesOf = themeCopies => {
|
|
24
|
+
const planned = [...themeCopies].map(([breakpoint, copies]) => ({ breakpoint, declarations: [...copies.values()] }))
|
|
25
|
+
return planned.filter(({ declarations }) => declarations.length > 0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} selector
|
|
30
|
+
* @param {BreakpointCopies} breakpointCopies
|
|
31
|
+
* @returns {import('postcss').ChildNode}
|
|
32
|
+
*/
|
|
33
|
+
const breakpointBlockOf = (selector, { breakpoint, declarations }) => {
|
|
34
|
+
const nodes = declarations.map(declaration => declaration.clone({ important: false, raws: {} }))
|
|
35
|
+
const rule = postcss.rule({ selector, nodes })
|
|
36
|
+
if (breakpoint === DESKTOP) return rule
|
|
37
|
+
return postcss.atRule({ name: 'media', params: breakpoint, nodes: [rule] })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {import('postcss').ChildNode[]} nodes
|
|
42
|
+
* @param {import('postcss').AtRule} wrappingAtRule
|
|
43
|
+
* @returns {import('postcss').ChildNode[]}
|
|
44
|
+
*/
|
|
45
|
+
const wrapInAtRuleClone = (nodes, wrappingAtRule) => {
|
|
46
|
+
const clone = wrappingAtRule.clone({ nodes: [] })
|
|
47
|
+
clone.append(nodes)
|
|
48
|
+
return [clone]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {readonly BreakpointCopies[]} allBreakpointCopies
|
|
53
|
+
* @param {SectionGroup} group
|
|
54
|
+
* @returns {import('postcss').ChildNode[]}
|
|
55
|
+
*/
|
|
56
|
+
const groupBlocksOf = (allBreakpointCopies, { wrappingAtRules, selectors }) => {
|
|
57
|
+
const selector = `:where(${selectors.join(', ')})`
|
|
58
|
+
const blocks = allBreakpointCopies.map(breakpointCopies => breakpointBlockOf(selector, breakpointCopies))
|
|
59
|
+
return wrappingAtRules.reduceRight(wrapInAtRuleClone, blocks)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {readonly BreakpointCopies[]} allBreakpointCopies
|
|
64
|
+
* @param {readonly SectionGroup[]} groups
|
|
65
|
+
*/
|
|
66
|
+
const copyLayerOf = (allBreakpointCopies, groups) => {
|
|
67
|
+
const nodes = groups.flatMap(group => groupBlocksOf(allBreakpointCopies, group))
|
|
68
|
+
return postcss.atRule({ name: 'layer', params: THEME_COPIES_LAYER, nodes })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {import('postcss').ChildNode} node */
|
|
72
|
+
const isLeadingAtRule = node => node.type === 'atrule' && LEADING_AT_RULES.has(node.name)
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {import('postcss').Root} root
|
|
76
|
+
* @param {import('postcss').AtRule} layer
|
|
77
|
+
*/
|
|
78
|
+
const prependAfterLeadingAtRules = (root, layer) => {
|
|
79
|
+
const firstOther = root.nodes.find(node => node.type !== 'comment' && !isLeadingAtRule(node))
|
|
80
|
+
if (firstOther === undefined) {
|
|
81
|
+
root.append(layer)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
firstOther.before(layer)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {import('postcss').Root} root
|
|
89
|
+
* @param {ThemeCopies} themeCopies
|
|
90
|
+
* @param {readonly Section[]} sections
|
|
91
|
+
*/
|
|
92
|
+
export const writeCopies = (root, themeCopies, sections) => {
|
|
93
|
+
const allBreakpointCopies = breakpointCopiesOf(themeCopies)
|
|
94
|
+
if (allBreakpointCopies.length === 0 || sections.length === 0) return
|
|
95
|
+
const groups = groupSectionsByWrappingAtRules(sections)
|
|
96
|
+
const layer = copyLayerOf(allBreakpointCopies, groups)
|
|
97
|
+
prependAfterLeadingAtRules(root, layer)
|
|
98
|
+
}
|
|
@@ -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,
|
|
@@ -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,
|
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])
|
|
@@ -13,8 +13,12 @@ const STRUCTURE_FILES = new Set(['structure.tsx', 'structure.jsx'])
|
|
|
13
13
|
// `117a96_1cff23187eeb4050a2a691bcdcad6502~mv2.jpg`: an account prefix, an underscore and a file name.
|
|
14
14
|
const WIX_MEDIA_ID = /^[A-Za-z0-9]+_[A-Za-z0-9_~.-]+\.[A-Za-z0-9]+$/
|
|
15
15
|
|
|
16
|
-
/** @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>} */
|
|
17
20
|
export const mediaValidationPlugin = {
|
|
21
|
+
checks,
|
|
18
22
|
name: 'wix-import-media-validation',
|
|
19
23
|
// Alongside the layout rules: before anything rewrites the tree.
|
|
20
24
|
order: -100,
|
|
@@ -22,6 +26,7 @@ export const mediaValidationPlugin = {
|
|
|
22
26
|
|
|
23
27
|
transform: (program, context) => {
|
|
24
28
|
const file = context.relativePath ?? context.filePath
|
|
29
|
+
/** @type {import('../../transform/types.js').Violation<Check>[]} */
|
|
25
30
|
const violations = []
|
|
26
31
|
let first
|
|
27
32
|
program.traverse({
|
|
@@ -34,9 +39,9 @@ export const mediaValidationPlugin = {
|
|
|
34
39
|
if (uri?.value.type !== 'StringLiteral' || WIX_MEDIA_ID.test(uri.value.value)) return
|
|
35
40
|
const id = attributes.find(attribute => attribute.name.name === 'id')?.value?.value
|
|
36
41
|
const line = uri.loc?.start.line
|
|
37
|
-
violations.push(
|
|
42
|
+
violations.push({ check: 'media_not_wix', message:
|
|
38
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.`
|
|
39
|
-
)
|
|
44
|
+
})
|
|
40
45
|
first ??= { file: context.filePath, line, column: uri.loc?.start.column }
|
|
41
46
|
},
|
|
42
47
|
})
|
|
@@ -44,10 +49,10 @@ export const mediaValidationPlugin = {
|
|
|
44
49
|
if (context.command === 'serve') {
|
|
45
50
|
for (const violation of violations) {
|
|
46
51
|
warn('media_not_wix')
|
|
47
|
-
console.warn(`[origami] ${violation}`)
|
|
52
|
+
console.warn(`[origami] ${violation.message}`)
|
|
48
53
|
}
|
|
49
54
|
return
|
|
50
55
|
}
|
|
51
|
-
throw new LayoutError(violations, first)
|
|
56
|
+
throw new LayoutError(mediaValidationPlugin, violations, first)
|
|
52
57
|
},
|
|
53
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,
|
package/src/transform/bi.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentRow } from '@wix/origami-bi'
|
|
2
|
+
import { LayoutError } from './layoutError.js'
|
|
2
3
|
const summaries = new WeakMap()
|
|
3
4
|
|
|
4
5
|
/** Aggregate completed dev invocations; paths are only in this local set, never in rows. */
|
|
@@ -32,3 +33,32 @@ export function warn(key) {
|
|
|
32
33
|
const parent = current?.step_type === 'build_plugin' ? current.parent : current
|
|
33
34
|
parent?.runInContext(() => parent.run.openRow('warning', key, { error_name: key }).end())
|
|
34
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
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the author gets every plugin's violations in one error, at the first one's position, not one plugin's per build.
|
|
7
7
|
|
|
8
8
|
import { currentRow, namedError } from '@wix/origami-bi'
|
|
9
|
-
import { countPlugin } from '../bi.js'
|
|
9
|
+
import { countPlugin, declaredCheck, recordPluginFailure } from '../bi.js'
|
|
10
10
|
import { pageIdFor } from '../context.js'
|
|
11
11
|
import { LayoutError } from '../layoutError.js'
|
|
12
12
|
|
|
@@ -16,12 +16,12 @@ const reason = cause => (cause instanceof Error ? cause.message : String(cause))
|
|
|
16
16
|
export const pluginError = (plugin, context, cause) =>
|
|
17
17
|
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
18
18
|
// plugin may throw any value, so it is read without assuming an Error.
|
|
19
|
-
namedError('plugin_error', `CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`, {
|
|
19
|
+
namedError(declaredCheck(plugin, cause) !== undefined ? 'layout_error' : 'plugin_error', `CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`, {
|
|
20
20
|
cause,
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
export const runCssPlugins = ({ plugins, root, context }) => {
|
|
24
|
-
/** @type {LayoutError[]} */
|
|
24
|
+
/** @type {LayoutError<string>[]} */
|
|
25
25
|
const layoutErrors = []
|
|
26
26
|
for (const phase of ['validate', 'transform']) {
|
|
27
27
|
for (const plugin of plugins) {
|
|
@@ -38,10 +38,7 @@ export const runCssPlugins = ({ plugins, root, context }) => {
|
|
|
38
38
|
if (context.command === 'serve') row?.discard()
|
|
39
39
|
else row?.end()
|
|
40
40
|
} catch (cause) {
|
|
41
|
-
|
|
42
|
-
row?.end(false, { error_name: layout ? 'layout_error' : 'plugin_error' }, {
|
|
43
|
-
outcome: layout ? 'layout_error' : 'plugin_error', violations: layout ? cause.violations.length : 0,
|
|
44
|
-
})
|
|
41
|
+
if (row) row.runInContext(() => recordPluginFailure(plugin, cause, pageId))
|
|
45
42
|
if (cause instanceof LayoutError) {
|
|
46
43
|
layoutErrors.push(cause)
|
|
47
44
|
continue
|
|
@@ -58,6 +55,7 @@ export const runCssPlugins = ({ plugins, root, context }) => {
|
|
|
58
55
|
if (layoutErrors.length === 1) throw layoutErrors[0]
|
|
59
56
|
if (layoutErrors.length > 1) {
|
|
60
57
|
throw new LayoutError(
|
|
58
|
+
{ checks: layoutErrors.flatMap(error => error.plugin.checks) },
|
|
61
59
|
layoutErrors.flatMap(error => error.violations),
|
|
62
60
|
layoutErrors[0].location
|
|
63
61
|
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// plugin and the file that caused it. A LayoutError is the author's, so it passes through.
|
|
4
4
|
|
|
5
5
|
import { currentRow, namedError } from '@wix/origami-bi'
|
|
6
|
-
import { countPlugin } from '../bi.js'
|
|
6
|
+
import { countPlugin, declaredCheck, recordPluginFailure } from '../bi.js'
|
|
7
7
|
import { pageIdFor } from '../context.js'
|
|
8
8
|
import { LayoutError } from '../layoutError.js'
|
|
9
9
|
|
|
@@ -23,14 +23,11 @@ export const runJsxPlugins = ({ plugins, program, context }) => {
|
|
|
23
23
|
if (context.command === 'serve') row?.discard()
|
|
24
24
|
else row?.end()
|
|
25
25
|
} catch (cause) {
|
|
26
|
-
|
|
27
|
-
row?.end(false, { error_name: layout ? 'layout_error' : 'plugin_error' }, {
|
|
28
|
-
outcome: layout ? 'layout_error' : 'plugin_error', violations: layout ? cause.violations.length : 0,
|
|
29
|
-
})
|
|
26
|
+
if (row) row.runInContext(() => recordPluginFailure(plugin, cause, pageId))
|
|
30
27
|
if (cause instanceof LayoutError) throw cause
|
|
31
28
|
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
32
29
|
// plugin may throw any value, so it is read without assuming an Error.
|
|
33
|
-
throw namedError('plugin_error',
|
|
30
|
+
throw namedError(declaredCheck(plugin, cause) !== undefined ? 'layout_error' : 'plugin_error',
|
|
34
31
|
`JSX transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`,
|
|
35
32
|
{ cause }
|
|
36
33
|
)
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
// without naming the plugin, and it carries no stack: the bundler already names the file, and the
|
|
3
3
|
// messages say where in it.
|
|
4
4
|
|
|
5
|
+
/** @template {string} Check */
|
|
5
6
|
export class LayoutError extends Error {
|
|
6
7
|
/**
|
|
7
|
-
* @param {
|
|
8
|
+
* @param {Pick<import('./types.js').TransformPluginBase<Check>, 'checks'>} plugin
|
|
9
|
+
* @param {readonly import('./types.js').Violation<NoInfer<Check>>[]} violations
|
|
8
10
|
* @param {{ file?: string, line?: number, column?: number }} [location] The first violation's, which the
|
|
9
11
|
* bundler prints as the error's position (Vite's CSS stage reads `file`, `line` and `column`).
|
|
10
12
|
*/
|
|
11
|
-
constructor(violations, location = {}) {
|
|
12
|
-
|
|
13
|
+
constructor(plugin, violations, location = {}) {
|
|
14
|
+
const messages = violations.map(violation => violation.message)
|
|
15
|
+
super(messages.length === 1 ? messages[0] : `\n ${messages.join('\n ')}`)
|
|
13
16
|
this.name = 'LayoutError'
|
|
14
17
|
this.error_name = 'layout_error'
|
|
15
18
|
this.stack = ''
|
|
19
|
+
this.plugin = plugin
|
|
16
20
|
this.violations = violations
|
|
17
21
|
this.location = location
|
|
18
22
|
Object.assign(this, location)
|
|
@@ -30,8 +30,8 @@ const createState = ({ layout, registries, command }) => {
|
|
|
30
30
|
/**
|
|
31
31
|
* @param {{
|
|
32
32
|
* layout: import('./types.js').SiteLayout,
|
|
33
|
-
* jsxPlugins?: readonly import('./types.js').JsxTransformPlugin[],
|
|
34
|
-
* cssPlugins?: readonly import('./types.js').CssTransformPlugin[],
|
|
33
|
+
* jsxPlugins?: readonly import('./types.js').JsxTransformPlugin<string>[],
|
|
34
|
+
* cssPlugins?: readonly import('./types.js').CssTransformPlugin<string>[],
|
|
35
35
|
* }} options
|
|
36
36
|
*/
|
|
37
37
|
export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins, cssPlugins = defaultCssPlugins }) => {
|
package/src/transform/types.js
CHANGED
|
@@ -18,15 +18,32 @@
|
|
|
18
18
|
* @property {SiteLayout} layout
|
|
19
19
|
* @property {'serve' | 'build'} command
|
|
20
20
|
* @property {boolean} ssr Always false for the CSS stage, which PostCSS drives without an SSR signal.
|
|
21
|
-
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @template {string} Check
|
|
25
|
+
* @typedef {{ check: Check, message: string }} Violation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @template {string} Check
|
|
30
|
+
* @typedef {Error & { check: Check }} CheckError
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @template {string} Check
|
|
22
35
|
* @typedef {object} TransformPluginBase
|
|
36
|
+
* @property {readonly Check[]} checks
|
|
23
37
|
* @property {string} name
|
|
24
38
|
* @property {number} order Ascending. Ties break by registration order.
|
|
25
39
|
* @property {(context: TransformContext) => boolean} [appliesTo] Whether the plugin runs for this
|
|
26
40
|
* file. Must depend only on the file's identity, not on anything that can change between transforms
|
|
27
41
|
* of the same file: the pipeline memoizes the selection per file path.
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @template {string} Check
|
|
46
|
+
* @typedef {TransformPluginBase<Check> & {
|
|
30
47
|
* transform: (program: any, context: TransformContext) => void,
|
|
31
48
|
* parserPlugins?: readonly string[]
|
|
32
49
|
* }} JsxTransformPlugin
|
|
@@ -34,8 +51,11 @@
|
|
|
34
51
|
* nothing walks it for you, so a plugin that can rule itself out cheaply should just return.
|
|
35
52
|
* `parserPlugins` are Babel parser plugins the file needs; they are unioned across selected plugins
|
|
36
53
|
* and applied before any of them run.
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @template {string} Check
|
|
58
|
+
* @typedef {TransformPluginBase<Check> & {
|
|
39
59
|
* prepare?: (root: import('postcss').Root, context: CssTransformContext) => Promise<void | { fetched: number, failed: number }> | void,
|
|
40
60
|
* validate?: (root: import('postcss').Root, context: CssTransformContext) => void,
|
|
41
61
|
* transform: (root: import('postcss').Root, context: CssTransformContext) => void
|
|
@@ -43,7 +63,9 @@
|
|
|
43
63
|
* `prepare` loads what the plugin needs that can't be read synchronously, such as a file from a CDN, before any
|
|
44
64
|
* plugin's `validate` or `transform` runs. It returns a promise only when it has something to load, so a stylesheet
|
|
45
65
|
* that needs nothing loaded stays synchronous for PostCSS.
|
|
46
|
-
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
47
69
|
* @typedef {TransformContext & {
|
|
48
70
|
* addDependency?: (file: string) => void,
|
|
49
71
|
* warn?: (node: import('postcss').Node, text: string, key?: 'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded') => void
|