@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.
Files changed (75) hide show
  1. package/dist/cssPlugins/componentPosition.d.ts +6 -2
  2. package/dist/cssPlugins/customEnums.d.ts +5 -2
  3. package/dist/cssPlugins/customMedia.d.ts +7 -2
  4. package/dist/cssPlugins/fonts.d.ts +6 -2
  5. package/dist/cssPlugins/index.d.ts +2 -2
  6. package/dist/cssPlugins/layout/index.d.ts +7 -2
  7. package/dist/cssPlugins/mixins.d.ts +7 -2
  8. package/dist/cssPlugins/presets.d.ts +6 -2
  9. package/dist/cssPlugins/shapeDividers.d.ts +6 -2
  10. package/dist/cssPlugins/spx/calls.d.ts +3 -0
  11. package/dist/cssPlugins/spx/index.d.ts +7 -2
  12. package/dist/cssPlugins/spxThemeVariables/index.d.ts +6 -0
  13. package/dist/cssPlugins/spxThemeVariables/utils/breakpointUtils.d.ts +23 -0
  14. package/dist/cssPlugins/spxThemeVariables/utils/copyPlanUtils.d.ts +14 -0
  15. package/dist/cssPlugins/spxThemeVariables/utils/cssTreeUtils.d.ts +10 -0
  16. package/dist/cssPlugins/spxThemeVariables/utils/restateUtils.d.ts +9 -0
  17. package/dist/cssPlugins/spxThemeVariables/utils/sectionUtils.d.ts +6 -0
  18. package/dist/cssPlugins/spxThemeVariables/utils/stylesheetUtils.d.ts +10 -0
  19. package/dist/cssPlugins/spxThemeVariables/utils/themeUtils.d.ts +6 -0
  20. package/dist/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.d.ts +10 -0
  21. package/dist/cssPlugins/spxThemeVariables/utils/writeUtils.d.ts +17 -0
  22. package/dist/cssPlugins/stackChildren.d.ts +5 -2
  23. package/dist/cssPlugins/wixVectorUrls.d.ts +6 -2
  24. package/dist/jsxPlugins/index.d.ts +2 -2
  25. package/dist/jsxPlugins/mediaValidation/index.d.ts +5 -2
  26. package/dist/jsxPlugins/origamiCompatibility/index.d.ts +6 -2
  27. package/dist/jsxPlugins/pageSlots.d.ts +7 -6
  28. package/dist/jsxPlugins/validation/checks.d.ts +7 -7
  29. package/dist/jsxPlugins/validation/index.d.ts +5 -2
  30. package/dist/jsxPlugins/validation/rules.d.ts +25 -0
  31. package/dist/transform/bi.d.ts +9 -0
  32. package/dist/transform/layoutError.d.ts +7 -4
  33. package/dist/transform/pipeline.d.ts +2 -2
  34. package/dist/transform/types.d.ts +11 -11
  35. package/package.json +3 -3
  36. package/src/cssPlugins/componentPosition.js +5 -1
  37. package/src/cssPlugins/customEnums.js +17 -11
  38. package/src/cssPlugins/customMedia.js +10 -3
  39. package/src/cssPlugins/fonts.js +5 -1
  40. package/src/cssPlugins/index.js +3 -1
  41. package/src/cssPlugins/layout/index.js +8 -1
  42. package/src/cssPlugins/layout/parser.js +15 -14
  43. package/src/cssPlugins/mixins.js +13 -6
  44. package/src/cssPlugins/presets.js +5 -1
  45. package/src/cssPlugins/shapeDividers.js +5 -1
  46. package/src/cssPlugins/spx/README.md +12 -3
  47. package/src/cssPlugins/spx/calls.js +28 -1
  48. package/src/cssPlugins/spx/cascade.js +26 -6
  49. package/src/cssPlugins/spx/index.js +8 -1
  50. package/src/cssPlugins/spxThemeVariables/README.md +14 -0
  51. package/src/cssPlugins/spxThemeVariables/index.js +36 -0
  52. package/src/cssPlugins/spxThemeVariables/utils/breakpointUtils.js +126 -0
  53. package/src/cssPlugins/spxThemeVariables/utils/copyPlanUtils.js +162 -0
  54. package/src/cssPlugins/spxThemeVariables/utils/cssTreeUtils.js +58 -0
  55. package/src/cssPlugins/spxThemeVariables/utils/restateUtils.js +134 -0
  56. package/src/cssPlugins/spxThemeVariables/utils/sectionUtils.js +32 -0
  57. package/src/cssPlugins/spxThemeVariables/utils/stylesheetUtils.js +98 -0
  58. package/src/cssPlugins/spxThemeVariables/utils/themeUtils.js +90 -0
  59. package/src/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.js +68 -0
  60. package/src/cssPlugins/spxThemeVariables/utils/writeUtils.js +98 -0
  61. package/src/cssPlugins/stackChildren.js +6 -2
  62. package/src/cssPlugins/wixVectorUrls.js +5 -1
  63. package/src/jsxPlugins/index.js +1 -1
  64. package/src/jsxPlugins/mediaValidation/index.js +10 -5
  65. package/src/jsxPlugins/origamiCompatibility/index.js +5 -1
  66. package/src/jsxPlugins/pageSlots.js +6 -1
  67. package/src/jsxPlugins/validation/checks.js +15 -8
  68. package/src/jsxPlugins/validation/index.js +9 -3
  69. package/src/jsxPlugins/validation/rules.js +14 -0
  70. package/src/transform/bi.js +30 -0
  71. package/src/transform/css/runPlugins.js +5 -7
  72. package/src/transform/jsx/runPlugins.js +3 -6
  73. package/src/transform/layoutError.js +7 -3
  74. package/src/transform/pipeline.js +2 -2
  75. 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 {import('../transform/types.js').CssTransformPlugin} */
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 {import('../transform/types.js').CssTransformPlugin} */
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,
@@ -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 {import('../../transform/types.js').JsxTransformPlugin} */
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 {import('../../transform/types.js').JsxTransformPlugin} */
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
- /** Transitional support for DM's literal Page children alongside authored slot props. */
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
- const fail = got => report(`${prefix}; found ${got}.`)
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, fail }
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 {import('../../transform/types.js').JsxTransformPlugin} */
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
- const report = message => violations.push(message)
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,
@@ -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
- const layout = cause instanceof LayoutError
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
- const layout = cause instanceof LayoutError
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 {readonly string[]} violations
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
- super(violations.length === 1 ? violations[0] : `\n ${violations.join('\n ')}`)
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 }) => {
@@ -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
- * @typedef {TransformPluginBase & {
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
- * @typedef {TransformPluginBase & {
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