@bamboocss/eslint-plugin 1.37.13 → 1.39.0

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.
Files changed (3) hide show
  1. package/dist/index.cjs +118 -18
  2. package/dist/index.mjs +118 -18
  3. package/package.json +8 -8
package/dist/index.cjs CHANGED
@@ -8,7 +8,7 @@ require("@typescript-eslint/utils/ts-eslint");
8
8
  let _bamboocss_shared = require("@bamboocss/shared");
9
9
  //#region package.json
10
10
  var name = "@bamboocss/eslint-plugin";
11
- var version = "1.37.13";
11
+ var version = "1.39.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/gajus/bamboocss/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -302,8 +302,8 @@ function isRecipeVariant(node, context) {
302
302
  }
303
303
  //#endregion
304
304
  //#region src/rules/file-not-included.ts
305
- const RULE_NAME$21 = "file-not-included";
306
- const rule$21 = createRule({
305
+ const RULE_NAME$22 = "file-not-included";
306
+ const rule$22 = createRule({
307
307
  create(context) {
308
308
  if (isValidFile(context)) return {};
309
309
  let hasReported = false;
@@ -324,11 +324,11 @@ const rule$21 = createRule({
324
324
  schema: [],
325
325
  type: "problem"
326
326
  },
327
- name: RULE_NAME$21
327
+ name: RULE_NAME$22
328
328
  });
329
329
  //#endregion
330
330
  //#region src/rules/no-config-function-in-source.ts
331
- const RULE_NAME$20 = "no-config-function-in-source";
331
+ const RULE_NAME$21 = "no-config-function-in-source";
332
332
  const CONFIG_FUNCTIONS = new Set([
333
333
  "defineConfig",
334
334
  "defineGlobalStyles",
@@ -343,7 +343,7 @@ const CONFIG_FUNCTIONS = new Set([
343
343
  "defineTokens",
344
344
  "defineUtility"
345
345
  ]);
346
- const rule$20 = createRule({
346
+ const rule$21 = createRule({
347
347
  create(context) {
348
348
  if (!hasPkgImport(context)) return {};
349
349
  if (!isValidFile(context)) return {};
@@ -383,12 +383,12 @@ const rule$20 = createRule({
383
383
  schema: [],
384
384
  type: "problem"
385
385
  },
386
- name: RULE_NAME$20
386
+ name: RULE_NAME$21
387
387
  });
388
388
  //#endregion
389
389
  //#region src/rules/no-debug.ts
390
- const RULE_NAME$19 = "no-debug";
391
- const rule$19 = createRule({
390
+ const RULE_NAME$20 = "no-debug";
391
+ const rule$20 = createRule({
392
392
  create(context) {
393
393
  const processedNodes = /* @__PURE__ */ new WeakSet();
394
394
  const checkObjectForDebug = (object) => {
@@ -457,12 +457,12 @@ const rule$19 = createRule({
457
457
  schema: [],
458
458
  type: "problem"
459
459
  },
460
- name: RULE_NAME$19
460
+ name: RULE_NAME$20
461
461
  });
462
462
  //#endregion
463
463
  //#region src/rules/no-deprecated-tokens.ts
464
- const RULE_NAME$18 = "no-deprecated-tokens";
465
- const rule$18 = createRule({
464
+ const RULE_NAME$19 = "no-deprecated-tokens";
465
+ const rule$19 = createRule({
466
466
  create(context) {
467
467
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
468
468
  const sendReport = (property, node, value) => {
@@ -516,6 +516,100 @@ const rule$18 = createRule({
516
516
  schema: [],
517
517
  type: "problem"
518
518
  },
519
+ name: RULE_NAME$19
520
+ });
521
+ //#endregion
522
+ //#region src/rules/no-descendant-selectors.ts
523
+ const RULE_NAME$18 = "no-descendant-selectors";
524
+ /**
525
+ * Split a selector on combinators that are not inside brackets, parentheses or quotes.
526
+ *
527
+ * `:is(p, li)` and `[data-label="a b"]` both contain characters that separate compounds
528
+ * elsewhere, and neither is a combinator. Nesting is shallow by construction — a selector
529
+ * cannot nest brackets inside quotes inside parentheses in a way this misreads — so a depth
530
+ * counter is enough, and it keeps the rule free of a selector parser.
531
+ */
532
+ const splitTopLevel = (selector, separators) => {
533
+ const parts = [];
534
+ let depth = 0;
535
+ let quote;
536
+ let escaped = false;
537
+ let current = "";
538
+ for (const character of selector) {
539
+ if (escaped) {
540
+ escaped = false;
541
+ current += character;
542
+ continue;
543
+ }
544
+ if (character === "\\") {
545
+ escaped = true;
546
+ current += character;
547
+ continue;
548
+ }
549
+ if (quote) {
550
+ current += character;
551
+ if (character === quote) quote = void 0;
552
+ continue;
553
+ }
554
+ if (character === "\"" || character === "'") {
555
+ quote = character;
556
+ current += character;
557
+ continue;
558
+ }
559
+ if (character === "(" || character === "[") depth++;
560
+ if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
561
+ if (depth === 0 && separators.includes(character)) {
562
+ if (current.trim()) parts.push(current.trim());
563
+ current = "";
564
+ continue;
565
+ }
566
+ current += character;
567
+ }
568
+ if (current.trim()) parts.push(current.trim());
569
+ return parts;
570
+ };
571
+ /**
572
+ * Does this selector style something other than the element the class is on?
573
+ *
574
+ * Decided by the **subject** — the last compound — rather than by the presence of a
575
+ * combinator. `'.dark &'` and `'.group:hover &'` both contain one and both still style `&`
576
+ * itself, which is how conditions are written and not what this is about. `'& p'`,
577
+ * `'& > .card'` and `'& :is(p, li) a'` end somewhere else, and that is the case that outranks
578
+ * whatever is applied to that element directly.
579
+ */
580
+ const reachesAnotherElement = (selector) => splitTopLevel(selector, ",").some((part) => {
581
+ const compounds = splitTopLevel(part, " \n>+~");
582
+ const subject = compounds.at(-1);
583
+ return compounds.length > 1 && subject !== void 0 && !subject.includes("&");
584
+ });
585
+ const selectorOf = (node) => {
586
+ if (isLiteral(node) && typeof node.value === "string") return node.value;
587
+ if (isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0]?.value.raw;
588
+ };
589
+ const rule$18 = createRule({
590
+ create(context) {
591
+ return { "Property[key.type!=/Identifier/][value.type=\"ObjectExpression\"]"(node) {
592
+ const selector = selectorOf(node.key);
593
+ if (!selector?.includes("&")) return;
594
+ if (!reachesAnotherElement(selector)) return;
595
+ if (!isInBambooFunction(node, context) && !isInJSXProp(node, context)) return;
596
+ context.report({
597
+ messageId: "descendantSelector",
598
+ node: node.key
599
+ });
600
+ } };
601
+ },
602
+ defaultOptions: [],
603
+ meta: {
604
+ docs: { description: "Disallow nested selectors that style a different element than the class is applied to." },
605
+ messages: { descendantSelector: [
606
+ "This styles another element, and outranks anything applied to that element directly.",
607
+ "Cascade layers do not separate them — two `css()` calls are always in the same layer, where specificity decides, and a nested selector is more specific than a class.",
608
+ "Style that element itself. Narrowing the selector (`& > p`) bounds what it can reach, which is worth doing, but a descendant rule still outranks a class on its target — so this rule reports it either way."
609
+ ].join(" ") },
610
+ schema: [],
611
+ type: "problem"
612
+ },
519
613
  name: RULE_NAME$18
520
614
  });
521
615
  //#endregion
@@ -1365,10 +1459,15 @@ const RULE_NAME$8 = "no-unlayered-override";
1365
1459
  * The call whose result is a class string in the `utilities` layer — the same layer a
1366
1460
  * consumer's `css()` is in, so neither can win over the other by cascade.
1367
1461
  *
1368
- * `cva` and `sva` are deliberately absent. Their output is named semantically and emitted
1369
- * into `recipes`, whether they are declared inline or in `theme.recipes`, so a consumer's
1370
- * `css()` beats them by layer in every build. That is the fix this rule points at, and it
1371
- * would be incoherent to report it.
1462
+ * `cva` and `sva` are deliberately absent. On the extraction path their output is named
1463
+ * semantically and emitted into `recipes`, whether they are declared inline or in
1464
+ * `theme.recipes`, so a consumer's `css()` beats them by layer. That is one of the fixes this
1465
+ * rule points at, and it would be incoherent to report it.
1466
+ *
1467
+ * Under the Vite compiler there is no `recipes` layer — selections are resolved to the same
1468
+ * global atoms `css()` uses, in `utilities` — so that fix does not apply there and the style
1469
+ * object one does. The rule cannot tell which build it is linting for, so the message names
1470
+ * both.
1372
1471
  */
1373
1472
  const STYLING_CALLS = new Set(["css"]);
1374
1473
  const calleeName = (node) => {
@@ -1410,7 +1509,7 @@ const rule$8 = createRule({
1410
1509
  messages: { unlayeredOverride: [
1411
1510
  "`cx` joins class names, it does not resolve conflicts between them.",
1412
1511
  "These styles and the ones being joined are both in the `utilities` layer, so which applies is decided by stylesheet order rather than by the caller.",
1413
- "Declare the component styles with `cva` so they land in the `recipes` layer, or accept a style object and merge it with `css(base, props.css)`."
1512
+ "Accept a style object and merge it with `css(base, props.css)`, which resolves per property in every build — or, on the extraction path only, declare the component styles with `cva` so they land in the `recipes` layer."
1414
1513
  ].join(" ") },
1415
1514
  schema: [],
1416
1515
  type: "problem"
@@ -2487,6 +2586,7 @@ const rule = createRule({
2487
2586
  //#endregion
2488
2587
  //#region src/rules/index.ts
2489
2588
  const rules = {
2589
+ [RULE_NAME$22]: rule$22,
2490
2590
  [RULE_NAME$21]: rule$21,
2491
2591
  [RULE_NAME$20]: rule$20,
2492
2592
  [RULE_NAME$19]: rule$19,
@@ -2513,8 +2613,8 @@ const rules = {
2513
2613
  //#endregion
2514
2614
  //#region src/configs/all.ts
2515
2615
  const errorRules = [
2616
+ RULE_NAME$22,
2516
2617
  RULE_NAME$21,
2517
- RULE_NAME$20,
2518
2618
  RULE_NAME$12
2519
2619
  ];
2520
2620
  //#endregion
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import "@typescript-eslint/utils/ts-eslint";
8
8
  import { getArbitraryValue, parseFallbackValue } from "@bamboocss/shared";
9
9
  //#region package.json
10
10
  var name = "@bamboocss/eslint-plugin";
11
- var version = "1.37.13";
11
+ var version = "1.39.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/gajus/bamboocss/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -302,8 +302,8 @@ function isRecipeVariant(node, context) {
302
302
  }
303
303
  //#endregion
304
304
  //#region src/rules/file-not-included.ts
305
- const RULE_NAME$21 = "file-not-included";
306
- const rule$21 = createRule({
305
+ const RULE_NAME$22 = "file-not-included";
306
+ const rule$22 = createRule({
307
307
  create(context) {
308
308
  if (isValidFile(context)) return {};
309
309
  let hasReported = false;
@@ -324,11 +324,11 @@ const rule$21 = createRule({
324
324
  schema: [],
325
325
  type: "problem"
326
326
  },
327
- name: RULE_NAME$21
327
+ name: RULE_NAME$22
328
328
  });
329
329
  //#endregion
330
330
  //#region src/rules/no-config-function-in-source.ts
331
- const RULE_NAME$20 = "no-config-function-in-source";
331
+ const RULE_NAME$21 = "no-config-function-in-source";
332
332
  const CONFIG_FUNCTIONS = new Set([
333
333
  "defineConfig",
334
334
  "defineGlobalStyles",
@@ -343,7 +343,7 @@ const CONFIG_FUNCTIONS = new Set([
343
343
  "defineTokens",
344
344
  "defineUtility"
345
345
  ]);
346
- const rule$20 = createRule({
346
+ const rule$21 = createRule({
347
347
  create(context) {
348
348
  if (!hasPkgImport(context)) return {};
349
349
  if (!isValidFile(context)) return {};
@@ -383,12 +383,12 @@ const rule$20 = createRule({
383
383
  schema: [],
384
384
  type: "problem"
385
385
  },
386
- name: RULE_NAME$20
386
+ name: RULE_NAME$21
387
387
  });
388
388
  //#endregion
389
389
  //#region src/rules/no-debug.ts
390
- const RULE_NAME$19 = "no-debug";
391
- const rule$19 = createRule({
390
+ const RULE_NAME$20 = "no-debug";
391
+ const rule$20 = createRule({
392
392
  create(context) {
393
393
  const processedNodes = /* @__PURE__ */ new WeakSet();
394
394
  const checkObjectForDebug = (object) => {
@@ -457,12 +457,12 @@ const rule$19 = createRule({
457
457
  schema: [],
458
458
  type: "problem"
459
459
  },
460
- name: RULE_NAME$19
460
+ name: RULE_NAME$20
461
461
  });
462
462
  //#endregion
463
463
  //#region src/rules/no-deprecated-tokens.ts
464
- const RULE_NAME$18 = "no-deprecated-tokens";
465
- const rule$18 = createRule({
464
+ const RULE_NAME$19 = "no-deprecated-tokens";
465
+ const rule$19 = createRule({
466
466
  create(context) {
467
467
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
468
468
  const sendReport = (property, node, value) => {
@@ -516,6 +516,100 @@ const rule$18 = createRule({
516
516
  schema: [],
517
517
  type: "problem"
518
518
  },
519
+ name: RULE_NAME$19
520
+ });
521
+ //#endregion
522
+ //#region src/rules/no-descendant-selectors.ts
523
+ const RULE_NAME$18 = "no-descendant-selectors";
524
+ /**
525
+ * Split a selector on combinators that are not inside brackets, parentheses or quotes.
526
+ *
527
+ * `:is(p, li)` and `[data-label="a b"]` both contain characters that separate compounds
528
+ * elsewhere, and neither is a combinator. Nesting is shallow by construction — a selector
529
+ * cannot nest brackets inside quotes inside parentheses in a way this misreads — so a depth
530
+ * counter is enough, and it keeps the rule free of a selector parser.
531
+ */
532
+ const splitTopLevel = (selector, separators) => {
533
+ const parts = [];
534
+ let depth = 0;
535
+ let quote;
536
+ let escaped = false;
537
+ let current = "";
538
+ for (const character of selector) {
539
+ if (escaped) {
540
+ escaped = false;
541
+ current += character;
542
+ continue;
543
+ }
544
+ if (character === "\\") {
545
+ escaped = true;
546
+ current += character;
547
+ continue;
548
+ }
549
+ if (quote) {
550
+ current += character;
551
+ if (character === quote) quote = void 0;
552
+ continue;
553
+ }
554
+ if (character === "\"" || character === "'") {
555
+ quote = character;
556
+ current += character;
557
+ continue;
558
+ }
559
+ if (character === "(" || character === "[") depth++;
560
+ if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
561
+ if (depth === 0 && separators.includes(character)) {
562
+ if (current.trim()) parts.push(current.trim());
563
+ current = "";
564
+ continue;
565
+ }
566
+ current += character;
567
+ }
568
+ if (current.trim()) parts.push(current.trim());
569
+ return parts;
570
+ };
571
+ /**
572
+ * Does this selector style something other than the element the class is on?
573
+ *
574
+ * Decided by the **subject** — the last compound — rather than by the presence of a
575
+ * combinator. `'.dark &'` and `'.group:hover &'` both contain one and both still style `&`
576
+ * itself, which is how conditions are written and not what this is about. `'& p'`,
577
+ * `'& > .card'` and `'& :is(p, li) a'` end somewhere else, and that is the case that outranks
578
+ * whatever is applied to that element directly.
579
+ */
580
+ const reachesAnotherElement = (selector) => splitTopLevel(selector, ",").some((part) => {
581
+ const compounds = splitTopLevel(part, " \n>+~");
582
+ const subject = compounds.at(-1);
583
+ return compounds.length > 1 && subject !== void 0 && !subject.includes("&");
584
+ });
585
+ const selectorOf = (node) => {
586
+ if (isLiteral(node) && typeof node.value === "string") return node.value;
587
+ if (isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0]?.value.raw;
588
+ };
589
+ const rule$18 = createRule({
590
+ create(context) {
591
+ return { "Property[key.type!=/Identifier/][value.type=\"ObjectExpression\"]"(node) {
592
+ const selector = selectorOf(node.key);
593
+ if (!selector?.includes("&")) return;
594
+ if (!reachesAnotherElement(selector)) return;
595
+ if (!isInBambooFunction(node, context) && !isInJSXProp(node, context)) return;
596
+ context.report({
597
+ messageId: "descendantSelector",
598
+ node: node.key
599
+ });
600
+ } };
601
+ },
602
+ defaultOptions: [],
603
+ meta: {
604
+ docs: { description: "Disallow nested selectors that style a different element than the class is applied to." },
605
+ messages: { descendantSelector: [
606
+ "This styles another element, and outranks anything applied to that element directly.",
607
+ "Cascade layers do not separate them — two `css()` calls are always in the same layer, where specificity decides, and a nested selector is more specific than a class.",
608
+ "Style that element itself. Narrowing the selector (`& > p`) bounds what it can reach, which is worth doing, but a descendant rule still outranks a class on its target — so this rule reports it either way."
609
+ ].join(" ") },
610
+ schema: [],
611
+ type: "problem"
612
+ },
519
613
  name: RULE_NAME$18
520
614
  });
521
615
  //#endregion
@@ -1365,10 +1459,15 @@ const RULE_NAME$8 = "no-unlayered-override";
1365
1459
  * The call whose result is a class string in the `utilities` layer — the same layer a
1366
1460
  * consumer's `css()` is in, so neither can win over the other by cascade.
1367
1461
  *
1368
- * `cva` and `sva` are deliberately absent. Their output is named semantically and emitted
1369
- * into `recipes`, whether they are declared inline or in `theme.recipes`, so a consumer's
1370
- * `css()` beats them by layer in every build. That is the fix this rule points at, and it
1371
- * would be incoherent to report it.
1462
+ * `cva` and `sva` are deliberately absent. On the extraction path their output is named
1463
+ * semantically and emitted into `recipes`, whether they are declared inline or in
1464
+ * `theme.recipes`, so a consumer's `css()` beats them by layer. That is one of the fixes this
1465
+ * rule points at, and it would be incoherent to report it.
1466
+ *
1467
+ * Under the Vite compiler there is no `recipes` layer — selections are resolved to the same
1468
+ * global atoms `css()` uses, in `utilities` — so that fix does not apply there and the style
1469
+ * object one does. The rule cannot tell which build it is linting for, so the message names
1470
+ * both.
1372
1471
  */
1373
1472
  const STYLING_CALLS = new Set(["css"]);
1374
1473
  const calleeName = (node) => {
@@ -1410,7 +1509,7 @@ const rule$8 = createRule({
1410
1509
  messages: { unlayeredOverride: [
1411
1510
  "`cx` joins class names, it does not resolve conflicts between them.",
1412
1511
  "These styles and the ones being joined are both in the `utilities` layer, so which applies is decided by stylesheet order rather than by the caller.",
1413
- "Declare the component styles with `cva` so they land in the `recipes` layer, or accept a style object and merge it with `css(base, props.css)`."
1512
+ "Accept a style object and merge it with `css(base, props.css)`, which resolves per property in every build — or, on the extraction path only, declare the component styles with `cva` so they land in the `recipes` layer."
1414
1513
  ].join(" ") },
1415
1514
  schema: [],
1416
1515
  type: "problem"
@@ -2487,6 +2586,7 @@ const rule = createRule({
2487
2586
  //#endregion
2488
2587
  //#region src/rules/index.ts
2489
2588
  const rules = {
2589
+ [RULE_NAME$22]: rule$22,
2490
2590
  [RULE_NAME$21]: rule$21,
2491
2591
  [RULE_NAME$20]: rule$20,
2492
2592
  [RULE_NAME$19]: rule$19,
@@ -2513,8 +2613,8 @@ const rules = {
2513
2613
  //#endregion
2514
2614
  //#region src/configs/all.ts
2515
2615
  const errorRules = [
2616
+ RULE_NAME$22,
2516
2617
  RULE_NAME$21,
2517
- RULE_NAME$20,
2518
2618
  RULE_NAME$12
2519
2619
  ];
2520
2620
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/eslint-plugin",
3
- "version": "1.37.13",
3
+ "version": "1.39.0",
4
4
  "description": "ESLint plugin for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "@typescript-eslint/utils": "^8.21.0",
37
37
  "micromatch": "^4.0.8",
38
38
  "synckit": "^0.9.0",
39
- "@bamboocss/config": "1.37.13",
40
- "@bamboocss/generator": "1.37.13",
41
- "@bamboocss/shared": "1.37.13"
39
+ "@bamboocss/config": "1.39.0",
40
+ "@bamboocss/generator": "1.39.0",
41
+ "@bamboocss/shared": "1.39.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/micromatch": "^4.0.10",
@@ -46,10 +46,10 @@
46
46
  "eslint": "^9.35.0",
47
47
  "multiline-ts": "^4.0.1",
48
48
  "typescript": "^5.7.2",
49
- "@bamboocss/dev": "1.37.13",
50
- "@bamboocss/preset-bamboo": "1.37.13",
51
- "@bamboocss/preset-base": "1.37.13",
52
- "@bamboocss/types": "1.37.13"
49
+ "@bamboocss/dev": "1.39.0",
50
+ "@bamboocss/preset-bamboo": "1.39.0",
51
+ "@bamboocss/preset-base": "1.39.0",
52
+ "@bamboocss/types": "1.39.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": "*"