@tenphi/eslint-plugin-tasty 1.9.1 → 1.10.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/README.md CHANGED
@@ -227,7 +227,7 @@ not matched.
227
227
  | `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 |
228
228
  | `tasty/no-runtime-styles-mutation` | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead. Module-scope constants, imports, `undefined` and static `mergeStyles(...)` calls are fixed at load and allowed |
229
229
  | `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 |
230
- | `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop ([details](docs/rules/no-style-prop.md)) |
230
+ | `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction ([details](docs/rules/no-style-prop.md)) |
231
231
  | `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
232
232
  | `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
233
233
  | `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers |
@@ -384,6 +384,12 @@ attribute or spread property. JSX child comments can use
384
384
  `{/* eslint-disable-next-line tasty/no-classname-prop -- library requires this class */}`
385
385
  immediately above the affected element when the attribute is on that next line.
386
386
 
387
+ If `react/forbid-component-props` or `react/forbid-dom-props` also forbids
388
+ `style`, a `style` exception has to name both rules, and the React part hides
389
+ every other prop that rule forbids on the line. Set `tasty/no-style-prop`'s
390
+ `elements` option and remove `style` from the React rules, so one precise rule
391
+ handles it; see [replacing React's `style` restriction](docs/rules/no-style-prop.md#replacing-reacts-style-restriction).
392
+
387
393
  Suppression uses ESLint's normal directives. The plugin does not enforce comment
388
394
  reasons or infer exceptions automatically. A warning severity alone does not mean
389
395
  the value is valid: checks for unknown names, malformed syntax, state order, and
@@ -1,22 +1,46 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { TastyContext } from "../context.js";
3
- import { tastyComponentPropListeners } from "./tasty-component-props.js";
3
+ import { jsxPropListeners } from "./tasty-component-props.js";
4
4
 
5
5
  //#region src/rules/no-style-prop.ts
6
+ const KINDS = {
7
+ tasty: new Set(["tasty"]),
8
+ components: new Set(["tasty", "component"]),
9
+ all: new Set([
10
+ "tasty",
11
+ "component",
12
+ "element"
13
+ ])
14
+ };
6
15
  var no_style_prop_default = createRule({
7
16
  name: "no-style-prop",
8
17
  meta: {
9
18
  type: "suggestion",
10
19
  docs: { description: "Prefer tokens over React inline style on Tasty components" },
11
- messages: { noStyleProp: "Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason." },
12
- schema: []
20
+ messages: {
21
+ noStyleProp: "Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.",
22
+ noInlineStyle: "Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason."
23
+ },
24
+ schema: [{
25
+ type: "object",
26
+ properties: { elements: {
27
+ type: "string",
28
+ enum: [
29
+ "tasty",
30
+ "components",
31
+ "all"
32
+ ]
33
+ } },
34
+ additionalProperties: false
35
+ }]
13
36
  },
14
- defaultOptions: [],
15
- create(context) {
16
- return tastyComponentPropListeners(new TastyContext(context), (name, node) => {
17
- if (name === "style") context.report({
37
+ defaultOptions: [{ elements: "tasty" }],
38
+ create(context, [{ elements = "tasty" }]) {
39
+ return jsxPropListeners(new TastyContext(context), KINDS[elements], (name, node, kind) => {
40
+ if (name !== "style") return;
41
+ context.report({
18
42
  node,
19
- messageId: "noStyleProp"
43
+ messageId: kind === "tasty" ? "noStyleProp" : "noInlineStyle"
20
44
  });
21
45
  });
22
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { tastyComponentPropListeners } from './tasty-component-props.js';\n\nexport default createRule<[], 'noStyleProp'>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n return tastyComponentPropListeners(ctx, (name, node) => {\n if (name === 'style') context.report({ node, messageId: 'noStyleProp' });\n });\n },\n});\n"],"mappings":";;;;;AAIA,4BAAe,WAA8B;CAC3C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU,EACR,aACE,0SACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;AAEd,SAAO,4BADK,IAAI,aAAa,QAAQ,GACI,MAAM,SAAS;AACtD,OAAI,SAAS,QAAS,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAe,CAAC;IACxE;;CAEL,CAAC"}
1
+ {"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport {\n type JSXElementKind,\n jsxPropListeners,\n} from './tasty-component-props.js';\n\ntype Elements = 'tasty' | 'components' | 'all';\n\nconst KINDS: Record<Elements, ReadonlySet<JSXElementKind>> = {\n tasty: new Set(['tasty']),\n components: new Set(['tasty', 'component']),\n all: new Set(['tasty', 'component', 'element']),\n};\n\nexport default createRule<\n [{ elements?: Elements }],\n 'noStyleProp' | 'noInlineStyle'\n>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n noInlineStyle:\n \"Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n elements: { type: 'string', enum: ['tasty', 'components', 'all'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ elements: 'tasty' }],\n create(context, [{ elements = 'tasty' }]) {\n const ctx = new TastyContext(context);\n return jsxPropListeners(ctx, KINDS[elements], (name, node, kind) => {\n if (name !== 'style') return;\n context.report({\n node,\n messageId: kind === 'tasty' ? 'noStyleProp' : 'noInlineStyle',\n });\n });\n },\n});\n"],"mappings":";;;;;AASA,MAAM,QAAuD;CAC3D,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC;CACzB,YAAY,IAAI,IAAI,CAAC,SAAS,YAAY,CAAC;CAC3C,KAAK,IAAI,IAAI;EAAC;EAAS;EAAa;EAAU,CAAC;CAChD;AAED,4BAAe,WAGb;CACA,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU;GACR,aACE;GACF,eACE;GACH;EACD,QAAQ,CACN;GACE,MAAM;GACN,YAAY,EACV,UAAU;IAAE,MAAM;IAAU,MAAM;KAAC;KAAS;KAAc;KAAM;IAAE,EACnE;GACD,sBAAsB;GACvB,CACF;EACF;CACD,gBAAgB,CAAC,EAAE,UAAU,SAAS,CAAC;CACvC,OAAO,SAAS,CAAC,EAAE,WAAW,YAAY;AAExC,SAAO,iBADK,IAAI,aAAa,QAAQ,EACR,MAAM,YAAY,MAAM,MAAM,SAAS;AAClE,OAAI,SAAS,QAAS;AACtB,WAAQ,OAAO;IACb;IACA,WAAW,SAAS,UAAU,gBAAgB;IAC/C,CAAC;IACF;;CAEL,CAAC"}
@@ -1,33 +1,46 @@
1
1
  import { getKeyName, unwrapExpression } from "../utils.js";
2
2
 
3
3
  //#region src/rules/tasty-component-props.ts
4
- /** Inspect explicit props and inline object spreads without evaluating spread bindings. */
5
- function tastyComponentPropListeners(ctx, check) {
6
- function visitSpread(node) {
4
+ /**
5
+ * Inspect explicit props and inline object spreads on every JSX element without
6
+ * evaluating spread bindings. `kinds` limits which elements are classified and
7
+ * checked, so a Tasty-only rule never runs component recognition elsewhere.
8
+ */
9
+ function jsxPropListeners(ctx, kinds, check) {
10
+ function visitSpread(node, kind) {
7
11
  node = unwrapExpression(node);
8
12
  if (node.type !== "ObjectExpression") return;
9
- for (const prop of node.properties) if (prop.type === "SpreadElement") visitSpread(prop.argument);
13
+ for (const prop of node.properties) if (prop.type === "SpreadElement") visitSpread(prop.argument, kind);
10
14
  else {
11
15
  const name = prop.computed ? getKeyName(unwrapExpression(prop.key)) : getKeyName(prop.key);
12
- if (name !== null && (!prop.computed || unwrapExpression(prop.key).type === "Literal")) check(name, prop);
16
+ if (name !== null && (!prop.computed || unwrapExpression(prop.key).type === "Literal")) check(name, prop, kind);
13
17
  }
14
18
  }
19
+ function classify(node) {
20
+ let name = node.name;
21
+ while (name.type === "JSXMemberExpression") name = name.object;
22
+ if (name.type !== "JSXIdentifier" || node.name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return kinds.has("element") ? "element" : null;
23
+ if (kinds.has("tasty") && ctx.isTastyJSXComponent(node, name.name)) return "tasty";
24
+ return kinds.has("component") ? "component" : null;
25
+ }
15
26
  return {
16
27
  ImportDeclaration(node) {
17
28
  ctx.trackImport(node);
18
29
  },
19
30
  JSXOpeningElement(node) {
20
- let name = node.name;
21
- while (name.type === "JSXMemberExpression") name = name.object;
22
- if (name.type !== "JSXIdentifier") return;
23
- if (node.name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return;
24
- if (!ctx.isTastyJSXComponent(node, name.name)) return;
25
- for (const attribute of node.attributes) if (attribute.type === "JSXSpreadAttribute") visitSpread(attribute.argument);
26
- else if (attribute.name.type === "JSXIdentifier") check(attribute.name.name, attribute);
31
+ const kind = classify(node);
32
+ if (!kind) return;
33
+ for (const attribute of node.attributes) if (attribute.type === "JSXSpreadAttribute") visitSpread(attribute.argument, kind);
34
+ else if (attribute.name.type === "JSXIdentifier") check(attribute.name.name, attribute, kind);
27
35
  }
28
36
  };
29
37
  }
38
+ const TASTY_ONLY = new Set(["tasty"]);
39
+ /** Inspect explicit props and inline object spreads on recognized Tasty components. */
40
+ function tastyComponentPropListeners(ctx, check) {
41
+ return jsxPropListeners(ctx, TASTY_ONLY, (name, node) => check(name, node));
42
+ }
30
43
 
31
44
  //#endregion
32
- export { tastyComponentPropListeners };
45
+ export { jsxPropListeners, tastyComponentPropListeners };
33
46
  //# sourceMappingURL=tasty-component-props.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tasty-component-props.js","names":[],"sources":["../../src/rules/tasty-component-props.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\n/** Inspect explicit props and inline object spreads without evaluating spread bindings. */\nexport function tastyComponentPropListeners(\n ctx: TastyContext,\n check: (name: string, node: TSESTree.Node) => void,\n) {\n function visitSpread(node: TSESTree.Node): void {\n node = unwrapExpression(node);\n if (node.type !== 'ObjectExpression') return;\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n visitSpread(prop.argument);\n } else {\n const name = prop.computed\n ? getKeyName(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // A computed identifier is dynamic; a computed string literal is visible.\n if (\n name !== null &&\n (!prop.computed || unwrapExpression(prop.key).type === 'Literal')\n ) {\n check(name, prop);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n JSXOpeningElement(node: TSESTree.JSXOpeningElement) {\n let name = node.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n if (name.type !== 'JSXIdentifier') return;\n if (node.name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name))\n return;\n if (!ctx.isTastyJSXComponent(node, name.name)) return;\n for (const attribute of node.attributes) {\n if (attribute.type === 'JSXSpreadAttribute')\n visitSpread(attribute.argument);\n else if (attribute.name.type === 'JSXIdentifier')\n check(attribute.name.name, attribute);\n }\n },\n };\n}\n"],"mappings":";;;;AAKA,SAAgB,4BACd,KACA,OACA;CACA,SAAS,YAAY,MAA2B;AAC9C,SAAO,iBAAiB,KAAK;AAC7B,MAAI,KAAK,SAAS,mBAAoB;AACtC,OAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,gBAChB,aAAY,KAAK,SAAS;OACrB;GACL,MAAM,OAAO,KAAK,WACd,WAAW,iBAAiB,KAAK,IAAI,CAAC,GACtC,WAAW,KAAK,IAAI;AAExB,OACE,SAAS,SACR,CAAC,KAAK,YAAY,iBAAiB,KAAK,IAAI,CAAC,SAAS,WAEvD,OAAM,MAAM,KAAK;;;AAMzB,QAAO;EACL,kBAAkB,MAAkC;AAClD,OAAI,YAAY,KAAK;;EAEvB,kBAAkB,MAAkC;GAClD,IAAI,OAAO,KAAK;AAChB,UAAO,KAAK,SAAS,sBAAuB,QAAO,KAAK;AACxD,OAAI,KAAK,SAAS,gBAAiB;AACnC,OAAI,KAAK,KAAK,SAAS,mBAAmB,CAAC,SAAS,KAAK,KAAK,KAAK,CACjE;AACF,OAAI,CAAC,IAAI,oBAAoB,MAAM,KAAK,KAAK,CAAE;AAC/C,QAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,qBACrB,aAAY,UAAU,SAAS;YACxB,UAAU,KAAK,SAAS,gBAC/B,OAAM,UAAU,KAAK,MAAM,UAAU;;EAG5C"}
1
+ {"version":3,"file":"tasty-component-props.js","names":[],"sources":["../../src/rules/tasty-component-props.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\n/**\n * What a JSX element is, for prop rules: a recognized Tasty component, any\n * other component (capitalized or member tag), or a native element.\n */\nexport type JSXElementKind = 'tasty' | 'component' | 'element';\n\n/**\n * Inspect explicit props and inline object spreads on every JSX element without\n * evaluating spread bindings. `kinds` limits which elements are classified and\n * checked, so a Tasty-only rule never runs component recognition elsewhere.\n */\nexport function jsxPropListeners(\n ctx: TastyContext,\n kinds: ReadonlySet<JSXElementKind>,\n check: (name: string, node: TSESTree.Node, kind: JSXElementKind) => void,\n) {\n function visitSpread(node: TSESTree.Node, kind: JSXElementKind): void {\n node = unwrapExpression(node);\n if (node.type !== 'ObjectExpression') return;\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n visitSpread(prop.argument, kind);\n } else {\n const name = prop.computed\n ? getKeyName(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // A computed identifier is dynamic; a computed string literal is visible.\n if (\n name !== null &&\n (!prop.computed || unwrapExpression(prop.key).type === 'Literal')\n ) {\n check(name, prop, kind);\n }\n }\n }\n }\n\n function classify(node: TSESTree.JSXOpeningElement): JSXElementKind | null {\n let name = node.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n // `<svg:rect>` and lowercase tags are native; `<motion.div>` is a component.\n if (\n name.type !== 'JSXIdentifier' ||\n (node.name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name))\n ) {\n return kinds.has('element') ? 'element' : null;\n }\n if (kinds.has('tasty') && ctx.isTastyJSXComponent(node, name.name))\n return 'tasty';\n return kinds.has('component') ? 'component' : null;\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n JSXOpeningElement(node: TSESTree.JSXOpeningElement) {\n const kind = classify(node);\n if (!kind) return;\n for (const attribute of node.attributes) {\n if (attribute.type === 'JSXSpreadAttribute')\n visitSpread(attribute.argument, kind);\n else if (attribute.name.type === 'JSXIdentifier')\n check(attribute.name.name, attribute, kind);\n }\n },\n };\n}\n\nconst TASTY_ONLY = new Set<JSXElementKind>(['tasty']);\n\n/** Inspect explicit props and inline object spreads on recognized Tasty components. */\nexport function tastyComponentPropListeners(\n ctx: TastyContext,\n check: (name: string, node: TSESTree.Node) => void,\n) {\n return jsxPropListeners(ctx, TASTY_ONLY, (name, node) => check(name, node));\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,iBACd,KACA,OACA,OACA;CACA,SAAS,YAAY,MAAqB,MAA4B;AACpE,SAAO,iBAAiB,KAAK;AAC7B,MAAI,KAAK,SAAS,mBAAoB;AACtC,OAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,gBAChB,aAAY,KAAK,UAAU,KAAK;OAC3B;GACL,MAAM,OAAO,KAAK,WACd,WAAW,iBAAiB,KAAK,IAAI,CAAC,GACtC,WAAW,KAAK,IAAI;AAExB,OACE,SAAS,SACR,CAAC,KAAK,YAAY,iBAAiB,KAAK,IAAI,CAAC,SAAS,WAEvD,OAAM,MAAM,MAAM,KAAK;;;CAM/B,SAAS,SAAS,MAAyD;EACzE,IAAI,OAAO,KAAK;AAChB,SAAO,KAAK,SAAS,sBAAuB,QAAO,KAAK;AAExD,MACE,KAAK,SAAS,mBACb,KAAK,KAAK,SAAS,mBAAmB,CAAC,SAAS,KAAK,KAAK,KAAK,CAEhE,QAAO,MAAM,IAAI,UAAU,GAAG,YAAY;AAE5C,MAAI,MAAM,IAAI,QAAQ,IAAI,IAAI,oBAAoB,MAAM,KAAK,KAAK,CAChE,QAAO;AACT,SAAO,MAAM,IAAI,YAAY,GAAG,cAAc;;AAGhD,QAAO;EACL,kBAAkB,MAAkC;AAClD,OAAI,YAAY,KAAK;;EAEvB,kBAAkB,MAAkC;GAClD,MAAM,OAAO,SAAS,KAAK;AAC3B,OAAI,CAAC,KAAM;AACX,QAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,qBACrB,aAAY,UAAU,UAAU,KAAK;YAC9B,UAAU,KAAK,SAAS,gBAC/B,OAAM,UAAU,KAAK,MAAM,WAAW,KAAK;;EAGlD;;AAGH,MAAM,aAAa,IAAI,IAAoB,CAAC,QAAQ,CAAC;;AAGrD,SAAgB,4BACd,KACA,OACA;AACA,QAAO,iBAAiB,KAAK,aAAa,MAAM,SAAS,MAAM,MAAM,KAAK,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {