@bamboocss/eslint-plugin 1.29.0 → 1.30.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 +200 -44
  2. package/dist/index.mjs +200 -44
  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.29.0";
11
+ var version = "1.30.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/bamboocss/bamboo/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -307,8 +307,8 @@ function isRecipeVariant(node, context) {
307
307
  }
308
308
  //#endregion
309
309
  //#region src/rules/file-not-included.ts
310
- const RULE_NAME$20 = "file-not-included";
311
- const rule$20 = createRule({
310
+ const RULE_NAME$21 = "file-not-included";
311
+ const rule$21 = createRule({
312
312
  create(context) {
313
313
  if (isValidFile(context)) return {};
314
314
  let hasReported = false;
@@ -329,11 +329,11 @@ const rule$20 = createRule({
329
329
  schema: [],
330
330
  type: "problem"
331
331
  },
332
- name: RULE_NAME$20
332
+ name: RULE_NAME$21
333
333
  });
334
334
  //#endregion
335
335
  //#region src/rules/no-config-function-in-source.ts
336
- const RULE_NAME$19 = "no-config-function-in-source";
336
+ const RULE_NAME$20 = "no-config-function-in-source";
337
337
  const CONFIG_FUNCTIONS = new Set([
338
338
  "defineConfig",
339
339
  "defineGlobalStyles",
@@ -350,7 +350,7 @@ const CONFIG_FUNCTIONS = new Set([
350
350
  "defineTokens",
351
351
  "defineUtility"
352
352
  ]);
353
- const rule$19 = createRule({
353
+ const rule$20 = createRule({
354
354
  create(context) {
355
355
  if (!hasPkgImport(context)) return {};
356
356
  if (!isValidFile(context)) return {};
@@ -390,12 +390,12 @@ const rule$19 = createRule({
390
390
  schema: [],
391
391
  type: "problem"
392
392
  },
393
- name: RULE_NAME$19
393
+ name: RULE_NAME$20
394
394
  });
395
395
  //#endregion
396
396
  //#region src/rules/no-debug.ts
397
- const RULE_NAME$18 = "no-debug";
398
- const rule$18 = createRule({
397
+ const RULE_NAME$19 = "no-debug";
398
+ const rule$19 = createRule({
399
399
  create(context) {
400
400
  const processedNodes = /* @__PURE__ */ new WeakSet();
401
401
  const checkObjectForDebug = (object) => {
@@ -464,12 +464,12 @@ const rule$18 = createRule({
464
464
  schema: [],
465
465
  type: "problem"
466
466
  },
467
- name: RULE_NAME$18
467
+ name: RULE_NAME$19
468
468
  });
469
469
  //#endregion
470
470
  //#region src/rules/no-deprecated-tokens.ts
471
- const RULE_NAME$17 = "no-deprecated-tokens";
472
- const rule$17 = createRule({
471
+ const RULE_NAME$18 = "no-deprecated-tokens";
472
+ const rule$18 = createRule({
473
473
  create(context) {
474
474
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
475
475
  const sendReport = (property, node, value) => {
@@ -523,12 +523,12 @@ const rule$17 = createRule({
523
523
  schema: [],
524
524
  type: "problem"
525
525
  },
526
- name: RULE_NAME$17
526
+ name: RULE_NAME$18
527
527
  });
528
528
  //#endregion
529
529
  //#region src/rules/no-dynamic-styling.ts
530
- const RULE_NAME$16 = "no-dynamic-styling";
531
- const rule$16 = createRule({
530
+ const RULE_NAME$17 = "no-dynamic-styling";
531
+ const rule$17 = createRule({
532
532
  create(context) {
533
533
  function isStaticValue(node) {
534
534
  if (!node) return false;
@@ -599,12 +599,12 @@ const rule$16 = createRule({
599
599
  schema: [],
600
600
  type: "problem"
601
601
  },
602
- name: RULE_NAME$16
602
+ name: RULE_NAME$17
603
603
  });
604
604
  //#endregion
605
605
  //#region src/rules/no-escape-hatch.ts
606
- const RULE_NAME$15 = "no-escape-hatch";
607
- const rule$15 = createRule({
606
+ const RULE_NAME$16 = "no-escape-hatch";
607
+ const rule$16 = createRule({
608
608
  create(context) {
609
609
  const removeBrackets = (range) => {
610
610
  const [start, end] = range;
@@ -677,12 +677,12 @@ const rule$15 = createRule({
677
677
  schema: [],
678
678
  type: "problem"
679
679
  },
680
- name: RULE_NAME$15
680
+ name: RULE_NAME$16
681
681
  });
682
682
  //#endregion
683
683
  //#region src/rules/no-hardcoded-color.ts
684
- const RULE_NAME$14 = "no-hardcoded-color";
685
- const rule$14 = createRule({
684
+ const RULE_NAME$15 = "no-hardcoded-color";
685
+ const rule$15 = createRule({
686
686
  create(context) {
687
687
  const noOpacity = context.options[0]?.noOpacity;
688
688
  const whitelist = context.options[0]?.whitelist ?? [];
@@ -778,14 +778,14 @@ const rule$14 = createRule({
778
778
  }],
779
779
  type: "problem"
780
780
  },
781
- name: RULE_NAME$14
781
+ name: RULE_NAME$15
782
782
  });
783
783
  //#endregion
784
784
  //#region src/rules/no-important.ts
785
785
  const exclamationRegex = /\s*!$/;
786
786
  const importantRegex = /\s*!important\s*$/;
787
- const RULE_NAME$13 = "no-important";
788
- const rule$13 = createRule({
787
+ const RULE_NAME$14 = "no-important";
788
+ const rule$14 = createRule({
789
789
  create(context) {
790
790
  const removeQuotes = (range) => {
791
791
  const [start, end] = range;
@@ -889,12 +889,12 @@ const rule$13 = createRule({
889
889
  schema: [],
890
890
  type: "problem"
891
891
  },
892
- name: RULE_NAME$13
892
+ name: RULE_NAME$14
893
893
  });
894
894
  //#endregion
895
895
  //#region src/rules/no-invalid-nesting.ts
896
- const RULE_NAME$12 = "no-invalid-nesting";
897
- const rule$12 = createRule({
896
+ const RULE_NAME$13 = "no-invalid-nesting";
897
+ const rule$13 = createRule({
898
898
  create(context) {
899
899
  const bambooFunctionCache = /* @__PURE__ */ new WeakMap();
900
900
  const jsxPropertyCache = /* @__PURE__ */ new WeakMap();
@@ -949,12 +949,12 @@ const rule$12 = createRule({
949
949
  schema: [],
950
950
  type: "problem"
951
951
  },
952
- name: RULE_NAME$12
952
+ name: RULE_NAME$13
953
953
  });
954
954
  //#endregion
955
955
  //#region src/rules/no-invalid-token-paths.ts
956
- const RULE_NAME$11 = "no-invalid-token-paths";
957
- const rule$11 = createRule({
956
+ const RULE_NAME$12 = "no-invalid-token-paths";
957
+ const rule$12 = createRule({
958
958
  create(context) {
959
959
  const invalidTokensCache = /* @__PURE__ */ new Map();
960
960
  const sendReport = (node, value) => {
@@ -1031,12 +1031,12 @@ const rule$11 = createRule({
1031
1031
  schema: [],
1032
1032
  type: "problem"
1033
1033
  },
1034
- name: RULE_NAME$11
1034
+ name: RULE_NAME$12
1035
1035
  });
1036
1036
  //#endregion
1037
1037
  //#region src/rules/no-margin-properties.ts
1038
- const RULE_NAME$10 = "no-margin-properties";
1039
- const rule$10 = createRule({
1038
+ const RULE_NAME$11 = "no-margin-properties";
1039
+ const rule$11 = createRule({
1040
1040
  create(context) {
1041
1041
  const whitelist = context.options[0]?.whitelist ?? [];
1042
1042
  const longhandCache = /* @__PURE__ */ new Map();
@@ -1112,7 +1112,7 @@ const rule$10 = createRule({
1112
1112
  }],
1113
1113
  type: "suggestion"
1114
1114
  },
1115
- name: RULE_NAME$10
1115
+ name: RULE_NAME$11
1116
1116
  });
1117
1117
  //#endregion
1118
1118
  //#region src/utils/physical-properties.ts
@@ -1156,7 +1156,7 @@ const physicalPropertyValues = { textAlign: {
1156
1156
  } };
1157
1157
  //#endregion
1158
1158
  //#region src/rules/no-physical-properties.ts
1159
- const RULE_NAME$9 = "no-physical-properties";
1159
+ const RULE_NAME$10 = "no-physical-properties";
1160
1160
  const MESSAGES = {
1161
1161
  physical: "Use logical property instead of {{physical}}. Prefer `{{logical}}`.",
1162
1162
  physicalValue: "Use logical value instead of {{physical}}. Prefer `{{logical}}`.",
@@ -1238,7 +1238,7 @@ const createValueReport = (valueNode, valueText, logical, context) => {
1238
1238
  }]
1239
1239
  });
1240
1240
  };
1241
- const rule$9 = createRule({
1241
+ const rule$10 = createRule({
1242
1242
  create(context) {
1243
1243
  const whitelist = context.options[0]?.whitelist ?? [];
1244
1244
  const cache = new PropertyCache();
@@ -1294,12 +1294,12 @@ const rule$9 = createRule({
1294
1294
  }],
1295
1295
  type: "suggestion"
1296
1296
  },
1297
- name: RULE_NAME$9
1297
+ name: RULE_NAME$10
1298
1298
  });
1299
1299
  //#endregion
1300
1300
  //#region src/rules/no-property-renaming.ts
1301
- const RULE_NAME$8 = "no-property-renaming";
1302
- const rule$8 = createRule({
1301
+ const RULE_NAME$9 = "no-property-renaming";
1302
+ const rule$9 = createRule({
1303
1303
  create(context) {
1304
1304
  const bambooPropertyCache = /* @__PURE__ */ new WeakMap();
1305
1305
  const bambooAttributeCache = /* @__PURE__ */ new WeakMap();
@@ -1363,11 +1363,11 @@ const rule$8 = createRule({
1363
1363
  schema: [],
1364
1364
  type: "problem"
1365
1365
  },
1366
- name: RULE_NAME$8
1366
+ name: RULE_NAME$9
1367
1367
  });
1368
1368
  //#endregion
1369
1369
  //#region src/rules/no-unlayered-override.ts
1370
- const RULE_NAME$7 = "no-unlayered-override";
1370
+ const RULE_NAME$8 = "no-unlayered-override";
1371
1371
  /**
1372
1372
  * The call whose result is a class string in the `utilities` layer — the same layer a
1373
1373
  * consumer's `css()` is in, so neither can win over the other by cascade.
@@ -1392,7 +1392,7 @@ const isOpaque = (node) => {
1392
1392
  if (isIdentifier(node) && node.name !== "undefined") return true;
1393
1393
  return false;
1394
1394
  };
1395
- const rule$7 = createRule({
1395
+ const rule$8 = createRule({
1396
1396
  create(context) {
1397
1397
  if (!isValidFile(context)) return {};
1398
1398
  const isOwnStyles = (node) => {
@@ -1422,6 +1422,161 @@ const rule$7 = createRule({
1422
1422
  schema: [],
1423
1423
  type: "problem"
1424
1424
  },
1425
+ name: RULE_NAME$8
1426
+ });
1427
+ //#endregion
1428
+ //#region src/rules/require-literal-token-path.ts
1429
+ const RULE_NAME$7 = "require-literal-token-path";
1430
+ /**
1431
+ * The wrappers a typed caller writes around a path, which carry no runtime meaning.
1432
+ *
1433
+ * The generated `Token` type is a union of template literals, so a `string`-typed substitution
1434
+ * does not typecheck without an assertion — `` token(`colors.${shade}` as Token) `` is the
1435
+ * idiomatic spelling, and reading only the outermost node would report it as dynamic when the
1436
+ * build bounds it perfectly well.
1437
+ */
1438
+ const unwrap = (node) => {
1439
+ switch (node.type) {
1440
+ case "TSAsExpression":
1441
+ case "TSSatisfiesExpression":
1442
+ case "TSNonNullExpression":
1443
+ case "TSTypeAssertion": return unwrap(node.expression);
1444
+ default: return node;
1445
+ }
1446
+ };
1447
+ /**
1448
+ * How much of a token path the build can see.
1449
+ *
1450
+ * `literal` is a path spelled at the call. `bounded` is a template whose static head names a
1451
+ * category — the build cannot say which token it wants but knows which it cannot, so it keeps
1452
+ * that category and prunes the rest. `opaque` is everything else.
1453
+ *
1454
+ * Mirrors `literalPath` in `@bamboocss/node`, down to reading the *cooked* head and treating an
1455
+ * empty one as no bound at all. The rule's whole value is predicting what the build will do,
1456
+ * so a disagreement here is worse than no rule.
1457
+ */
1458
+ const reach = (argument) => {
1459
+ if (!argument) return "opaque";
1460
+ const node = unwrap(argument);
1461
+ if (isLiteral(node) && typeof node.value === "string") return "literal";
1462
+ if (isTemplateLiteral(node)) {
1463
+ if (node.expressions.length === 0) return "literal";
1464
+ return node.quasis[0]?.value.cooked ? "bounded" : "opaque";
1465
+ }
1466
+ return "opaque";
1467
+ };
1468
+ /** A member name, whether written `a.b` or `a['b']`. */
1469
+ const memberName = (node) => {
1470
+ if (!node.computed) return isIdentifier(node.property) ? node.property.name : void 0;
1471
+ return isLiteral(node.property) && typeof node.property.value === "string" ? node.property.value : void 0;
1472
+ };
1473
+ const rule$7 = createRule({
1474
+ create(context) {
1475
+ /**
1476
+ * Names bound to the generated token artifact in this file.
1477
+ *
1478
+ * Collected here rather than through the shared `getTokenImport`, which searches *every*
1479
+ * module for a specifier named `token` and takes the first. That is wrong in both
1480
+ * directions: an unrelated `import { token as authToken } from './auth'` made this rule
1481
+ * report on a file with no Bamboo in it, and — worse — an unrelated `token` import
1482
+ * declared above the real one *masked* it, so whether a genuinely dynamic path was
1483
+ * reported depended on import order.
1484
+ *
1485
+ * The specifier test is a substring, like the build's cheap pass. It over-matches
1486
+ * `./tokenserver`, which is harmless: the build declines a `token` call through any
1487
+ * namespace it cannot classify, so over-matching agrees with it. It under-matches a
1488
+ * tsconfig path alias, which the build resolves and this cannot — that gap is documented
1489
+ * rather than papered over.
1490
+ */
1491
+ let bindings;
1492
+ const bound = () => {
1493
+ if (bindings) return bindings;
1494
+ bindings = {
1495
+ direct: /* @__PURE__ */ new Set(),
1496
+ namespaces: /* @__PURE__ */ new Set()
1497
+ };
1498
+ for (const node of context.sourceCode.ast.body) {
1499
+ if (node.type !== "ImportDeclaration") continue;
1500
+ if (!String(node.source.value).includes("tokens")) continue;
1501
+ for (const specifier of node.specifiers) {
1502
+ if (specifier.type === "ImportNamespaceSpecifier") bindings.namespaces.add(specifier.local.name);
1503
+ if (specifier.type === "ImportSpecifier" && isIdentifier(specifier.imported)) {
1504
+ if (specifier.imported.name === "token") bindings.direct.add(specifier.local.name);
1505
+ }
1506
+ }
1507
+ }
1508
+ return bindings;
1509
+ };
1510
+ /**
1511
+ * Whether this call reaches the generated `token`.
1512
+ *
1513
+ * A bare `token(…)` counts even with no import to bind it, because the build counts it: its
1514
+ * accounting keys on the name, so a local helper of that name declines there too. Staying
1515
+ * quiet about it would mean `strict` failing a build the editor called clean.
1516
+ */
1517
+ const isTokenCall = (node) => {
1518
+ const callee = node.type === "CallExpression" ? node.callee : node.tag;
1519
+ const { direct, namespaces } = bound();
1520
+ if (isIdentifier(callee)) return direct.has(callee.name) || callee.name === "token";
1521
+ if (callee.type !== "MemberExpression") return false;
1522
+ if (callee.computed) return isComputedTokenCallee(callee, direct, namespaces) ? "computed" : false;
1523
+ const property = memberName(callee);
1524
+ if (property === void 0) return false;
1525
+ if (isIdentifier(callee.object) && (direct.has(callee.object.name) || callee.object.name === "token")) return property === "value";
1526
+ if (property === "token") return isIdentifier(callee.object) && namespaces.has(callee.object.name);
1527
+ return property === "value" && callee.object.type === "MemberExpression" && memberName(callee.object) === "token" && isIdentifier(callee.object.object) && namespaces.has(callee.object.object.name);
1528
+ };
1529
+ /** The same shapes as below, but reached through a computed member. */
1530
+ const isComputedTokenCallee = (callee, direct, namespaces) => {
1531
+ const property = memberName(callee);
1532
+ if (property === void 0) return false;
1533
+ if (isIdentifier(callee.object) && (direct.has(callee.object.name) || callee.object.name === "token")) return property === "value";
1534
+ if (property === "token") return isIdentifier(callee.object) && namespaces.has(callee.object.name);
1535
+ return false;
1536
+ };
1537
+ const report = (node, found, at) => {
1538
+ context.report({
1539
+ messageId: found === "bounded" ? "boundedTokenPath" : "opaqueTokenPath",
1540
+ node: at
1541
+ });
1542
+ };
1543
+ return {
1544
+ CallExpression(node) {
1545
+ const kind = isTokenCall(node);
1546
+ if (!kind) return;
1547
+ const found = kind === "computed" ? "opaque" : reach(node.arguments[0]);
1548
+ if (found === "literal") return;
1549
+ report(node, found, node.arguments[0] ?? node);
1550
+ },
1551
+ /**
1552
+ * `` token`colors.${s}` `` — a different node, and one the build declines outright since
1553
+ * the artifact exports a function rather than a tag. Reported as opaque whatever its
1554
+ * head, because the call never resolves at all.
1555
+ */
1556
+ TaggedTemplateExpression(node) {
1557
+ if (!isTokenCall(node)) return;
1558
+ report(node, "opaque", node.quasi);
1559
+ }
1560
+ };
1561
+ },
1562
+ defaultOptions: [],
1563
+ meta: {
1564
+ docs: { description: "Require a token path the build can resolve, so `pruneUnusedTokens` can drop the declarations nothing asks for." },
1565
+ messages: {
1566
+ /**
1567
+ * The build keeps every token declaration for this, because the path could name any of
1568
+ * them. Under `pruneUnusedTokens: 'strict'` it is an error rather than a size cost.
1569
+ */
1570
+ opaqueTokenPath: "Token path cannot be resolved at build time, so every token declaration is kept. Spell the path at the call, or give a template a static prefix.",
1571
+ /**
1572
+ * Not wrong — the build keeps the category and prunes the rest — but a spelled-out path
1573
+ * keeps one declaration where this keeps hundreds, so it is worth knowing about.
1574
+ */
1575
+ boundedTokenPath: "Token path is bounded by its prefix rather than resolved, so the whole category is kept. Spell the path at the call to keep only what you use."
1576
+ },
1577
+ schema: [],
1578
+ type: "problem"
1579
+ },
1425
1580
  name: RULE_NAME$7
1426
1581
  });
1427
1582
  //#endregion
@@ -2339,6 +2494,7 @@ const rule = createRule({
2339
2494
  //#endregion
2340
2495
  //#region src/rules/index.ts
2341
2496
  const rules = {
2497
+ [RULE_NAME$21]: rule$21,
2342
2498
  [RULE_NAME$20]: rule$20,
2343
2499
  [RULE_NAME$19]: rule$19,
2344
2500
  [RULE_NAME$18]: rule$18,
@@ -2364,9 +2520,9 @@ const rules = {
2364
2520
  //#endregion
2365
2521
  //#region src/configs/all.ts
2366
2522
  const errorRules = [
2523
+ RULE_NAME$21,
2367
2524
  RULE_NAME$20,
2368
- RULE_NAME$19,
2369
- RULE_NAME$11
2525
+ RULE_NAME$12
2370
2526
  ];
2371
2527
  //#endregion
2372
2528
  //#region src/index.ts
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.29.0";
11
+ var version = "1.30.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/bamboocss/bamboo/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -307,8 +307,8 @@ function isRecipeVariant(node, context) {
307
307
  }
308
308
  //#endregion
309
309
  //#region src/rules/file-not-included.ts
310
- const RULE_NAME$20 = "file-not-included";
311
- const rule$20 = createRule({
310
+ const RULE_NAME$21 = "file-not-included";
311
+ const rule$21 = createRule({
312
312
  create(context) {
313
313
  if (isValidFile(context)) return {};
314
314
  let hasReported = false;
@@ -329,11 +329,11 @@ const rule$20 = createRule({
329
329
  schema: [],
330
330
  type: "problem"
331
331
  },
332
- name: RULE_NAME$20
332
+ name: RULE_NAME$21
333
333
  });
334
334
  //#endregion
335
335
  //#region src/rules/no-config-function-in-source.ts
336
- const RULE_NAME$19 = "no-config-function-in-source";
336
+ const RULE_NAME$20 = "no-config-function-in-source";
337
337
  const CONFIG_FUNCTIONS = new Set([
338
338
  "defineConfig",
339
339
  "defineGlobalStyles",
@@ -350,7 +350,7 @@ const CONFIG_FUNCTIONS = new Set([
350
350
  "defineTokens",
351
351
  "defineUtility"
352
352
  ]);
353
- const rule$19 = createRule({
353
+ const rule$20 = createRule({
354
354
  create(context) {
355
355
  if (!hasPkgImport(context)) return {};
356
356
  if (!isValidFile(context)) return {};
@@ -390,12 +390,12 @@ const rule$19 = createRule({
390
390
  schema: [],
391
391
  type: "problem"
392
392
  },
393
- name: RULE_NAME$19
393
+ name: RULE_NAME$20
394
394
  });
395
395
  //#endregion
396
396
  //#region src/rules/no-debug.ts
397
- const RULE_NAME$18 = "no-debug";
398
- const rule$18 = createRule({
397
+ const RULE_NAME$19 = "no-debug";
398
+ const rule$19 = createRule({
399
399
  create(context) {
400
400
  const processedNodes = /* @__PURE__ */ new WeakSet();
401
401
  const checkObjectForDebug = (object) => {
@@ -464,12 +464,12 @@ const rule$18 = createRule({
464
464
  schema: [],
465
465
  type: "problem"
466
466
  },
467
- name: RULE_NAME$18
467
+ name: RULE_NAME$19
468
468
  });
469
469
  //#endregion
470
470
  //#region src/rules/no-deprecated-tokens.ts
471
- const RULE_NAME$17 = "no-deprecated-tokens";
472
- const rule$17 = createRule({
471
+ const RULE_NAME$18 = "no-deprecated-tokens";
472
+ const rule$18 = createRule({
473
473
  create(context) {
474
474
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
475
475
  const sendReport = (property, node, value) => {
@@ -523,12 +523,12 @@ const rule$17 = createRule({
523
523
  schema: [],
524
524
  type: "problem"
525
525
  },
526
- name: RULE_NAME$17
526
+ name: RULE_NAME$18
527
527
  });
528
528
  //#endregion
529
529
  //#region src/rules/no-dynamic-styling.ts
530
- const RULE_NAME$16 = "no-dynamic-styling";
531
- const rule$16 = createRule({
530
+ const RULE_NAME$17 = "no-dynamic-styling";
531
+ const rule$17 = createRule({
532
532
  create(context) {
533
533
  function isStaticValue(node) {
534
534
  if (!node) return false;
@@ -599,12 +599,12 @@ const rule$16 = createRule({
599
599
  schema: [],
600
600
  type: "problem"
601
601
  },
602
- name: RULE_NAME$16
602
+ name: RULE_NAME$17
603
603
  });
604
604
  //#endregion
605
605
  //#region src/rules/no-escape-hatch.ts
606
- const RULE_NAME$15 = "no-escape-hatch";
607
- const rule$15 = createRule({
606
+ const RULE_NAME$16 = "no-escape-hatch";
607
+ const rule$16 = createRule({
608
608
  create(context) {
609
609
  const removeBrackets = (range) => {
610
610
  const [start, end] = range;
@@ -677,12 +677,12 @@ const rule$15 = createRule({
677
677
  schema: [],
678
678
  type: "problem"
679
679
  },
680
- name: RULE_NAME$15
680
+ name: RULE_NAME$16
681
681
  });
682
682
  //#endregion
683
683
  //#region src/rules/no-hardcoded-color.ts
684
- const RULE_NAME$14 = "no-hardcoded-color";
685
- const rule$14 = createRule({
684
+ const RULE_NAME$15 = "no-hardcoded-color";
685
+ const rule$15 = createRule({
686
686
  create(context) {
687
687
  const noOpacity = context.options[0]?.noOpacity;
688
688
  const whitelist = context.options[0]?.whitelist ?? [];
@@ -778,14 +778,14 @@ const rule$14 = createRule({
778
778
  }],
779
779
  type: "problem"
780
780
  },
781
- name: RULE_NAME$14
781
+ name: RULE_NAME$15
782
782
  });
783
783
  //#endregion
784
784
  //#region src/rules/no-important.ts
785
785
  const exclamationRegex = /\s*!$/;
786
786
  const importantRegex = /\s*!important\s*$/;
787
- const RULE_NAME$13 = "no-important";
788
- const rule$13 = createRule({
787
+ const RULE_NAME$14 = "no-important";
788
+ const rule$14 = createRule({
789
789
  create(context) {
790
790
  const removeQuotes = (range) => {
791
791
  const [start, end] = range;
@@ -889,12 +889,12 @@ const rule$13 = createRule({
889
889
  schema: [],
890
890
  type: "problem"
891
891
  },
892
- name: RULE_NAME$13
892
+ name: RULE_NAME$14
893
893
  });
894
894
  //#endregion
895
895
  //#region src/rules/no-invalid-nesting.ts
896
- const RULE_NAME$12 = "no-invalid-nesting";
897
- const rule$12 = createRule({
896
+ const RULE_NAME$13 = "no-invalid-nesting";
897
+ const rule$13 = createRule({
898
898
  create(context) {
899
899
  const bambooFunctionCache = /* @__PURE__ */ new WeakMap();
900
900
  const jsxPropertyCache = /* @__PURE__ */ new WeakMap();
@@ -949,12 +949,12 @@ const rule$12 = createRule({
949
949
  schema: [],
950
950
  type: "problem"
951
951
  },
952
- name: RULE_NAME$12
952
+ name: RULE_NAME$13
953
953
  });
954
954
  //#endregion
955
955
  //#region src/rules/no-invalid-token-paths.ts
956
- const RULE_NAME$11 = "no-invalid-token-paths";
957
- const rule$11 = createRule({
956
+ const RULE_NAME$12 = "no-invalid-token-paths";
957
+ const rule$12 = createRule({
958
958
  create(context) {
959
959
  const invalidTokensCache = /* @__PURE__ */ new Map();
960
960
  const sendReport = (node, value) => {
@@ -1031,12 +1031,12 @@ const rule$11 = createRule({
1031
1031
  schema: [],
1032
1032
  type: "problem"
1033
1033
  },
1034
- name: RULE_NAME$11
1034
+ name: RULE_NAME$12
1035
1035
  });
1036
1036
  //#endregion
1037
1037
  //#region src/rules/no-margin-properties.ts
1038
- const RULE_NAME$10 = "no-margin-properties";
1039
- const rule$10 = createRule({
1038
+ const RULE_NAME$11 = "no-margin-properties";
1039
+ const rule$11 = createRule({
1040
1040
  create(context) {
1041
1041
  const whitelist = context.options[0]?.whitelist ?? [];
1042
1042
  const longhandCache = /* @__PURE__ */ new Map();
@@ -1112,7 +1112,7 @@ const rule$10 = createRule({
1112
1112
  }],
1113
1113
  type: "suggestion"
1114
1114
  },
1115
- name: RULE_NAME$10
1115
+ name: RULE_NAME$11
1116
1116
  });
1117
1117
  //#endregion
1118
1118
  //#region src/utils/physical-properties.ts
@@ -1156,7 +1156,7 @@ const physicalPropertyValues = { textAlign: {
1156
1156
  } };
1157
1157
  //#endregion
1158
1158
  //#region src/rules/no-physical-properties.ts
1159
- const RULE_NAME$9 = "no-physical-properties";
1159
+ const RULE_NAME$10 = "no-physical-properties";
1160
1160
  const MESSAGES = {
1161
1161
  physical: "Use logical property instead of {{physical}}. Prefer `{{logical}}`.",
1162
1162
  physicalValue: "Use logical value instead of {{physical}}. Prefer `{{logical}}`.",
@@ -1238,7 +1238,7 @@ const createValueReport = (valueNode, valueText, logical, context) => {
1238
1238
  }]
1239
1239
  });
1240
1240
  };
1241
- const rule$9 = createRule({
1241
+ const rule$10 = createRule({
1242
1242
  create(context) {
1243
1243
  const whitelist = context.options[0]?.whitelist ?? [];
1244
1244
  const cache = new PropertyCache();
@@ -1294,12 +1294,12 @@ const rule$9 = createRule({
1294
1294
  }],
1295
1295
  type: "suggestion"
1296
1296
  },
1297
- name: RULE_NAME$9
1297
+ name: RULE_NAME$10
1298
1298
  });
1299
1299
  //#endregion
1300
1300
  //#region src/rules/no-property-renaming.ts
1301
- const RULE_NAME$8 = "no-property-renaming";
1302
- const rule$8 = createRule({
1301
+ const RULE_NAME$9 = "no-property-renaming";
1302
+ const rule$9 = createRule({
1303
1303
  create(context) {
1304
1304
  const bambooPropertyCache = /* @__PURE__ */ new WeakMap();
1305
1305
  const bambooAttributeCache = /* @__PURE__ */ new WeakMap();
@@ -1363,11 +1363,11 @@ const rule$8 = createRule({
1363
1363
  schema: [],
1364
1364
  type: "problem"
1365
1365
  },
1366
- name: RULE_NAME$8
1366
+ name: RULE_NAME$9
1367
1367
  });
1368
1368
  //#endregion
1369
1369
  //#region src/rules/no-unlayered-override.ts
1370
- const RULE_NAME$7 = "no-unlayered-override";
1370
+ const RULE_NAME$8 = "no-unlayered-override";
1371
1371
  /**
1372
1372
  * The call whose result is a class string in the `utilities` layer — the same layer a
1373
1373
  * consumer's `css()` is in, so neither can win over the other by cascade.
@@ -1392,7 +1392,7 @@ const isOpaque = (node) => {
1392
1392
  if (isIdentifier(node) && node.name !== "undefined") return true;
1393
1393
  return false;
1394
1394
  };
1395
- const rule$7 = createRule({
1395
+ const rule$8 = createRule({
1396
1396
  create(context) {
1397
1397
  if (!isValidFile(context)) return {};
1398
1398
  const isOwnStyles = (node) => {
@@ -1422,6 +1422,161 @@ const rule$7 = createRule({
1422
1422
  schema: [],
1423
1423
  type: "problem"
1424
1424
  },
1425
+ name: RULE_NAME$8
1426
+ });
1427
+ //#endregion
1428
+ //#region src/rules/require-literal-token-path.ts
1429
+ const RULE_NAME$7 = "require-literal-token-path";
1430
+ /**
1431
+ * The wrappers a typed caller writes around a path, which carry no runtime meaning.
1432
+ *
1433
+ * The generated `Token` type is a union of template literals, so a `string`-typed substitution
1434
+ * does not typecheck without an assertion — `` token(`colors.${shade}` as Token) `` is the
1435
+ * idiomatic spelling, and reading only the outermost node would report it as dynamic when the
1436
+ * build bounds it perfectly well.
1437
+ */
1438
+ const unwrap = (node) => {
1439
+ switch (node.type) {
1440
+ case "TSAsExpression":
1441
+ case "TSSatisfiesExpression":
1442
+ case "TSNonNullExpression":
1443
+ case "TSTypeAssertion": return unwrap(node.expression);
1444
+ default: return node;
1445
+ }
1446
+ };
1447
+ /**
1448
+ * How much of a token path the build can see.
1449
+ *
1450
+ * `literal` is a path spelled at the call. `bounded` is a template whose static head names a
1451
+ * category — the build cannot say which token it wants but knows which it cannot, so it keeps
1452
+ * that category and prunes the rest. `opaque` is everything else.
1453
+ *
1454
+ * Mirrors `literalPath` in `@bamboocss/node`, down to reading the *cooked* head and treating an
1455
+ * empty one as no bound at all. The rule's whole value is predicting what the build will do,
1456
+ * so a disagreement here is worse than no rule.
1457
+ */
1458
+ const reach = (argument) => {
1459
+ if (!argument) return "opaque";
1460
+ const node = unwrap(argument);
1461
+ if (isLiteral(node) && typeof node.value === "string") return "literal";
1462
+ if (isTemplateLiteral(node)) {
1463
+ if (node.expressions.length === 0) return "literal";
1464
+ return node.quasis[0]?.value.cooked ? "bounded" : "opaque";
1465
+ }
1466
+ return "opaque";
1467
+ };
1468
+ /** A member name, whether written `a.b` or `a['b']`. */
1469
+ const memberName = (node) => {
1470
+ if (!node.computed) return isIdentifier(node.property) ? node.property.name : void 0;
1471
+ return isLiteral(node.property) && typeof node.property.value === "string" ? node.property.value : void 0;
1472
+ };
1473
+ const rule$7 = createRule({
1474
+ create(context) {
1475
+ /**
1476
+ * Names bound to the generated token artifact in this file.
1477
+ *
1478
+ * Collected here rather than through the shared `getTokenImport`, which searches *every*
1479
+ * module for a specifier named `token` and takes the first. That is wrong in both
1480
+ * directions: an unrelated `import { token as authToken } from './auth'` made this rule
1481
+ * report on a file with no Bamboo in it, and — worse — an unrelated `token` import
1482
+ * declared above the real one *masked* it, so whether a genuinely dynamic path was
1483
+ * reported depended on import order.
1484
+ *
1485
+ * The specifier test is a substring, like the build's cheap pass. It over-matches
1486
+ * `./tokenserver`, which is harmless: the build declines a `token` call through any
1487
+ * namespace it cannot classify, so over-matching agrees with it. It under-matches a
1488
+ * tsconfig path alias, which the build resolves and this cannot — that gap is documented
1489
+ * rather than papered over.
1490
+ */
1491
+ let bindings;
1492
+ const bound = () => {
1493
+ if (bindings) return bindings;
1494
+ bindings = {
1495
+ direct: /* @__PURE__ */ new Set(),
1496
+ namespaces: /* @__PURE__ */ new Set()
1497
+ };
1498
+ for (const node of context.sourceCode.ast.body) {
1499
+ if (node.type !== "ImportDeclaration") continue;
1500
+ if (!String(node.source.value).includes("tokens")) continue;
1501
+ for (const specifier of node.specifiers) {
1502
+ if (specifier.type === "ImportNamespaceSpecifier") bindings.namespaces.add(specifier.local.name);
1503
+ if (specifier.type === "ImportSpecifier" && isIdentifier(specifier.imported)) {
1504
+ if (specifier.imported.name === "token") bindings.direct.add(specifier.local.name);
1505
+ }
1506
+ }
1507
+ }
1508
+ return bindings;
1509
+ };
1510
+ /**
1511
+ * Whether this call reaches the generated `token`.
1512
+ *
1513
+ * A bare `token(…)` counts even with no import to bind it, because the build counts it: its
1514
+ * accounting keys on the name, so a local helper of that name declines there too. Staying
1515
+ * quiet about it would mean `strict` failing a build the editor called clean.
1516
+ */
1517
+ const isTokenCall = (node) => {
1518
+ const callee = node.type === "CallExpression" ? node.callee : node.tag;
1519
+ const { direct, namespaces } = bound();
1520
+ if (isIdentifier(callee)) return direct.has(callee.name) || callee.name === "token";
1521
+ if (callee.type !== "MemberExpression") return false;
1522
+ if (callee.computed) return isComputedTokenCallee(callee, direct, namespaces) ? "computed" : false;
1523
+ const property = memberName(callee);
1524
+ if (property === void 0) return false;
1525
+ if (isIdentifier(callee.object) && (direct.has(callee.object.name) || callee.object.name === "token")) return property === "value";
1526
+ if (property === "token") return isIdentifier(callee.object) && namespaces.has(callee.object.name);
1527
+ return property === "value" && callee.object.type === "MemberExpression" && memberName(callee.object) === "token" && isIdentifier(callee.object.object) && namespaces.has(callee.object.object.name);
1528
+ };
1529
+ /** The same shapes as below, but reached through a computed member. */
1530
+ const isComputedTokenCallee = (callee, direct, namespaces) => {
1531
+ const property = memberName(callee);
1532
+ if (property === void 0) return false;
1533
+ if (isIdentifier(callee.object) && (direct.has(callee.object.name) || callee.object.name === "token")) return property === "value";
1534
+ if (property === "token") return isIdentifier(callee.object) && namespaces.has(callee.object.name);
1535
+ return false;
1536
+ };
1537
+ const report = (node, found, at) => {
1538
+ context.report({
1539
+ messageId: found === "bounded" ? "boundedTokenPath" : "opaqueTokenPath",
1540
+ node: at
1541
+ });
1542
+ };
1543
+ return {
1544
+ CallExpression(node) {
1545
+ const kind = isTokenCall(node);
1546
+ if (!kind) return;
1547
+ const found = kind === "computed" ? "opaque" : reach(node.arguments[0]);
1548
+ if (found === "literal") return;
1549
+ report(node, found, node.arguments[0] ?? node);
1550
+ },
1551
+ /**
1552
+ * `` token`colors.${s}` `` — a different node, and one the build declines outright since
1553
+ * the artifact exports a function rather than a tag. Reported as opaque whatever its
1554
+ * head, because the call never resolves at all.
1555
+ */
1556
+ TaggedTemplateExpression(node) {
1557
+ if (!isTokenCall(node)) return;
1558
+ report(node, "opaque", node.quasi);
1559
+ }
1560
+ };
1561
+ },
1562
+ defaultOptions: [],
1563
+ meta: {
1564
+ docs: { description: "Require a token path the build can resolve, so `pruneUnusedTokens` can drop the declarations nothing asks for." },
1565
+ messages: {
1566
+ /**
1567
+ * The build keeps every token declaration for this, because the path could name any of
1568
+ * them. Under `pruneUnusedTokens: 'strict'` it is an error rather than a size cost.
1569
+ */
1570
+ opaqueTokenPath: "Token path cannot be resolved at build time, so every token declaration is kept. Spell the path at the call, or give a template a static prefix.",
1571
+ /**
1572
+ * Not wrong — the build keeps the category and prunes the rest — but a spelled-out path
1573
+ * keeps one declaration where this keeps hundreds, so it is worth knowing about.
1574
+ */
1575
+ boundedTokenPath: "Token path is bounded by its prefix rather than resolved, so the whole category is kept. Spell the path at the call to keep only what you use."
1576
+ },
1577
+ schema: [],
1578
+ type: "problem"
1579
+ },
1425
1580
  name: RULE_NAME$7
1426
1581
  });
1427
1582
  //#endregion
@@ -2339,6 +2494,7 @@ const rule = createRule({
2339
2494
  //#endregion
2340
2495
  //#region src/rules/index.ts
2341
2496
  const rules = {
2497
+ [RULE_NAME$21]: rule$21,
2342
2498
  [RULE_NAME$20]: rule$20,
2343
2499
  [RULE_NAME$19]: rule$19,
2344
2500
  [RULE_NAME$18]: rule$18,
@@ -2364,9 +2520,9 @@ const rules = {
2364
2520
  //#endregion
2365
2521
  //#region src/configs/all.ts
2366
2522
  const errorRules = [
2523
+ RULE_NAME$21,
2367
2524
  RULE_NAME$20,
2368
- RULE_NAME$19,
2369
- RULE_NAME$11
2525
+ RULE_NAME$12
2370
2526
  ];
2371
2527
  //#endregion
2372
2528
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/eslint-plugin",
3
- "version": "1.29.0",
3
+ "version": "1.30.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.29.0",
40
- "@bamboocss/generator": "1.29.0",
41
- "@bamboocss/shared": "1.29.0"
39
+ "@bamboocss/config": "1.30.0",
40
+ "@bamboocss/generator": "1.30.0",
41
+ "@bamboocss/shared": "1.30.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.29.0",
50
- "@bamboocss/preset-bamboo": "1.29.0",
51
- "@bamboocss/preset-base": "1.29.0",
52
- "@bamboocss/types": "1.29.0"
49
+ "@bamboocss/dev": "1.30.0",
50
+ "@bamboocss/preset-bamboo": "1.30.0",
51
+ "@bamboocss/preset-base": "1.30.0",
52
+ "@bamboocss/types": "1.30.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": "*"