@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({ rulesApi, report }) {
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
- const scopes = mergeLayoutScopes(blocks);
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
- const fix = isLayoutProperty(decl.prop) ? `move it to the top of ${element}'s rule` : `move it to ${element}'s rule in style.css`;
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
- for (const decl of (child.css.layout[prop] ?? []).filter(isOnElement))
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.41",
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": "cd79205599adab600f2e5287c59ffd8f71d60a92c28f544cf5fe719f"
57
+ "falconPackageHash": "4f9d0cd18bc1e203bfcc50088bfb980aad3cc77d7ca7f1bdc933c5ca"
58
58
  }