@tenphi/eslint-plugin-tasty 1.12.1 → 1.13.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
@@ -114,6 +114,33 @@ list through `extends`. See [`tasty/no-protected-restyle`](docs/rules/no-protect
114
114
  > `functions` was called `funcs` before v1. The old spelling is still read as a
115
115
  > deprecated alias.
116
116
 
117
+ ### Versioning a shared config
118
+
119
+ A config that relies on newer plugin features can say so with `requires`, a
120
+ semver range of `@tenphi/eslint-plugin-tasty` versions:
121
+
122
+ ```ts
123
+ // tasty.config.ts of a design system that consumers extend
124
+ export default {
125
+ requires: '>=1.13.0',
126
+ protectedComponents: [{ source: '@my-org/ui', exclude: ['Button.Group'] }],
127
+ };
128
+ ```
129
+
130
+ Every config in the `extends` chain may declare one, and linting fails when the
131
+ installed plugin does not satisfy them all, naming the config, the range and the
132
+ installed version. A consumer that lags behind its design system finds out on
133
+ the first lint run instead of getting silently different results.
134
+
135
+ The plugin also warns, on stderr and once per config file, about keys it does not
136
+ read (a typo, or a key from a newer plugin), about known keys of the wrong shape
137
+ (`states: '@mobile'` instead of a list), and about an `extends` that names
138
+ nothing usable: a non-string value, a package that cannot be resolved, a package
139
+ without a `tasty.config.*`, or a chain that leads back to itself. A package
140
+ `extends` can also name a file inside the package, such as
141
+ `@my-org/ui/lint/tasty.config`. `presetDescriptions` and `stateDescriptions`,
142
+ which the Tasty VS Code extension reads from the same file, are accepted.
143
+
117
144
  ### Custom style functions
118
145
 
119
146
  Design systems can wrap Tasty with their own component factories and style
package/dist/config.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { DEFAULT_IMPORT_SOURCES } from "./constants.js";
2
+ import { version } from "./version.js";
2
3
  import { existsSync, readFileSync, statSync } from "fs";
3
4
  import { dirname, join, resolve } from "path";
4
5
  import { createJiti } from "jiti";
6
+ import semver from "semver";
5
7
 
6
8
  //#region src/config.ts
7
9
  const CONFIG_FILENAMES = [
@@ -11,6 +13,97 @@ const CONFIG_FILENAMES = [
11
13
  "tasty.config.json"
12
14
  ];
13
15
  const configCache = /* @__PURE__ */ new Map();
16
+ /**
17
+ * Every key a config may carry; anything else is a typo or from a newer plugin.
18
+ * Typed against the config interface so a new key cannot be missed here.
19
+ */
20
+ const KNOWN_KEYS = new Set(Object.keys({
21
+ extends: true,
22
+ requires: true,
23
+ tokens: true,
24
+ units: true,
25
+ functions: true,
26
+ funcs: true,
27
+ states: true,
28
+ presets: true,
29
+ recipes: true,
30
+ styles: true,
31
+ importSources: true,
32
+ ownedSources: true,
33
+ styleFunctions: true,
34
+ protectedComponents: true,
35
+ presetDescriptions: true,
36
+ stateDescriptions: true
37
+ }));
38
+ const warnings = /* @__PURE__ */ new Set();
39
+ /** A config problem worth a message but not a failed run, said once per process. */
40
+ function warnOnce(message) {
41
+ if (warnings.has(message)) return;
42
+ warnings.add(message);
43
+ console.warn(`[eslint-plugin-tasty] ${message}`);
44
+ }
45
+ /** Fails the run when the installed plugin is outside the config's `requires`. */
46
+ function checkRequires(config, configPath) {
47
+ const range = config.requires;
48
+ if (range === void 0) return;
49
+ if (typeof range !== "string" || range.trim() === "" || semver.validRange(range) === null) throw new Error(`tasty: ${configPath}: 'requires' must be a semver range of @tenphi/eslint-plugin-tasty versions, such as '>=1.13.0'.`);
50
+ if (!semver.satisfies(version, range, { includePrerelease: true })) throw new Error(`tasty: ${configPath} requires @tenphi/eslint-plugin-tasty ${range}, but ${version} is installed. Upgrade the plugin.`);
51
+ }
52
+ /** List keys, and whether `false` (turning the check off) is allowed. */
53
+ const LIST_KEYS = {
54
+ tokens: true,
55
+ units: true,
56
+ functions: true,
57
+ funcs: true,
58
+ states: false,
59
+ presets: false,
60
+ recipes: false,
61
+ styles: false,
62
+ importSources: false,
63
+ ownedSources: false
64
+ };
65
+ /** Keys Tasty's runtime config (`configure()`) holds as objects keyed by name. */
66
+ const RUNTIME_OBJECT_KEYS = [
67
+ "tokens",
68
+ "units",
69
+ "functions",
70
+ "states",
71
+ "presets",
72
+ "recipes"
73
+ ];
74
+ function isPlainObject(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+ function describe(value) {
78
+ if (value === null) return "null";
79
+ if (Array.isArray(value)) return value.every((item) => typeof item === "string") ? "a list" : "a list with a non-string entry";
80
+ return isPlainObject(value) ? "an object" : `a ${typeof value}`;
81
+ }
82
+ /**
83
+ * Warns about keys the plugin does not read and known keys of the wrong shape,
84
+ * and returns the config without the latter, so `states: '@mobile'` is not
85
+ * read character by character.
86
+ */
87
+ function checkKeys(config, configPath) {
88
+ const runtime = RUNTIME_OBJECT_KEYS.some((key) => isPlainObject(config[key])) ? " It looks like Tasty's runtime config, which the plugin does not read." : "";
89
+ const unknown = Object.keys(config).filter((key) => !KNOWN_KEYS.has(key) && key !== "__esModule");
90
+ if (unknown.length > 0) {
91
+ const keys = unknown.map((key) => `'${key}'`).join(", ");
92
+ warnOnce(`${configPath}: ignoring unknown ${unknown.length === 1 ? "key" : "keys"} ${keys}.${runtime || ` Check the spelling, or upgrade @tenphi/eslint-plugin-tasty (${version} is installed) if the config was written for a newer version.`}`);
93
+ }
94
+ const checked = { ...config };
95
+ const ignore = (key, expected) => {
96
+ warnOnce(`${configPath}: ignoring '${key}', which must be ${expected}, not ${describe(checked[key])}.${runtime}`);
97
+ delete checked[key];
98
+ };
99
+ for (const [key, allowsFalse] of Object.entries(LIST_KEYS)) {
100
+ const value = checked[key];
101
+ if (value === void 0 || value === false && allowsFalse || Array.isArray(value) && value.every((item) => typeof item === "string")) continue;
102
+ ignore(key, `${allowsFalse ? "false or " : ""}a list of names`);
103
+ }
104
+ if (checked.styleFunctions !== void 0 && !isPlainObject(checked.styleFunctions)) ignore("styleFunctions", "an object keyed by function name");
105
+ return checked;
106
+ }
14
107
  function findProjectRoot(startDir) {
15
108
  let dir = startDir;
16
109
  while (dir !== dirname(dir)) {
@@ -155,19 +248,8 @@ function loadRawConfig(configPath) {
155
248
  }
156
249
  function mergeConfigs(parent, child) {
157
250
  const result = { ...parent };
158
- for (const key of [
159
- "tokens",
160
- "units",
161
- "functions",
162
- "funcs",
163
- "states",
164
- "presets",
165
- "recipes",
166
- "styles",
167
- "importSources",
168
- "ownedSources",
169
- "protectedComponents"
170
- ]) {
251
+ const arrayKeys = [...Object.keys(LIST_KEYS), "protectedComponents"];
252
+ for (const key of arrayKeys) {
171
253
  const childVal = child[key];
172
254
  if (childVal === void 0) continue;
173
255
  if (childVal === false) {
@@ -184,40 +266,74 @@ function mergeConfigs(parent, child) {
184
266
  };
185
267
  return result;
186
268
  }
187
- function resolveConfigChain(configPath, visited = /* @__PURE__ */ new Set()) {
269
+ /**
270
+ * The config file `extends` points to, or null when there is none. A relative
271
+ * or absolute path that is not a file fails the run, as reading it always has; a
272
+ * value that names nothing usable is skipped with a warning.
273
+ */
274
+ function resolveExtends(config, configPath, touched) {
275
+ const target = config.extends;
276
+ if (target === void 0) return null;
277
+ const configDir = dirname(configPath);
278
+ if (typeof target !== "string" || target === "") {
279
+ warnOnce(`${configPath}: 'extends' must be a config path or a package name, not ${JSON.stringify(target)}. It is ignored.`);
280
+ return null;
281
+ }
282
+ if (target.startsWith(".") || target.startsWith("/")) {
283
+ const parentPath = resolve(configDir, target);
284
+ touched.push(parentPath);
285
+ if (!isFile(parentPath)) throw new Error(`tasty: ${configPath}: 'extends' points to ${parentPath}, which is not a file.`);
286
+ return parentPath;
287
+ }
288
+ const packageName = target.split("/").slice(0, target.startsWith("@") ? 2 : 1).join("/");
289
+ const subpath = target.slice(packageName.length + 1);
290
+ const pkgDir = resolvePackageDir(packageName, configDir);
291
+ if (!pkgDir) {
292
+ warnOnce(`${configPath}: 'extends' names '${packageName}', which cannot be resolved from ${configDir}. It is ignored.`);
293
+ return null;
294
+ }
295
+ const pkgConfig = subpath ? findConfigFile(join(pkgDir, subpath)) : findConfigInDir(pkgDir);
296
+ if (!pkgConfig) warnOnce(subpath ? `${configPath}: 'extends' names '${target}', but ${join(pkgDir, subpath)} is not a config file. It is ignored.` : `${configPath}: 'extends' names '${target}', which has no tasty.config.* at ${pkgDir}. It is ignored.`);
297
+ return pkgConfig;
298
+ }
299
+ function isFile(path) {
300
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
301
+ }
302
+ /** A config file at `path`, with or without its extension, or in that directory. */
303
+ function findConfigFile(path) {
304
+ for (const ext of [
305
+ "",
306
+ ".ts",
307
+ ".js",
308
+ ".mjs",
309
+ ".json"
310
+ ]) if (isFile(path + ext)) return path + ext;
311
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() ? findConfigInDir(path) : null;
312
+ }
313
+ /**
314
+ * Loads a config and the chain it extends, parents merged first. Every file
315
+ * read (or expected) is pushed to `touched`, whose mtimes decide when the
316
+ * result, or a failure, is stale.
317
+ */
318
+ function resolveConfigChain(configPath, touched, visited = /* @__PURE__ */ new Set()) {
188
319
  const absPath = resolve(configPath);
189
- if (visited.has(absPath)) return {
190
- config: {},
191
- chainPaths: []
192
- };
320
+ if (visited.has(absPath)) {
321
+ warnOnce(`${absPath} is extended again by its own extends chain. The cycle is ignored.`);
322
+ return {};
323
+ }
193
324
  visited.add(absPath);
194
- const config = loadRawConfig(absPath);
195
- const chainPaths = [absPath, ...extractImportPaths(absPath)];
196
- if (!config.extends) return {
197
- config,
198
- chainPaths
199
- };
200
- let parentPath;
201
- if (config.extends.startsWith(".") || config.extends.startsWith("/")) parentPath = resolve(dirname(absPath), config.extends);
202
- else {
203
- const pkgDir = resolvePackageDir(config.extends, dirname(absPath));
204
- if (pkgDir) {
205
- const pkgConfig = findConfigInDir(pkgDir);
206
- if (pkgConfig) parentPath = pkgConfig;
207
- else return {
208
- config,
209
- chainPaths
210
- };
211
- } else return {
212
- config,
213
- chainPaths
214
- };
325
+ touched.push(absPath);
326
+ let config = loadRawConfig(absPath);
327
+ touched.push(...extractImportPaths(absPath));
328
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
329
+ warnOnce(`${absPath}: the config must be an object. It is ignored.`);
330
+ config = {};
215
331
  }
216
- const parentResult = resolveConfigChain(parentPath, visited);
217
- return {
218
- config: mergeConfigs(parentResult.config, config),
219
- chainPaths: [...parentResult.chainPaths, ...chainPaths]
220
- };
332
+ checkRequires(config, absPath);
333
+ config = checkKeys(config, absPath);
334
+ const parentPath = resolveExtends(config, absPath, touched);
335
+ if (!parentPath) return config;
336
+ return mergeConfigs(resolveConfigChain(parentPath, touched, visited), config);
221
337
  }
222
338
  function toResolved(config) {
223
339
  return {
@@ -271,29 +387,25 @@ function loadConfig(filePath) {
271
387
  if (configFiles.length === 0 && !pkgConfigFile) return DEFAULT_CONFIG;
272
388
  const cacheKey = [pkgConfigFile ?? "", ...configFiles].join("\0");
273
389
  const cached = configCache.get(cacheKey);
274
- if (cached) {
275
- const currentMtimes = getMtimes([...cached.fileMtimes.keys()]);
276
- if (mtimesMatch(cached.fileMtimes, currentMtimes)) return cached.config;
390
+ if (cached && mtimesMatch(cached.fileMtimes, getMtimes([...cached.fileMtimes.keys()]))) {
391
+ if (cached.result instanceof Error) throw cached.result;
392
+ return cached.result;
277
393
  }
278
- let merged = {};
279
- const allChainPaths = [];
280
- if (pkgConfigFile) {
281
- const result = resolveConfigChain(pkgConfigFile);
282
- merged = result.config;
283
- allChainPaths.push(...result.chainPaths);
284
- }
285
- for (let i = configFiles.length - 1; i >= 0; i--) {
286
- const { config, chainPaths } = resolveConfigChain(configFiles[i]);
287
- merged = mergeConfigs(merged, config);
288
- allChainPaths.push(...chainPaths);
394
+ const touched = [];
395
+ let result;
396
+ try {
397
+ let merged = pkgConfigFile ? resolveConfigChain(pkgConfigFile, touched) : {};
398
+ for (let i = configFiles.length - 1; i >= 0; i--) merged = mergeConfigs(merged, resolveConfigChain(configFiles[i], touched));
399
+ result = toResolved(merged);
400
+ } catch (error) {
401
+ result = error instanceof Error ? error : new Error(String(error));
289
402
  }
290
- const resolved = toResolved(merged);
291
- const fileMtimes = getMtimes(allChainPaths);
292
403
  configCache.set(cacheKey, {
293
- config: resolved,
294
- fileMtimes
404
+ result,
405
+ fileMtimes: getMtimes(touched)
295
406
  });
296
- return resolved;
407
+ if (result instanceof Error) throw result;
408
+ return result;
297
409
  }
298
410
 
299
411
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'fs';\nimport { dirname, join, resolve } from 'path';\n\nimport { createJiti } from 'jiti';\n\nimport type { ResolvedConfig, TastyValidationConfig } from './types.js';\nimport { DEFAULT_IMPORT_SOURCES } from './constants.js';\n\nconst CONFIG_FILENAMES = [\n 'tasty.config.ts',\n 'tasty.config.js',\n 'tasty.config.mjs',\n 'tasty.config.json',\n];\n\ninterface CachedConfig {\n config: ResolvedConfig;\n fileMtimes: Map<string, number>;\n}\n\nconst configCache = new Map<string, CachedConfig>();\n\nfunction findProjectRoot(startDir: string): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n if (existsSync(join(dir, 'package.json'))) {\n return dir;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nfunction resolvePackageDir(\n packageName: string,\n startDir: string,\n): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n const candidate = join(dir, 'node_modules', ...packageName.split('/'));\n if (existsSync(candidate) && existsSync(join(candidate, 'package.json'))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nfunction findConfigInDir(dir: string): string | null {\n for (const name of CONFIG_FILENAMES) {\n const path = join(dir, name);\n if (existsSync(path)) {\n return path;\n }\n }\n return null;\n}\n\nfunction collectConfigFiles(startDir: string, projectRoot: string): string[] {\n const configs: string[] = [];\n let dir = startDir;\n const root = resolve(projectRoot);\n\n while (true) {\n const found = findConfigInDir(dir);\n if (found) configs.push(found);\n if (resolve(dir) === root) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return configs;\n}\n\nconst TASTY_PACKAGE_NAME = '@tenphi/tasty';\n\nfunction stripComments(source: string): string {\n let result = '';\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n result += ch;\n i++;\n while (i < source.length && source[i] !== ch) {\n if (source[i] === '\\\\') {\n result += source[i++];\n }\n if (i < source.length) {\n result += source[i++];\n }\n }\n if (i < source.length) {\n result += source[i++];\n }\n continue;\n }\n if (ch === '/' && source[i + 1] === '*') {\n i += 2;\n while (\n i < source.length &&\n !(source[i] === '*' && source[i + 1] === '/')\n ) {\n i++;\n }\n i += 2;\n continue;\n }\n if (ch === '/' && source[i + 1] === '/') {\n while (i < source.length && source[i] !== '\\n') {\n i++;\n }\n continue;\n }\n result += source[i++];\n }\n return result;\n}\n\nfunction stripImports(source: string): string {\n return source.replace(/^\\s*import\\s+.*?;\\s*$/gm, '');\n}\n\nfunction extractBalancedBraces(content: string, start: number): string | null {\n if (content[start] !== '{') return null;\n let depth = 0;\n for (let i = start; i < content.length; i++) {\n const ch = content[i];\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n i++;\n while (i < content.length && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n continue;\n }\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n if (depth === 0) return content.slice(start, i + 1);\n }\n return null;\n}\n\nfunction extractImportPaths(configPath: string): string[] {\n const source = readFileSync(configPath, 'utf-8');\n const configDir = dirname(configPath);\n const paths: string[] = [];\n const re = /^\\s*import\\s+.*?\\s+from\\s+['\"]([^'\"]+)['\"]/gm;\n let m;\n\n while ((m = re.exec(source)) !== null) {\n const specifier = m[1];\n\n if (!specifier.startsWith('.')) continue;\n\n const abs = resolve(configDir, specifier);\n\n for (const ext of ['', '.ts', '.js', '.mjs', '.tsx', '.jsx']) {\n const candidate = abs + ext;\n\n if (existsSync(candidate)) {\n paths.push(candidate);\n break;\n }\n }\n }\n\n return paths;\n}\n\nfunction stripTypeScriptSyntax(source: string): string {\n return source\n .replace(/\\bas\\s+const\\b/g, '')\n .replace(/\\bsatisfies\\s+[A-Z]\\w*(?:<[^>]*>)?/g, '')\n .replace(/\\bas\\s+[A-Z]\\w*(?:<[^>]*>)?/g, '');\n}\n\nfunction loadRawConfig(configPath: string): TastyValidationConfig {\n if (configPath.endsWith('.json')) {\n return JSON.parse(\n readFileSync(configPath, 'utf-8'),\n ) as TastyValidationConfig;\n }\n\n // Try jiti first — it resolves imports and TypeScript natively.\n try {\n const jiti = createJiti(dirname(configPath), { moduleCache: false });\n const mod = jiti(configPath) as Record<string, unknown>;\n const config = (mod.default ?? mod) as TastyValidationConfig;\n\n if (config && typeof config === 'object') {\n return config;\n }\n } catch {\n // Fall through to text-based parsing.\n }\n\n // Fallback: text-based extraction (no import support).\n const content = readFileSync(configPath, 'utf-8');\n const stripped = stripImports(stripComments(content));\n const match = stripped.match(/export\\s+default\\s+/);\n\n if (match && match.index != null) {\n const braceStart = match.index + match[0].length;\n const objectStr = extractBalancedBraces(stripped, braceStart);\n\n if (objectStr) {\n const cleaned = stripTypeScriptSyntax(objectStr);\n\n try {\n const fn = new Function(`return (${cleaned})`);\n return fn() as TastyValidationConfig;\n } catch (err) {\n console.warn(\n `[eslint-plugin-tasty] Failed to parse config file ${configPath}: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n }\n\n return {};\n}\n\nfunction mergeConfigs(\n parent: TastyValidationConfig,\n child: TastyValidationConfig,\n): TastyValidationConfig {\n const result: TastyValidationConfig = { ...parent };\n\n const arrayKeys = [\n 'tokens',\n 'units',\n 'functions',\n 'funcs',\n 'states',\n 'presets',\n 'recipes',\n 'styles',\n 'importSources',\n 'ownedSources',\n 'protectedComponents',\n ] as const;\n\n for (const key of arrayKeys) {\n const childVal = child[key];\n if (childVal === undefined) continue;\n\n if (childVal === false) {\n (result as Record<string, unknown>)[key] = false;\n continue;\n }\n\n const parentVal = parent[key];\n if (Array.isArray(parentVal) && Array.isArray(childVal)) {\n (result as Record<string, unknown>)[key] = [\n ...new Set([...parentVal, ...childVal]),\n ];\n } else {\n (result as Record<string, unknown>)[key] = childVal;\n }\n }\n\n if (child.styleFunctions !== undefined) {\n result.styleFunctions = {\n ...parent.styleFunctions,\n ...child.styleFunctions,\n };\n }\n\n return result;\n}\n\ninterface ConfigChainResult {\n config: TastyValidationConfig;\n chainPaths: string[];\n}\n\nfunction resolveConfigChain(\n configPath: string,\n visited = new Set<string>(),\n): ConfigChainResult {\n const absPath = resolve(configPath);\n if (visited.has(absPath)) return { config: {}, chainPaths: [] };\n visited.add(absPath);\n\n const config = loadRawConfig(absPath);\n const importPaths = extractImportPaths(absPath);\n const chainPaths = [absPath, ...importPaths];\n\n if (!config.extends) return { config, chainPaths };\n\n let parentPath: string;\n if (config.extends.startsWith('.') || config.extends.startsWith('/')) {\n parentPath = resolve(dirname(absPath), config.extends);\n } else {\n const pkgDir = resolvePackageDir(config.extends, dirname(absPath));\n if (pkgDir) {\n const pkgConfig = findConfigInDir(pkgDir);\n if (pkgConfig) {\n parentPath = pkgConfig;\n } else {\n return { config, chainPaths };\n }\n } else {\n return { config, chainPaths };\n }\n }\n\n const parentResult = resolveConfigChain(parentPath, visited);\n return {\n config: mergeConfigs(parentResult.config, config),\n chainPaths: [...parentResult.chainPaths, ...chainPaths],\n };\n}\n\nfunction toResolved(config: TastyValidationConfig): ResolvedConfig {\n return {\n tokens: config.tokens ?? [],\n units: config.units ?? [],\n // `funcs` is the v2 spelling; `functions` wins when both are present.\n functions: config.functions ?? config.funcs ?? [],\n states: config.states ?? [],\n presets: config.presets ?? [],\n recipes: config.recipes ?? [],\n styles: config.styles ?? [],\n importSources: config.importSources ?? DEFAULT_IMPORT_SOURCES,\n ownedSources: config.ownedSources ?? [],\n styleFunctions: config.styleFunctions ?? {},\n protectedComponents: config.protectedComponents || [],\n };\n}\n\nconst DEFAULT_CONFIG: ResolvedConfig = {\n tokens: [],\n units: [],\n functions: [],\n states: [],\n presets: [],\n recipes: [],\n styles: [],\n importSources: DEFAULT_IMPORT_SOURCES,\n ownedSources: [],\n styleFunctions: {},\n protectedComponents: [],\n};\n\nfunction getMtimes(paths: string[]): Map<string, number> {\n const mtimes = new Map<string, number>();\n for (const p of paths) {\n try {\n mtimes.set(p, statSync(p).mtimeMs);\n } catch {\n mtimes.set(p, -1);\n }\n }\n return mtimes;\n}\n\nfunction mtimesMatch(\n cached: Map<string, number>,\n current: Map<string, number>,\n): boolean {\n if (cached.size !== current.size) return false;\n for (const [path, mtime] of cached) {\n if (current.get(path) !== mtime) return false;\n }\n return true;\n}\n\nexport function loadConfig(filePath: string): ResolvedConfig {\n const fileDir = dirname(resolve(filePath));\n const projectRoot = findProjectRoot(fileDir);\n if (!projectRoot) return DEFAULT_CONFIG;\n\n const configFiles = collectConfigFiles(fileDir, projectRoot);\n\n const pkgDir = resolvePackageDir(TASTY_PACKAGE_NAME, projectRoot);\n const pkgConfigFile = pkgDir ? findConfigInDir(pkgDir) : null;\n\n if (configFiles.length === 0 && !pkgConfigFile) return DEFAULT_CONFIG;\n\n const cacheKey = [pkgConfigFile ?? '', ...configFiles].join('\\0');\n\n const cached = configCache.get(cacheKey);\n if (cached) {\n const currentMtimes = getMtimes([...cached.fileMtimes.keys()]);\n if (mtimesMatch(cached.fileMtimes, currentMtimes)) {\n return cached.config;\n }\n }\n\n let merged: TastyValidationConfig = {};\n const allChainPaths: string[] = [];\n\n if (pkgConfigFile) {\n const result = resolveConfigChain(pkgConfigFile);\n merged = result.config;\n allChainPaths.push(...result.chainPaths);\n }\n\n for (let i = configFiles.length - 1; i >= 0; i--) {\n const { config, chainPaths } = resolveConfigChain(configFiles[i]);\n merged = mergeConfigs(merged, config);\n allChainPaths.push(...chainPaths);\n }\n\n const resolved = toResolved(merged);\n const fileMtimes = getMtimes(allChainPaths);\n\n configCache.set(cacheKey, { config: resolved, fileMtimes });\n\n return resolved;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.js","names":["pluginVersion"],"sources":["../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'fs';\nimport { dirname, join, resolve } from 'path';\n\nimport { createJiti } from 'jiti';\nimport semver from 'semver';\n\nimport type { ResolvedConfig, TastyValidationConfig } from './types.js';\nimport { DEFAULT_IMPORT_SOURCES } from './constants.js';\nimport { version as pluginVersion } from './version.js';\n\nconst CONFIG_FILENAMES = [\n 'tasty.config.ts',\n 'tasty.config.js',\n 'tasty.config.mjs',\n 'tasty.config.json',\n];\n\ninterface CachedConfig {\n /** The resolved config, or the error that loading it failed with. */\n result: ResolvedConfig | Error;\n fileMtimes: Map<string, number>;\n}\n\nconst configCache = new Map<string, CachedConfig>();\n\n/**\n * Every key a config may carry; anything else is a typo or from a newer plugin.\n * Typed against the config interface so a new key cannot be missed here.\n */\nconst KNOWN_KEYS = new Set(\n Object.keys({\n extends: true,\n requires: true,\n tokens: true,\n units: true,\n functions: true,\n funcs: true,\n states: true,\n presets: true,\n recipes: true,\n styles: true,\n importSources: true,\n ownedSources: true,\n styleFunctions: true,\n protectedComponents: true,\n // The Tasty VS Code extension reads these from the same file.\n presetDescriptions: true,\n stateDescriptions: true,\n } satisfies Record<keyof TastyValidationConfig, true>),\n);\n\nconst warnings = new Set<string>();\n\n/** A config problem worth a message but not a failed run, said once per process. */\nfunction warnOnce(message: string): void {\n if (warnings.has(message)) return;\n warnings.add(message);\n console.warn(`[eslint-plugin-tasty] ${message}`);\n}\n\n/** Fails the run when the installed plugin is outside the config's `requires`. */\nfunction checkRequires(config: TastyValidationConfig, configPath: string) {\n const range: unknown = config.requires;\n if (range === undefined) return;\n if (\n typeof range !== 'string' ||\n range.trim() === '' ||\n semver.validRange(range) === null\n ) {\n throw new Error(\n `tasty: ${configPath}: 'requires' must be a semver range of @tenphi/eslint-plugin-tasty versions, such as '>=1.13.0'.`,\n );\n }\n // A snapshot of the next release still satisfies `>=` the current one.\n if (!semver.satisfies(pluginVersion, range, { includePrerelease: true })) {\n throw new Error(\n `tasty: ${configPath} requires @tenphi/eslint-plugin-tasty ${range}, but ${pluginVersion} is installed. Upgrade the plugin.`,\n );\n }\n}\n\n/** List keys, and whether `false` (turning the check off) is allowed. */\nconst LIST_KEYS = {\n tokens: true,\n units: true,\n functions: true,\n funcs: true,\n states: false,\n presets: false,\n recipes: false,\n styles: false,\n importSources: false,\n ownedSources: false,\n} as const;\n\n/** Keys Tasty's runtime config (`configure()`) holds as objects keyed by name. */\nconst RUNTIME_OBJECT_KEYS = [\n 'tokens',\n 'units',\n 'functions',\n 'states',\n 'presets',\n 'recipes',\n];\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) {\n return value.every((item) => typeof item === 'string')\n ? 'a list'\n : 'a list with a non-string entry';\n }\n return isPlainObject(value) ? 'an object' : `a ${typeof value}`;\n}\n\n/**\n * Warns about keys the plugin does not read and known keys of the wrong shape,\n * and returns the config without the latter, so `states: '@mobile'` is not\n * read character by character.\n */\nfunction checkKeys(\n config: TastyValidationConfig,\n configPath: string,\n): TastyValidationConfig {\n // Tasty's SSR guide keeps the runtime config in a `tasty.config.ts` too.\n const runtime = RUNTIME_OBJECT_KEYS.some((key) =>\n isPlainObject((config as Record<string, unknown>)[key]),\n )\n ? \" It looks like Tasty's runtime config, which the plugin does not read.\"\n : '';\n\n const unknown = Object.keys(config).filter(\n (key) => !KNOWN_KEYS.has(key) && key !== '__esModule',\n );\n if (unknown.length > 0) {\n const keys = unknown.map((key) => `'${key}'`).join(', ');\n warnOnce(\n `${configPath}: ignoring unknown ${unknown.length === 1 ? 'key' : 'keys'} ${keys}.${\n runtime ||\n ` Check the spelling, or upgrade @tenphi/eslint-plugin-tasty (${pluginVersion} is installed) if the config was written for a newer version.`\n }`,\n );\n }\n\n const checked: Record<string, unknown> = { ...config };\n const ignore = (key: string, expected: string) => {\n warnOnce(\n `${configPath}: ignoring '${key}', which must be ${expected}, not ${describe(checked[key])}.${runtime}`,\n );\n delete checked[key];\n };\n for (const [key, allowsFalse] of Object.entries(LIST_KEYS)) {\n const value = checked[key];\n if (\n value === undefined ||\n (value === false && allowsFalse) ||\n (Array.isArray(value) && value.every((item) => typeof item === 'string'))\n ) {\n continue;\n }\n ignore(key, `${allowsFalse ? 'false or ' : ''}a list of names`);\n }\n if (\n checked.styleFunctions !== undefined &&\n !isPlainObject(checked.styleFunctions)\n ) {\n ignore('styleFunctions', 'an object keyed by function name');\n }\n return checked as TastyValidationConfig;\n}\n\nfunction findProjectRoot(startDir: string): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n if (existsSync(join(dir, 'package.json'))) {\n return dir;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nfunction resolvePackageDir(\n packageName: string,\n startDir: string,\n): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n const candidate = join(dir, 'node_modules', ...packageName.split('/'));\n if (existsSync(candidate) && existsSync(join(candidate, 'package.json'))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nfunction findConfigInDir(dir: string): string | null {\n for (const name of CONFIG_FILENAMES) {\n const path = join(dir, name);\n if (existsSync(path)) {\n return path;\n }\n }\n return null;\n}\n\nfunction collectConfigFiles(startDir: string, projectRoot: string): string[] {\n const configs: string[] = [];\n let dir = startDir;\n const root = resolve(projectRoot);\n\n while (true) {\n const found = findConfigInDir(dir);\n if (found) configs.push(found);\n if (resolve(dir) === root) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return configs;\n}\n\nconst TASTY_PACKAGE_NAME = '@tenphi/tasty';\n\nfunction stripComments(source: string): string {\n let result = '';\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n result += ch;\n i++;\n while (i < source.length && source[i] !== ch) {\n if (source[i] === '\\\\') {\n result += source[i++];\n }\n if (i < source.length) {\n result += source[i++];\n }\n }\n if (i < source.length) {\n result += source[i++];\n }\n continue;\n }\n if (ch === '/' && source[i + 1] === '*') {\n i += 2;\n while (\n i < source.length &&\n !(source[i] === '*' && source[i + 1] === '/')\n ) {\n i++;\n }\n i += 2;\n continue;\n }\n if (ch === '/' && source[i + 1] === '/') {\n while (i < source.length && source[i] !== '\\n') {\n i++;\n }\n continue;\n }\n result += source[i++];\n }\n return result;\n}\n\nfunction stripImports(source: string): string {\n return source.replace(/^\\s*import\\s+.*?;\\s*$/gm, '');\n}\n\nfunction extractBalancedBraces(content: string, start: number): string | null {\n if (content[start] !== '{') return null;\n let depth = 0;\n for (let i = start; i < content.length; i++) {\n const ch = content[i];\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n i++;\n while (i < content.length && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n continue;\n }\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n if (depth === 0) return content.slice(start, i + 1);\n }\n return null;\n}\n\nfunction extractImportPaths(configPath: string): string[] {\n const source = readFileSync(configPath, 'utf-8');\n const configDir = dirname(configPath);\n const paths: string[] = [];\n const re = /^\\s*import\\s+.*?\\s+from\\s+['\"]([^'\"]+)['\"]/gm;\n let m;\n\n while ((m = re.exec(source)) !== null) {\n const specifier = m[1];\n\n if (!specifier.startsWith('.')) continue;\n\n const abs = resolve(configDir, specifier);\n\n for (const ext of ['', '.ts', '.js', '.mjs', '.tsx', '.jsx']) {\n const candidate = abs + ext;\n\n if (existsSync(candidate)) {\n paths.push(candidate);\n break;\n }\n }\n }\n\n return paths;\n}\n\nfunction stripTypeScriptSyntax(source: string): string {\n return source\n .replace(/\\bas\\s+const\\b/g, '')\n .replace(/\\bsatisfies\\s+[A-Z]\\w*(?:<[^>]*>)?/g, '')\n .replace(/\\bas\\s+[A-Z]\\w*(?:<[^>]*>)?/g, '');\n}\n\nfunction loadRawConfig(configPath: string): TastyValidationConfig {\n if (configPath.endsWith('.json')) {\n return JSON.parse(\n readFileSync(configPath, 'utf-8'),\n ) as TastyValidationConfig;\n }\n\n // Try jiti first — it resolves imports and TypeScript natively.\n try {\n const jiti = createJiti(dirname(configPath), { moduleCache: false });\n const mod = jiti(configPath) as Record<string, unknown>;\n const config = (mod.default ?? mod) as TastyValidationConfig;\n\n if (config && typeof config === 'object') {\n return config;\n }\n } catch {\n // Fall through to text-based parsing.\n }\n\n // Fallback: text-based extraction (no import support).\n const content = readFileSync(configPath, 'utf-8');\n const stripped = stripImports(stripComments(content));\n const match = stripped.match(/export\\s+default\\s+/);\n\n if (match && match.index != null) {\n const braceStart = match.index + match[0].length;\n const objectStr = extractBalancedBraces(stripped, braceStart);\n\n if (objectStr) {\n const cleaned = stripTypeScriptSyntax(objectStr);\n\n try {\n const fn = new Function(`return (${cleaned})`);\n return fn() as TastyValidationConfig;\n } catch (err) {\n console.warn(\n `[eslint-plugin-tasty] Failed to parse config file ${configPath}: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n }\n\n return {};\n}\n\nfunction mergeConfigs(\n parent: TastyValidationConfig,\n child: TastyValidationConfig,\n): TastyValidationConfig {\n const result: TastyValidationConfig = { ...parent };\n\n const arrayKeys = [\n ...(Object.keys(LIST_KEYS) as (keyof typeof LIST_KEYS)[]),\n 'protectedComponents',\n ] as const;\n\n for (const key of arrayKeys) {\n const childVal = child[key];\n if (childVal === undefined) continue;\n\n if (childVal === false) {\n (result as Record<string, unknown>)[key] = false;\n continue;\n }\n\n const parentVal = parent[key];\n if (Array.isArray(parentVal) && Array.isArray(childVal)) {\n (result as Record<string, unknown>)[key] = [\n ...new Set([...parentVal, ...childVal]),\n ];\n } else {\n (result as Record<string, unknown>)[key] = childVal;\n }\n }\n\n if (child.styleFunctions !== undefined) {\n result.styleFunctions = {\n ...parent.styleFunctions,\n ...child.styleFunctions,\n };\n }\n\n return result;\n}\n\n/**\n * The config file `extends` points to, or null when there is none. A relative\n * or absolute path that is not a file fails the run, as reading it always has; a\n * value that names nothing usable is skipped with a warning.\n */\nfunction resolveExtends(\n config: TastyValidationConfig,\n configPath: string,\n touched: string[],\n): string | null {\n const target: unknown = config.extends;\n if (target === undefined) return null;\n const configDir = dirname(configPath);\n if (typeof target !== 'string' || target === '') {\n warnOnce(\n `${configPath}: 'extends' must be a config path or a package name, not ${JSON.stringify(target)}. It is ignored.`,\n );\n return null;\n }\n\n if (target.startsWith('.') || target.startsWith('/')) {\n const parentPath = resolve(configDir, target);\n // Watched, so creating the file clears a cached failure.\n touched.push(parentPath);\n if (!isFile(parentPath)) {\n throw new Error(\n `tasty: ${configPath}: 'extends' points to ${parentPath}, which is not a file.`,\n );\n }\n return parentPath;\n }\n\n // `@scope/pkg/path/to/config` names a file inside the package.\n const parts = target.split('/');\n const packageName = parts.slice(0, target.startsWith('@') ? 2 : 1).join('/');\n const subpath = target.slice(packageName.length + 1);\n const pkgDir = resolvePackageDir(packageName, configDir);\n if (!pkgDir) {\n warnOnce(\n `${configPath}: 'extends' names '${packageName}', which cannot be resolved from ${configDir}. It is ignored.`,\n );\n return null;\n }\n const pkgConfig = subpath\n ? findConfigFile(join(pkgDir, subpath))\n : findConfigInDir(pkgDir);\n if (!pkgConfig) {\n warnOnce(\n subpath\n ? `${configPath}: 'extends' names '${target}', but ${join(pkgDir, subpath)} is not a config file. It is ignored.`\n : `${configPath}: 'extends' names '${target}', which has no tasty.config.* at ${pkgDir}. It is ignored.`,\n );\n }\n return pkgConfig;\n}\n\nfunction isFile(path: string): boolean {\n return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;\n}\n\n/** A config file at `path`, with or without its extension, or in that directory. */\nfunction findConfigFile(path: string): string | null {\n for (const ext of ['', '.ts', '.js', '.mjs', '.json']) {\n if (isFile(path + ext)) return path + ext;\n }\n return statSync(path, { throwIfNoEntry: false })?.isDirectory()\n ? findConfigInDir(path)\n : null;\n}\n\n/**\n * Loads a config and the chain it extends, parents merged first. Every file\n * read (or expected) is pushed to `touched`, whose mtimes decide when the\n * result, or a failure, is stale.\n */\nfunction resolveConfigChain(\n configPath: string,\n touched: string[],\n visited = new Set<string>(),\n): TastyValidationConfig {\n const absPath = resolve(configPath);\n if (visited.has(absPath)) {\n warnOnce(\n `${absPath} is extended again by its own extends chain. The cycle is ignored.`,\n );\n return {};\n }\n visited.add(absPath);\n touched.push(absPath);\n\n let config = loadRawConfig(absPath);\n touched.push(...extractImportPaths(absPath));\n\n if (!config || typeof config !== 'object' || Array.isArray(config)) {\n warnOnce(`${absPath}: the config must be an object. It is ignored.`);\n config = {};\n }\n checkRequires(config, absPath);\n config = checkKeys(config, absPath);\n\n const parentPath = resolveExtends(config, absPath, touched);\n if (!parentPath) return config;\n\n return mergeConfigs(resolveConfigChain(parentPath, touched, visited), config);\n}\n\nfunction toResolved(config: TastyValidationConfig): ResolvedConfig {\n return {\n tokens: config.tokens ?? [],\n units: config.units ?? [],\n // `funcs` is the v2 spelling; `functions` wins when both are present.\n functions: config.functions ?? config.funcs ?? [],\n states: config.states ?? [],\n presets: config.presets ?? [],\n recipes: config.recipes ?? [],\n styles: config.styles ?? [],\n importSources: config.importSources ?? DEFAULT_IMPORT_SOURCES,\n ownedSources: config.ownedSources ?? [],\n styleFunctions: config.styleFunctions ?? {},\n protectedComponents: config.protectedComponents || [],\n };\n}\n\nconst DEFAULT_CONFIG: ResolvedConfig = {\n tokens: [],\n units: [],\n functions: [],\n states: [],\n presets: [],\n recipes: [],\n styles: [],\n importSources: DEFAULT_IMPORT_SOURCES,\n ownedSources: [],\n styleFunctions: {},\n protectedComponents: [],\n};\n\nfunction getMtimes(paths: string[]): Map<string, number> {\n const mtimes = new Map<string, number>();\n for (const p of paths) {\n try {\n mtimes.set(p, statSync(p).mtimeMs);\n } catch {\n mtimes.set(p, -1);\n }\n }\n return mtimes;\n}\n\nfunction mtimesMatch(\n cached: Map<string, number>,\n current: Map<string, number>,\n): boolean {\n if (cached.size !== current.size) return false;\n for (const [path, mtime] of cached) {\n if (current.get(path) !== mtime) return false;\n }\n return true;\n}\n\nexport function loadConfig(filePath: string): ResolvedConfig {\n const fileDir = dirname(resolve(filePath));\n const projectRoot = findProjectRoot(fileDir);\n if (!projectRoot) return DEFAULT_CONFIG;\n\n const configFiles = collectConfigFiles(fileDir, projectRoot);\n\n const pkgDir = resolvePackageDir(TASTY_PACKAGE_NAME, projectRoot);\n const pkgConfigFile = pkgDir ? findConfigInDir(pkgDir) : null;\n\n if (configFiles.length === 0 && !pkgConfigFile) return DEFAULT_CONFIG;\n\n const cacheKey = [pkgConfigFile ?? '', ...configFiles].join('\\0');\n\n const cached = configCache.get(cacheKey);\n if (\n cached &&\n mtimesMatch(cached.fileMtimes, getMtimes([...cached.fileMtimes.keys()]))\n ) {\n if (cached.result instanceof Error) throw cached.result;\n return cached.result;\n }\n\n // A failure is cached too: oxlint keeps linting after a plugin error, and\n // every rule on every file would otherwise reload the chain to fail again.\n const touched: string[] = [];\n let result: ResolvedConfig | Error;\n try {\n let merged: TastyValidationConfig = pkgConfigFile\n ? resolveConfigChain(pkgConfigFile, touched)\n : {};\n for (let i = configFiles.length - 1; i >= 0; i--) {\n merged = mergeConfigs(\n merged,\n resolveConfigChain(configFiles[i], touched),\n );\n }\n result = toResolved(merged);\n } catch (error) {\n result = error instanceof Error ? error : new Error(String(error));\n }\n\n configCache.set(cacheKey, { result, fileMtimes: getMtimes(touched) });\n if (result instanceof Error) throw result;\n return result;\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { version } from "./version.js";
1
2
  import known_property_default from "./rules/known-property.js";
2
3
  import valid_value_default from "./rules/valid-value.js";
3
4
  import valid_color_token_default from "./rules/valid-color-token.js";
@@ -43,7 +44,6 @@ import prefer_hide_default from "./rules/prefer-hide.js";
43
44
  import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
44
45
  import prefer_longhand_property_default from "./rules/prefer-longhand-property.js";
45
46
  import { recommended, strict } from "./configs.js";
46
- import { createRequire } from "node:module";
47
47
 
48
48
  //#region src/index.ts
49
49
  const ruleMap = {
@@ -92,7 +92,6 @@ const ruleMap = {
92
92
  "prefer-directional-shorthand": prefer_directional_shorthand_default,
93
93
  "prefer-longhand-property": prefer_longhand_property_default
94
94
  };
95
- const { version } = createRequire(import.meta.url)("../package.json");
96
95
  const plugin = {
97
96
  meta: {
98
97
  name: "@tenphi/eslint-plugin-tasty",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","rules.preferElementSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.noStyleProp","rules.noClassNameProp","rules.noProtectedRestyle","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.noIgnoredPseudoElementState","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'no-state-in-selector': rules.noStateInSelector,\n 'prefer-element-selector': rules.preferElementSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'no-style-prop': rules.noStyleProp,\n 'no-classname-prop': rules.noClassNameProp,\n 'no-protected-restyle': rules.noProtectedRestyle,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'no-ignored-pseudo-element-state': rules.noIgnoredPseudoElementState,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TypeAwareJSXOptions } from './type-aware-jsx.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n ProtectedComponentConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,wBAAwBC;CACxB,2BAA2BC;CAC3B,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,iBAAiBC;CACjB,qBAAqBC;CACrB,wBAAwBC;CACxB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,mCAAmCC;CACnC,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
1
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","rules.preferElementSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.noStyleProp","rules.noClassNameProp","rules.noProtectedRestyle","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.noIgnoredPseudoElementState","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\nimport { version } from './version.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'no-state-in-selector': rules.noStateInSelector,\n 'prefer-element-selector': rules.preferElementSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'no-style-prop': rules.noStyleProp,\n 'no-classname-prop': rules.noClassNameProp,\n 'no-protected-restyle': rules.noProtectedRestyle,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'no-ignored-pseudo-element-state': rules.noIgnoredPseudoElementState,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TypeAwareJSXOptions } from './type-aware-jsx.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n ProtectedComponentConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,wBAAwBC;CACxB,2BAA2BC;CAC3B,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,iBAAiBC;CACjB,qBAAqBC;CACrB,wBAAwBC;CACxB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,mCAAmCC;CACnC,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAED,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
package/dist/types.d.ts CHANGED
@@ -32,6 +32,12 @@ interface ProtectedComponentConfig {
32
32
  }
33
33
  interface TastyValidationConfig {
34
34
  extends?: string;
35
+ /**
36
+ * The `@tenphi/eslint-plugin-tasty` versions this config needs, as a semver
37
+ * range such as `'>=1.13.0'`. Every config in the `extends` chain may declare
38
+ * one, and linting fails when the installed plugin does not satisfy them all.
39
+ */
40
+ requires?: string;
35
41
  tokens?: false | string[];
36
42
  units?: false | string[];
37
43
  /**
@@ -66,6 +72,10 @@ interface TastyValidationConfig {
66
72
  * restyle. Entries from an extended config come first; `false` drops them.
67
73
  */
68
74
  protectedComponents?: false | ProtectedComponentConfig[];
75
+ /** Read by the Tasty VS Code extension, which shares this file; ignored here. */
76
+ presetDescriptions?: Record<string, string>;
77
+ /** Read by the Tasty VS Code extension, which shares this file; ignored here. */
78
+ stateDescriptions?: Record<string, string>;
69
79
  }
70
80
  interface ResolvedConfig {
71
81
  tokens: false | string[];
@@ -0,0 +1,8 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ //#region src/version.ts
4
+ const { version } = createRequire(import.meta.url)("../package.json");
5
+
6
+ //#endregion
7
+ export { version };
8
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["import { createRequire } from 'node:module';\n\n// Read from package.json so the version cannot drift from the released one, as\n// a hardcoded literal once did. `../package.json` resolves to the package root\n// from both `src/` and the unbundled `dist/`.\nexport const { version } = createRequire(import.meta.url)(\n '../package.json',\n) as { version: string };\n"],"mappings":";;;AAKA,MAAa,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CACvD,kBACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -46,7 +46,8 @@
46
46
  "dependencies": {
47
47
  "@typescript-eslint/utils": "^8.56.0",
48
48
  "jiti": "^2.6.1",
49
- "postcss-selector-parser": "^7.1.6"
49
+ "postcss-selector-parser": "^7.1.6",
50
+ "semver": "^7.7.4"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@changesets/changelog-github": "^0.5.2",
@@ -54,6 +55,7 @@
54
55
  "@eslint/js": "^10.0.1",
55
56
  "@tenphi/tasty": "^3.8.0",
56
57
  "@types/node": "^22.0.0",
58
+ "@types/semver": "^7.8.0",
57
59
  "@typescript-eslint/parser": "^8.56.1",
58
60
  "@typescript-eslint/rule-tester": "^8.56.1",
59
61
  "@webref/css": "^8.7.8",