@bamboocss/eslint-plugin 1.16.0 → 1.17.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.
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.16.0";
11
+ var version = "1.17.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$19 = "file-not-included";
311
- const rule$19 = createRule({
310
+ const RULE_NAME$20 = "file-not-included";
311
+ const rule$20 = createRule({
312
312
  create(context) {
313
313
  if (isValidFile(context)) return {};
314
314
  let hasReported = false;
@@ -329,11 +329,11 @@ const rule$19 = createRule({
329
329
  schema: [],
330
330
  type: "problem"
331
331
  },
332
- name: RULE_NAME$19
332
+ name: RULE_NAME$20
333
333
  });
334
334
  //#endregion
335
335
  //#region src/rules/no-config-function-in-source.ts
336
- const RULE_NAME$18 = "no-config-function-in-source";
336
+ const RULE_NAME$19 = "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$18 = createRule({
353
+ const rule$19 = createRule({
354
354
  create(context) {
355
355
  if (!hasPkgImport(context)) return {};
356
356
  if (!isValidFile(context)) return {};
@@ -390,12 +390,12 @@ const rule$18 = createRule({
390
390
  schema: [],
391
391
  type: "problem"
392
392
  },
393
- name: RULE_NAME$18
393
+ name: RULE_NAME$19
394
394
  });
395
395
  //#endregion
396
396
  //#region src/rules/no-debug.ts
397
- const RULE_NAME$17 = "no-debug";
398
- const rule$17 = createRule({
397
+ const RULE_NAME$18 = "no-debug";
398
+ const rule$18 = createRule({
399
399
  create(context) {
400
400
  const processedNodes = /* @__PURE__ */ new WeakSet();
401
401
  const checkObjectForDebug = (object) => {
@@ -464,12 +464,12 @@ const rule$17 = createRule({
464
464
  schema: [],
465
465
  type: "problem"
466
466
  },
467
- name: RULE_NAME$17
467
+ name: RULE_NAME$18
468
468
  });
469
469
  //#endregion
470
470
  //#region src/rules/no-deprecated-tokens.ts
471
- const RULE_NAME$16 = "no-deprecated-tokens";
472
- const rule$16 = createRule({
471
+ const RULE_NAME$17 = "no-deprecated-tokens";
472
+ const rule$17 = 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$16 = createRule({
523
523
  schema: [],
524
524
  type: "problem"
525
525
  },
526
- name: RULE_NAME$16
526
+ name: RULE_NAME$17
527
527
  });
528
528
  //#endregion
529
529
  //#region src/rules/no-dynamic-styling.ts
530
- const RULE_NAME$15 = "no-dynamic-styling";
531
- const rule$15 = createRule({
530
+ const RULE_NAME$16 = "no-dynamic-styling";
531
+ const rule$16 = createRule({
532
532
  create(context) {
533
533
  function isStaticValue(node) {
534
534
  if (!node) return false;
@@ -599,12 +599,12 @@ const rule$15 = createRule({
599
599
  schema: [],
600
600
  type: "problem"
601
601
  },
602
- name: RULE_NAME$15
602
+ name: RULE_NAME$16
603
603
  });
604
604
  //#endregion
605
605
  //#region src/rules/no-escape-hatch.ts
606
- const RULE_NAME$14 = "no-escape-hatch";
607
- const rule$14 = createRule({
606
+ const RULE_NAME$15 = "no-escape-hatch";
607
+ const rule$15 = createRule({
608
608
  create(context) {
609
609
  const removeBrackets = (range) => {
610
610
  const [start, end] = range;
@@ -677,12 +677,12 @@ const rule$14 = createRule({
677
677
  schema: [],
678
678
  type: "problem"
679
679
  },
680
- name: RULE_NAME$14
680
+ name: RULE_NAME$15
681
681
  });
682
682
  //#endregion
683
683
  //#region src/rules/no-hardcoded-color.ts
684
- const RULE_NAME$13 = "no-hardcoded-color";
685
- const rule$13 = createRule({
684
+ const RULE_NAME$14 = "no-hardcoded-color";
685
+ const rule$14 = 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$13 = createRule({
778
778
  }],
779
779
  type: "problem"
780
780
  },
781
- name: RULE_NAME$13
781
+ name: RULE_NAME$14
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$12 = "no-important";
788
- const rule$12 = createRule({
787
+ const RULE_NAME$13 = "no-important";
788
+ const rule$13 = createRule({
789
789
  create(context) {
790
790
  const removeQuotes = (range) => {
791
791
  const [start, end] = range;
@@ -889,12 +889,12 @@ const rule$12 = createRule({
889
889
  schema: [],
890
890
  type: "problem"
891
891
  },
892
- name: RULE_NAME$12
892
+ name: RULE_NAME$13
893
893
  });
894
894
  //#endregion
895
895
  //#region src/rules/no-invalid-nesting.ts
896
- const RULE_NAME$11 = "no-invalid-nesting";
897
- const rule$11 = createRule({
896
+ const RULE_NAME$12 = "no-invalid-nesting";
897
+ const rule$12 = 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$11 = createRule({
949
949
  schema: [],
950
950
  type: "problem"
951
951
  },
952
- name: RULE_NAME$11
952
+ name: RULE_NAME$12
953
953
  });
954
954
  //#endregion
955
955
  //#region src/rules/no-invalid-token-paths.ts
956
- const RULE_NAME$10 = "no-invalid-token-paths";
957
- const rule$10 = createRule({
956
+ const RULE_NAME$11 = "no-invalid-token-paths";
957
+ const rule$11 = createRule({
958
958
  create(context) {
959
959
  const invalidTokensCache = /* @__PURE__ */ new Map();
960
960
  const sendReport = (node, value) => {
@@ -1031,12 +1031,12 @@ const rule$10 = createRule({
1031
1031
  schema: [],
1032
1032
  type: "problem"
1033
1033
  },
1034
- name: RULE_NAME$10
1034
+ name: RULE_NAME$11
1035
1035
  });
1036
1036
  //#endregion
1037
1037
  //#region src/rules/no-margin-properties.ts
1038
- const RULE_NAME$9 = "no-margin-properties";
1039
- const rule$9 = createRule({
1038
+ const RULE_NAME$10 = "no-margin-properties";
1039
+ const rule$10 = 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$9 = createRule({
1112
1112
  }],
1113
1113
  type: "suggestion"
1114
1114
  },
1115
- name: RULE_NAME$9
1115
+ name: RULE_NAME$10
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$8 = "no-physical-properties";
1159
+ const RULE_NAME$9 = "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$8 = createRule({
1241
+ const rule$9 = 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$8 = createRule({
1294
1294
  }],
1295
1295
  type: "suggestion"
1296
1296
  },
1297
- name: RULE_NAME$8
1297
+ name: RULE_NAME$9
1298
1298
  });
1299
1299
  //#endregion
1300
1300
  //#region src/rules/no-property-renaming.ts
1301
- const RULE_NAME$7 = "no-property-renaming";
1302
- const rule$7 = createRule({
1301
+ const RULE_NAME$8 = "no-property-renaming";
1302
+ const rule$8 = 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$7 = createRule({
1363
1363
  schema: [],
1364
1364
  type: "problem"
1365
1365
  },
1366
- name: RULE_NAME$7
1366
+ name: RULE_NAME$8
1367
1367
  });
1368
1368
  //#endregion
1369
1369
  //#region src/rules/no-unlayered-override.ts
1370
- const RULE_NAME$6 = "no-unlayered-override";
1370
+ const RULE_NAME$7 = "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$6 = createRule({
1395
+ const rule$7 = createRule({
1396
1396
  create(context) {
1397
1397
  if (!isValidFile(context)) return {};
1398
1398
  const isOwnStyles = (node) => {
@@ -1422,6 +1422,73 @@ const rule$6 = createRule({
1422
1422
  schema: [],
1423
1423
  type: "problem"
1424
1424
  },
1425
+ name: RULE_NAME$7
1426
+ });
1427
+ //#endregion
1428
+ //#region src/rules/require-recipe-class-name.ts
1429
+ const RULE_NAME$6 = "require-recipe-class-name";
1430
+ const RECIPE_CALLS = new Set(["cva", "sva"]);
1431
+ /**
1432
+ * A property whose value the build reads as written, so the config it hashes is the config
1433
+ * the browser holds.
1434
+ *
1435
+ * Deliberately narrow. A spread, a computed key, a call — anything the extractor might not
1436
+ * resolve — makes the config's *content* uncertain, and the content is what the name is
1437
+ * derived from.
1438
+ */
1439
+ const isStatic = (node) => {
1440
+ switch (node.type) {
1441
+ case "Literal": return true;
1442
+ case "TemplateLiteral": return node.expressions.length === 0;
1443
+ case "TSAsExpression":
1444
+ case "TSSatisfiesExpression":
1445
+ case "TSNonNullExpression": return isStatic(node.expression);
1446
+ case "ArrayExpression": return node.elements.every((element) => element !== null && isStatic(element));
1447
+ case "ObjectExpression": return node.properties.every((property) => {
1448
+ if (property.type !== "Property" || property.computed) return false;
1449
+ return isStatic(property.value);
1450
+ });
1451
+ default: return false;
1452
+ }
1453
+ };
1454
+ const rule$6 = createRule({
1455
+ create(context) {
1456
+ if (!isValidFile(context)) return {};
1457
+ const mode = context.options[0]?.mode ?? "always";
1458
+ return { CallExpression(node) {
1459
+ if (!isIdentifier(node.callee) || !RECIPE_CALLS.has(node.callee.name)) return;
1460
+ const config = node.arguments[0];
1461
+ if (!config) return;
1462
+ const unwrapped = config.type === "TSAsExpression" || config.type === "TSSatisfiesExpression" ? config.expression : config;
1463
+ if (unwrapped.type === "ObjectExpression") {
1464
+ if (unwrapped.properties.some((property) => property.type === "Property" && !property.computed && (property.key.type === "Identifier" && property.key.name === "className" || property.key.type === "Literal" && property.key.value === "className") && property.value.type === "Literal" && typeof property.value.value === "string" && property.value.value !== "")) return;
1465
+ if (mode === "dynamic-only" && isStatic(unwrapped)) return;
1466
+ }
1467
+ context.report({
1468
+ messageId: "requireRecipeClassName",
1469
+ node: node.callee
1470
+ });
1471
+ } };
1472
+ },
1473
+ defaultOptions: [{ mode: "always" }],
1474
+ meta: {
1475
+ docs: { description: "Require a `className` on a recipe, so its class names do not depend on what the build could read." },
1476
+ messages: { requireRecipeClassName: [
1477
+ "This recipe has no `className`, so its classes are named by hashing its config.",
1478
+ "The build hashes the config it could read and the browser hashes the one it holds, so anything the build cannot resolve — a spread of a call, a value it cannot evaluate — gives the two different names and the element renders with no styles at all.",
1479
+ "Add `className: 'name'` to fix the name, and get readable classes with it."
1480
+ ].join(" ") },
1481
+ schema: [{
1482
+ additionalProperties: false,
1483
+ properties: { mode: {
1484
+ description: "`always` requires a name on every recipe. `dynamic-only` requires one only where the config is not a plain static literal, which is where the failure is possible.",
1485
+ enum: ["always", "dynamic-only"],
1486
+ type: "string"
1487
+ } },
1488
+ type: "object"
1489
+ }],
1490
+ type: "problem"
1491
+ },
1425
1492
  name: RULE_NAME$6
1426
1493
  });
1427
1494
  //#endregion
@@ -2272,6 +2339,7 @@ const rule = createRule({
2272
2339
  //#endregion
2273
2340
  //#region src/rules/index.ts
2274
2341
  const rules = {
2342
+ [RULE_NAME$20]: rule$20,
2275
2343
  [RULE_NAME$19]: rule$19,
2276
2344
  [RULE_NAME$18]: rule$18,
2277
2345
  [RULE_NAME$17]: rule$17,
@@ -2296,9 +2364,9 @@ const rules = {
2296
2364
  //#endregion
2297
2365
  //#region src/configs/all.ts
2298
2366
  const errorRules = [
2367
+ RULE_NAME$20,
2299
2368
  RULE_NAME$19,
2300
- RULE_NAME$18,
2301
- RULE_NAME$10
2369
+ RULE_NAME$11
2302
2370
  ];
2303
2371
  //#endregion
2304
2372
  //#region src/index.ts
@@ -2327,6 +2395,7 @@ const plugin = {
2327
2395
  "bamboo/no-invalid-token-paths": "error",
2328
2396
  "bamboo/no-property-renaming": "warn",
2329
2397
  "bamboo/no-unlayered-override": "warn",
2398
+ "bamboo/require-recipe-class-name": ["warn", { mode: "dynamic-only" }],
2330
2399
  "bamboo/no-unsafe-token-fn-usage": "warn"
2331
2400
  }
2332
2401
  }
package/dist/index.d.cts CHANGED
@@ -28,6 +28,9 @@ declare const plugin: {
28
28
  'bamboo/no-invalid-token-paths': string;
29
29
  'bamboo/no-property-renaming': string;
30
30
  'bamboo/no-unlayered-override': string;
31
+ 'bamboo/require-recipe-class-name': (string | {
32
+ mode: string;
33
+ })[];
31
34
  'bamboo/no-unsafe-token-fn-usage': string;
32
35
  };
33
36
  };
package/dist/index.d.mts CHANGED
@@ -28,6 +28,9 @@ declare const plugin: {
28
28
  'bamboo/no-invalid-token-paths': string;
29
29
  'bamboo/no-property-renaming': string;
30
30
  'bamboo/no-unlayered-override': string;
31
+ 'bamboo/require-recipe-class-name': (string | {
32
+ mode: string;
33
+ })[];
31
34
  'bamboo/no-unsafe-token-fn-usage': string;
32
35
  };
33
36
  };
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.16.0";
11
+ var version = "1.17.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$19 = "file-not-included";
311
- const rule$19 = createRule({
310
+ const RULE_NAME$20 = "file-not-included";
311
+ const rule$20 = createRule({
312
312
  create(context) {
313
313
  if (isValidFile(context)) return {};
314
314
  let hasReported = false;
@@ -329,11 +329,11 @@ const rule$19 = createRule({
329
329
  schema: [],
330
330
  type: "problem"
331
331
  },
332
- name: RULE_NAME$19
332
+ name: RULE_NAME$20
333
333
  });
334
334
  //#endregion
335
335
  //#region src/rules/no-config-function-in-source.ts
336
- const RULE_NAME$18 = "no-config-function-in-source";
336
+ const RULE_NAME$19 = "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$18 = createRule({
353
+ const rule$19 = createRule({
354
354
  create(context) {
355
355
  if (!hasPkgImport(context)) return {};
356
356
  if (!isValidFile(context)) return {};
@@ -390,12 +390,12 @@ const rule$18 = createRule({
390
390
  schema: [],
391
391
  type: "problem"
392
392
  },
393
- name: RULE_NAME$18
393
+ name: RULE_NAME$19
394
394
  });
395
395
  //#endregion
396
396
  //#region src/rules/no-debug.ts
397
- const RULE_NAME$17 = "no-debug";
398
- const rule$17 = createRule({
397
+ const RULE_NAME$18 = "no-debug";
398
+ const rule$18 = createRule({
399
399
  create(context) {
400
400
  const processedNodes = /* @__PURE__ */ new WeakSet();
401
401
  const checkObjectForDebug = (object) => {
@@ -464,12 +464,12 @@ const rule$17 = createRule({
464
464
  schema: [],
465
465
  type: "problem"
466
466
  },
467
- name: RULE_NAME$17
467
+ name: RULE_NAME$18
468
468
  });
469
469
  //#endregion
470
470
  //#region src/rules/no-deprecated-tokens.ts
471
- const RULE_NAME$16 = "no-deprecated-tokens";
472
- const rule$16 = createRule({
471
+ const RULE_NAME$17 = "no-deprecated-tokens";
472
+ const rule$17 = 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$16 = createRule({
523
523
  schema: [],
524
524
  type: "problem"
525
525
  },
526
- name: RULE_NAME$16
526
+ name: RULE_NAME$17
527
527
  });
528
528
  //#endregion
529
529
  //#region src/rules/no-dynamic-styling.ts
530
- const RULE_NAME$15 = "no-dynamic-styling";
531
- const rule$15 = createRule({
530
+ const RULE_NAME$16 = "no-dynamic-styling";
531
+ const rule$16 = createRule({
532
532
  create(context) {
533
533
  function isStaticValue(node) {
534
534
  if (!node) return false;
@@ -599,12 +599,12 @@ const rule$15 = createRule({
599
599
  schema: [],
600
600
  type: "problem"
601
601
  },
602
- name: RULE_NAME$15
602
+ name: RULE_NAME$16
603
603
  });
604
604
  //#endregion
605
605
  //#region src/rules/no-escape-hatch.ts
606
- const RULE_NAME$14 = "no-escape-hatch";
607
- const rule$14 = createRule({
606
+ const RULE_NAME$15 = "no-escape-hatch";
607
+ const rule$15 = createRule({
608
608
  create(context) {
609
609
  const removeBrackets = (range) => {
610
610
  const [start, end] = range;
@@ -677,12 +677,12 @@ const rule$14 = createRule({
677
677
  schema: [],
678
678
  type: "problem"
679
679
  },
680
- name: RULE_NAME$14
680
+ name: RULE_NAME$15
681
681
  });
682
682
  //#endregion
683
683
  //#region src/rules/no-hardcoded-color.ts
684
- const RULE_NAME$13 = "no-hardcoded-color";
685
- const rule$13 = createRule({
684
+ const RULE_NAME$14 = "no-hardcoded-color";
685
+ const rule$14 = 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$13 = createRule({
778
778
  }],
779
779
  type: "problem"
780
780
  },
781
- name: RULE_NAME$13
781
+ name: RULE_NAME$14
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$12 = "no-important";
788
- const rule$12 = createRule({
787
+ const RULE_NAME$13 = "no-important";
788
+ const rule$13 = createRule({
789
789
  create(context) {
790
790
  const removeQuotes = (range) => {
791
791
  const [start, end] = range;
@@ -889,12 +889,12 @@ const rule$12 = createRule({
889
889
  schema: [],
890
890
  type: "problem"
891
891
  },
892
- name: RULE_NAME$12
892
+ name: RULE_NAME$13
893
893
  });
894
894
  //#endregion
895
895
  //#region src/rules/no-invalid-nesting.ts
896
- const RULE_NAME$11 = "no-invalid-nesting";
897
- const rule$11 = createRule({
896
+ const RULE_NAME$12 = "no-invalid-nesting";
897
+ const rule$12 = 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$11 = createRule({
949
949
  schema: [],
950
950
  type: "problem"
951
951
  },
952
- name: RULE_NAME$11
952
+ name: RULE_NAME$12
953
953
  });
954
954
  //#endregion
955
955
  //#region src/rules/no-invalid-token-paths.ts
956
- const RULE_NAME$10 = "no-invalid-token-paths";
957
- const rule$10 = createRule({
956
+ const RULE_NAME$11 = "no-invalid-token-paths";
957
+ const rule$11 = createRule({
958
958
  create(context) {
959
959
  const invalidTokensCache = /* @__PURE__ */ new Map();
960
960
  const sendReport = (node, value) => {
@@ -1031,12 +1031,12 @@ const rule$10 = createRule({
1031
1031
  schema: [],
1032
1032
  type: "problem"
1033
1033
  },
1034
- name: RULE_NAME$10
1034
+ name: RULE_NAME$11
1035
1035
  });
1036
1036
  //#endregion
1037
1037
  //#region src/rules/no-margin-properties.ts
1038
- const RULE_NAME$9 = "no-margin-properties";
1039
- const rule$9 = createRule({
1038
+ const RULE_NAME$10 = "no-margin-properties";
1039
+ const rule$10 = 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$9 = createRule({
1112
1112
  }],
1113
1113
  type: "suggestion"
1114
1114
  },
1115
- name: RULE_NAME$9
1115
+ name: RULE_NAME$10
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$8 = "no-physical-properties";
1159
+ const RULE_NAME$9 = "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$8 = createRule({
1241
+ const rule$9 = 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$8 = createRule({
1294
1294
  }],
1295
1295
  type: "suggestion"
1296
1296
  },
1297
- name: RULE_NAME$8
1297
+ name: RULE_NAME$9
1298
1298
  });
1299
1299
  //#endregion
1300
1300
  //#region src/rules/no-property-renaming.ts
1301
- const RULE_NAME$7 = "no-property-renaming";
1302
- const rule$7 = createRule({
1301
+ const RULE_NAME$8 = "no-property-renaming";
1302
+ const rule$8 = 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$7 = createRule({
1363
1363
  schema: [],
1364
1364
  type: "problem"
1365
1365
  },
1366
- name: RULE_NAME$7
1366
+ name: RULE_NAME$8
1367
1367
  });
1368
1368
  //#endregion
1369
1369
  //#region src/rules/no-unlayered-override.ts
1370
- const RULE_NAME$6 = "no-unlayered-override";
1370
+ const RULE_NAME$7 = "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$6 = createRule({
1395
+ const rule$7 = createRule({
1396
1396
  create(context) {
1397
1397
  if (!isValidFile(context)) return {};
1398
1398
  const isOwnStyles = (node) => {
@@ -1422,6 +1422,73 @@ const rule$6 = createRule({
1422
1422
  schema: [],
1423
1423
  type: "problem"
1424
1424
  },
1425
+ name: RULE_NAME$7
1426
+ });
1427
+ //#endregion
1428
+ //#region src/rules/require-recipe-class-name.ts
1429
+ const RULE_NAME$6 = "require-recipe-class-name";
1430
+ const RECIPE_CALLS = new Set(["cva", "sva"]);
1431
+ /**
1432
+ * A property whose value the build reads as written, so the config it hashes is the config
1433
+ * the browser holds.
1434
+ *
1435
+ * Deliberately narrow. A spread, a computed key, a call — anything the extractor might not
1436
+ * resolve — makes the config's *content* uncertain, and the content is what the name is
1437
+ * derived from.
1438
+ */
1439
+ const isStatic = (node) => {
1440
+ switch (node.type) {
1441
+ case "Literal": return true;
1442
+ case "TemplateLiteral": return node.expressions.length === 0;
1443
+ case "TSAsExpression":
1444
+ case "TSSatisfiesExpression":
1445
+ case "TSNonNullExpression": return isStatic(node.expression);
1446
+ case "ArrayExpression": return node.elements.every((element) => element !== null && isStatic(element));
1447
+ case "ObjectExpression": return node.properties.every((property) => {
1448
+ if (property.type !== "Property" || property.computed) return false;
1449
+ return isStatic(property.value);
1450
+ });
1451
+ default: return false;
1452
+ }
1453
+ };
1454
+ const rule$6 = createRule({
1455
+ create(context) {
1456
+ if (!isValidFile(context)) return {};
1457
+ const mode = context.options[0]?.mode ?? "always";
1458
+ return { CallExpression(node) {
1459
+ if (!isIdentifier(node.callee) || !RECIPE_CALLS.has(node.callee.name)) return;
1460
+ const config = node.arguments[0];
1461
+ if (!config) return;
1462
+ const unwrapped = config.type === "TSAsExpression" || config.type === "TSSatisfiesExpression" ? config.expression : config;
1463
+ if (unwrapped.type === "ObjectExpression") {
1464
+ if (unwrapped.properties.some((property) => property.type === "Property" && !property.computed && (property.key.type === "Identifier" && property.key.name === "className" || property.key.type === "Literal" && property.key.value === "className") && property.value.type === "Literal" && typeof property.value.value === "string" && property.value.value !== "")) return;
1465
+ if (mode === "dynamic-only" && isStatic(unwrapped)) return;
1466
+ }
1467
+ context.report({
1468
+ messageId: "requireRecipeClassName",
1469
+ node: node.callee
1470
+ });
1471
+ } };
1472
+ },
1473
+ defaultOptions: [{ mode: "always" }],
1474
+ meta: {
1475
+ docs: { description: "Require a `className` on a recipe, so its class names do not depend on what the build could read." },
1476
+ messages: { requireRecipeClassName: [
1477
+ "This recipe has no `className`, so its classes are named by hashing its config.",
1478
+ "The build hashes the config it could read and the browser hashes the one it holds, so anything the build cannot resolve — a spread of a call, a value it cannot evaluate — gives the two different names and the element renders with no styles at all.",
1479
+ "Add `className: 'name'` to fix the name, and get readable classes with it."
1480
+ ].join(" ") },
1481
+ schema: [{
1482
+ additionalProperties: false,
1483
+ properties: { mode: {
1484
+ description: "`always` requires a name on every recipe. `dynamic-only` requires one only where the config is not a plain static literal, which is where the failure is possible.",
1485
+ enum: ["always", "dynamic-only"],
1486
+ type: "string"
1487
+ } },
1488
+ type: "object"
1489
+ }],
1490
+ type: "problem"
1491
+ },
1425
1492
  name: RULE_NAME$6
1426
1493
  });
1427
1494
  //#endregion
@@ -2272,6 +2339,7 @@ const rule = createRule({
2272
2339
  //#endregion
2273
2340
  //#region src/rules/index.ts
2274
2341
  const rules = {
2342
+ [RULE_NAME$20]: rule$20,
2275
2343
  [RULE_NAME$19]: rule$19,
2276
2344
  [RULE_NAME$18]: rule$18,
2277
2345
  [RULE_NAME$17]: rule$17,
@@ -2296,9 +2364,9 @@ const rules = {
2296
2364
  //#endregion
2297
2365
  //#region src/configs/all.ts
2298
2366
  const errorRules = [
2367
+ RULE_NAME$20,
2299
2368
  RULE_NAME$19,
2300
- RULE_NAME$18,
2301
- RULE_NAME$10
2369
+ RULE_NAME$11
2302
2370
  ];
2303
2371
  //#endregion
2304
2372
  //#region src/index.ts
@@ -2327,6 +2395,7 @@ const plugin = {
2327
2395
  "bamboo/no-invalid-token-paths": "error",
2328
2396
  "bamboo/no-property-renaming": "warn",
2329
2397
  "bamboo/no-unlayered-override": "warn",
2398
+ "bamboo/require-recipe-class-name": ["warn", { mode: "dynamic-only" }],
2330
2399
  "bamboo/no-unsafe-token-fn-usage": "warn"
2331
2400
  }
2332
2401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/eslint-plugin",
3
- "version": "1.16.0",
3
+ "version": "1.17.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.16.0",
40
- "@bamboocss/generator": "1.16.0",
41
- "@bamboocss/shared": "1.16.0"
39
+ "@bamboocss/config": "1.17.0",
40
+ "@bamboocss/generator": "1.17.0",
41
+ "@bamboocss/shared": "1.17.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.16.0",
50
- "@bamboocss/preset-bamboo": "1.16.0",
51
- "@bamboocss/preset-base": "1.16.0",
52
- "@bamboocss/types": "1.16.0"
49
+ "@bamboocss/dev": "1.17.0",
50
+ "@bamboocss/preset-bamboo": "1.17.0",
51
+ "@bamboocss/preset-base": "1.17.0",
52
+ "@bamboocss/types": "1.17.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": "*"