@wix/origami-transforms 0.1.28 → 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.
Files changed (52) 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/index.d.ts +7 -2
  11. package/dist/cssPlugins/stackChildren.d.ts +5 -2
  12. package/dist/cssPlugins/wixVectorUrls.d.ts +6 -2
  13. package/dist/jsxPlugins/index.d.ts +2 -2
  14. package/dist/jsxPlugins/mediaValidation/index.d.ts +5 -2
  15. package/dist/jsxPlugins/origamiCompatibility/index.d.ts +6 -2
  16. package/dist/jsxPlugins/pageSlots.d.ts +7 -6
  17. package/dist/jsxPlugins/validation/checks.d.ts +7 -7
  18. package/dist/jsxPlugins/validation/index.d.ts +5 -2
  19. package/dist/jsxPlugins/validation/rules.d.ts +25 -0
  20. package/dist/transform/bi.d.ts +9 -0
  21. package/dist/transform/layoutError.d.ts +7 -4
  22. package/dist/transform/pipeline.d.ts +2 -2
  23. package/dist/transform/types.d.ts +11 -11
  24. package/package.json +3 -3
  25. package/src/cssPlugins/componentPosition.js +5 -1
  26. package/src/cssPlugins/customEnums.js +17 -11
  27. package/src/cssPlugins/customMedia.js +10 -3
  28. package/src/cssPlugins/fonts.js +5 -1
  29. package/src/cssPlugins/index.js +1 -1
  30. package/src/cssPlugins/layout/index.js +8 -1
  31. package/src/cssPlugins/layout/parser.js +15 -14
  32. package/src/cssPlugins/mixins.js +13 -6
  33. package/src/cssPlugins/presets.js +5 -1
  34. package/src/cssPlugins/shapeDividers.js +5 -1
  35. package/src/cssPlugins/spx/calls.js +2 -1
  36. package/src/cssPlugins/spx/cascade.js +3 -2
  37. package/src/cssPlugins/spx/index.js +8 -1
  38. package/src/cssPlugins/stackChildren.js +6 -2
  39. package/src/cssPlugins/wixVectorUrls.js +5 -1
  40. package/src/jsxPlugins/index.js +1 -1
  41. package/src/jsxPlugins/mediaValidation/index.js +10 -5
  42. package/src/jsxPlugins/origamiCompatibility/index.js +5 -1
  43. package/src/jsxPlugins/pageSlots.js +6 -1
  44. package/src/jsxPlugins/validation/checks.js +15 -8
  45. package/src/jsxPlugins/validation/index.js +9 -3
  46. package/src/jsxPlugins/validation/rules.js +14 -0
  47. package/src/transform/bi.js +30 -0
  48. package/src/transform/css/runPlugins.js +5 -7
  49. package/src/transform/jsx/runPlugins.js +3 -6
  50. package/src/transform/layoutError.js +7 -3
  51. package/src/transform/pipeline.js +2 -2
  52. package/src/transform/types.js +28 -6
@@ -1,2 +1,6 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const componentPositionPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
3
+ export const componentPositionPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,3 +1,6 @@
1
1
  export function removeCustomEnums(root: any): void;
2
- /** @type {import('../transform/types.js').CssTransformPlugin} */
3
- export const customEnumsPlugin: import("../transform/types.js").CssTransformPlugin;
2
+ export const checks: readonly ["duplicate_enum", "non_literal_enum_value", "unknown_enum_value", "enum_parent_selector_list"];
3
+ /** @typedef {typeof checks[number]} Check */
4
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
5
+ export const customEnumsPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
6
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,7 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const customMediaPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ export const checks: readonly ["unresolved_custom_media"];
2
+ export function fail<E extends Error>(check: Check, error: E): E & {
3
+ check: "unresolved_custom_media";
4
+ };
5
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
6
+ export const customMediaPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
7
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,6 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const fontsPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
3
+ export const fontsPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,2 +1,2 @@
1
- /** @type {readonly import('../transform/types.js').CssTransformPlugin[]} */
2
- export const cssPlugins: readonly import("../transform/types.js").CssTransformPlugin[];
1
+ /** @type {readonly import('../transform/types.js').CssTransformPlugin<string>[]} */
2
+ export const cssPlugins: readonly import("../transform/types.js").CssTransformPlugin<string>[];
@@ -1,2 +1,7 @@
1
- /** @type {import('../../transform/types.js').CssTransformPlugin} */
2
- export const layoutPlugin: import("../../transform/types.js").CssTransformPlugin;
1
+ export const checks: readonly ["unknown_target", "target_requires_component_id", "invalid_target_envelope", "target_not_site_layout", "duplicate_target", "target_requires_declarations", "duplicate_target_property", "property_wrong_target", "content_position_not_relative", "unsupported_overflow", "unsupported_content_alignment", "inconsistent_container_display", "duplicate_root_property", "property_requires_target"];
2
+ export function fail<E extends Error>(check: Check, error: E): E & {
3
+ check: "unknown_target" | "target_requires_component_id" | "invalid_target_envelope" | "target_not_site_layout" | "duplicate_target" | "target_requires_declarations" | "duplicate_target_property" | "property_wrong_target" | "content_position_not_relative" | "unsupported_overflow" | "unsupported_content_alignment" | "inconsistent_container_display" | "duplicate_root_property" | "property_requires_target";
4
+ };
5
+ /** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
6
+ export const layoutPlugin: import("../../transform/types.js").CssTransformPlugin<Check>;
7
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,7 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const mixinsPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ export const checks: readonly ["missing_mixin", "cyclic_apply", "expansion_depth_exceeded", "cloned_node_budget_exceeded", "cloned_css_budget_exceeded"];
2
+ export function fail<E extends Error>(check: Check, error: E): E & {
3
+ check: "missing_mixin" | "cyclic_apply" | "expansion_depth_exceeded" | "cloned_node_budget_exceeded" | "cloned_css_budget_exceeded";
4
+ };
5
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
6
+ export const mixinsPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
7
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,6 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const presetsPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
3
+ export const presetsPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,2 +1,6 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const shapeDividersPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
3
+ export const shapeDividersPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,2 +1,7 @@
1
- /** @type {import('../../transform/types.js').CssTransformPlugin} */
2
- export const spxPlugin: import("../../transform/types.js").CssTransformPlugin;
1
+ export const checks: readonly ["unsupported_spx_resolver", "invalid_spx_reference_width", "non_literal_spx_argument"];
2
+ export function fail<E extends Error>(check: Check, error: E): E & {
3
+ check: "unsupported_spx_resolver" | "invalid_spx_reference_width" | "non_literal_spx_argument";
4
+ };
5
+ /** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
6
+ export const spxPlugin: import("../../transform/types.js").CssTransformPlugin<Check>;
7
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,5 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const stackChildrenPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ export const checks: readonly ["child_not_in_stack"];
2
+ /** @typedef {typeof checks[number]} Check */
3
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
4
+ export const stackChildrenPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
5
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,6 @@
1
- /** @type {import('../transform/types.js').CssTransformPlugin} */
2
- export const wixVectorUrlsPlugin: import("../transform/types.js").CssTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
3
+ export const wixVectorUrlsPlugin: import("../transform/types.js").CssTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,2 +1,2 @@
1
- /** @type {readonly import('../transform/types.js').JsxTransformPlugin[]} */
2
- export const jsxPlugins: readonly import("../transform/types.js").JsxTransformPlugin[];
1
+ /** @type {readonly import('../transform/types.js').JsxTransformPlugin<string>[]} */
2
+ export const jsxPlugins: readonly import("../transform/types.js").JsxTransformPlugin<string>[];
@@ -1,2 +1,5 @@
1
- /** @type {import('../../transform/types.js').JsxTransformPlugin} */
2
- export const mediaValidationPlugin: import("../../transform/types.js").JsxTransformPlugin;
1
+ export const checks: readonly ["media_not_wix"];
2
+ /** @typedef {typeof checks[number]} Check */
3
+ /** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
4
+ export const mediaValidationPlugin: import("../../transform/types.js").JsxTransformPlugin<Check>;
5
+ export type Check = (typeof checks)[number];
@@ -1,2 +1,6 @@
1
- /** @type {import('../../transform/types.js').JsxTransformPlugin} */
2
- export const origamiCompatibilityPlugin: import("../../transform/types.js").JsxTransformPlugin;
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
3
+ export const origamiCompatibilityPlugin: import("../../transform/types.js").JsxTransformPlugin<Check>;
4
+ export type Check = (typeof checks)[number];
5
+ declare const checks: readonly [];
6
+ export {};
@@ -1,6 +1,7 @@
1
- export namespace pageSlotsPlugin {
2
- let name: string;
3
- let order: number;
4
- function appliesTo(context: any): boolean;
5
- function transform(program: any): void;
6
- }
1
+ /** @typedef {typeof checks[number]} Check */
2
+ /** Transitional support for DM's literal Page children alongside authored slot props.
3
+ * @type {import('../transform/types.js').JsxTransformPlugin<Check>} */
4
+ export const pageSlotsPlugin: import("../transform/types.js").JsxTransformPlugin<Check>;
5
+ export type Check = (typeof checks)[number];
6
+ declare const checks: readonly [];
7
+ export {};
@@ -1,12 +1,12 @@
1
1
  export function assertKnownConstraints(rules: any): void;
2
2
  export function constrainsChildren(rule: any): boolean;
3
- export function checkRule(rule: any, { node, componentOf, report }: {
3
+ export function checkRule(rule: import("./rules.js").Rule, { node, componentOf, report }: {
4
4
  node: any;
5
- componentOf: any;
6
- report: any;
5
+ componentOf: (node: any) => string | undefined;
6
+ report: (check: import("./index.js").Check, message: string) => void;
7
7
  }): void;
8
- export function checkCreateElement(rule: any, { node, component, report }: {
8
+ export function checkCreateElement(rule: import("./rules.js").Rule, { node, component, report }: {
9
9
  node: any;
10
- component: any;
11
- report: any;
12
- }): any;
10
+ component: string;
11
+ report: (check: import("./index.js").Check, message: string) => void;
12
+ }): void;
@@ -1,2 +1,5 @@
1
- /** @type {import('../../transform/types.js').JsxTransformPlugin} */
2
- export const layoutValidationPlugin: import("../../transform/types.js").JsxTransformPlugin;
1
+ export const checks: readonly ["section_wrong_child", "section_too_few_children", "section_too_many_children", "section_non_literal_children", "section_create_element"];
2
+ /** @typedef {typeof checks[number]} Check */
3
+ /** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
4
+ export const layoutValidationPlugin: import("../../transform/types.js").JsxTransformPlugin<Check>;
5
+ export type Check = (typeof checks)[number];
@@ -1,7 +1,32 @@
1
+ /**
2
+ * @typedef {object} Rule
3
+ * @property {string} id
4
+ * @property {string[]} components
5
+ * @property {string[]} allowedChildren
6
+ * @property {number} minChildren
7
+ * @property {number} maxChildren
8
+ * @property {Record<'allowedChildren' | 'minChildren' | 'maxChildren' | 'literal' | 'createElement', import('./index.js').Check>} checks
9
+ */
10
+ /** @satisfies {Rule[]} */
1
11
  export const RULES: {
2
12
  id: string;
13
+ checks: {
14
+ allowedChildren: "section_wrong_child";
15
+ minChildren: "section_too_few_children";
16
+ maxChildren: "section_too_many_children";
17
+ literal: "section_non_literal_children";
18
+ createElement: "section_create_element";
19
+ };
3
20
  components: string[];
4
21
  allowedChildren: string[];
5
22
  minChildren: number;
6
23
  maxChildren: number;
7
24
  }[];
25
+ export type Rule = {
26
+ id: string;
27
+ components: string[];
28
+ allowedChildren: string[];
29
+ minChildren: number;
30
+ maxChildren: number;
31
+ checks: Record<"allowedChildren" | "minChildren" | "maxChildren" | "literal" | "createElement", import("./index.js").Check>;
32
+ };
@@ -2,3 +2,12 @@
2
2
  export function countPlugin(context: any, duration: any): void;
3
3
  /** @param {'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded' | 'media_not_wix'} key */
4
4
  export function warn(key: "malformed_custom_enum" | "unmappable_preset" | "svg_not_loaded" | "media_not_wix"): void;
5
+ /**
6
+ * @template {string} Check
7
+ * @param {import('./types.js').TransformPluginBase<Check>} plugin
8
+ * @param {unknown} cause
9
+ * @returns {Check | undefined}
10
+ */
11
+ export function declaredCheck<Check extends string>(plugin: import("./types.js").TransformPluginBase<Check>, cause: unknown): Check | undefined;
12
+ /** Finish a plugin failure, projecting only its declared keys into check children. */
13
+ export function recordPluginFailure(plugin: any, cause: any, pageId: any): void;
@@ -1,17 +1,20 @@
1
- export class LayoutError extends Error {
1
+ /** @template {string} Check */
2
+ export class LayoutError<Check extends string> extends Error {
2
3
  /**
3
- * @param {readonly string[]} violations
4
+ * @param {Pick<import('./types.js').TransformPluginBase<Check>, 'checks'>} plugin
5
+ * @param {readonly import('./types.js').Violation<NoInfer<Check>>[]} violations
4
6
  * @param {{ file?: string, line?: number, column?: number }} [location] The first violation's, which the
5
7
  * bundler prints as the error's position (Vite's CSS stage reads `file`, `line` and `column`).
6
8
  */
7
- constructor(violations: readonly string[], location?: {
9
+ constructor(plugin: Pick<import("./types.js").TransformPluginBase<Check>, "checks">, violations: readonly import("./types.js").Violation<NoInfer<Check>>[], location?: {
8
10
  file?: string;
9
11
  line?: number;
10
12
  column?: number;
11
13
  });
12
14
  error_name: string;
13
15
  stack: string;
14
- violations: readonly string[];
16
+ plugin: Pick<import("./types.js").TransformPluginBase<Check>, "checks">;
17
+ violations: readonly import("./types.js").Violation<NoInfer<Check>>[];
15
18
  location: {
16
19
  file?: string;
17
20
  line?: number;
@@ -1,7 +1,7 @@
1
1
  export function createTransformPipeline({ layout, jsxPlugins, cssPlugins }: {
2
2
  layout: import("./types.js").SiteLayout;
3
- jsxPlugins?: readonly import("./types.js").JsxTransformPlugin[];
4
- cssPlugins?: readonly import("./types.js").CssTransformPlugin[];
3
+ jsxPlugins?: readonly import("./types.js").JsxTransformPlugin<string>[];
4
+ cssPlugins?: readonly import("./types.js").CssTransformPlugin<string>[];
5
5
  }): {
6
6
  /** Whether any JSX transform selects this module (structure.jsx/.tsx by default).
7
7
  * @param {string} id
@@ -32,15 +32,15 @@ export type TransformContext = {
32
32
  */
33
33
  ssr: boolean;
34
34
  };
35
- /**
36
- * The transform-plugin contract.
37
- *
38
- * Both stages have the same shape: an engine parses the file once, and each plugin is handed that
39
- * AST plus the file's context and does what it likes with it. Babel and PostCSS are engines here,
40
- * not plugin hosts — a plugin never has to know either one's own plugin protocol. Which stage a
41
- * plugin belongs to is decided by the list it is registered in, so no descriptor restates it.
42
- */
43
- export type TransformPluginBase = {
35
+ export type Violation<Check extends string> = {
36
+ check: Check;
37
+ message: string;
38
+ };
39
+ export type CheckError<Check extends string> = Error & {
40
+ check: Check;
41
+ };
42
+ export type TransformPluginBase<Check extends string> = {
43
+ checks: readonly Check[];
44
44
  name: string;
45
45
  /**
46
46
  * Ascending. Ties break by registration order.
@@ -59,7 +59,7 @@ export type TransformPluginBase = {
59
59
  * `parserPlugins` are Babel parser plugins the file needs; they are unioned across selected plugins
60
60
  * and applied before any of them run.
61
61
  */
62
- export type JsxTransformPlugin = TransformPluginBase & {
62
+ export type JsxTransformPlugin<Check extends string> = TransformPluginBase<Check> & {
63
63
  transform: (program: any, context: TransformContext) => void;
64
64
  parserPlugins?: readonly string[];
65
65
  };
@@ -68,7 +68,7 @@ export type JsxTransformPlugin = TransformPluginBase & {
68
68
  * plugin's `validate` or `transform` runs. It returns a promise only when it has something to load, so a stylesheet
69
69
  * that needs nothing loaded stays synchronous for PostCSS.
70
70
  */
71
- export type CssTransformPlugin = TransformPluginBase & {
71
+ export type CssTransformPlugin<Check extends string> = TransformPluginBase<Check> & {
72
72
  prepare?: (root: import("postcss").Root, context: CssTransformContext) => Promise<void | {
73
73
  fetched: number;
74
74
  failed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-transforms",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "Babel and PostCSS checks and transforms for Wix origami page files.",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -13,7 +13,7 @@
13
13
  "test": "node --test test/*.spec.js",
14
14
  "test:browser": "node test/spx.browser.mjs && node test/semanticContainer.browser.mjs",
15
15
  "clean": "rm -rf dist tsconfig.tsbuildinfo",
16
- "typecheck": "tsc --noEmit"
16
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.checks.json"
17
17
  },
18
18
  "dependencies": {
19
19
  "@babel/core": "^7.26.0",
@@ -71,5 +71,5 @@
71
71
  },
72
72
  "type": "module",
73
73
  "types": "./dist/index.d.ts",
74
- "falconPackageHash": "1fbeabb662063771eaf3dbbb2b84d3ea8ba71598556ee82a91d545fa"
74
+ "falconPackageHash": "26f18af79b8f5dd98a7aba58e68be7f9dd09c52bdd522e0eb33180c4"
75
75
  }
@@ -6,8 +6,12 @@ import { isLayoutSource } from './layout/parser.js'
6
6
  // One class or id: DM writes `.compId`, and layout.css still accepts `#compId` (see layout/parser.js).
7
7
  const SINGLE_CLASS_OR_ID = /^[.#][\w-]+$/
8
8
 
9
- /** @type {import('../transform/types.js').CssTransformPlugin} */
9
+ const checks = /** @type {const} */ ([])
10
+ /** @typedef {typeof checks[number]} Check */
11
+
12
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
10
13
  export const componentPositionPlugin = {
14
+ checks,
11
15
  name: 'component-position',
12
16
  order: 14,
13
17
  transform(root, context) {
@@ -222,8 +222,12 @@ export const removeCustomEnums = root => {
222
222
  for (const rule of emptied) if (rule?.type === 'rule' && rule.nodes.length === 0) rule.remove()
223
223
  }
224
224
 
225
- /** @type {import('../transform/types.js').CssTransformPlugin} */
225
+ export const checks = /** @type {const} */ (['duplicate_enum', 'non_literal_enum_value', 'unknown_enum_value', 'enum_parent_selector_list'])
226
+ /** @typedef {typeof checks[number]} Check */
227
+
228
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
226
229
  export const customEnumsPlugin = {
230
+ checks,
227
231
  name: 'custom-enums',
228
232
  // After mixins copy their bodies in, before presets and layout move declarations out of the instance's rule.
229
233
  order: 11,
@@ -311,13 +315,13 @@ export const customEnumsPlugin = {
311
315
  const matching = enums.filter(item => item.key === declaration.prop && item.inline === nearest)
312
316
  if (matching.length === 0) return null
313
317
  const key = declaration.prop.slice(2)
314
- if (matching.length > 1) return `the element has custom enum ${key} more than once, so it can't say which.`
318
+ if (matching.length > 1) return /** @satisfies {[Check, string]} */ (['duplicate_enum', `the element has custom enum ${key} more than once, so it can't say which.`])
315
319
  const [found] = matching
316
320
  const value = declaration.value.trim()
317
321
  const allowed = Object.keys(found.options).join(', ')
318
- if (/\bvar\(/.test(value)) return `a custom enum's value must be literal, one of ${allowed}.`
322
+ if (/\bvar\(/.test(value)) return /** @satisfies {[Check, string]} */ (['non_literal_enum_value', `a custom enum's value must be literal, one of ${allowed}.`])
319
323
  const options = found.options[value]
320
- if (options === undefined) return `"${value}" is not an option of ${key}; use one of ${allowed}.`
324
+ if (options === undefined) return /** @satisfies {[Check, string]} */ (['unknown_enum_value', `"${value}" is not an option of ${key}; use one of ${allowed}.`])
321
325
  return options
322
326
  }
323
327
 
@@ -337,7 +341,8 @@ export const customEnumsPlugin = {
337
341
  }
338
342
 
339
343
  // Every declaration is checked before any is rewritten: a failed run leaves the stylesheet as it was.
340
- const violations = new Set()
344
+ /** @type {Map<string, import('../transform/types.js').Violation<Check>>} */
345
+ const violations = new Map()
341
346
  /** @type {any} The first violation's source, whose position the bundler prints. */
342
347
  let first
343
348
  // The rules with an enum that resolves, the only ones a selector list is split in.
@@ -348,17 +353,18 @@ export const customEnumsPlugin = {
348
353
  const results = resolve(rule, own).map(selector => lookup(declaration, selector))
349
354
  if (results.some(result => result !== null)) resolving.add(rule)
350
355
  if (results.length > 1 && results.some(result => result !== null)) {
351
- return ['set a custom enum on one instance rule, not under a parent selector list.']
356
+ return [/** @satisfies {[Check, string]} */ (['enum_parent_selector_list', 'set a custom enum on one instance rule, not under a parent selector list.'])]
352
357
  }
353
- return results.filter(result => typeof result === 'string')
358
+ return results.filter(result => Array.isArray(result))
354
359
  })
355
- for (const problem of problems) {
360
+ for (const [check, problem] of problems) {
356
361
  first ??= declaration.source
357
- violations.add(`custom-enums: ${declaration.prop}: ${declaration.value}${where(declaration)}: ${problem}`)
362
+ const message = `custom-enums: ${declaration.prop}: ${declaration.value}${where(declaration)}: ${problem}`
363
+ violations.set(message, { check, message })
358
364
  }
359
365
  }
360
366
  if (violations.size) {
361
- throw new LayoutError([...violations], {
367
+ throw new LayoutError(customEnumsPlugin, [...violations.values()], {
362
368
  file: first?.input.file,
363
369
  line: first?.start?.line,
364
370
  column: first?.start?.column,
@@ -380,7 +386,7 @@ export const customEnumsPlugin = {
380
386
  const rule = ruleOf(declaration)
381
387
  if (rule === undefined) continue
382
388
  const [selector] = resolve(rule, rule.selector)
383
- const options = /** @type {Exclude<ReturnType<typeof lookup>, string>} */ (lookup(declaration, selector))
389
+ const options = /** @type {Exclude<ReturnType<typeof lookup>, string[]>} */ (lookup(declaration, selector))
384
390
  if (options !== null) declaration.replaceWith(expansion(declaration, options))
385
391
  }
386
392
  },
@@ -27,8 +27,15 @@ const usesCustomMedia = root => {
27
27
  return used
28
28
  }
29
29
 
30
- /** @type {import('../transform/types.js').CssTransformPlugin} */
30
+ export const checks = /** @type {const} */ (['unresolved_custom_media'])
31
+ /** @typedef {typeof checks[number]} Check */
32
+
33
+ /** @template {Error} E @param {Check} check @param {E} error */
34
+ export const fail = (check, error) => Object.assign(error, { check })
35
+
36
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
31
37
  export const customMediaPlugin = {
38
+ checks,
32
39
  name: 'custom-media',
33
40
  order: 0,
34
41
  transform(root, context) {
@@ -43,9 +50,9 @@ export const customMediaPlugin = {
43
50
  // disables the whole block.
44
51
  root.walkAtRules('media', atRule => {
45
52
  if (CUSTOM_MEDIA_QUERY.test(atRule.params)) {
46
- throw new Error(
53
+ throw fail('unresolved_custom_media', new Error(
47
54
  `@media ${atRule.params} has no usable @custom-media definition: it is missing or cyclic`
48
- )
55
+ ))
49
56
  }
50
57
  })
51
58
  },
@@ -49,8 +49,12 @@ const fontIdsOf = declaration => {
49
49
  return family.split(',').map(name => name.trim().replace(/^(['"])(.*)\1$/, '$2').toLowerCase())
50
50
  }
51
51
 
52
- /** @type {import('../transform/types.js').CssTransformPlugin} */
52
+ const checks = /** @type {const} */ ([])
53
+ /** @typedef {typeof checks[number]} Check */
54
+
55
+ /** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
53
56
  export const fontsPlugin = {
57
+ checks,
54
58
  name: 'fonts',
55
59
  // Reads the declarations as authored, before spx rewrites the sizes the family follows.
56
60
  order: 5,
@@ -29,7 +29,7 @@ import { spxPlugin } from './spx/index.js'
29
29
  import { stackChildrenPlugin } from './stackChildren.js'
30
30
  import { wixVectorUrlsPlugin } from './wixVectorUrls.js'
31
31
 
32
- /** @type {readonly import('../transform/types.js').CssTransformPlugin[]} */
32
+ /** @type {readonly import('../transform/types.js').CssTransformPlugin<string>[]} */
33
33
  export const cssPlugins = Object.freeze([
34
34
  customMediaPlugin,
35
35
  fontsPlugin,
@@ -1,8 +1,15 @@
1
1
  import { compileContainers } from './compiler.js'
2
2
  import { parseTargets } from './parser.js'
3
3
 
4
- /** @type {import('../../transform/types.js').CssTransformPlugin} */
4
+ export const checks = /** @type {const} */ (['unknown_target', 'target_requires_component_id', 'invalid_target_envelope', 'target_not_site_layout', 'duplicate_target', 'target_requires_declarations', 'duplicate_target_property', 'property_wrong_target', 'content_position_not_relative', 'unsupported_overflow', 'unsupported_content_alignment', 'inconsistent_container_display', 'duplicate_root_property', 'property_requires_target'])
5
+ /** @typedef {typeof checks[number]} Check */
6
+
7
+ /** @template {Error} E @param {Check} check @param {E} error */
8
+ export const fail = (check, error) => Object.assign(error, { check })
9
+
10
+ /** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
5
11
  export const layoutPlugin = {
12
+ checks,
6
13
  name: 'layout',
7
14
  order: 15,
8
15
  validate: (root, context) => {
@@ -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
  }
@@ -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 {import('../transform/types.js').CssTransformPlugin} */
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 {import('../transform/types.js').CssTransformPlugin} */
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) {
@@ -101,8 +101,12 @@ const layerDeclarations = (compId, vars) => {
101
101
  }
102
102
  }
103
103
 
104
- /** @type {import('../transform/types.js').CssTransformPlugin} */
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.
@@ -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 {import('../../transform/types.js').CssTransformPlugin} */
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 {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