@tenphi/eslint-plugin-tasty 1.0.3 → 1.0.5

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.
@@ -34,32 +34,51 @@ var prefer_shorthand_property_default = createRule({
34
34
  type: "suggestion",
35
35
  fixable: "code",
36
36
  docs: { description: "Suggest tasty shorthand when a native CSS property with a tasty alternative is used" },
37
- messages: { preferShorthand: "Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'." },
37
+ messages: {
38
+ preferShorthand: "Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.",
39
+ preferShorthandExtending: "'{{native}}' patches the base component's '{{property}}' from an extension layer. Expose a token in the base's '{{property}}' — e.g. padding: '$v-padding $h-padding' — and set it from here; '{{alternative}}' would replace the whole '{{property}}'."
40
+ },
38
41
  schema: []
39
42
  },
40
43
  defaultOptions: [],
41
44
  create(context) {
42
45
  const ctx = new TastyContext(context);
43
46
  function handleStyleObject(node) {
44
- if (!ctx.isStyleObject(node)) return;
47
+ const styleCtx = ctx.getStyleContext(node);
48
+ if (!styleCtx) return;
45
49
  for (const prop of node.properties) {
46
50
  if (prop.type !== "Property" || prop.computed) continue;
47
51
  const key = getKeyName(prop.key);
48
52
  if (key === null) continue;
49
53
  const mapping = SHORTHAND_MAPPING[key];
50
54
  const hint = shorthandHint(key, prop);
51
- if (mapping && hint) context.report({
52
- node: prop.key,
53
- messageId: "preferShorthand",
54
- data: {
55
- native: key,
56
- alternative: hint
57
- },
58
- fix(fixer) {
59
- if (!mapping.safeFix) return null;
60
- return fixer.replaceText(prop.key, mapping.property);
55
+ if (mapping && hint) {
56
+ if (styleCtx.isExtending) {
57
+ if (!ctx.isOwnedComponent(styleCtx.baseComponent)) continue;
58
+ context.report({
59
+ node: prop.key,
60
+ messageId: "preferShorthandExtending",
61
+ data: {
62
+ native: key,
63
+ alternative: hint,
64
+ property: mapping.property
65
+ }
66
+ });
67
+ continue;
61
68
  }
62
- });
69
+ context.report({
70
+ node: prop.key,
71
+ messageId: "preferShorthand",
72
+ data: {
73
+ native: key,
74
+ alternative: hint
75
+ },
76
+ fix(fixer) {
77
+ if (!mapping.safeFix) return null;
78
+ return fixer.replaceText(prop.key, mapping.property);
79
+ }
80
+ });
81
+ }
63
82
  }
64
83
  }
65
84
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand';\n\nconst CSS_WIDE_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/**\n * `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,\n * so it cannot express a CSS-wide keyword — `font: 'inherit'` emits\n * `font-family: inherit, …`, which is invalid and drops the inherit.\n *\n * `preset` is the property that handles these: a CSS-wide keyword used as the\n * preset name short-circuits token lookup and is emitted verbatim across the\n * whole typography group, so `preset: 'inherit'` yields a real\n * `font-family: inherit`. Point at that instead of `font`.\n */\nfunction shorthandHint(key: string, prop: TSESTree.Property): string | null {\n const mapping = SHORTHAND_MAPPING[key];\n if (!mapping) return null;\n if (key !== 'fontFamily') return mapping.hint;\n\n const value = getStringValue(prop.value)?.trim().toLowerCase();\n return value && CSS_WIDE_KEYWORDS.has(value)\n ? `preset: '${value}'`\n : mapping.hint;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const mapping = SHORTHAND_MAPPING[key];\n const hint = shorthandHint(key, prop);\n if (mapping && hint) {\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative: hint },\n fix(fixer) {\n // Only auto-fix the carry-over subset where the value passes\n // through unchanged (e.g. backgroundColor → fill, borderRadius →\n // radius). Directional / min/max / border-* renames change\n // semantics and stay report-only.\n if (!mapping.safeFix) return null;\n return fixer.replaceText(prop.key, mapping.property);\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,SAAS,cAAc,KAAa,MAAwC;CAC1E,MAAM,UAAU,kBAAkB;AAClC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,aAAc,QAAO,QAAQ;CAEzC,MAAM,QAAQ,eAAe,KAAK,MAAM,EAAE,MAAM,CAAC,aAAa;AAC9D,QAAO,SAAS,kBAAkB,IAAI,MAAM,GACxC,YAAY,MAAM,KAClB,QAAQ;;AAGd,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,qEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;IAElB,MAAM,UAAU,kBAAkB;IAClC,MAAM,OAAO,cAAc,KAAK,KAAK;AACrC,QAAI,WAAW,KACb,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE,QAAQ;MAAK,aAAa;MAAM;KACxC,IAAI,OAAO;AAKT,UAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,aAAO,MAAM,YAAY,KAAK,KAAK,QAAQ,SAAS;;KAEvD,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand' | 'preferShorthandExtending';\n\nconst CSS_WIDE_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/**\n * `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,\n * so it cannot express a CSS-wide keyword — `font: 'inherit'` emits\n * `font-family: inherit, …`, which is invalid and drops the inherit.\n *\n * `preset` is the property that handles these: a CSS-wide keyword used as the\n * preset name short-circuits token lookup and is emitted verbatim across the\n * whole typography group, so `preset: 'inherit'` yields a real\n * `font-family: inherit`. Point at that instead of `font`.\n */\nfunction shorthandHint(key: string, prop: TSESTree.Property): string | null {\n const mapping = SHORTHAND_MAPPING[key];\n if (!mapping) return null;\n if (key !== 'fontFamily') return mapping.hint;\n\n const value = getStringValue(prop.value)?.trim().toLowerCase();\n return value && CSS_WIDE_KEYWORDS.has(value)\n ? `preset: '${value}'`\n : mapping.hint;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\n preferShorthandExtending:\n \"'{{native}}' patches the base component's '{{property}}' from an extension layer. Expose a token in the base's '{{property}}' — e.g. padding: '$v-padding $h-padding' — and set it from here; '{{alternative}}' would replace the whole '{{property}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n const styleCtx = ctx.getStyleContext(node);\n if (!styleCtx) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const mapping = SHORTHAND_MAPPING[key];\n const hint = shorthandHint(key, prop);\n if (mapping && hint) {\n // An extension layer merges per key, so renaming the key replaces the\n // base component's whole `mapping.property` instead of patching the\n // one part written here — `paddingTop: '2x'` -> `padding: '2x top'`\n // drops the base's other three edges to 0. The token seam belongs in\n // the base component, which is another file and may not even be the\n // author's to edit, so this stays a report with no fix.\n if (styleCtx.isExtending) {\n // The token seam has to be added to the base component's own\n // definition, so the suggestion is only actionable when that file\n // belongs to this project. Over an imported base — a UI kit, say —\n // the author's remaining options are the longhand they already\n // wrote or a rewrite that would clobber the base, and a warning\n // recommending neither is just noise. `ownedSources` opts a\n // published-from-this-monorepo design system back in.\n if (!ctx.isOwnedComponent(styleCtx.baseComponent)) continue;\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthandExtending',\n data: {\n native: key,\n alternative: hint,\n property: mapping.property,\n },\n });\n\n continue;\n }\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative: hint },\n fix(fixer) {\n // Only auto-fix the carry-over subset where the value passes\n // through unchanged (e.g. backgroundColor → fill, borderRadius →\n // radius). Directional / min/max / border-* renames change\n // semantics and stay report-only.\n if (!mapping.safeFix) return null;\n return fixer.replaceText(prop.key, mapping.property);\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,SAAS,cAAc,KAAa,MAAwC;CAC1E,MAAM,UAAU,kBAAkB;AAClC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,aAAc,QAAO,QAAQ;CAEzC,MAAM,QAAQ,eAAe,KAAK,MAAM,EAAE,MAAM,CAAC,aAAa;AAC9D,QAAO,SAAS,kBAAkB,IAAI,MAAM,GACxC,YAAY,MAAM,KAClB,QAAQ;;AAGd,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,uFACH;EACD,UAAU;GACR,iBACE;GACF,0BACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;GAC1D,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAC1C,OAAI,CAAC,SAAU;AAEf,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;IAElB,MAAM,UAAU,kBAAkB;IAClC,MAAM,OAAO,cAAc,KAAK,KAAK;AACrC,QAAI,WAAW,MAAM;AAOnB,SAAI,SAAS,aAAa;AAQxB,UAAI,CAAC,IAAI,iBAAiB,SAAS,cAAc,CAAE;AAEnD,cAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACX,MAAM;QACJ,QAAQ;QACR,aAAa;QACb,UAAU,QAAQ;QACnB;OACF,CAAC;AAEF;;AAGF,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM;OAAE,QAAQ;OAAK,aAAa;OAAM;MACxC,IAAI,OAAO;AAKT,WAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,cAAO,MAAM,YAAY,KAAK,KAAK,QAAQ,SAAS;;MAEvD,CAAC;;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
package/dist/types.d.ts CHANGED
@@ -17,6 +17,17 @@ interface TastyValidationConfig {
17
17
  recipes?: string[];
18
18
  styles?: string[];
19
19
  importSources?: string[];
20
+ /**
21
+ * Import sources whose components this project can edit — a design system
22
+ * published from the same monorepo, for instance. Rules that would send the
23
+ * author to a base component's own definition stay quiet when the base comes
24
+ * from somewhere else, since that file is not theirs to change.
25
+ *
26
+ * Relative, absolute, `~`, `#` and `@/` specifiers, plus components declared
27
+ * in the same file, count as owned without being listed. `*` matches any run
28
+ * of characters, so `@my-org/*` covers every package under the scope.
29
+ */
30
+ ownedSources?: string[];
20
31
  }
21
32
  interface ResolvedConfig {
22
33
  tokens: false | string[];
@@ -27,6 +38,14 @@ interface ResolvedConfig {
27
38
  recipes: string[];
28
39
  styles: string[];
29
40
  importSources: string[];
41
+ /**
42
+ * Optional, unlike every key above it, and deliberately so: `ResolvedConfig` is
43
+ * part of the published surface, and a consumer's existing object literal must
44
+ * keep compiling when the plugin learns a new key. `loadConfig` always fills
45
+ * this in, so plugin code can read it directly; anything else should treat a
46
+ * missing value as the empty list. See `types.compat.ts` for the guard.
47
+ */
48
+ ownedSources?: string[];
30
49
  }
31
50
  //#endregion
32
51
  export { ResolvedConfig, TastyValidationConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,6 +56,7 @@
56
56
  "@typescript-eslint/rule-tester": "^8.56.1",
57
57
  "eslint": "^10.0.0",
58
58
  "eslint-config-prettier": "^10.1.8",
59
+ "known-css-properties": "^0.37.0",
59
60
  "prettier": "^3.8.1",
60
61
  "react": "^19.2.8",
61
62
  "tsdown": "^0.20.3",
@@ -78,6 +79,7 @@
78
79
  "lint:fix": "eslint src --fix",
79
80
  "format": "prettier --write \"src/**/*.ts\"",
80
81
  "format:check": "prettier --check \"src/**/*.ts\"",
82
+ "generate:css-properties": "node scripts/generate-css-properties.mjs",
81
83
  "changeset": "changeset",
82
84
  "version": "changeset version",
83
85
  "release": "changeset publish"