@wix/origami-lint 1.34.19 → 1.34.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bi/read-stats.js +9 -2
- package/dist/bi/stats.js +3 -2
- package/dist/project-rules-runner/run-project-rules.js +10 -4
- package/dist/rules/index.js +1 -0
- package/dist/rules/project/home-page-folder.js +6 -4
- package/dist/rules/project/index.js +1 -0
- package/dist/rules/site-app/allowed-imports.js +9 -7
- package/dist/rules/site-app/apply-and-presets.js +15 -13
- package/dist/rules/site-app/bindings.js +53 -51
- package/dist/rules/site-app/breakpoints.js +8 -6
- package/dist/rules/site-app/css-file-nesting.js +15 -13
- package/dist/rules/site-app/css-layout-properties.js +16 -14
- package/dist/rules/site-app/css-selectors.js +20 -18
- package/dist/rules/site-app/data-values.js +20 -18
- package/dist/rules/site-app/global-styles.js +18 -16
- package/dist/rules/site-app/no-absolute-section.js +3 -1
- package/dist/rules/site-app/no-free-code.js +6 -4
- package/dist/rules/site-app/page-structure.js +16 -14
- package/dist/rules/site-app/slot-attributes.js +6 -4
- package/dist/rules/site-app/theme-font-ids.js +3 -1
- package/dist/rules/site-app/theme-root.js +9 -7
- package/dist/rules-adapter/oxlint/adapt-rule.js +1 -1
- package/dist/rules-adapter/utils.js +5 -2
- package/dist/rules-api/report.js +3 -3
- package/package.json +3 -3
|
@@ -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
|
}));
|
package/dist/rules-api/report.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.34.21",
|
|
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.
|
|
28
|
+
"@wix/origami-bi": "0.10.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": "
|
|
55
|
+
"falconPackageHash": "0f2527e40f513eb3405691ab2a073ba167b04043e3337b6d18056900"
|
|
56
56
|
}
|