@wix/origami-lint 1.34.36 → 1.34.38

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.
@@ -13,10 +13,11 @@ import noAbsoluteSection from "./site-app/no-absolute-section.js";
13
13
  import pageStructure from "./site-app/page-structure.js";
14
14
  import slotAttributes from "./site-app/slot-attributes.js";
15
15
  import slotPlacement from "./site-app/slot-placement.js";
16
+ import spxValues from "./site-app/spx-values.js";
16
17
  import themeFontIds from "./site-app/theme-font-ids.js";
17
18
  import themeFontKnown from "./site-app/theme-font-known.js";
18
19
  import themeRoot from "./site-app/theme-root.js";
19
20
  import noFreeCode from "./site-app/no-free-code.js";
20
21
  // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
21
- export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags, layoutDesignNotes];
22
+ export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags, layoutDesignNotes, spxValues];
22
23
  export { defineRule } from "./site-app/define-rule.js";
@@ -1,4 +1,4 @@
1
- import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, OVERFLOW_TARGET } from "../../rules-api/index.js";
1
+ import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, mergeLayoutScopes, OVERFLOW_TARGET, readLayoutRuleBlocks, SPX_PROPS, SPX_RESOLVER_TYPES } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
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'];
4
4
  // ── the tables (propSpecs.ts) ──
@@ -80,9 +80,6 @@ const CONTAINER_PROPS = {
80
80
  '--spx-stopper-min': both(SIZE, 'root'),
81
81
  '--spx-stopper-max': both(SIZE, 'root'),
82
82
  };
83
- /** The reference its `--spx()` sizes scale by (spxTransformer.ts SPX_PROPS); checked together, :417. */
84
- const SPX_PROPS = ['--spx-ref-width', '--spx-resolver-type'];
85
- const SPX_RESOLVER_TYPES = ['scale', 'fixed'];
86
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. */
87
84
  const isRootProperty = (prop) => prop in ELEMENT_PROPS || prop in ITEM_PROPS || SPX_PROPS.includes(prop) || CONTAINER_PROPS[prop]?.target === 'root';
88
85
  const isLayoutProperty = (prop) => isRootProperty(prop) || prop in CONTAINER_PROPS;
@@ -95,16 +92,17 @@ export default defineRule({
95
92
  JSXElement: checkItemParents,
96
93
  },
97
94
  });
95
+ // ── the file: each rule block, then each element at each breakpoint ──
98
96
  /** The page's layout.css: each rule block, then each element at each breakpoint. */
99
97
  function checkLayoutFile({ rulesApi, report }) {
100
98
  const root = rulesApi.page.css.files.layout;
101
99
  if (!root)
102
100
  return;
103
101
  const reportAt = createCssFileReporter('layout.css', report);
104
- const blocks = readRuleBlocks(root);
102
+ const blocks = readLayoutRuleBlocks(root);
105
103
  for (const block of blocks)
106
104
  checkBlock(block, reportAt);
107
- const scopes = mergeScopes(blocks);
105
+ const scopes = mergeLayoutScopes(blocks);
108
106
  const defaultTypes = new Map();
109
107
  for (const scope of scopes) {
110
108
  const inherited = scope.media === null ? undefined : defaultTypes.get(scope.element);
@@ -279,55 +277,6 @@ const isOnElement = (decl) => decl.selector === '&';
279
277
  /** How the import names the element: its class (the selector's), or its key when it has no class (and so no layout CSS). */
280
278
  const nameOf = (component) => component.classNames[0] ?? component.key;
281
279
  // ── reading the file ──
282
- /** A layout rule's selector: one element's class. Any other selector is the selectors subject's. */
283
- const CLASS_SELECTOR = /^\.(\S+)$/;
284
- /** The rule blocks of the file in order: at the top level, in each `@media`, and in each `@scope`'s `@media`. The preamble layer is skipped. */
285
- function readRuleBlocks(root) {
286
- const blocks = [];
287
- const readRules = (container, media) => {
288
- for (const node of container.nodes ?? []) {
289
- if (node.type === 'rule') {
290
- const element = CLASS_SELECTOR.exec(node.selector.trim())?.[1];
291
- if (element)
292
- blocks.push(readRuleBlock(element, media, node));
293
- }
294
- else if (node.type === 'atrule' && media === null) {
295
- if (node.name === 'media')
296
- readRules(node, node.params.trim());
297
- else if (node.name === 'scope')
298
- readRules(node, null);
299
- }
300
- }
301
- };
302
- readRules(root, null);
303
- return blocks;
304
- }
305
- function readRuleBlock(element, media, rule) {
306
- const block = { element, media, rule, declarations: [], targets: new Map() };
307
- for (const node of rule.nodes) {
308
- if (node.type === 'decl')
309
- block.declarations.push(node);
310
- else if (node.type === 'atrule' && node.name === 'origami-target' && node.nodes) {
311
- const target = node.params.trim();
312
- block.targets.set(target, [...(block.targets.get(target) ?? []), ...node.nodes.filter((child) => child.type === 'decl')]);
313
- }
314
- }
315
- return block;
316
- }
317
- /** The blocks merged per element and breakpoint, the default scopes first (a breakpoint inherits the default's container type); a target's declarations join the rule's. */
318
- function mergeScopes(blocks) {
319
- const scopes = new Map();
320
- for (const block of blocks) {
321
- const key = `${block.media ?? ''}\n${block.element}`;
322
- const scope = scopes.get(key) ?? { element: block.element, media: block.media, values: new Map(), rulesWithTargets: [] };
323
- scopes.set(key, scope);
324
- for (const decl of [...block.declarations, ...[...block.targets.values()].flat()])
325
- scope.values.set(decl.prop, decl);
326
- if (block.targets.size)
327
- scope.rulesWithTargets.push(block.rule);
328
- }
329
- return [...scopes.values()].sort((a, b) => Number(a.media !== null) - Number(b.media !== null));
330
- }
331
280
  // ── reading values (valueDeserializers.ts) ──
332
281
  /** A value reads as the import reads it: a site layout variable's token, `var(--name, <fallback>)`, by its fallback (layoutVariables.ts literalOf); `--spx(12)` as `12spx`. */
333
282
  function isReadable(spec, value) {
@@ -0,0 +1,112 @@
1
+ import { createCssFileReporter, mergeLayoutScopes, PREAMBLE_LAYER, readLayoutRuleBlocks, SPX_PROPS, SPX_RESOLVER_TYPES } from "../../rules-api/index.js";
2
+ import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['invalid_spx_resolver_type', 'invalid_spx_ref_width', 'non_literal_spx_call'];
4
+ /** A literal `--spx()` call as the build compiles it (spx/calls.js CALL): a number, spaces around it. */
5
+ const SPX_LITERAL_CALL = /--spx\(\s*-?(?:\d+(?:\.\d*)?|\.\d+)\s*\)/g;
6
+ export default defineRule({
7
+ name: 'spx-values',
8
+ checks: CHECKS,
9
+ editors: 'all',
10
+ callbacks: {
11
+ Program: ({ rulesApi, report }) => checkCssFiles(rulesApi, report),
12
+ },
13
+ });
14
+ // ── the steps ──
15
+ /** The page's layout.css, style.css and theme.css, each checked on its own. */
16
+ function checkCssFiles(rulesApi, report) {
17
+ for (const kind of ['layout', 'style', 'theme']) {
18
+ const root = rulesApi.page.css.files[kind];
19
+ if (root)
20
+ checkSpxValues(root, kind, rulesApi, report);
21
+ }
22
+ }
23
+ /** Each declaration of the file outside the `@function --spx(…)`, in order, with the checks no other rule makes on it. */
24
+ function checkSpxValues(root, kind, rulesApi, report) {
25
+ const reportInFile = createCssFileReporter(`${kind}.css`, report);
26
+ const judgedElsewhere = getJudgedElsewhere(root, kind, rulesApi);
27
+ root.walkDecls((declaration) => {
28
+ if (isInSpxFunction(declaration, rulesApi))
29
+ return;
30
+ const judged = judgedElsewhere.has(declaration);
31
+ // css-layout-properties asks the last value only for a number (CONV-layoutParser.ts:417); the --spx() calls are checked on every
32
+ // declaration, as it takes some properties' values as they are (position, align-items)
33
+ if (judged && kind === 'layout')
34
+ checkRefWidth(declaration, 'number', reportInFile);
35
+ if (!judged)
36
+ checkResolverType(declaration, reportInFile);
37
+ if (!judged)
38
+ checkRefWidth(declaration, 'any', reportInFile);
39
+ checkSpxCalls(declaration, reportInFile);
40
+ });
41
+ }
42
+ /** The declarations another rule judges, as the import reads them: in layout.css, css-layout-properties' (the last of each property per
43
+ * element and breakpoint, its rules merged as it reads them: readRuleBlocks, mergeScopes; not an spx context with an empty value, which it
44
+ * leaves to its empty-value row); in theme.css, theme-root's (the last spx property of a `:root` rule that holds only the spx context, with
45
+ * no preset, outside a `@mixin`: checkThemeRoot). None in style.css. */
46
+ function getJudgedElsewhere(root, kind, rulesApi) {
47
+ if (kind === 'layout')
48
+ return getLastLayoutValues(root);
49
+ const judged = new Set();
50
+ if (kind === 'theme')
51
+ root.walkRules((rule) => {
52
+ if (rule.selector.trim() !== ':root' || isInPreamble(rule, rulesApi) || rulesApi.getEnclosingAtRules(rule).some(({ name }) => name === 'mixin'))
53
+ return;
54
+ const declarations = rule.nodes.filter((node) => node.type === 'decl');
55
+ if (!declarations.every(({ prop }) => SPX_PROPS.includes(prop)))
56
+ return;
57
+ for (const prop of SPX_PROPS) {
58
+ const last = declarations.filter((declaration) => declaration.prop === prop).at(-1);
59
+ if (last)
60
+ judged.add(last);
61
+ }
62
+ });
63
+ return judged;
64
+ }
65
+ /** css-layout-properties' view of layout.css, read with the same shared merge (readLayoutRuleBlocks, mergeLayoutScopes): the last
66
+ * declaration of each property per element and breakpoint. */
67
+ function getLastLayoutValues(root) {
68
+ return new Set(mergeLayoutScopes(readLayoutRuleBlocks(root)).flatMap(({ values }) => {
69
+ const hasEmptySpx = SPX_PROPS.some((prop) => values.get(prop)?.value.trim() === '');
70
+ return [...values.values()].filter(({ prop }) => !hasEmptySpx || !SPX_PROPS.includes(prop) || values.get(prop).value.trim() === '');
71
+ }));
72
+ }
73
+ /** Reports a `--spx-resolver-type` that isn't scale or fixed (OUR-028). */
74
+ function checkResolverType(declaration, reportInFile) {
75
+ if (declaration.prop !== '--spx-resolver-type' || SPX_RESOLVER_TYPES.includes(declaration.value.trim()))
76
+ return;
77
+ reportInFile(declaration, 'invalid_spx_resolver_type', `${describeDeclaration(declaration)}: write scale or fixed.`);
78
+ }
79
+ /** Reports a `--spx-ref-width` that isn't a number above 0 (OUR-029); `'number'`: only a number at or below 0, as css-layout-properties
80
+ * reports one that isn't a number (CONV-layoutParser.ts:417) or is empty (:430). */
81
+ function checkRefWidth(declaration, values, reportInFile) {
82
+ if (declaration.prop !== '--spx-ref-width')
83
+ return;
84
+ const value = declaration.value.trim();
85
+ const isNumber = value !== '' && Number.isFinite(Number(value));
86
+ if (isNumber && Number(value) > 0)
87
+ return;
88
+ if (values === 'number' && !isNumber)
89
+ return;
90
+ reportInFile(declaration, 'invalid_spx_ref_width', `${describeDeclaration(declaration)}: write the reference width as a number above 0, like 1280.`);
91
+ }
92
+ /** Reports a `--spx()` call whose argument isn't a number as written (OUR-030). */
93
+ function checkSpxCalls(declaration, reportInFile) {
94
+ if (!declaration.value.replace(SPX_LITERAL_CALL, '').includes('--spx('))
95
+ return;
96
+ reportInFile(declaration, 'non_literal_spx_call', `${describeDeclaration(declaration)}: --spx() takes a number as written, like --spx(16), not a var or an expression.`);
97
+ }
98
+ // ── reading the CSS ──
99
+ /** Whether the rule is in the `@layer spx-preamble { … }` block the export writes, which theme-root skips. */
100
+ const isInPreamble = (node, rulesApi) => rulesApi.getEnclosingAtRules(node).some(({ name, params }) => name === 'layer' && params.trim() === PREAMBLE_LAYER);
101
+ /** Whether the declaration is in the `@function --spx(…)` the export's preamble defines, which the build removes (spxPlugin) before it
102
+ * walks the declarations. */
103
+ const isInSpxFunction = (declaration, rulesApi) => rulesApi.getEnclosingAtRules(declaration).some(({ name, params }) => name === 'function' && params.trimStart().startsWith('--spx('));
104
+ // ── reporting ──
105
+ /** '`--spx-ref-width: 0` in `.comp-a`': the rule it's in, through an `@origami-target` block. */
106
+ function describeDeclaration(declaration) {
107
+ let rule = declaration.parent;
108
+ while (rule && rule.type !== 'rule' && rule.type !== 'root')
109
+ rule = rule.parent;
110
+ const where = rule?.type === 'rule' ? ` in \`${rule.selector.trim()}\`` : '';
111
+ return `\`${declaration.prop}: ${declaration.value.trim()}\`${where}`;
112
+ }
@@ -4,4 +4,4 @@ export { createCssFileReporter, describeAtRule, reportCssProblem } from "./repor
4
4
  export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isRecord, isReservedAttribute, isStructure, isReadByImport, ownerOf, } from "../shared/utils.js";
5
5
  export { BUILDERS, CATALOG_FONTS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, INTERACTIONS_PACKAGES, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
6
6
  export { isElementDeclaration } from "./utils.js";
7
- export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getLayoutLiteral, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
7
+ export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getLayoutLiteral, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, mergeLayoutScopes, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readLayoutRuleBlocks, readSegment, SPX_PROPS, SPX_RESOLVER_TYPES } from "../shared/css.js";
@@ -186,3 +186,56 @@ export function getSegmentClass(segment) {
186
186
  const { base, unread } = readSegment(segment);
187
187
  return base === undefined || unread ? null : base;
188
188
  }
189
+ // ── layout.css, as the import reads it (layoutParser.ts) ──
190
+ /** The spx context: the reference an element's or the theme's `--spx()` sizes scale by (spxTransformer.ts SPX_PROPS), and the resolvers it takes. */
191
+ export const SPX_PROPS = ['--spx-ref-width', '--spx-resolver-type'];
192
+ export const SPX_RESOLVER_TYPES = ['scale', 'fixed'];
193
+ /** A layout rule's selector, as the import reads one: one element's class. */
194
+ const LAYOUT_CLASS_SELECTOR = /^\.(\S+)$/;
195
+ /** layout.css's rule blocks in order, as the import reads them: at the top level, in each `@media`, and in each `@scope`'s `@media`. The preamble layer is skipped. */
196
+ export function readLayoutRuleBlocks(root) {
197
+ const blocks = [];
198
+ const readRules = (container, media) => {
199
+ for (const node of container.nodes ?? []) {
200
+ if (node.type === 'rule') {
201
+ const element = LAYOUT_CLASS_SELECTOR.exec(node.selector.trim())?.[1];
202
+ if (element)
203
+ blocks.push(readLayoutRuleBlock(element, media, node));
204
+ }
205
+ else if (node.type === 'atrule' && media === null) {
206
+ if (node.name === 'media')
207
+ readRules(node, node.params.trim());
208
+ else if (node.name === 'scope')
209
+ readRules(node, null);
210
+ }
211
+ }
212
+ };
213
+ readRules(root, null);
214
+ return blocks;
215
+ }
216
+ function readLayoutRuleBlock(element, media, rule) {
217
+ const block = { element, media, rule, declarations: [], targets: new Map() };
218
+ for (const node of rule.nodes) {
219
+ if (node.type === 'decl')
220
+ block.declarations.push(node);
221
+ else if (node.type === 'atrule' && node.name === 'origami-target' && node.nodes) {
222
+ const target = node.params.trim();
223
+ block.targets.set(target, [...(block.targets.get(target) ?? []), ...node.nodes.filter((child) => child.type === 'decl')]);
224
+ }
225
+ }
226
+ return block;
227
+ }
228
+ /** The blocks merged per element and breakpoint, the default scopes first (a breakpoint inherits the default's container type); a target's declarations join the rule's. */
229
+ export function mergeLayoutScopes(blocks) {
230
+ const scopes = new Map();
231
+ for (const block of blocks) {
232
+ const key = `${block.media ?? ''}\n${block.element}`;
233
+ const scope = scopes.get(key) ?? { element: block.element, media: block.media, values: new Map(), rulesWithTargets: [] };
234
+ scopes.set(key, scope);
235
+ for (const decl of [...block.declarations, ...[...block.targets.values()].flat()])
236
+ scope.values.set(decl.prop, decl);
237
+ if (block.targets.size)
238
+ scope.rulesWithTargets.push(block.rule);
239
+ }
240
+ return [...scopes.values()].sort((a, b) => Number(a.media !== null) - Number(b.media !== null));
241
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.36",
3
+ "version": "1.34.38",
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": {
@@ -26,7 +26,7 @@
26
26
  "eslint": "^10.12.0",
27
27
  "postcss": "^8.5.3",
28
28
  "typescript": "^5.8.3 || ~6.0.3",
29
- "@wix/origami-bi": "0.14.0"
29
+ "@wix/origami-bi": "0.15.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.0.0",
@@ -54,5 +54,5 @@
54
54
  ]
55
55
  }
56
56
  },
57
- "falconPackageHash": "5ed1f78b0c887661ab15c42ac1bb33758bdc0b4f1afd67d94b64c989"
57
+ "falconPackageHash": "c431ea5cde0e620296c9af3b1ba90bbaaf1d6d4167b10bc3d93465b0"
58
58
  }