@wix/origami-lint 1.34.33 → 1.34.35
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,6 +1,6 @@
|
|
|
1
1
|
import { describeAtRule, reportCssProblem } from "../../rules-api/index.js";
|
|
2
2
|
import { defineRule } from "./define-rule.js";
|
|
3
|
-
const CHECKS = ['malformed_custom_media', 'duplicate_breakpoint', 'unknown_breakpoint_name', 'changed_breakpoint_range', '
|
|
3
|
+
const CHECKS = ['malformed_custom_media', 'duplicate_breakpoint', 'unknown_breakpoint_name', 'changed_breakpoint_range', 'unknown_breakpoint_range'];
|
|
4
4
|
/** The converter's shape for a declaration's name: a dashed identifier. */
|
|
5
5
|
const DASHED_NAME = /^--[A-Za-z0-9_-]+$/;
|
|
6
6
|
export default defineRule({
|
|
@@ -63,27 +63,27 @@ function keepsSiteRange(file, declaration, site, report) {
|
|
|
63
63
|
reportCssProblem(report, file.name, declaration.line, 'changed_breakpoint_range', `\`@custom-media ${declaration.name} ${declaration.query}\` changes the site's range for ${declaration.name}, which is \`${exported}\`. Restore it; a breakpoint's width is changed in the editor, not in CSS.`);
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
|
-
/** Reports a `@media (--name)`
|
|
66
|
+
/** Reports a `@media (--name)` whose name isn't one of the site's breakpoints (CONV-breakpoints.ts:150): the import reads the name from the site's
|
|
67
|
+
* breakpoints, so the file needs no `@custom-media` of its own. Nothing without a theme to compare with. A name the file declares is reported on its
|
|
68
|
+
* declaration (checkDeclarations), so the use gets no second line. */
|
|
67
69
|
function checkNamedUses(file, site, report) {
|
|
70
|
+
if (!site.hasTheme)
|
|
71
|
+
return;
|
|
68
72
|
const declared = new Set(file.declared.flatMap(({ name }) => [name, `--${name}`]));
|
|
69
73
|
for (const { name, line } of file.used) {
|
|
70
|
-
if (!name || declared.has(name))
|
|
74
|
+
if (!name || declared.has(name) || getSiteRange(name, site) !== undefined)
|
|
71
75
|
continue;
|
|
72
|
-
|
|
73
|
-
const fix = range
|
|
74
|
-
? `add \`@custom-media ${name} ${range};\` at the top of ${file.name}, or write the range itself, \`@media ${range}\``
|
|
75
|
-
: `add \`@custom-media ${name} (<its range>);\` at the top of ${file.name} (the range the site gives it), or write the range itself`;
|
|
76
|
-
reportCssProblem(report, file.name, line, 'undeclared_breakpoint', `\`@media (${name})\` uses a name the file doesn't declare: ${fix}.`);
|
|
76
|
+
reportCssProblem(report, file.name, line, 'unknown_breakpoint_name', `\`@media (${name})\` names a breakpoint the site doesn't have. Use one of the theme's breakpoints: ${describe(site.breakpoints)}.`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
/** Reports each `@media` of a file whose range is none of the site's (P4-R01, CONV-breakpoints.ts:75); nothing without a theme to compare with. A name is read
|
|
79
|
+
/** Reports each `@media` of a file whose range is none of the site's (P4-R01, CONV-breakpoints.ts:75); nothing without a theme to compare with. A name is read as the site's range for it; an unknown name is checkNamedUses'. A component's own breakpoint (inside `@scope`) is compared once unmapped.json is read. */
|
|
80
80
|
function checkRanges(file, site, report) {
|
|
81
81
|
if (!site.hasTheme)
|
|
82
82
|
return;
|
|
83
83
|
for (const media of file.used) {
|
|
84
84
|
if (media.scope !== null)
|
|
85
85
|
continue;
|
|
86
|
-
const query = resolve(media,
|
|
86
|
+
const query = resolve(media, site);
|
|
87
87
|
if (query === null || site.breakpoints.some((breakpoint) => canonical(breakpoint.query) === canonical(query)))
|
|
88
88
|
continue;
|
|
89
89
|
reportCssProblem(report, file.name, media.line, 'unknown_breakpoint_range', `\`${describeAtRule({ name: 'media', params: media.query })}\` is a breakpoint the site doesn't have. Write one of the theme's breakpoints: ${describe(site.breakpoints)}. The import refuses a value for a breakpoint the site doesn't have.`);
|
|
@@ -112,13 +112,11 @@ function getMedia(file, isPage, rulesApi) {
|
|
|
112
112
|
const hasShape = ({ name, query }) => DASHED_NAME.test(name) && query !== '';
|
|
113
113
|
/** The site's range for a name; undefined when the theme has no well-formed declaration of it. */
|
|
114
114
|
const getSiteRange = (name, site) => site.breakpoints.find((breakpoint) => breakpoint.name === name)?.query;
|
|
115
|
-
/** The range to compare: the @media's own; for `(--name)`, the range
|
|
116
|
-
function resolve(media,
|
|
115
|
+
/** The range to compare: the @media's own; for `(--name)`, the site's range for that name, as the import reads it (an unknown name is checkNamedUses'); null when there is nothing to compare. */
|
|
116
|
+
function resolve(media, site) {
|
|
117
117
|
if (!media.name)
|
|
118
118
|
return media.query;
|
|
119
|
-
|
|
120
|
-
const exported = getSiteRange(media.name, site);
|
|
121
|
-
return declaration && exported !== undefined && exported.trim() === declaration.query.trim() ? declaration.query : null;
|
|
119
|
+
return getSiteRange(media.name, site) ?? null;
|
|
122
120
|
}
|
|
123
121
|
/** How the import reads a range: `(max-width:1023px)` and `(max-width: 1023px)` are the same; anything else, a different case or other spaces included, is another query. */
|
|
124
122
|
const canonical = (query) => query.trim().replace(/:\s+/g, ':');
|
|
@@ -148,7 +148,7 @@ function isOutsideInternalContainers({ node, rulesApi, report }) {
|
|
|
148
148
|
return;
|
|
149
149
|
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.` });
|
|
150
150
|
}
|
|
151
|
-
/** Reports a wrapper that doesn't hold exactly one element: none, two, text, or a {…}
|
|
151
|
+
/** Reports a wrapper that doesn't hold exactly one element: none, two, text, or a {…} beside it; a comment is fine (CONV-parseJsx.ts:1028). */
|
|
152
152
|
function holdsOneElement({ node, rulesApi, report }) {
|
|
153
153
|
if (holdsOne(node))
|
|
154
154
|
return;
|
|
@@ -161,13 +161,13 @@ function checkNoRemoveInRemove({ node, rulesApi, report }) {
|
|
|
161
161
|
return;
|
|
162
162
|
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>.` });
|
|
163
163
|
}
|
|
164
|
-
/** Reports text, a {…}
|
|
164
|
+
/** Reports text, a {…}, a fragment or a spread between a component's tags (CONV-parseJsx.ts:1668). A comment is fine, as whitespace is: the import
|
|
165
|
+
* skips both (it reads a `@name` comment as the component's name). The root steps report the rest in <Page> and its sections. */
|
|
165
166
|
function hasOnlyElements({ node, rulesApi, report }) {
|
|
166
|
-
|
|
167
|
+
if (isRootTreeHolder(node, rulesApi))
|
|
168
|
+
return;
|
|
167
169
|
for (const child of node.children) {
|
|
168
|
-
if (child.type === 'JSXElement' || (child
|
|
169
|
-
continue;
|
|
170
|
-
if (commentsOnly && !isComment(child))
|
|
170
|
+
if (child.type === 'JSXElement' || isFiller(child))
|
|
171
171
|
continue;
|
|
172
172
|
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.` });
|
|
173
173
|
}
|
|
@@ -283,13 +283,13 @@ function isRootTreeHolder(holder, rulesApi) {
|
|
|
283
283
|
}
|
|
284
284
|
/** Whether the element is one of the holder's children (through wrappers), not something passed in one of its attributes. */
|
|
285
285
|
const isStructuralChild = (holder, element, rulesApi) => getStructuralChildren(holder, rulesApi).includes(element);
|
|
286
|
-
/** A wrapper holds exactly one element: non-blank text, a {…}
|
|
286
|
+
/** A wrapper holds exactly one element: non-blank text, a {…} and a fragment count as what it holds; whitespace and comments don't. */
|
|
287
287
|
const holdsOne = (wrapper) => {
|
|
288
|
-
const inside = wrapper.children.filter((child) => child
|
|
288
|
+
const inside = wrapper.children.filter((child) => !isFiller(child));
|
|
289
289
|
return inside.length === 1 && inside[0].type === 'JSXElement';
|
|
290
290
|
};
|
|
291
|
-
/**
|
|
292
|
-
const
|
|
291
|
+
/** What the import skips between tags: whitespace, and a JSX comment (an expression container with nothing in it). */
|
|
292
|
+
const isFiller = (child) => (child.type === 'JSXText' && child.value.trim() === '') || (child.type === 'JSXExpressionContainer' && child.expression.type === 'JSXEmptyExpression');
|
|
293
293
|
/** `backgroundLayer={[<Background … />]}`: an array literal whose every entry is an element. */
|
|
294
294
|
function isArrayOfElements(attribute) {
|
|
295
295
|
const { value } = attribute;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.35",
|
|
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.14.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": "3167f248bc0c6e01893243829d375999c8ee55bc2d649ba40becbf55"
|
|
56
56
|
}
|