@wix/origami-lint 1.34.19 → 1.34.20

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.
@@ -1,5 +1,6 @@
1
1
  import { STRUCTURE } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['invalid_structure_declaration', 'free_code_outside_structure', 'typescript_syntax'];
3
4
  const FREE_CODE = 'The import refuses this statement: a structure file holds only imports, `const` / `let` / `var` declarations and `export const Structure = () => …` (tracker CONV-parseJsx.ts:287).';
4
5
  const STRUCTURE_SHAPE = 'Declare Structure once, as `export const Structure = () => ( <Page …/> )`: an exported const arrow that returns the root element directly, with no `{ return … }`, fragment or condition around it (tracker CONV-parseJsx.ts:335).';
5
6
  const typeScript = (code) => `\`${code}\` is TypeScript. The import reads a structure file as plain JavaScript and fails on it: remove it (tracker CONV-parseJsx.ts:1845).`;
@@ -21,6 +22,7 @@ const TYPESCRIPT = [
21
22
  ];
22
23
  export default defineRule({
23
24
  name: 'no-free-code',
25
+ checks: CHECKS,
24
26
  editors: 'all',
25
27
  callbacks: {
26
28
  ...on(STATEMENTS, isAllowedStatement),
@@ -48,19 +50,19 @@ function isAllowedStatement({ node, rulesApi, report, context }) {
48
50
  return;
49
51
  if (declaresStructure(node)) {
50
52
  if (!structureArrowOf(node))
51
- report({ loc: node.loc, message: STRUCTURE_SHAPE });
53
+ report({ check: 'invalid_structure_declaration', loc: node.loc, message: STRUCTURE_SHAPE });
52
54
  return;
53
55
  }
54
56
  // The import drops every variable declaration before it checks the statements (parseJsx.ts rejectTopLevelStatements).
55
57
  if (declaration?.type !== 'VariableDeclaration')
56
- report({ loc: node.loc, message: FREE_CODE });
58
+ report({ check: 'free_code_outside_structure', loc: node.loc, message: FREE_CODE });
57
59
  }
58
60
  /** Skips a file with no root element (page-structure reports it) and a body that is TypeScript (hasNoTypeScript reports it). */
59
61
  function returnsElement({ node, rulesApi, report }) {
60
62
  const body = unwrap(node.body);
61
63
  if (body.type === 'JSXElement' || rulesApi.isTypeScript(body) || !rulesApi.getRoot())
62
64
  return;
63
- report({ loc: node.body.loc, message: STRUCTURE_SHAPE });
65
+ report({ check: 'invalid_structure_declaration', loc: node.body.loc, message: STRUCTURE_SHAPE });
64
66
  }
65
67
  /** In a structure.jsx it doesn't parse; this is for a structure.tsx, which the import parses as JavaScript too. */
66
68
  function hasNoTypeScript({ node, rulesApi, report, context }) {
@@ -68,7 +70,7 @@ function hasNoTypeScript({ node, rulesApi, report, context }) {
68
70
  reportTypeScript(node, report, context);
69
71
  }
70
72
  function reportTypeScript(node, report, context) {
71
- report({ loc: node.loc, message: typeScript(shortCode(context.sourceCode.getText(node))) });
73
+ report({ check: 'typescript_syntax', loc: node.loc, message: typeScript(shortCode(context.sourceCode.getText(node))) });
72
74
  }
73
75
  // ── reading the code ──
74
76
  function on(names, check) {
@@ -22,8 +22,10 @@
22
22
  import { FILE_START } from "../../page-model/index.js";
23
23
  import { LAYER_ATTRIBUTES, SECTIONS, WRAPPERS } from "../../rules-api/index.js";
24
24
  import { defineRule } from "./define-rule.js";
25
+ const CHECKS = ['missing_root', 'invalid_global_root', 'missing_root_id', 'invalid_page_root', 'non_element_child', 'invalid_section_container', 'nested_page', 'landmark_inside_container', 'invalid_wrapper_children', 'nested_remove', 'invalid_element_array', 'invalid_landmark_section'];
25
26
  export default defineRule({
26
27
  name: 'page-structure',
28
+ checks: CHECKS,
27
29
  editors: 'all',
28
30
  callbacks: {
29
31
  Program: (input) => hasRoot(input),
@@ -64,9 +66,9 @@ function hasRoot({ rulesApi, report }) {
64
66
  if (rulesApi.getRoot())
65
67
  return;
66
68
  if (rulesApi.page.kind === 'page')
67
- report({ loc: FILE_START, message: 'A page must start with <Page>, found nothing.' });
69
+ report({ check: 'missing_root', loc: FILE_START, message: 'A page must start with <Page>, found nothing.' });
68
70
  else
69
- report({ loc: FILE_START, message: 'A global component must start with a section (HeaderSection, FooterSection or Section), found nothing (tracker CONV-runValidation.ts:9).' });
71
+ report({ check: 'missing_root', loc: FILE_START, message: 'A global component must start with a section (HeaderSection, FooterSection or Section), found nothing (tracker CONV-runValidation.ts:9).' });
70
72
  }
71
73
  /** On a global component, reports a top element that isn't a section (P1-R13); the steps after it are for a section. */
72
74
  function isSectionAtTop({ node, rulesApi, report }) {
@@ -75,7 +77,7 @@ function isSectionAtTop({ node, rulesApi, report }) {
75
77
  const type = rulesApi.getType(node);
76
78
  if (isLandmark(node, rulesApi) || (type && SECTIONS.includes(type)))
77
79
  return true;
78
- report({ loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} sits directly in the master page. A global component's top element is a section (HeaderSection, FooterSection or Section); put it inside one (rule P1-R13).` });
80
+ report({ check: 'invalid_global_root', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} sits directly in the master page. A global component's top element is a section (HeaderSection, FooterSection or Section); put it inside one (rule P1-R13).` });
79
81
  return false;
80
82
  }
81
83
  /** Reports a root element with no string id (CONV-runValidation.ts:9). The tree steps go on: the import reports them all in one run. */
@@ -88,13 +90,13 @@ function rootHasId({ node, rulesApi, report }) {
88
90
  && attribute.value.expression.name === 'id') && receivesInstanceId(node))
89
91
  return;
90
92
  const tag = rulesApi.getTagName(node);
91
- report({ loc: getLoc(node, rulesApi), message: `<${tag}> at the top has no id. Give the root element its id, like \`<${tag} id="c1dmp">\` (tracker CONV-runValidation.ts:9).` });
93
+ report({ check: 'missing_root_id', loc: getLoc(node, rulesApi), message: `<${tag}> at the top has no id. Give the root element its id, like \`<${tag} id="c1dmp">\` (tracker CONV-runValidation.ts:9).` });
92
94
  }
93
95
  /** Reports a page's root that isn't <Page>. */
94
96
  function startsWithPage({ node, rulesApi, report }) {
95
97
  if (rulesApi.getType(node) === 'page')
96
98
  return true;
97
- report({ loc: getLoc(node, rulesApi), message: `A page must start with <Page>, found ${describe(node, rulesApi)}.` });
99
+ report({ check: 'invalid_page_root', loc: getLoc(node, rulesApi), message: `A page must start with <Page>, found ${describe(node, rulesApi)}.` });
98
100
  return false;
99
101
  }
100
102
  /** Reports text and {…} directly in <Page> (CONV-parseJsx.ts:1668). Any component is accepted there: a section, the page's <Background>, a pinned element, as the import accepts it. */
@@ -102,7 +104,7 @@ function checkPageChildren({ node: page, rulesApi, report }) {
102
104
  for (const child of getStructuralChildren(page, rulesApi)) {
103
105
  if (child.type === 'JSXElement')
104
106
  continue;
105
- report({ loc: getLoc(page, rulesApi), message: `<Page> has ${describe(child, rulesApi)} as a child. Only elements go directly inside <Page>: a section, or a component pinned to the page (tracker CONV-parseJsx.ts:1668).` });
107
+ report({ check: 'non_element_child', loc: getLoc(page, rulesApi), message: `<Page> has ${describe(child, rulesApi)} as a child. Only elements go directly inside <Page>: a section, or a component pinned to the page (tracker CONV-parseJsx.ts:1668).` });
106
108
  }
107
109
  }
108
110
  /** Reports a section, anywhere in the file, that doesn't hold exactly one <InternalContainer>, and nothing else (S2-SECTION-001, OUR-001). A wrapper in it
@@ -115,7 +117,7 @@ function hasOneInternalContainer(section, { rulesApi, report }) {
115
117
  if (inside.length === 1 && inside[0].type === 'JSXElement' && rulesApi.getType(inside[0]) === 'innerElement')
116
118
  return;
117
119
  const name = rulesApi.getId(section) ?? `<${rulesApi.getTagName(section)}>`;
118
- report({ loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}. Put the section's content inside its one <InternalContainer> (rule S2-SECTION-001).` });
120
+ report({ check: 'invalid_section_container', loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}. Put the section's content inside its one <InternalContainer> (rule S2-SECTION-001).` });
119
121
  }
120
122
  /** Reports a <Page> held by another element (P1-R16), unless the root steps above report that placement already (a section's children). */
121
123
  function isNotNestedPage({ node, rulesApi, report }) {
@@ -124,7 +126,7 @@ function isNotNestedPage({ node, rulesApi, report }) {
124
126
  const holder = getHolder(node, rulesApi);
125
127
  if (!holder || (rulesApi.getType(holder) !== 'page' && isRootTreeHolder(holder, rulesApi) && isStructuralChild(holder, node, rulesApi)))
126
128
  return;
127
- report({ loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} is inside ${describeElement(holder, rulesApi)}. A page is only the top element of its own page (rule P1-R16).` });
129
+ report({ check: 'nested_page', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} is inside ${describeElement(holder, rulesApi)}. A page is only the top element of its own page (rule P1-R16).` });
128
130
  }
129
131
  /** Reports a header or footer, or a page's reference to the global one, with an <InternalContainer> among its ancestors (P1-R03). */
130
132
  function isOutsideInternalContainers({ node, rulesApi, report }) {
@@ -134,20 +136,20 @@ function isOutsideInternalContainers({ node, rulesApi, report }) {
134
136
  const container = getAncestors(node).find((ancestor) => rulesApi.getType(ancestor) === 'innerElement');
135
137
  if (!container)
136
138
  return;
137
- report({ loc: getLoc(node, rulesApi), message: `The ${kind} ${describeElement(node, rulesApi)} is inside the internal container ${describeElement(container, rulesApi)}. Move it out of every internal container: the header and footer sit directly in the page or the master page (rule P1-R03).` });
139
+ report({ check: 'landmark_inside_container', loc: getLoc(node, rulesApi), message: `The ${kind} ${describeElement(node, rulesApi)} is inside the internal container ${describeElement(container, rulesApi)}. Move it out of every internal container: the header and footer sit directly in the page or the master page (rule P1-R03).` });
138
140
  }
139
141
  /** Reports a wrapper that doesn't hold exactly one element: none, two, text, or a {…} (a comment too) beside it (CONV-parseJsx.ts:1028). */
140
142
  function holdsOneElement({ node, rulesApi, report }) {
141
143
  if (holdsOne(node))
142
144
  return;
143
- report({ loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> wraps exactly one component element: give each component its own wrapper (tracker CONV-parseJsx.ts:1028).` });
145
+ report({ check: 'invalid_wrapper_children', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> wraps exactly one component element: give each component its own wrapper (tracker CONV-parseJsx.ts:1028).` });
144
146
  }
145
147
  /** Reports a <Remove> right inside another <Remove> (CONV-parseJsx.ts:1237): <Remove> wraps the component itself, so nothing nests in it. Compared with the element right around it, as the import does. */
146
148
  function checkNoRemoveInRemove({ node, rulesApi, report }) {
147
149
  const outer = getAncestors(node)[0];
148
150
  if (!outer || rulesApi.getType(outer) !== 'remove')
149
151
  return;
150
- report({ loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> is inside another <Remove>. <Remove> wraps the component itself: merge the two conditions into one <Remove> (tracker CONV-parseJsx.ts:1237).` });
152
+ report({ check: 'nested_remove', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> is inside another <Remove>. <Remove> wraps the component itself: merge the two conditions into one <Remove> (tracker CONV-parseJsx.ts:1237).` });
151
153
  }
152
154
  /** Reports text, a {…} (a comment too), a fragment or a spread between a component's tags (CONV-parseJsx.ts:1668). The root steps report those in <Page> and its sections, except comments, which they don't see. */
153
155
  function hasOnlyElements({ node, rulesApi, report }) {
@@ -157,7 +159,7 @@ function hasOnlyElements({ node, rulesApi, report }) {
157
159
  continue;
158
160
  if (commentsOnly && !isComment(child))
159
161
  continue;
160
- report({ loc: child.type === 'JSXText' ? getTextLoc(child) : child.loc, message: `${describeElement(node, rulesApi)} has text or {…} as a child. Put the value in an attribute: only elements go between the tags (tracker CONV-parseJsx.ts:1668).` });
162
+ report({ check: 'non_element_child', loc: child.type === 'JSXText' ? getTextLoc(child) : child.loc, message: `${describeElement(node, rulesApi)} has text or {…} as a child. Put the value in an attribute: only elements go between the tags (tracker CONV-parseJsx.ts:1668).` });
161
163
  }
162
164
  }
163
165
  /** Reports an <InternalContainer>'s backgroundLayer / pinnedLayer that isn't an array literal of elements (CONV-parseJsx.ts:1630). */
@@ -169,7 +171,7 @@ function hasLayerArrays({ node, rulesApi, report }) {
169
171
  continue;
170
172
  if (isArrayOfElements(attribute))
171
173
  continue;
172
- report({ loc: attribute.loc, message: `${describeElement(node, rulesApi)}'s ${attribute.name.name} must be an array of elements: ${attribute.name.name}={[<… />]} (tracker CONV-parseJsx.ts:1630).` });
174
+ report({ check: 'invalid_element_array', loc: attribute.loc, message: `${describeElement(node, rulesApi)}'s ${attribute.name.name} must be an array of elements: ${attribute.name.name}={[<… />]} (tracker CONV-parseJsx.ts:1630).` });
173
175
  }
174
176
  }
175
177
  // ── reading the tree ──
@@ -265,7 +267,7 @@ function hasSectionInLandmark(input) {
265
267
  const children = getStructuralChildren(node, rulesApi);
266
268
  if (children.length === 1 && children[0].type === 'JSXElement' && rulesApi.getType(children[0]) === expected)
267
269
  return;
268
- report({ loc: getLoc(node, rulesApi), message: `<${rulesApi.getTagName(node)}> must contain exactly one ${expected === 'header' ? 'HeaderSection' : 'FooterSection'}.` });
270
+ report({ check: 'invalid_landmark_section', loc: getLoc(node, rulesApi), message: `<${rulesApi.getTagName(node)}> must contain exactly one ${expected === 'header' ? 'HeaderSection' : 'FooterSection'}.` });
269
271
  }
270
272
  function receivesInstanceId(node) {
271
273
  for (let parent = node.parent; parent; parent = parent.parent) {
@@ -7,8 +7,10 @@
7
7
  // Children are the element children and the layer elements (backgroundLayer, pinnedLayer); <Remove> and <Interaction> are transparent.
8
8
  import { LAYER_ATTRIBUTES, WRAPPERS } from "../../rules-api/index.js";
9
9
  import { defineRule } from "./define-rule.js";
10
+ const CHECKS = ['root_has_slot', 'attribute_child_has_slot', 'mixed_slot_forms', 'duplicate_slot'];
10
11
  export default defineRule({
11
12
  name: 'slot-attributes',
13
+ checks: CHECKS,
12
14
  editors: ['studio2'],
13
15
  callbacks: {
14
16
  JSXElement: (input) => {
@@ -28,7 +30,7 @@ function rootHasNoSlot({ node, rulesApi, report }) {
28
30
  const slot = rulesApi.getSlot(node);
29
31
  if (slot === null || !isRoot(node, rulesApi))
30
32
  return;
31
- report({ loc: getLoc(node, rulesApi), message: `The root ${describe(node, rulesApi)} can't have a slot attribute. Remove slot="${slot}" (tracker CONV-fromBundle.ts:1312).` });
33
+ report({ check: 'root_has_slot', loc: getLoc(node, rulesApi), message: `The root ${describe(node, rulesApi)} can't have a slot attribute. Remove slot="${slot}" (tracker CONV-fromBundle.ts:1312).` });
32
34
  }
33
35
  /** The root element: the top of Structure, or the element a <Remove> / <Interaction> at the top holds (the page model makes that wrapper the root). */
34
36
  function isRoot(node, rulesApi) {
@@ -46,7 +48,7 @@ function attributeElementHasNoSlot(element, attribute, { rulesApi, report }) {
46
48
  const name = getAttributeName(attribute);
47
49
  if (LAYER_ATTRIBUTES.includes(name) || rulesApi.getSlot(element) === null)
48
50
  return;
49
- report({ loc: getLoc(element, rulesApi), message: `${describe(element, rulesApi)} is placed in the attribute '${name}' and also has slot="${rulesApi.getSlot(element)}". Remove slot=: its place is the attribute (tracker CONV-parseJsx.ts:1507).` });
51
+ report({ check: 'attribute_child_has_slot', loc: getLoc(element, rulesApi), message: `${describe(element, rulesApi)} is placed in the attribute '${name}' and also has slot="${rulesApi.getSlot(element)}". Remove slot=: its place is the attribute (tracker CONV-parseJsx.ts:1507).` });
50
52
  }
51
53
  /** Reports an owner that holds elements in its attributes and also has a child with slot=, or an elementProps entry with slots: {…} (CONV-parseJsx.ts:1693). */
52
54
  function usesSlotsOnlyOneWay({ node, rulesApi, report }) {
@@ -54,7 +56,7 @@ function usesSlotsOnlyOneWay({ node, rulesApi, report }) {
54
56
  return;
55
57
  if (!getSlottedChildren(node, rulesApi).length && !hasEntrySlots(node))
56
58
  return;
57
- report({ loc: getLoc(node, rulesApi), message: `${describe(node, rulesApi)} places slotted children both in attributes and with slot= (or slots: {…}). Use one way: elements in attributes, or children with slot= (tracker CONV-parseJsx.ts:1693).` });
59
+ report({ check: 'mixed_slot_forms', loc: getLoc(node, rulesApi), message: `${describe(node, rulesApi)} places slotted children both in attributes and with slot= (or slots: {…}). Use one way: elements in attributes, or children with slot= (tracker CONV-parseJsx.ts:1693).` });
58
60
  }
59
61
  /** Reports a slot key that two children of the owner share (CONV-slots.ts:555), at the later one in the owner's list: children first, then layer elements. */
60
62
  function usesEachSlotKeyOnce({ node, rulesApi, report }) {
@@ -65,7 +67,7 @@ function usesEachSlotKeyOnce({ node, rulesApi, report }) {
65
67
  if (!other)
66
68
  first.set(slot, child);
67
69
  else
68
- report({ loc: getLoc(child, rulesApi), message: `Slot "${slot}" of ${describe(node, rulesApi)} is used by both ${describe(other, rulesApi)} and ${describe(child, rulesApi)}. Give each slotted child its own slot key (tracker CONV-slots.ts:555).` });
70
+ report({ check: 'duplicate_slot', loc: getLoc(child, rulesApi), message: `Slot "${slot}" of ${describe(node, rulesApi)} is used by both ${describe(other, rulesApi)} and ${describe(child, rulesApi)}. Give each slotted child its own slot key (tracker CONV-slots.ts:555).` });
69
71
  }
70
72
  }
71
73
  // ── reading the owner ──
@@ -7,6 +7,7 @@
7
7
  import { fonts } from '@wix/origami-fonts';
8
8
  import { reportCssProblem } from "../../rules-api/index.js";
9
9
  import { defineRule } from "./define-rule.js";
10
+ const CHECKS = ['invalid_font_id'];
10
11
  const FONT_TOKEN = /^--wst-[\w-]+-font$/;
11
12
  // The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
12
13
  // never overlap, so a long run of digits that is not a size fails in linear time.
@@ -14,6 +15,7 @@ const SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|v
14
15
  const FONT_IDS = new Set(fonts.filter((font) => font.permissions !== 'legacy').map((font) => font.id));
15
16
  export default defineRule({
16
17
  name: 'theme-font-ids',
18
+ checks: CHECKS,
17
19
  editors: 'all',
18
20
  callbacks: {
19
21
  Program: (input) => {
@@ -32,7 +34,7 @@ function hasCatalogFontId({ name, value, line }, { report }) {
32
34
  const family = getFontFamily(value);
33
35
  if (family === undefined || FONT_IDS.has(family))
34
36
  return;
35
- reportCssProblem(report, 'theme.css', line, `${name} must name exactly one Wix font id from the catalog; found ${family}. Run \`wix origami search-font --font <name>\` for the id.`);
37
+ reportCssProblem(report, 'theme.css', line, 'invalid_font_id', `${name} must name exactly one Wix font id from the catalog; found ${family}. Run \`wix origami search-font --font <name>\` for the id.`);
36
38
  }
37
39
  // ── reading a font value ──
38
40
  /** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one part. */
@@ -1,10 +1,12 @@
1
1
  import { describeAtRule, isPresetProperty, PREAMBLE_LAYER, reportCssProblem } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['root_selector_suffix', 'root_in_page_css', 'preset_on_root', 'non_token_root_property', 'mixed_spx_context', 'invalid_spx_context', 'invalid_spx_scope'];
3
4
  const SPX_PROPS = ['--spx-ref-width', '--spx-resolver-type'];
4
5
  const SPX_RESOLVERS = ['scale', 'fixed'];
5
6
  /** A named breakpoint: `(--tablet)`. */
6
7
  export default defineRule({
7
8
  name: 'theme-root',
9
+ checks: CHECKS,
8
10
  editors: ['studio2'],
9
11
  callbacks: {
10
12
  Program: (input) => {
@@ -32,13 +34,13 @@ export default defineRule({
32
34
  function isAlone(file, rule, { report }) {
33
35
  if (rule.selector === ':root')
34
36
  return true;
35
- reportCssProblem(report, file, rule.line, `\`${rule.selector}\` puts a part after :root. Write \`:root { … }\` alone; style elements by their own class (\`.comp-a { … }\`) (tracker CONV-cssSelectors.ts:388).`);
37
+ reportCssProblem(report, file, rule.line, 'root_selector_suffix', `\`${rule.selector}\` puts a part after :root. Write \`:root { … }\` alone; style elements by their own class (\`.comp-a { … }\`) (tracker CONV-cssSelectors.ts:388).`);
36
38
  return false;
37
39
  }
38
40
  /** Reports a `:root` rule in a page's own CSS: in style.css (CONV-styleParser.ts:443); in layout.css too, which the import refuses as well (seen live, no tracker row yet). */
39
41
  function isNotInPage(file, rule, { report }) {
40
42
  const source = file === 'style' ? 'tracker CONV-styleParser.ts:443' : 'the import refuses it: seen live';
41
- reportCssProblem(report, `${file}.css`, rule.line, `\`:root\` can't be used in a page's ${file}.css: set site variables in global/themes/theme.css (${source}).`);
43
+ reportCssProblem(report, `${file}.css`, rule.line, 'root_in_page_css', `\`:root\` can't be used in a page's ${file}.css: set site variables in global/themes/theme.css (${source}).`);
42
44
  }
43
45
  /** A theme `:root` rule: a preset (step 4), then only tokens or only the spx context (step 1); an spx context's values (step 2) and place (step 3). A `:root` inside a `@mixin` is the global-styles rows' (its selector must start with `&` there). */
44
46
  function checkThemeRoot(root, input) {
@@ -58,14 +60,14 @@ function hasNoPreset(root, { report }) {
58
60
  const preset = root.declarations.find(({ prop }) => isPresetProperty(prop));
59
61
  if (!preset)
60
62
  return true;
61
- reportCssProblem(report, 'theme.css', root.line, `\`:root\` can't set ${preset.prop}; theme.css holds only :root tokens, the spx context and @mixin global styles. Set a preset on the element's rule in its page's style.css (tracker CONV-parseTheme.ts:224).`);
63
+ reportCssProblem(report, 'theme.css', root.line, 'preset_on_root', `\`:root\` can't set ${preset.prop}; theme.css holds only :root tokens, the spx context and @mixin global styles. Set a preset on the element's rule in its page's style.css (tracker CONV-parseTheme.ts:224).`);
62
64
  return false;
63
65
  }
64
66
  /** Reports a plain property, or tokens mixed with the spx context (CONV-parseTheme.ts:170). 'tokens' / 'spx' when the rule is one of the two. */
65
67
  function holdsTokensOrSpx(root, { report }) {
66
68
  const plain = root.declarations.find(({ prop }) => !prop.startsWith('--'));
67
69
  if (plain) {
68
- reportCssProblem(report, 'theme.css', root.line, `this :root rule has \`${plain.prop}\`; a :root rule holds only tokens (--…), or only --spx-ref-width and --spx-resolver-type. Move other properties to an element's rule in style.css (tracker CONV-parseTheme.ts:170).`);
70
+ reportCssProblem(report, 'theme.css', root.line, 'non_token_root_property', `this :root rule has \`${plain.prop}\`; a :root rule holds only tokens (--…), or only --spx-ref-width and --spx-resolver-type. Move other properties to an element's rule in style.css (tracker CONV-parseTheme.ts:170).`);
69
71
  return null;
70
72
  }
71
73
  const spx = root.declarations.filter(isSpx);
@@ -73,7 +75,7 @@ function holdsTokensOrSpx(root, { report }) {
73
75
  return 'tokens';
74
76
  if (spx.length === root.declarations.length)
75
77
  return 'spx';
76
- reportCssProblem(report, 'theme.css', root.line, `this :root rule mixes the spx context (--spx-ref-width, --spx-resolver-type) with other lines; write the spx context as its own :root rule (tracker CONV-parseTheme.ts:170).`);
78
+ reportCssProblem(report, 'theme.css', root.line, 'mixed_spx_context', `this :root rule mixes the spx context (--spx-ref-width, --spx-resolver-type) with other lines; write the spx context as its own :root rule (tracker CONV-parseTheme.ts:170).`);
77
79
  return null;
78
80
  }
79
81
  /** Reports an spx context missing one of its two properties, with a width that isn't a positive plain number, or a resolver that isn't scale / fixed (CONV-parseTheme.ts:177). */
@@ -82,7 +84,7 @@ function isValidSpxContext(root, { report }) {
82
84
  const resolver = lastValue(root, '--spx-resolver-type');
83
85
  if (width !== undefined && Number.isFinite(Number(width)) && Number(width) > 0 && resolver !== undefined && SPX_RESOLVERS.includes(resolver))
84
86
  return true;
85
- reportCssProblem(report, 'theme.css', root.line, `this :root spx rule must set both --spx-ref-width (a positive number, like 1280) and --spx-resolver-type (scale or fixed) (tracker CONV-parseTheme.ts:177).`);
87
+ reportCssProblem(report, 'theme.css', root.line, 'invalid_spx_context', `this :root spx rule must set both --spx-ref-width (a positive number, like 1280) and --spx-resolver-type (scale or fixed) (tracker CONV-parseTheme.ts:177).`);
86
88
  return false;
87
89
  }
88
90
  /** Reports an spx context under anything but one `@media (--name)` the theme declares: two conditions, a plain range, another at-rule (CONV-parseTheme.ts:82). At the top level it is the default range's. */
@@ -92,7 +94,7 @@ function isSpxContextInPlace(root, { rulesApi, report }) {
92
94
  const name = root.enclosing.length === 1 && root.enclosing[0].startsWith('@media ') ? rulesApi.getBreakpointName(root.enclosing[0].slice('@media '.length)) : null;
93
95
  if (name && rulesApi.getCustomMedia('theme').some((breakpoint) => breakpoint.name === name))
94
96
  return;
95
- reportCssProblem(report, 'theme.css', root.line, `set the spx context at the top level or under \`@media (--<named range>)\`, one of the theme's @custom-media names; not under ${root.enclosing.map((atRule) => `\`${atRule}\``).join(', ')} (tracker CONV-parseTheme.ts:82).`);
97
+ reportCssProblem(report, 'theme.css', root.line, 'invalid_spx_scope', `set the spx context at the top level or under \`@media (--<named range>)\`, one of the theme's @custom-media names; not under ${root.enclosing.map((atRule) => `\`${atRule}\``).join(', ')} (tracker CONV-parseTheme.ts:82).`);
96
98
  }
97
99
  // ── reading the CSS ──
98
100
  /** Every rule whose selector starts with `:root` in a parsed file, in order, with what's around it and its declarations. No file → none. */
@@ -8,7 +8,7 @@ export function toOxlintRule(rule) {
8
8
  meta: { type: 'problem' },
9
9
  // oxlint calls this once per rule per file. The file's rules API is shared by every rule (getOrCreateRulesApi caches it).
10
10
  create: (context) => {
11
- const stats = startRuleStats(rule.name, path.relative(context.cwd, context.filename), context.sourceCode.text, context.sourceCode.ast);
11
+ const stats = startRuleStats(rule.name, rule.checks, path.relative(context.cwd, context.filename), context.sourceCode.text, context.sourceCode.ast);
12
12
  const report = createReport((problem) => context.report(problem), stats);
13
13
  return createToolCallbacks(rule, { rulesApi: () => getOrCreateRulesApi(toLintedFile(context)), report, context, stats });
14
14
  },
@@ -7,8 +7,10 @@ export function createReport(send, stats) {
7
7
  if (reported.has(key))
8
8
  return;
9
9
  reported.add(key);
10
- if (stats)
10
+ if (stats) {
11
11
  stats.violations++;
12
+ stats.checks[problem.check]++;
13
+ }
12
14
  send(problem);
13
15
  };
14
16
  }
@@ -23,8 +25,9 @@ export function createToolCallbacks(rule, { rulesApi, report, context, stats })
23
25
  check({ node, get rulesApi() { return rulesApi(); }, report, context });
24
26
  }
25
27
  finally {
26
- if (stats)
28
+ if (stats) {
27
29
  stats.duration_ms += performance.now() - started;
30
+ }
28
31
  }
29
32
  }];
30
33
  }));
@@ -1,11 +1,11 @@
1
1
  import { FILE_START } from "../page-model/index.js";
2
2
  /** Reports a problem on a line of a CSS file: at the page's start (CSS isn't a file the lint tool lints), the message led by `file:line: `. */
3
- export function reportCssProblem(report, file, line, message) {
4
- report({ loc: FILE_START, message: `${file}:${line}: ${message}` });
3
+ export function reportCssProblem(report, file, line, check, message) {
4
+ report({ check, loc: FILE_START, message: `${file}:${line}: ${message}` });
5
5
  }
6
6
  /** A reporter for one CSS file: each problem at the page's start, led by `file:line: ` with the node's line (reportCssProblem). */
7
7
  export function createCssFileReporter(file, report) {
8
- return (node, message) => reportCssProblem(report, file, node.source?.start?.line ?? 1, message);
8
+ return (node, check, message) => reportCssProblem(report, file, node.source?.start?.line ?? 1, check, message);
9
9
  }
10
10
  /** An at-rule as a message writes it: '@media (max-width: 1px)', or '@media' with no params. */
11
11
  export function describeAtRule({ name, params }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.19",
3
+ "version": "1.34.20",
4
4
  "description": "Offline lint for an exported Origami site: lints the pages that changed since the last passing run",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "oxc-parser": "^0.151.0",
26
26
  "oxlint": "^1.39.0",
27
27
  "postcss": "^8.5.3",
28
- "@wix/origami-bi": "0.8.0"
28
+ "@wix/origami-bi": "0.9.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.0.0",
@@ -52,5 +52,5 @@
52
52
  ]
53
53
  }
54
54
  },
55
- "falconPackageHash": "791d9d5c3c5c2e15912796c5bfa7def4c696084174385a57d767c143"
55
+ "falconPackageHash": "e3239902b0ec089758e46198da73c8213fd6b61f54d8b444d6fd0a30"
56
56
  }