@tenphi/eslint-plugin-tasty 0.1.0 → 0.2.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/parser.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BUILT_IN_UNITS } from "./constants.mjs";
2
- import { StyleParser } from "@tenphi/tasty/parser";
2
+ import { StyleParser } from "@tenphi/tasty/core";
3
3
 
4
4
  //#region src/parser.ts
5
5
  const BUILT_IN_UNIT_STUBS = {
@@ -1 +1 @@
1
- {"version":3,"file":"parser.mjs","names":[],"sources":["../src/parser.ts"],"sourcesContent":["import { StyleParser } from '@tenphi/tasty/parser';\nimport type { ParserOptions } from '@tenphi/tasty/parser';\nimport type { ResolvedConfig } from './types.js';\nimport { BUILT_IN_UNITS } from './constants.js';\n\nconst BUILT_IN_UNIT_STUBS: Record<string, string> = {\n x: 'var(--gap)',\n r: 'var(--radius)',\n cr: 'var(--card-radius)',\n bw: 'var(--border-width)',\n ow: 'var(--outline-width)',\n fs: 'var(--font-size)',\n lh: 'var(--line-height)',\n sf: 'var(--scale-factor)',\n};\n\nlet cachedParser: { parser: StyleParser; configKey: string } | null = null;\n\nfunction configKey(config: ResolvedConfig): string {\n const units = config.units === false ? 'false' : JSON.stringify(config.units);\n const funcs = config.funcs === false ? 'false' : JSON.stringify(config.funcs);\n return `${units}|${funcs}`;\n}\n\n/**\n * Build a StyleParser from the ESLint plugin config.\n * Unit handlers are stubs (they produce placeholder CSS) because we only\n * care about bucket classification, not actual CSS output.\n */\nexport function getParser(config: ResolvedConfig): StyleParser {\n const key = configKey(config);\n if (cachedParser && cachedParser.configKey === key) {\n return cachedParser.parser;\n }\n\n const units: Record<string, string> = { ...BUILT_IN_UNIT_STUBS };\n\n if (Array.isArray(config.units)) {\n for (const u of config.units) {\n if (!units[u]) {\n units[u] = `var(--${u})`;\n }\n }\n } else if (config.units !== false) {\n for (const u of BUILT_IN_UNITS) {\n units[u] = BUILT_IN_UNIT_STUBS[u] ?? `var(--${u})`;\n }\n }\n\n const funcs: ParserOptions['funcs'] = {};\n if (Array.isArray(config.funcs)) {\n for (const f of config.funcs) {\n funcs[f] = (groups) => groups.map((g) => g.output).join(', ');\n }\n }\n\n const opts: ParserOptions = {\n units: config.units === false ? undefined : units,\n funcs: Object.keys(funcs).length > 0 ? funcs : undefined,\n };\n\n const parser = new StyleParser(opts);\n\n cachedParser = { parser, configKey: key };\n return parser;\n}\n"],"mappings":";;;;AAKA,MAAM,sBAA8C;CAClD,GAAG;CACH,GAAG;CACH,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAED,IAAI,eAAkE;AAEtE,SAAS,UAAU,QAAgC;AAGjD,QAAO,GAFO,OAAO,UAAU,QAAQ,UAAU,KAAK,UAAU,OAAO,MAAM,CAE7D,GADF,OAAO,UAAU,QAAQ,UAAU,KAAK,UAAU,OAAO,MAAM;;;;;;;AAS/E,SAAgB,UAAU,QAAqC;CAC7D,MAAM,MAAM,UAAU,OAAO;AAC7B,KAAI,gBAAgB,aAAa,cAAc,IAC7C,QAAO,aAAa;CAGtB,MAAM,QAAgC,EAAE,GAAG,qBAAqB;AAEhE,KAAI,MAAM,QAAQ,OAAO,MAAM,EAC7B;OAAK,MAAM,KAAK,OAAO,MACrB,KAAI,CAAC,MAAM,GACT,OAAM,KAAK,SAAS,EAAE;YAGjB,OAAO,UAAU,MAC1B,MAAK,MAAM,KAAK,eACd,OAAM,KAAK,oBAAoB,MAAM,SAAS,EAAE;CAIpD,MAAM,QAAgC,EAAE;AACxC,KAAI,MAAM,QAAQ,OAAO,MAAM,CAC7B,MAAK,MAAM,KAAK,OAAO,MACrB,OAAM,MAAM,WAAW,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC,KAAK,KAAK;CASjE,MAAM,SAAS,IAAI,YALS;EAC1B,OAAO,OAAO,UAAU,QAAQ,SAAY;EAC5C,OAAO,OAAO,KAAK,MAAM,CAAC,SAAS,IAAI,QAAQ;EAChD,CAEmC;AAEpC,gBAAe;EAAE;EAAQ,WAAW;EAAK;AACzC,QAAO"}
1
+ {"version":3,"file":"parser.mjs","names":[],"sources":["../src/parser.ts"],"sourcesContent":["import { StyleParser } from '@tenphi/tasty/core';\nimport type { ParserOptions } from '@tenphi/tasty/core';\nimport type { ResolvedConfig } from './types.js';\nimport { BUILT_IN_UNITS } from './constants.js';\n\nconst BUILT_IN_UNIT_STUBS: Record<string, string> = {\n x: 'var(--gap)',\n r: 'var(--radius)',\n cr: 'var(--card-radius)',\n bw: 'var(--border-width)',\n ow: 'var(--outline-width)',\n fs: 'var(--font-size)',\n lh: 'var(--line-height)',\n sf: 'var(--scale-factor)',\n};\n\nlet cachedParser: { parser: StyleParser; configKey: string } | null = null;\n\nfunction configKey(config: ResolvedConfig): string {\n const units = config.units === false ? 'false' : JSON.stringify(config.units);\n const funcs = config.funcs === false ? 'false' : JSON.stringify(config.funcs);\n return `${units}|${funcs}`;\n}\n\n/**\n * Build a StyleParser from the ESLint plugin config.\n * Unit handlers are stubs (they produce placeholder CSS) because we only\n * care about bucket classification, not actual CSS output.\n */\nexport function getParser(config: ResolvedConfig): StyleParser {\n const key = configKey(config);\n if (cachedParser && cachedParser.configKey === key) {\n return cachedParser.parser;\n }\n\n const units: Record<string, string> = { ...BUILT_IN_UNIT_STUBS };\n\n if (Array.isArray(config.units)) {\n for (const u of config.units) {\n if (!units[u]) {\n units[u] = `var(--${u})`;\n }\n }\n } else if (config.units !== false) {\n for (const u of BUILT_IN_UNITS) {\n units[u] = BUILT_IN_UNIT_STUBS[u] ?? `var(--${u})`;\n }\n }\n\n const funcs: ParserOptions['funcs'] = {};\n if (Array.isArray(config.funcs)) {\n for (const f of config.funcs) {\n funcs[f] = (groups) => groups.map((g) => g.output).join(', ');\n }\n }\n\n const opts: ParserOptions = {\n units: config.units === false ? undefined : units,\n funcs: Object.keys(funcs).length > 0 ? funcs : undefined,\n };\n\n const parser = new StyleParser(opts);\n\n cachedParser = { parser, configKey: key };\n return parser;\n}\n"],"mappings":";;;;AAKA,MAAM,sBAA8C;CAClD,GAAG;CACH,GAAG;CACH,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAED,IAAI,eAAkE;AAEtE,SAAS,UAAU,QAAgC;AAGjD,QAAO,GAFO,OAAO,UAAU,QAAQ,UAAU,KAAK,UAAU,OAAO,MAAM,CAE7D,GADF,OAAO,UAAU,QAAQ,UAAU,KAAK,UAAU,OAAO,MAAM;;;;;;;AAS/E,SAAgB,UAAU,QAAqC;CAC7D,MAAM,MAAM,UAAU,OAAO;AAC7B,KAAI,gBAAgB,aAAa,cAAc,IAC7C,QAAO,aAAa;CAGtB,MAAM,QAAgC,EAAE,GAAG,qBAAqB;AAEhE,KAAI,MAAM,QAAQ,OAAO,MAAM,EAC7B;OAAK,MAAM,KAAK,OAAO,MACrB,KAAI,CAAC,MAAM,GACT,OAAM,KAAK,SAAS,EAAE;YAGjB,OAAO,UAAU,MAC1B,MAAK,MAAM,KAAK,eACd,OAAM,KAAK,oBAAoB,MAAM,SAAS,EAAE;CAIpD,MAAM,QAAgC,EAAE;AACxC,KAAI,MAAM,QAAQ,OAAO,MAAM,CAC7B,MAAK,MAAM,KAAK,OAAO,MACrB,OAAM,MAAM,WAAW,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC,KAAK,KAAK;CASjE,MAAM,SAAS,IAAI,YALS;EAC1B,OAAO,OAAO,UAAU,QAAQ,SAAY;EAC5C,OAAO,OAAO,KAAK,MAAM,CAAC,SAAS,IAAI,QAAQ;EAChD,CAEmC;AAEpC,gBAAe;EAAE;EAAQ,WAAW;EAAK;AACzC,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"property-expectations.mjs","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max'];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = [\n 'static',\n 'relative',\n 'absolute',\n 'fixed',\n 'sticky',\n];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: COLOR_ONLY,\n color: COLOR_ONLY,\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: COLOR_ONLY,\n\n border: {\n acceptsColor: true,\n acceptsMods: [...DIRECTIONAL_MODS, ...BORDER_STYLE_MODS],\n },\n outline: {\n acceptsColor: true,\n acceptsMods: BORDER_STYLE_MODS,\n },\n\n radius: {\n acceptsColor: false,\n acceptsMods: [\n ...RADIUS_DIRECTIONAL_MODS,\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n ],\n },\n\n padding: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n paddingInline: VALUE_ONLY,\n paddingBlock: VALUE_ONLY,\n margin: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n fade: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n inset: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n\n width: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n height: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n gap: VALUE_ONLY,\n columnGap: VALUE_ONLY,\n rowGap: VALUE_ONLY,\n flexBasis: VALUE_ONLY,\n flexGrow: VALUE_ONLY,\n flexShrink: VALUE_ONLY,\n flex: VALUE_ONLY,\n order: VALUE_ONLY,\n zIndex: VALUE_ONLY,\n opacity: VALUE_ONLY,\n aspectRatio: VALUE_ONLY,\n lineClamp: VALUE_ONLY,\n tabSize: VALUE_ONLY,\n\n flow: { acceptsColor: false, acceptsMods: FLOW_MODS },\n display: {\n acceptsColor: false,\n acceptsMods: [\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'none',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n ],\n },\n overflow: { acceptsColor: false, acceptsMods: OVERFLOW_MODS },\n position: { acceptsColor: false, acceptsMods: POSITION_MODS },\n};\n\n/**\n * Get expectations for a property. Properties not in the map\n * are treated as passthrough (accept everything).\n */\nexport function getExpectation(property: string): PropertyExpectation {\n return PROPERTY_EXPECTATIONS[property] ?? PASSTHROUGH;\n}\n"],"mappings":";AAsBA,MAAM,mBAAmB;CAAC;CAAO;CAAS;CAAU;CAAO;AAC3D,MAAM,0BAA0B;CAC9B,GAAG;CACH;CACA;CACA;CACA;CACD;AACD,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB,CAAC,OAAO,MAAM;AACrC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,cAAmC;CACvC,cAAc;CACd,aAAa;CACd;AAED,MAAa,wBAA6D;CACxE,MAAM;CACN,OAAO;CACP,YAAY;CACZ,aAAa;CACb,QAAQ;CAER,QAAQ;EACN,cAAc;EACd,aAAa,CAAC,GAAG,kBAAkB,GAAG,kBAAkB;EACzD;CACD,SAAS;EACP,cAAc;EACd,aAAa;EACd;CAED,QAAQ;EACN,cAAc;EACd,aAAa;GACX,GAAG;GACH;GACA;GACA;GACA;GACD;EACF;CAED,SAAS;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC/D,eAAe;CACf,cAAc;CACd,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC9D,MAAM;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC5D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAkB;CAE7D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAgB;CAC3D,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAgB;CAE5D,KAAK;CACL,WAAW;CACX,QAAQ;CACR,WAAW;CACX,UAAU;CACV,YAAY;CACZ,MAAM;CACN,OAAO;CACP,QAAQ;CACR,SAAS;CACT,aAAa;CACb,WAAW;CACX,SAAS;CAET,MAAM;EAAE,cAAc;EAAO,aAAa;EAAW;CACrD,SAAS;EACP,cAAc;EACd,aAAa;GACX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACF;CACD,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC7D,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC9D;;;;;AAMD,SAAgB,eAAe,UAAuC;AACpE,QAAO,sBAAsB,aAAa"}
1
+ {"version":3,"file":"property-expectations.mjs","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max'];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: COLOR_ONLY,\n color: COLOR_ONLY,\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: COLOR_ONLY,\n\n border: {\n acceptsColor: true,\n acceptsMods: [...DIRECTIONAL_MODS, ...BORDER_STYLE_MODS],\n },\n outline: {\n acceptsColor: true,\n acceptsMods: BORDER_STYLE_MODS,\n },\n\n radius: {\n acceptsColor: false,\n acceptsMods: [\n ...RADIUS_DIRECTIONAL_MODS,\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n ],\n },\n\n padding: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n paddingInline: VALUE_ONLY,\n paddingBlock: VALUE_ONLY,\n margin: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n fade: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n inset: { acceptsColor: false, acceptsMods: DIRECTIONAL_MODS },\n\n width: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n height: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n gap: VALUE_ONLY,\n columnGap: VALUE_ONLY,\n rowGap: VALUE_ONLY,\n flexBasis: VALUE_ONLY,\n flexGrow: VALUE_ONLY,\n flexShrink: VALUE_ONLY,\n flex: VALUE_ONLY,\n order: VALUE_ONLY,\n zIndex: VALUE_ONLY,\n opacity: VALUE_ONLY,\n aspectRatio: VALUE_ONLY,\n lineClamp: VALUE_ONLY,\n tabSize: VALUE_ONLY,\n\n flow: { acceptsColor: false, acceptsMods: FLOW_MODS },\n display: {\n acceptsColor: false,\n acceptsMods: [\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'none',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n ],\n },\n overflow: { acceptsColor: false, acceptsMods: OVERFLOW_MODS },\n position: { acceptsColor: false, acceptsMods: POSITION_MODS },\n};\n\n/**\n * Get expectations for a property. Properties not in the map\n * are treated as passthrough (accept everything).\n */\nexport function getExpectation(property: string): PropertyExpectation {\n return PROPERTY_EXPECTATIONS[property] ?? PASSTHROUGH;\n}\n"],"mappings":";AAsBA,MAAM,mBAAmB;CAAC;CAAO;CAAS;CAAU;CAAO;AAC3D,MAAM,0BAA0B;CAC9B,GAAG;CACH;CACA;CACA;CACA;CACD;AACD,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB,CAAC,OAAO,MAAM;AACrC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CAAC;CAAU;CAAY;CAAY;CAAS;CAAS;AAE3E,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,cAAmC;CACvC,cAAc;CACd,aAAa;CACd;AAED,MAAa,wBAA6D;CACxE,MAAM;CACN,OAAO;CACP,YAAY;CACZ,aAAa;CACb,QAAQ;CAER,QAAQ;EACN,cAAc;EACd,aAAa,CAAC,GAAG,kBAAkB,GAAG,kBAAkB;EACzD;CACD,SAAS;EACP,cAAc;EACd,aAAa;EACd;CAED,QAAQ;EACN,cAAc;EACd,aAAa;GACX,GAAG;GACH;GACA;GACA;GACA;GACD;EACF;CAED,SAAS;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC/D,eAAe;CACf,cAAc;CACd,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC9D,MAAM;EAAE,cAAc;EAAO,aAAa;EAAkB;CAC5D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAkB;CAE7D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAgB;CAC3D,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAgB;CAE5D,KAAK;CACL,WAAW;CACX,QAAQ;CACR,WAAW;CACX,UAAU;CACV,YAAY;CACZ,MAAM;CACN,OAAO;CACP,QAAQ;CACR,SAAS;CACT,aAAa;CACb,WAAW;CACX,SAAS;CAET,MAAM;EAAE,cAAc;EAAO,aAAa;EAAW;CACrD,SAAS;EACP,cAAc;EACd,aAAa;GACX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACF;CACD,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC7D,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC9D;;;;;AAMD,SAAgB,eAAe,UAAuC;AACpE,QAAO,sBAAsB,aAAa"}
@@ -16,11 +16,11 @@ var valid_recipe_default = createRule({
16
16
  const ctx = new TastyContext(context);
17
17
  function checkRecipeValue(value, node) {
18
18
  if (ctx.config.recipes.length === 0) return;
19
- const sections = value.split("|");
19
+ const sections = value.split("/");
20
20
  for (const section of sections) {
21
21
  const names = section.trim().split(/\s+/);
22
22
  for (const name of names) {
23
- if (name.length === 0) continue;
23
+ if (name.length === 0 || name === "none") continue;
24
24
  if (!ctx.config.recipes.includes(name)) context.report({
25
25
  node,
26
26
  messageId: "unknownRecipe",
@@ -1 +1 @@
1
- {"version":3,"file":"valid-recipe.mjs","names":[],"sources":["../../src/rules/valid-recipe.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'unknownRecipe';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-recipe',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate recipe property values against config',\n },\n messages: {\n unknownRecipe: \"Unknown recipe '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRecipeValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.recipes.length === 0) return;\n\n // Split by | for pre/post merge separation\n const sections = value.split('|');\n for (const section of sections) {\n const names = section.trim().split(/\\s+/);\n for (const name of names) {\n if (name.length === 0) continue;\n if (!ctx.config.recipes.includes(name)) {\n context.report({\n node,\n messageId: 'unknownRecipe',\n data: { name },\n });\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(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 !== 'recipe') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRecipeValue(str, prop.value);\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU,EACR,eAAe,8BAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;AAClE,OAAI,IAAI,OAAO,QAAQ,WAAW,EAAG;GAGrC,MAAM,WAAW,MAAM,MAAM,IAAI;AACjC,QAAK,MAAM,WAAW,UAAU;IAC9B,MAAM,QAAQ,QAAQ,MAAM,CAAC,MAAM,MAAM;AACzC,SAAK,MAAM,QAAQ,OAAO;AACxB,SAAI,KAAK,WAAW,EAAG;AACvB,SAAI,CAAC,IAAI,OAAO,QAAQ,SAAS,KAAK,CACpC,SAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM;MACf,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,SAAU;KAEtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,IACF,kBAAiB,KAAK,KAAK,MAAM;;;GAIxC;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-recipe.mjs","names":[],"sources":["../../src/rules/valid-recipe.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'unknownRecipe';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-recipe',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate recipe property values against config',\n },\n messages: {\n unknownRecipe: \"Unknown recipe '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRecipeValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.recipes.length === 0) return;\n\n // Split by / for pre/post merge separation\n const sections = value.split('/');\n for (const section of sections) {\n const names = section.trim().split(/\\s+/);\n for (const name of names) {\n if (name.length === 0 || name === 'none') continue;\n if (!ctx.config.recipes.includes(name)) {\n context.report({\n node,\n messageId: 'unknownRecipe',\n data: { name },\n });\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(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 !== 'recipe') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRecipeValue(str, prop.value);\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU,EACR,eAAe,8BAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;AAClE,OAAI,IAAI,OAAO,QAAQ,WAAW,EAAG;GAGrC,MAAM,WAAW,MAAM,MAAM,IAAI;AACjC,QAAK,MAAM,WAAW,UAAU;IAC9B,MAAM,QAAQ,QAAQ,MAAM,CAAC,MAAM,MAAM;AACzC,SAAK,MAAM,QAAQ,OAAO;AACxB,SAAI,KAAK,WAAW,KAAK,SAAS,OAAQ;AAC1C,SAAI,CAAC,IAAI,OAAO,QAAQ,SAAS,KAAK,CACpC,SAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM;MACf,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,SAAU;KAEtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,IACF,kBAAiB,KAAK,KAAK,MAAM;;;GAIxC;;CAEJ,CAAC"}
@@ -1,15 +1,72 @@
1
1
  import { createRule } from "../create-rule.mjs";
2
2
  import { TastyContext } from "../context.mjs";
3
- import { getKeyName, getStringValue, validateStateKey } from "../utils.mjs";
3
+ import { getKeyName, getStringValue } from "../utils.mjs";
4
+ import { createStateParserContext, parseStateKey, setGlobalPredefinedStates } from "@tenphi/tasty/core";
4
5
 
5
6
  //#region src/rules/valid-state-key.ts
7
+ function collectIssues(node, knownPredefined) {
8
+ const issues = [];
9
+ function walk(n) {
10
+ if (n.kind === "true" || n.kind === "false") return;
11
+ if (n.kind === "compound") {
12
+ for (const child of n.children) walk(child);
13
+ return;
14
+ }
15
+ switch (n.type) {
16
+ case "media":
17
+ if (n.subtype === "dimension" && !n.dimension && !n.lowerBound && !n.upperBound) issues.push(`Empty or invalid @media dimension query in '${n.raw}'.`);
18
+ break;
19
+ case "container":
20
+ if (n.subtype === "dimension" && !n.dimension && !n.lowerBound && !n.upperBound) issues.push(`Empty or invalid container dimension query in '${n.raw}'.`);
21
+ break;
22
+ case "own":
23
+ walk(n.innerCondition);
24
+ break;
25
+ case "pseudo":
26
+ if (n.raw.startsWith("@") && !knownPredefined.has(n.raw)) issues.push(`Unresolvable predefined state '${n.raw}'.`);
27
+ break;
28
+ default: break;
29
+ }
30
+ }
31
+ walk(node);
32
+ return issues;
33
+ }
34
+ function hasOwnState(node) {
35
+ if (node.kind === "true" || node.kind === "false") return false;
36
+ if (node.kind === "compound") return node.children.some(hasOwnState);
37
+ if (node.type === "own") return true;
38
+ return false;
39
+ }
40
+ /**
41
+ * Matches the same tokens as tasty's internal STATE_TOKEN_PATTERN.
42
+ * Characters not covered by this pattern (excluding whitespace/commas)
43
+ * are flagged as unrecognized.
44
+ */
45
+ const STATE_TOKEN_PATTERN = /([&|!^])|([()])|(@media:[a-z]+)|(@media\([^)]+\))|(@supports\([^()]*(?:\([^)]*\))?[^)]*\))|(@root\([^)]+\))|(@parent\([^)]+\))|(@own\([^)]+\))|(@\([^()]*(?:\([^)]*\))?[^)]*\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\^=|\$=|\*=|=)(?:"[^"]*"|'[^']*'|[^\s&|!^()]+))|([a-z][a-z0-9-]+)|(:[-a-z][a-z0-9-]*(?:\([^)]+\))?)|(\.[a-z][a-z0-9-]+)|(\[[^\]]+\])/gi;
46
+ function hasUnrecognizedTokens(stateKey) {
47
+ if (!stateKey.trim()) return null;
48
+ const covered = /* @__PURE__ */ new Set();
49
+ STATE_TOKEN_PATTERN.lastIndex = 0;
50
+ let match;
51
+ while ((match = STATE_TOKEN_PATTERN.exec(stateKey)) !== null) for (let i = match.index; i < match.index + match[0].length; i++) covered.add(i);
52
+ const uncovered = [];
53
+ for (let i = 0; i < stateKey.length; i++) {
54
+ const ch = stateKey[i];
55
+ if (ch === " " || ch === " " || ch === ",") continue;
56
+ if (!covered.has(i)) uncovered.push(ch);
57
+ }
58
+ if (uncovered.length > 0) return `Unrecognized characters '${[...new Set(uncovered)].join("")}' in state key '${stateKey}'.`;
59
+ return null;
60
+ }
6
61
  var valid_state_key_default = createRule({
7
62
  name: "valid-state-key",
8
63
  meta: {
9
64
  type: "problem",
10
- docs: { description: "Validate state key syntax in style mapping objects" },
65
+ docs: { description: "Validate state key syntax in style mapping objects using the tasty state parser" },
11
66
  messages: {
12
- invalidStateKey: "{{reason}}",
67
+ unparseable: "{{reason}}",
68
+ emptyAdvancedState: "{{reason}}",
69
+ unresolvablePredefined: "{{reason}}",
13
70
  ownOutsideSubElement: "@own() can only be used inside sub-element styles."
14
71
  },
15
72
  schema: []
@@ -17,6 +74,10 @@ var valid_state_key_default = createRule({
17
74
  defaultOptions: [],
18
75
  create(context) {
19
76
  const ctx = new TastyContext(context);
77
+ const predefinedStates = {};
78
+ for (const alias of ctx.config.states) predefinedStates[alias] = alias;
79
+ setGlobalPredefinedStates(predefinedStates);
80
+ const knownPredefined = new Set(ctx.config.states);
20
81
  function isInsideSubElement(node) {
21
82
  let current = node.parent;
22
83
  while (current) {
@@ -25,23 +86,29 @@ var valid_state_key_default = createRule({
25
86
  }
26
87
  return false;
27
88
  }
28
- function checkStateMapKeys(obj, insideSubElement) {
29
- for (const prop of obj.properties) {
30
- if (prop.type !== "Property") continue;
31
- const key = !prop.computed ? getKeyName(prop.key) : getStringValue(prop.key);
32
- if (key === null) continue;
33
- const error = validateStateKey(key);
34
- if (error) {
35
- context.report({
36
- node: prop.key,
37
- messageId: "invalidStateKey",
38
- data: { reason: error }
39
- });
40
- continue;
41
- }
42
- if (key.includes("@own(") && !insideSubElement) context.report({
43
- node: prop.key,
44
- messageId: "ownOutsideSubElement"
89
+ function checkStateKey(key, keyNode, insideSubElement) {
90
+ if (key === "") return;
91
+ const tokenError = hasUnrecognizedTokens(key);
92
+ if (tokenError) {
93
+ context.report({
94
+ node: keyNode,
95
+ messageId: "unparseable",
96
+ data: { reason: tokenError }
97
+ });
98
+ return;
99
+ }
100
+ const result = parseStateKey(key, { context: createStateParserContext() });
101
+ if (hasOwnState(result) && !insideSubElement) context.report({
102
+ node: keyNode,
103
+ messageId: "ownOutsideSubElement"
104
+ });
105
+ const issues = collectIssues(result, knownPredefined);
106
+ for (const reason of issues) {
107
+ const messageId = reason.startsWith("Unresolvable") ? "unresolvablePredefined" : reason.startsWith("Empty") ? "emptyAdvancedState" : "unparseable";
108
+ context.report({
109
+ node: keyNode,
110
+ messageId,
111
+ data: { reason }
45
112
  });
46
113
  }
47
114
  }
@@ -57,8 +124,12 @@ var valid_state_key_default = createRule({
57
124
  const key = getKeyName(prop.key);
58
125
  if (key === null) continue;
59
126
  if (/^[A-Z]/.test(key) || key.startsWith("@") || key.startsWith("&")) continue;
60
- if (prop.value.type === "ObjectExpression") {
61
- if (!/^[A-Z]/.test(key)) checkStateMapKeys(prop.value, insideSubElement);
127
+ if (prop.value.type !== "ObjectExpression") continue;
128
+ for (const stateProp of prop.value.properties) {
129
+ if (stateProp.type !== "Property") continue;
130
+ const stateKey = !stateProp.computed ? getKeyName(stateProp.key) : getStringValue(stateProp.key);
131
+ if (stateKey === null) continue;
132
+ checkStateKey(stateKey, stateProp.key, insideSubElement);
62
133
  }
63
134
  }
64
135
  }
@@ -1 +1 @@
1
- {"version":3,"file":"valid-state-key.mjs","names":[],"sources":["../../src/rules/valid-state-key.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue, validateStateKey } from '../utils.js';\n\ntype MessageIds = 'invalidStateKey' | 'ownOutsideSubElement';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-state-key',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate state key syntax in style mapping objects',\n },\n messages: {\n invalidStateKey: '{{reason}}',\n ownOutsideSubElement:\n '@own() can only be used inside sub-element styles.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkStateMapKeys(\n obj: TSESTree.ObjectExpression,\n insideSubElement: boolean,\n ): void {\n for (const prop of obj.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = !prop.computed\n ? getKeyName(prop.key)\n : getStringValue(prop.key);\n if (key === null) continue;\n\n // Validate syntax\n const error = validateStateKey(key);\n if (error) {\n context.report({\n node: prop.key,\n messageId: 'invalidStateKey',\n data: { reason: error },\n });\n continue;\n }\n\n // Check @own() usage\n if (key.includes('@own(') && !insideSubElement) {\n context.report({\n node: prop.key,\n messageId: 'ownOutsideSubElement',\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\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 // Skip non-style-property keys\n if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n // If value is an object, check state map keys\n if (prop.value.type === 'ObjectExpression') {\n // Determine if this is actually a sub-element\n const isSubEl = /^[A-Z]/.test(key);\n if (!isSubEl) {\n checkStateMapKeys(prop.value, insideSubElement);\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,sDACd;EACD,UAAU;GACR,iBAAiB;GACjB,sBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,mBAAmB,MAA8B;GACxD,IAAI,UAAqC,KAAK;AAC9C,UAAO,SAAS;AACd,QACE,QAAQ,SAAS,cACjB,CAAC,QAAQ,YACT,QAAQ,IAAI,SAAS,gBACrB,SAAS,KAAK,QAAQ,IAAI,KAAK,CAE/B,QAAO;AAET,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,kBACP,KACA,kBACM;AACN,QAAK,MAAM,QAAQ,IAAI,YAAY;AACjC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,CAAC,KAAK,WACd,WAAW,KAAK,IAAI,GACpB,eAAe,KAAK,IAAI;AAC5B,QAAI,QAAQ,KAAM;IAGlB,MAAM,QAAQ,iBAAiB,IAAI;AACnC,QAAI,OAAO;AACT,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM,EAAE,QAAQ,OAAO;MACxB,CAAC;AACF;;AAIF,QAAI,IAAI,SAAS,QAAQ,IAAI,CAAC,iBAC5B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACZ,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;IAE9B,MAAM,mBAAmB,mBAAmB,KAAK;AAEjD,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;KAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,SAAI,QAAQ,KAAM;AAGlB,SAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAGF,SAAI,KAAK,MAAM,SAAS,oBAGtB;UAAI,CADY,SAAS,KAAK,IAAI,CAEhC,mBAAkB,KAAK,OAAO,iBAAiB;;;;GAKxD;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-state-key.mjs","names":[],"sources":["../../src/rules/valid-state-key.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport {\n parseStateKey,\n createStateParserContext,\n setGlobalPredefinedStates,\n} from '@tenphi/tasty/core';\nimport type { ConditionNode } from '@tenphi/tasty/core';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds =\n | 'unparseable'\n | 'emptyAdvancedState'\n | 'unresolvablePredefined'\n | 'ownOutsideSubElement';\n\nfunction collectIssues(\n node: ConditionNode,\n knownPredefined: Set<string>,\n): string[] {\n const issues: string[] = [];\n\n function walk(n: ConditionNode): void {\n if (n.kind === 'true' || n.kind === 'false') return;\n\n if (n.kind === 'compound') {\n for (const child of n.children) {\n walk(child);\n }\n return;\n }\n\n switch (n.type) {\n case 'media':\n if (\n n.subtype === 'dimension' &&\n !n.dimension &&\n !n.lowerBound &&\n !n.upperBound\n ) {\n issues.push(`Empty or invalid @media dimension query in '${n.raw}'.`);\n }\n break;\n\n case 'container':\n if (\n n.subtype === 'dimension' &&\n !n.dimension &&\n !n.lowerBound &&\n !n.upperBound\n ) {\n issues.push(\n `Empty or invalid container dimension query in '${n.raw}'.`,\n );\n }\n break;\n\n case 'own':\n walk(n.innerCondition);\n break;\n\n case 'pseudo':\n if (n.raw.startsWith('@') && !knownPredefined.has(n.raw)) {\n issues.push(`Unresolvable predefined state '${n.raw}'.`);\n }\n break;\n\n default:\n break;\n }\n }\n\n walk(node);\n return issues;\n}\n\nfunction hasOwnState(node: ConditionNode): boolean {\n if (node.kind === 'true' || node.kind === 'false') return false;\n if (node.kind === 'compound') {\n return node.children.some(hasOwnState);\n }\n if (node.type === 'own') return true;\n return false;\n}\n\n/**\n * Matches the same tokens as tasty's internal STATE_TOKEN_PATTERN.\n * Characters not covered by this pattern (excluding whitespace/commas)\n * are flagged as unrecognized.\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]+\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^)]+\\))|(@parent\\([^)]+\\))|(@own\\([^)]+\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\nfunction hasUnrecognizedTokens(stateKey: string): string | null {\n if (!stateKey.trim()) return null;\n\n const covered = new Set<number>();\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = STATE_TOKEN_PATTERN.exec(stateKey)) !== null) {\n for (let i = match.index; i < match.index + match[0].length; i++) {\n covered.add(i);\n }\n }\n\n const uncovered: string[] = [];\n for (let i = 0; i < stateKey.length; i++) {\n const ch = stateKey[i];\n if (ch === ' ' || ch === '\\t' || ch === ',') continue;\n if (!covered.has(i)) {\n uncovered.push(ch);\n }\n }\n\n if (uncovered.length > 0) {\n const chars = [...new Set(uncovered)].join('');\n return `Unrecognized characters '${chars}' in state key '${stateKey}'.`;\n }\n\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-state-key',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate state key syntax in style mapping objects using the tasty state parser',\n },\n messages: {\n unparseable: '{{reason}}',\n emptyAdvancedState: '{{reason}}',\n unresolvablePredefined: '{{reason}}',\n ownOutsideSubElement:\n '@own() can only be used inside sub-element styles.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const predefinedStates: Record<string, string> = {};\n for (const alias of ctx.config.states) {\n predefinedStates[alias] = alias;\n }\n setGlobalPredefinedStates(predefinedStates);\n\n const knownPredefined = new Set(ctx.config.states);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkStateKey(\n key: string,\n keyNode: TSESTree.Node,\n insideSubElement: boolean,\n ): void {\n if (key === '') return;\n\n const tokenError = hasUnrecognizedTokens(key);\n if (tokenError) {\n context.report({\n node: keyNode,\n messageId: 'unparseable',\n data: { reason: tokenError },\n });\n return;\n }\n\n const parserContext = createStateParserContext();\n const result = parseStateKey(key, { context: parserContext });\n\n if (hasOwnState(result) && !insideSubElement) {\n context.report({\n node: keyNode,\n messageId: 'ownOutsideSubElement',\n });\n }\n\n const issues = collectIssues(result, knownPredefined);\n for (const reason of issues) {\n const messageId = reason.startsWith('Unresolvable')\n ? 'unresolvablePredefined'\n : reason.startsWith('Empty')\n ? 'emptyAdvancedState'\n : 'unparseable';\n\n context.report({\n node: keyNode,\n messageId,\n data: { reason },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\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 if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null) continue;\n\n checkStateKey(stateKey, stateProp.key, insideSubElement);\n }\n }\n },\n };\n },\n});\n"],"mappings":"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"}
package/dist/utils.mjs CHANGED
@@ -88,43 +88,6 @@ function isValidUnit(unit, config) {
88
88
  return false;
89
89
  }
90
90
  /**
91
- * Validates a state key string.
92
- * Returns null if valid, or an error message if invalid.
93
- */
94
- function validateStateKey(key) {
95
- if (key === "") return null;
96
- if (key.startsWith(":") || key.startsWith("::")) return null;
97
- if (key.startsWith(".")) {
98
- if (key.length <= 1) return "Empty class selector";
99
- return null;
100
- }
101
- if (key.startsWith("[") && key.endsWith("]")) return null;
102
- if (key.startsWith("@")) {
103
- for (const prefix of BUILT_IN_STATE_PREFIXES) if (key === prefix || key.startsWith(prefix + "(")) return null;
104
- if (key.startsWith("@(")) return null;
105
- return null;
106
- }
107
- if (key.includes("&") || key.includes("|") || key.includes("^")) return validateCombinedStateKey(key);
108
- if (key.startsWith("!")) {
109
- if (key.length <= 1) return "Empty negated state";
110
- return null;
111
- }
112
- if (key.startsWith("(") && key.endsWith(")")) return null;
113
- if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(key)) return null;
114
- if (/^[a-zA-Z][a-zA-Z0-9-]*[\^$*]?=/.test(key)) return null;
115
- return `Invalid state key syntax '${key}'`;
116
- }
117
- function validateCombinedStateKey(key) {
118
- const parts = key.split(/\s*[&|^]\s*/);
119
- for (const part of parts) {
120
- const trimmed = part.trim();
121
- if (trimmed.length === 0) return "Empty part in combined state expression";
122
- const partError = validateStateKey(trimmed.startsWith("!") ? trimmed : trimmed);
123
- if (partError) return partError;
124
- }
125
- return null;
126
- }
127
- /**
128
91
  * Checks if a state alias key (starting with @) is known.
129
92
  */
130
93
  function isKnownStateAlias(key, config) {
@@ -148,5 +111,5 @@ function isValidSelector(selector) {
148
111
  }
149
112
 
150
113
  //#endregion
151
- export { extractCustomUnit, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, validateColorTokenSyntax, validateStateKey };
114
+ export { extractCustomUnit, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, validateColorTokenSyntax };
152
115
  //# sourceMappingURL=utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;\n if (opacity < 0) return 'Opacity cannot be negative';\n if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Validates a state key string.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateStateKey(key: string): string | null {\n if (key === '') return null; // Default state\n\n // Pseudo-class/element\n if (key.startsWith(':') || key.startsWith('::')) return null;\n\n // Class selector\n if (key.startsWith('.')) {\n if (key.length <= 1) return 'Empty class selector';\n return null;\n }\n\n // Attribute selector\n if (key.startsWith('[') && key.endsWith(']')) return null;\n\n // @ prefixed — check for built-in or alias\n if (key.startsWith('@')) {\n // Built-in functional state prefix: @media(...), @root(...), etc.\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return null;\n }\n // Container query shorthand: @(...)\n if (key.startsWith('@(')) return null;\n // Otherwise it's an alias — validated elsewhere\n return null;\n }\n\n // Combined expressions with operators\n if (key.includes('&') || key.includes('|') || key.includes('^')) {\n return validateCombinedStateKey(key);\n }\n\n // Negation\n if (key.startsWith('!')) {\n if (key.length <= 1) return 'Empty negated state';\n return null;\n }\n\n // Parenthesized group\n if (key.startsWith('(') && key.endsWith(')')) return null;\n\n // Boolean modifier: simple identifier like 'hovered'\n if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(key)) return null;\n\n // Value modifier: name=value or name^=value etc.\n if (/^[a-zA-Z][a-zA-Z0-9-]*[\\^$*]?=/.test(key)) return null;\n\n return `Invalid state key syntax '${key}'`;\n}\n\nfunction validateCombinedStateKey(key: string): string | null {\n // Split by & and | operators, validate each part\n const parts = key.split(/\\s*[&|^]\\s*/);\n for (const part of parts) {\n const trimmed = part.trim();\n if (trimmed.length === 0) return 'Empty part in combined state expression';\n // Each part should be a valid individual state\n const partError = validateStateKey(\n trimmed.startsWith('!') ? trimmed : trimmed,\n );\n if (partError) return partError;\n }\n return null;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;\n if (opacity < 0) return 'Opacity cannot be negative';\n if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n"],"mappings":";;;;;;AAWA,SAAgB,WAAW,KAAmC;AAC5D,KAAI,IAAI,SAAS,aAAc,QAAO,IAAI;AAC1C,KAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AACxE,KAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,SACjD,QAAO,OAAO,IAAI,MAAM;AAC1B,QAAO;;;;;AAMT,SAAgB,eAAe,MAAoC;AACjE,KAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,SACnD,QAAO,KAAK;AAEd,KAAI,KAAK,SAAS,qBAAqB,KAAK,YAAY,WAAW,EACjE,QAAO,KAAK,OAAO,GAAG,MAAM,UAAU;AAExC,QAAO;;;;;AAMT,SAAgB,cAAc,MAA8B;AAC1D,KAAI,KAAK,SAAS,UAAW,QAAO;AACpC,KACE,KAAK,SAAS,qBACd,KAAK,aAAa,OAClB,KAAK,SAAS,SAAS,UAEvB,QAAO;AAET,KAAI,KAAK,SAAS,qBAAqB,KAAK,YAAY,WAAW,EACjE,QAAO;AAET,KAAI,KAAK,SAAS,kBAChB,QAAO,KAAK,SAAS,OAAO,OAAO,OAAO,QAAQ,cAAc,GAAG,CAAC;AAEtE,KAAI,KAAK,SAAS,mBAChB,QAAO,KAAK,WAAW,OACpB,SACC,KAAK,SAAS,cAAc,CAAC,KAAK,YAAY,cAAc,KAAK,MAAM,CAC1E;AAEH,QAAO;;;;;;AAOT,SAAgB,yBAAyB,OAA8B;CAErE,IAAI,OAAO;AACX,KAAI,KAAK,WAAW,KAAK,CACvB,QAAO,KAAK,MAAM,EAAE;UACX,KAAK,WAAW,IAAI,CAC7B,QAAO,KAAK,MAAM,EAAE;KAEpB,QAAO;AAGT,KAAI,KAAK,WAAW,EAAG,QAAO;CAG9B,MAAM,WAAW,KAAK,QAAQ,IAAI;AAClC,KAAI,aAAa,IAAI;EACnB,MAAM,YAAY,KAAK,MAAM,GAAG,SAAS;EACzC,MAAM,gBAAgB,KAAK,MAAM,WAAW,EAAE;AAE9C,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,MAAI,cAAc,WAAW,IAAI,CAE/B,QAAO;AAGT,MAAI,cAAc,WAAW,EAAG,QAAO;EAEvC,MAAM,UAAU,OAAO,cAAc;AACrC,MAAI,MAAM,QAAQ,CAAE,QAAO,0BAA0B,cAAc;AACnE,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,IAAK,QAAO,YAAY,cAAc;;AAGtD,QAAO;;;;;;AAOT,SAAgB,cAAc,OAAwB;AACpD,KAAI,CAAC,MAAM,WAAW,IAAI,CAAE,QAAO;CACnC,MAAM,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC;AACtC,KAAI,CAAC;EAAC;EAAG;EAAG;EAAG;EAAE,CAAC,SAAS,IAAI,OAAO,CAAE,QAAO;AAC/C,QAAO,iBAAiB,KAAK,IAAI;;;;;;AAOnC,SAAgB,kBAAkB,OAA8B;CAC9D,MAAM,QAAQ,MAAM,MAAM,wBAAwB;AAClD,KAAI,CAAC,MAAO,QAAO;AACnB,QAAO,MAAM;;;;;AAMf,SAAgB,YAAY,MAAc,QAAiC;AACzE,KAAI,OAAO,UAAU,MAAO,QAAO;AACnC,KAAI,eAAe,IAAI,KAAK,CAAE,QAAO;AACrC,KAAI,UAAU,IAAI,KAAK,CAAE,QAAO;AAChC,KAAI,MAAM,QAAQ,OAAO,MAAM,IAAI,OAAO,MAAM,SAAS,KAAK,CAAE,QAAO;AACvE,QAAO;;;;;AAMT,SAAgB,kBACd,KACA,QACS;AAET,MAAK,MAAM,UAAU,wBACnB,KAAI,QAAQ,UAAU,IAAI,WAAW,SAAS,IAAI,CAAE,QAAO;AAG7D,KAAI,IAAI,WAAW,KAAK,CAAE,QAAO;AAEjC,QAAO,OAAO,OAAO,SAAS,IAAI;;;;;AAMpC,SAAgB,gBAAgB,UAAiC;AAC/D,KAAI,SAAS,WAAW,EAAG,QAAO;CAGlC,IAAI,QAAQ;AACZ,MAAK,MAAM,QAAQ,UAAU;AAC3B,MAAI,SAAS,OAAO,SAAS,IAAK;AAClC,MAAI,SAAS,OAAO,SAAS,IAAK;AAClC,MAAI,QAAQ,EAAG,QAAO;;AAExB,KAAI,UAAU,EAAG,QAAO;AAExB,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,21 +18,6 @@
18
18
  "engines": {
19
19
  "node": ">=20"
20
20
  },
21
- "scripts": {
22
- "build": "tsdown",
23
- "test": "vitest run",
24
- "test:watch": "vitest",
25
- "test:coverage": "vitest run --coverage",
26
- "typecheck": "tsc --noEmit",
27
- "lint": "eslint src",
28
- "lint:fix": "eslint src --fix",
29
- "format": "prettier --write \"src/**/*.ts\"",
30
- "format:check": "prettier --check \"src/**/*.ts\"",
31
- "prepublishOnly": "pnpm run build",
32
- "changeset": "changeset",
33
- "version": "changeset version",
34
- "release": "changeset publish"
35
- },
36
21
  "repository": {
37
22
  "type": "git",
38
23
  "url": "git+https://github.com/tenphi/eslint-plugin-tasty.git"
@@ -69,6 +54,7 @@
69
54
  "@changesets/changelog-github": "^0.5.2",
70
55
  "@changesets/cli": "^2.29.8",
71
56
  "@eslint/js": "^10.0.1",
57
+ "@tenphi/tasty": "0.4.0",
72
58
  "@types/node": "^22.0.0",
73
59
  "@typescript-eslint/parser": "^8.56.1",
74
60
  "@typescript-eslint/rule-tester": "^8.56.1",
@@ -78,13 +64,20 @@
78
64
  "tsdown": "^0.20.3",
79
65
  "typescript": "^5.9.3",
80
66
  "typescript-eslint": "^8.56.0",
81
- "@tenphi/tasty": "^0.1.3",
82
67
  "vitest": "^4.0.18"
83
68
  },
84
- "pnpm": {
85
- "onlyBuiltDependencies": [
86
- "esbuild"
87
- ]
88
- },
89
- "packageManager": "pnpm@10.29.3"
90
- }
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "test": "vitest run",
72
+ "test:watch": "vitest",
73
+ "test:coverage": "vitest run --coverage",
74
+ "typecheck": "tsc --noEmit",
75
+ "lint": "eslint src",
76
+ "lint:fix": "eslint src --fix",
77
+ "format": "prettier --write \"src/**/*.ts\"",
78
+ "format:check": "prettier --check \"src/**/*.ts\"",
79
+ "changeset": "changeset",
80
+ "version": "changeset version",
81
+ "release": "changeset publish"
82
+ }
83
+ }