@wix/origami-lint 1.34.41 → 1.34.43
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
|
-
import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, mergeLayoutScopes, OVERFLOW_TARGET, readLayoutRuleBlocks, SPX_PROPS, SPX_RESOLVER_TYPES } from "../../rules-api/index.js";
|
|
1
|
+
import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, mergeLayoutScopes, OVERFLOW_TARGET, PREAMBLE_LAYER, readLayoutRuleBlocks, SPX_PROPS, SPX_RESOLVER_TYPES } from "../../rules-api/index.js";
|
|
2
2
|
import { defineRule } from "./define-rule.js";
|
|
3
|
-
const CHECKS = ['duplicate_property', 'non_layout_property', 'property_outside_target', 'property_in_wrong_target', 'non_container_target_property', 'missing_property_value', 'incompatible_container_property', 'invalid_layout_value', 'missing_container_display', 'missing_grid_column', 'invalid_spx_context', 'unsupported_flex_wrap', 'child_without_container_layout'];
|
|
3
|
+
const CHECKS = ['duplicate_property', 'non_layout_property', 'property_outside_target', 'property_in_wrong_target', 'non_container_target_property', 'missing_property_value', 'incompatible_container_property', 'invalid_layout_value', 'missing_container_display', 'missing_grid_column', 'invalid_spx_context', 'internal_spx_call', 'missing_spx_reference_width', 'unsupported_flex_wrap', 'child_without_container_layout'];
|
|
4
4
|
// ── the tables (propSpecs.ts) ──
|
|
5
5
|
const keywords = (...values) => ({ values, allowed: values.join(', ') });
|
|
6
6
|
const ANY = { allowed: 'any value' };
|
|
@@ -35,7 +35,7 @@ const ITEM_PROPS = {
|
|
|
35
35
|
bottom: SIZE,
|
|
36
36
|
left: SIZE,
|
|
37
37
|
right: SIZE,
|
|
38
|
-
'z-index': { reads: (value) => value === 'auto' || /^calc\(\s*infinity\s*\)$/.test(value), allowed: 'calc(infinity), auto' },
|
|
38
|
+
'z-index': { reads: (value) => value === 'auto' || /^calc\(\s*infinity\s*\)$/.test(value), allowed: 'calc(infinity), auto, or a layer: 1, 51, 100000' },
|
|
39
39
|
'align-self': SELF_ALIGNMENT,
|
|
40
40
|
'justify-self': SELF_ALIGNMENT,
|
|
41
41
|
'scroll-snap-align': keywords('start', 'center', 'end', 'none'),
|
|
@@ -83,6 +83,12 @@ const CONTAINER_PROPS = {
|
|
|
83
83
|
/** A property allowed at the top of a rule with `@origami-target` blocks (isRootLayoutProperty): the element's own, item and spx properties, and the root-target container property. */
|
|
84
84
|
const isRootProperty = (prop) => prop in ELEMENT_PROPS || prop in ITEM_PROPS || SPX_PROPS.includes(prop) || CONTAINER_PROPS[prop]?.target === 'root';
|
|
85
85
|
const isLayoutProperty = (prop) => isRootProperty(prop) || prop in CONTAINER_PROPS;
|
|
86
|
+
/** The element's transformation in layout.css (isLayoutTransformationDeclaration): these properties, and a z-index at a zLayer's value (Z_INDEXES). */
|
|
87
|
+
const TRANSFORMATION_PROPS = ['opacity', 'transform', 'transform-origin', 'filter'];
|
|
88
|
+
const Z_LAYER_INDEXES = ['1', '51', '100000'];
|
|
89
|
+
const isTransformation = (prop, value) => TRANSFORMATION_PROPS.includes(prop) || (prop === 'z-index' && Z_LAYER_INDEXES.includes(value.trim()));
|
|
90
|
+
/** A rule's top-level declarations as the import reads its layout: without the transformation, which it takes out first (withoutTransformationsOf). */
|
|
91
|
+
const withoutTransformation = (declarations) => declarations.filter((decl) => !isTransformation(decl.prop, decl.value));
|
|
86
92
|
export default defineRule({
|
|
87
93
|
name: 'css-layout-properties',
|
|
88
94
|
checks: CHECKS,
|
|
@@ -94,7 +100,9 @@ export default defineRule({
|
|
|
94
100
|
});
|
|
95
101
|
// ── the file: each rule block, then each element at each breakpoint ──
|
|
96
102
|
/** The page's layout.css: each rule block, then each element at each breakpoint. */
|
|
97
|
-
function checkLayoutFile(
|
|
103
|
+
function checkLayoutFile(input) {
|
|
104
|
+
checkInternalSpxCalls(input);
|
|
105
|
+
const { rulesApi, report } = input;
|
|
98
106
|
const root = rulesApi.page.css.files.layout;
|
|
99
107
|
if (!root)
|
|
100
108
|
return;
|
|
@@ -102,7 +110,9 @@ function checkLayoutFile({ rulesApi, report }) {
|
|
|
102
110
|
const blocks = readLayoutRuleBlocks(root);
|
|
103
111
|
for (const block of blocks)
|
|
104
112
|
checkBlock(block, reportAt);
|
|
105
|
-
|
|
113
|
+
// The import takes the transformation out of each rule before it reads the layout (withoutTransformationsOf), so `z-index: 5; z-index: 51` keeps the 5 as layout
|
|
114
|
+
const scopes = mergeLayoutScopes(blocks.map((block) => ({ ...block, declarations: withoutTransformation(block.declarations) })));
|
|
115
|
+
checkTransformationValues(blocks, reportAt);
|
|
106
116
|
const defaultTypes = new Map();
|
|
107
117
|
for (const scope of scopes) {
|
|
108
118
|
const inherited = scope.media === null ? undefined : defaultTypes.get(scope.element);
|
|
@@ -113,9 +123,10 @@ function checkLayoutFile({ rulesApi, report }) {
|
|
|
113
123
|
}
|
|
114
124
|
/** Each property once per block (:498); a rule without targets holds layout properties (:505); with targets, root properties at the top (:512) and each container property in its target (:524). */
|
|
115
125
|
function checkBlock(block, reportAt) {
|
|
116
|
-
checkPropertiesOnce(block.declarations, `${block.element}'s rule`, reportAt);
|
|
126
|
+
checkPropertiesOnce(withoutTransformation(block.declarations), `${block.element}'s rule`, reportAt);
|
|
117
127
|
for (const [target, declarations] of block.targets)
|
|
118
128
|
checkPropertiesOnce(declarations, `${block.element}'s \`@origami-target ${target}\` block`, reportAt);
|
|
129
|
+
checkOwnRefWidth(block, reportAt);
|
|
119
130
|
if (block.targets.size === 0)
|
|
120
131
|
return checkLayoutPropertiesOnly(block, reportAt);
|
|
121
132
|
checkRootPropertiesAtTop(block, reportAt);
|
|
@@ -131,7 +142,7 @@ function checkPropertiesOnce(declarations, where, reportAt) {
|
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
function checkLayoutPropertiesOnly({ element, declarations }, reportAt) {
|
|
134
|
-
for (const decl of declarations) {
|
|
145
|
+
for (const decl of withoutTransformation(declarations)) {
|
|
135
146
|
if (!isLayoutProperty(decl.prop))
|
|
136
147
|
reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)}.`);
|
|
137
148
|
}
|
|
@@ -144,7 +155,7 @@ function describeNonLayoutProperty(prop, element) {
|
|
|
144
155
|
return `\`${prop}\` on ${element} is not a layout property: move it to ${element}'s rule in style.css`;
|
|
145
156
|
}
|
|
146
157
|
function checkRootPropertiesAtTop({ element, declarations }, reportAt) {
|
|
147
|
-
for (const decl of declarations) {
|
|
158
|
+
for (const decl of withoutTransformation(declarations)) {
|
|
148
159
|
if (isRootProperty(decl.prop))
|
|
149
160
|
continue;
|
|
150
161
|
const container = CONTAINER_PROPS[decl.prop];
|
|
@@ -167,11 +178,22 @@ function checkTargetProperties(element, target, declarations, reportAt) {
|
|
|
167
178
|
reportAt(decl, 'property_in_wrong_target', `${element}'s \`${decl.prop}\` goes ${place}, not in ${target}.`);
|
|
168
179
|
}
|
|
169
180
|
else {
|
|
170
|
-
|
|
181
|
+
// A transformation is taken out at the top of the rule only (withoutTransformationsOf reads a rule's own declarations, not its targets)
|
|
182
|
+
const fix = isLayoutProperty(decl.prop) || TRANSFORMATION_PROPS.includes(decl.prop) ? `move it to the top of ${element}'s rule` : `move it to ${element}'s rule in style.css`;
|
|
171
183
|
reportAt(decl, 'non_container_target_property', `\`${decl.prop}\` in ${element}'s \`@origami-target ${target}\` block is not a container property: ${fix}.`);
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
}
|
|
187
|
+
/** A transformation at a rule's top has a value: the import reads each one by its last declaration per element and breakpoint (cssByNameOf), so an empty one before a value is not read. */
|
|
188
|
+
function checkTransformationValues(blocks, reportAt) {
|
|
189
|
+
const transformations = blocks.map((block) => ({ ...block, declarations: block.declarations.filter((decl) => isTransformation(decl.prop, decl.value)), targets: new Map() }));
|
|
190
|
+
for (const { element, values } of mergeLayoutScopes(transformations)) {
|
|
191
|
+
for (const [prop, decl] of values) {
|
|
192
|
+
if (decl.value.trim() === '')
|
|
193
|
+
reportAt(decl, 'missing_property_value', `${element}'s \`${prop}\` has no value: write a value or remove the declaration.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
175
197
|
/** An element at one breakpoint: its spx reference (:417), its container type (:218), each value (:430, :437, :440, :447), a display for its targets (:451), grid-row with grid-column (:264). Returns the container type, for the breakpoints that inherit it. */
|
|
176
198
|
function checkScope(scope, inherited, reportAt) {
|
|
177
199
|
const { element, values } = scope;
|
|
@@ -184,14 +206,16 @@ function checkScope(scope, inherited, reportAt) {
|
|
|
184
206
|
reportAt(decl, 'missing_property_value', `${element}'s \`${prop}\` has no value: write a value or remove the declaration.`);
|
|
185
207
|
continue;
|
|
186
208
|
}
|
|
187
|
-
if (SPX_PROPS.includes(prop))
|
|
188
|
-
continue;
|
|
209
|
+
if (SPX_PROPS.includes(prop) || isTransformation(prop, value))
|
|
210
|
+
continue; // a transformation in a target block: checkTargetProperties reports it
|
|
189
211
|
const container = CONTAINER_PROPS[prop];
|
|
190
212
|
if (container && type && !container[type]) {
|
|
191
213
|
const [own, other] = type === 'grid' ? ['grid', 'flex'] : ['flex', 'grid'];
|
|
192
214
|
reportAt(decl, 'incompatible_container_property', `${element} is a ${own} container (\`display: ${own}\`), and \`${prop}\` is a ${other} property: remove it, or use \`display: ${other}\`.`);
|
|
193
215
|
continue;
|
|
194
216
|
}
|
|
217
|
+
if (hasCssFunction(value, '--layout-spx'))
|
|
218
|
+
continue; // internal_spx_call reports it
|
|
195
219
|
if (prop === 'flex-wrap' && ownType === 'stack')
|
|
196
220
|
continue; // :218 reported it; an inherited flex reads it as a value (:437)
|
|
197
221
|
const spec = ELEMENT_PROPS[prop] ?? ITEM_PROPS[prop] ?? container?.spec;
|
|
@@ -221,6 +245,54 @@ function checkSpxReference({ element, values }, reportAt) {
|
|
|
221
245
|
return;
|
|
222
246
|
reportAt(first, 'invalid_spx_context', `${element}'s spx context is invalid: \`--spx-ref-width\` takes a plain number (1280, no px) and \`--spx-resolver-type\` takes scale or fixed.`);
|
|
223
247
|
}
|
|
248
|
+
/** A rule that uses `--spx()` sets its own `--spx-ref-width`: the build reads each call's width from its rule only (#579), so another
|
|
249
|
+
* rule's width doesn't count; an `@origami-target` block uses its holding rule's. */
|
|
250
|
+
function checkOwnRefWidth({ element, declarations, targets }, reportAt) {
|
|
251
|
+
if (declarations.some((decl) => decl.prop === '--spx-ref-width'))
|
|
252
|
+
return;
|
|
253
|
+
for (const decl of [...declarations, ...[...targets.values()].flat()]) {
|
|
254
|
+
if (hasCssFunction(decl.value, '--spx'))
|
|
255
|
+
reportAt(decl, 'missing_spx_reference_width', `${element} uses \`--spx()\` without \`--spx-ref-width\` in its rule: add \`--spx-ref-width\` to the same rule, like 1280.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** No hand-written `--layout-spx()` in layout.css, style.css or theme.css outside the preamble: the build writes it from each `--spx(N)`. */
|
|
259
|
+
function checkInternalSpxCalls({ rulesApi, report }) {
|
|
260
|
+
for (const kind of ['layout', 'style', 'theme']) {
|
|
261
|
+
const reportAt = createCssFileReporter(`${kind}.css`, report);
|
|
262
|
+
rulesApi.page.css.files[kind]?.walkDecls((decl) => {
|
|
263
|
+
if (rulesApi.getEnclosingAtRules(decl).some(({ name, params }) => name === 'layer' && params.trim() === PREAMBLE_LAYER))
|
|
264
|
+
return;
|
|
265
|
+
if (hasCssFunction(decl.value, '--layout-spx'))
|
|
266
|
+
reportAt(decl, 'internal_spx_call', '`--layout-spx()` is internal to the build: write `--spx(N)` instead.');
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Whether a CSS value calls this function, outside comments and quoted strings. */
|
|
271
|
+
function hasCssFunction(value, functionName) {
|
|
272
|
+
let quote;
|
|
273
|
+
for (let index = 0; index < value.length; index++) {
|
|
274
|
+
const character = value[index];
|
|
275
|
+
if (quote) {
|
|
276
|
+
if (character === '\\')
|
|
277
|
+
index++;
|
|
278
|
+
else if (character === quote)
|
|
279
|
+
quote = undefined;
|
|
280
|
+
}
|
|
281
|
+
else if (character === '/' && value[index + 1] === '*') {
|
|
282
|
+
const end = value.indexOf('*/', index + 2);
|
|
283
|
+
if (end === -1)
|
|
284
|
+
return false;
|
|
285
|
+
index = end + 1;
|
|
286
|
+
}
|
|
287
|
+
else if (character === "'" || character === '"') {
|
|
288
|
+
quote = character;
|
|
289
|
+
}
|
|
290
|
+
else if (!/[\w-]/.test(value[index - 1] ?? '') && value.startsWith(`${functionName}(`, index)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
224
296
|
/** The container type the scope's own display gives (resolveContainerType): grid, or flex with no flex-wrap other than nowrap (:218). */
|
|
225
297
|
function checkContainerType({ element, values }, reportAt) {
|
|
226
298
|
const display = values.get('display')?.value.trim();
|
|
@@ -252,7 +324,8 @@ const getItems = ({ node, rulesApi }) => rulesApi.getItemElements(node).flatMap(
|
|
|
252
324
|
function checkContainerParent(child, parent, { report, rulesApi }) {
|
|
253
325
|
const byMedia = new Map();
|
|
254
326
|
for (const prop of Object.keys(ITEM_PROPS)) {
|
|
255
|
-
|
|
327
|
+
const layout = (child.css.layout[prop] ?? []).filter((decl) => isOnElement(decl) && !isTransformation(prop, decl.value)); // a zLayer z-index is no item property
|
|
328
|
+
for (const decl of layout)
|
|
256
329
|
byMedia.set(decl.media, [...(byMedia.get(decl.media) ?? []), { prop, decl }]);
|
|
257
330
|
}
|
|
258
331
|
for (const [media, declarations] of byMedia) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.43",
|
|
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": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"falconPackageHash": "
|
|
57
|
+
"falconPackageHash": "4f9d0cd18bc1e203bfcc50088bfb980aad3cc77d7ca7f1bdc933c5ca"
|
|
58
58
|
}
|