@tenphi/eslint-plugin-tasty 1.5.0 → 1.5.1

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/README.md CHANGED
@@ -196,7 +196,7 @@ for typing shared signatures.
196
196
  | `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
197
197
  | `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
198
198
  | `tasty/no-runtime-styles-mutation` | warn | JavaScript variables, calls, conditionals, computed keys, or interpolated templates in Tasty style values; use states and tokens instead |
199
- | `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; suppress an intentional spread on its line with an ESLint comment and a reason |
199
+ | `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
200
200
  | `tasty/consistent-token-usage` | warn | Raw px values when custom units or tokens exist |
201
201
  | `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
202
202
  | `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
@@ -259,6 +259,27 @@ const Card = tasty({
259
259
  });
260
260
  ```
261
261
 
262
+ When a root style object contains two or more spreads, the rule reports one
263
+ composition warning at the first spread and recommends `mergeStyles`:
264
+
265
+ ```ts
266
+ import { mergeStyles, type Styles } from '@tenphi/tasty';
267
+
268
+ // Warning: shallow composition can discard sub-element properties and states.
269
+ const finalStyles: Styles = { ...outerStyles, ...styles };
270
+
271
+ // Use Tasty's merge semantics, in the same override order.
272
+ const mergedStyles: Styles = mergeStyles(outerStyles, styles);
273
+ ```
274
+
275
+ For example, spreading `Label: { fill: '#accent' }` over
276
+ `Label: { padding: '2x', fill: '#surface' }` loses `padding`; `mergeStyles`
277
+ preserves it. State maps without a default key extend inherited entries, while
278
+ maps with a `''` default deliberately replace them. This is a behavioral choice,
279
+ so the rule provides no automatic rewrite. If shallow replacement is intentional,
280
+ suppress the warning at the first spread with a reason. Single spreads and spreads
281
+ inside state maps, sub-elements or arrays retain the ordinary spread warning.
282
+
262
283
  `tastyStatic()` still rejects spreads and dynamic expressions as errors because
263
284
  its build-time extractor cannot evaluate them. An ESLint suppression does not
264
285
  make a spread valid there. To make warnings fail CI until addressed or explicitly
@@ -7,13 +7,17 @@ var no_style_spread_default = createRule({
7
7
  name: "no-style-spread",
8
8
  meta: {
9
9
  type: "suggestion",
10
- docs: { description: "Warn about spreads inside Tasty style objects" },
11
- messages: { styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason." },
10
+ docs: { description: "Warn about style spreads and recommend mergeStyles for composition" },
11
+ messages: {
12
+ styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.",
13
+ preferMergeStyles: "Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, suppress this warning on this line with a reason."
14
+ },
12
15
  schema: []
13
16
  },
14
17
  defaultOptions: [],
15
18
  create(context) {
16
19
  const ctx = new TastyContext(context);
20
+ const visited = /* @__PURE__ */ new WeakSet();
17
21
  function checkValue(node) {
18
22
  const value = unwrapExpression(node);
19
23
  if (value.type === "ObjectExpression") for (const prop of value.properties) if (prop.type === "SpreadElement") context.report({
@@ -31,15 +35,31 @@ var no_style_spread_default = createRule({
31
35
  }
32
36
  }
33
37
  function handleStyleObject(node) {
38
+ if (visited.has(node)) return;
34
39
  const styleCtx = ctx.getStyleContext(node);
35
40
  if (!styleCtx || styleCtx.isStaticCall) return;
36
41
  if (ctx.getRootStyleObject(node) !== node) return;
37
- checkValue(node);
42
+ visited.add(node);
43
+ const spreads = node.properties.filter((prop) => prop.type === "SpreadElement");
44
+ if (spreads.length < 2) {
45
+ checkValue(node);
46
+ return;
47
+ }
48
+ context.report({
49
+ node: spreads[0],
50
+ messageId: "preferMergeStyles"
51
+ });
52
+ for (const prop of node.properties) if (prop.type === "Property") checkValue(prop.value);
38
53
  }
39
54
  return {
40
55
  ImportDeclaration(node) {
41
56
  ctx.trackImport(node);
42
57
  },
58
+ VariableDeclarator(node) {
59
+ if (!node.init) return;
60
+ const value = unwrapExpression(node.init);
61
+ if (value.type === "ObjectExpression") handleStyleObject(value);
62
+ },
43
63
  ...styleObjectListeners(handleStyleObject)
44
64
  };
45
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn about spreads inside Tasty style objects',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkValue(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,iDACd;EACD,UAAU,EACR,aACE,uKACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,cAAW,KAAK;;AAGlB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread' | 'preferMergeStyles';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn about style spreads and recommend mergeStyles for composition',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.',\n preferMergeStyles:\n 'Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, suppress this warning on this line with a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n if (visited.has(node)) return;\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n visited.add(node);\n\n const spreads = node.properties.filter(\n (prop): prop is TSESTree.SpreadElement => prop.type === 'SpreadElement',\n );\n if (spreads.length < 2) {\n checkValue(node);\n return;\n }\n\n // Report composition once, at its first spread so the existing per-line\n // suppression convention still works. Nested state maps/arrays are not\n // Styles objects and must keep their ordinary spread diagnostics.\n context.report({ node: spreads[0], messageId: 'preferMergeStyles' });\n for (const prop of node.properties) {\n if (prop.type === 'Property') checkValue(prop.value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n VariableDeclarator(node) {\n if (!node.init) return;\n // The shared selectors cover only one as/satisfies wrapper. Typed\n // composition can also have nested wrappers or a non-null assertion.\n const value = unwrapExpression(node.init);\n if (value.type === 'ObjectExpression') handleStyleObject(value);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,sEACH;EACD,UAAU;GACR,aACE;GACF,mBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;AAChE,OAAI,QAAQ,IAAI,KAAK,CAAE;GACvB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAC3C,WAAQ,IAAI,KAAK;GAEjB,MAAM,UAAU,KAAK,WAAW,QAC7B,SAAyC,KAAK,SAAS,gBACzD;AACD,OAAI,QAAQ,SAAS,GAAG;AACtB,eAAW,KAAK;AAChB;;AAMF,WAAQ,OAAO;IAAE,MAAM,QAAQ;IAAI,WAAW;IAAqB,CAAC;AACpE,QAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,WAAY,YAAW,KAAK,MAAM;;AAIxD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,mBAAmB,MAAM;AACvB,QAAI,CAAC,KAAK,KAAM;IAGhB,MAAM,QAAQ,iBAAiB,KAAK,KAAK;AACzC,QAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;GAEjE,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {