@wix/origami-lint 1.34.25 → 1.34.27

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 { BREAKPOINT_AT_RULES, createCssFileReporter, describeAtRule, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isSafeCssIdentifier } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
- const CHECKS = ['invalid_mixin_id', 'invalid_mixin_result', 'missing_parent_selector', 'bare_parent_selector', 'nested_style_rule', 'apply_at_result_root', 'nested_mixin', 'invalid_result_at_rule', 'invalid_parent_selector', 'conditional_global_style_link', 'invalid_linked_style_id', 'missing_linked_style', 'duplicate_global_style_link', 'link_with_values', 'missing_global_style_link'];
3
+ const CHECKS = ['invalid_mixin_id', 'invalid_mixin_result', 'missing_parent_selector', 'bare_parent_selector', 'nested_style_rule', 'apply_at_result_root', 'nested_mixin', 'invalid_result_at_rule', 'invalid_parent_selector', 'conditional_global_style_link', 'invalid_linked_style_id', 'missing_linked_style', 'duplicate_global_style_link', 'link_with_values', 'missing_global_style_link', 'mixin_link_cycle'];
4
4
  export default defineRule({
5
5
  name: 'global-styles',
6
6
  checks: CHECKS,
@@ -22,9 +22,12 @@ function checkMixinsAndLinks(root, file, report) {
22
22
  const mixins = getCssChildren(root).filter((node) => node.type === 'atrule' && node.name === 'mixin');
23
23
  const rules = mixins.flatMap((mixin) => checkMixin(mixin, reportInFile));
24
24
  const declaredIds = new Set(mixins.flatMap((mixin) => getMixinId(mixin) ?? []));
25
- const linkedParts = checkLinks(rules.filter((rule) => rule.apply), declaredIds, reportInFile);
25
+ const { linkedParts, validLinks } = checkLinks(rules.filter((rule) => rule.apply), declaredIds, reportInFile);
26
26
  for (const group of groupBySelector(rules))
27
27
  checkLinkOrValues(group, linkedParts, reportInFile);
28
+ // the build keeps the last `@mixin` of an id (mixins.js bodies.set): its links are the ones it follows
29
+ const lastMixinById = new Map(mixins.flatMap((mixin) => { const id = getMixinId(mixin); return id === null ? [] : [[id, mixin]]; }));
30
+ checkLinkCycles(validLinks.filter((link) => getEnclosingMixin(link.node) === lastMixinById.get(link.mixin)), reportInFile);
28
31
  }
29
32
  /** `@mixin --id { @result { … } }` (CONV-astParser.ts:287, :293), then its `@result`; returns its `&` rules that pass. */
30
33
  function checkMixin(mixin, reportInFile) {
@@ -107,6 +110,7 @@ function checkSelector(rule, reportInFile) {
107
110
  function checkLinks(links, declaredIds, reportInFile) {
108
111
  const linkedParts = new Set();
109
112
  const validLinkParts = new Set();
113
+ const validLinks = [];
110
114
  for (const link of links) {
111
115
  const child = link.apply.params.trim();
112
116
  const linkDescription = `\`@mixin --${link.mixin}\`'s link \`@apply ${child}\``;
@@ -135,8 +139,43 @@ function checkLinks(links, declaredIds, reportInFile) {
135
139
  continue;
136
140
  }
137
141
  validLinkParts.add(part);
142
+ validLinks.push(link);
138
143
  }
139
- return linkedParts;
144
+ return { linkedParts, validLinks };
145
+ }
146
+ /** Reports each link that closes a cycle (OUR-007): a walk from each mixin in the file's order, depth first, visiting each mixin once; a link
147
+ * to a mixin still on the walk's path leads back to it. Every cycle holds such a link, so removing the reported ones breaks them all. */
148
+ function checkLinkCycles(validLinks, reportInFile) {
149
+ const linksByMixin = new Map();
150
+ for (const link of validLinks)
151
+ linksByMixin.set(link.mixin, [...(linksByMixin.get(link.mixin) ?? []), link]);
152
+ const visited = new Set();
153
+ for (const start of linksByMixin.keys()) {
154
+ if (visited.has(start))
155
+ continue;
156
+ visited.add(start);
157
+ // the walk's path: each mixin on it, and how many of its links were followed
158
+ const path = [{ mixin: start, next: 0 }];
159
+ while (path.length > 0) {
160
+ const step = path.at(-1);
161
+ const link = linksByMixin.get(step.mixin)?.[step.next++];
162
+ if (!link) {
163
+ path.pop();
164
+ continue;
165
+ }
166
+ const child = getDashedId(link.apply.params);
167
+ const onPath = path.findIndex(({ mixin }) => mixin === child);
168
+ if (onPath !== -1)
169
+ reportCycle(link, child, [...path.slice(onPath).map(({ mixin }) => mixin), child], reportInFile);
170
+ else if (!visited.has(child)) {
171
+ visited.add(child);
172
+ path.push({ mixin: child, next: 0 });
173
+ }
174
+ }
175
+ }
176
+ }
177
+ function reportCycle(link, child, chain, reportInFile) {
178
+ reportInFile(link.apply, 'mixin_link_cycle', `\`@mixin --${link.mixin}\` links \`--${child}\`, which leads back to it: ${chain.map((id) => `--${id}`).join(' → ')}. A global style can't contain itself: remove this link, or link a style that doesn't lead back to \`--${child}\` (tracker OUR-007).`);
140
179
  }
141
180
  /** One selector of a mixin (across its breakpoints): a link alone, or values; values on an inner part where a link names it (CONV-styleParser.ts:803, :698). */
142
181
  function checkLinkOrValues(group, linkedParts, reportInFile) {
@@ -152,6 +191,13 @@ function checkLinkOrValues(group, linkedParts, reportInFile) {
152
191
  reportInFile(first.node, 'missing_global_style_link', `\`@mixin --${first.mixin}\` styles \`${part}\` but links no global style there: add \`& ${part} { @apply --<child> }\` and move the values to that child's @mixin (tracker CONV-styleParser.ts:698).`);
153
192
  }
154
193
  // ── reading a mixin ──
194
+ /** The `@mixin` a rule of its `@result` sits in. */
195
+ function getEnclosingMixin(rule) {
196
+ for (let node = rule.parent; node; node = node.parent)
197
+ if (node.type === 'atrule' && node.name === 'mixin')
198
+ return node;
199
+ return undefined;
200
+ }
155
201
  /** The mixin's one `@result` with no params and a block, beside nothing; undefined otherwise. */
156
202
  function getResult(mixin) {
157
203
  if (mixin.nodes === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.25",
3
+ "version": "1.34.27",
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.12.0"
28
+ "@wix/origami-bi": "0.13.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": "e4031adeef0d38fd736d432532a85f648531c1c89cff31aa089a0fad"
55
+ "falconPackageHash": "b4ea23b3bf64c300ff406d11fcb377f370fb48f7fdf2e92f3a66f7c9"
56
56
  }