@santi020k/eslint-config-basic 3.5.4 → 3.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @santi020k/eslint-config-basic
2
2
 
3
+ ## 3.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Preserve Node 22.19 support with compatible stable dependencies and deliver the maintained braces recursion guard in affected adapter artifacts. ESLint, GraphQL, and TypeScript remain consumer peers.
8
+
9
+ - Add optional typed formatting preferences with workspace inheritance. Restrict Testing Library import heuristics in Playwright-owned scopes while retaining actual Testing Library checks and unit-test behavior.
10
+
11
+ Doctor now inventories local workspace lint declarations, reports sampled effective-config evidence for potentially redundant Astro workaround fields, and generates missing lint scripts with zero-warning enforcement. Existing scripts, defaults, and overrides remain unchanged.
12
+
13
+ Consumer regression fixtures cover mixed Astro/React workspaces, independently linted Expo apps with typed Astro scopes, Playwright/Testing Library coexistence, and React/Hono runtime scopes. No mandatory migration is required. For custom browser-test locations, configure `testingFiles.playwright`; review Doctor candidates across all affected globs before removing fields.
14
+
15
+ - Refresh the supported dependency ecosystem and security overrides, preserve Zod integration defaults across the plugin upgrade, guarantee TypeScript parsing in Astro frontmatter, and improve the release gates and npm documentation.
16
+
17
+ The Basic README gains light/dark artwork, package selection guidance, and published asset/support links. The documentation site uses npm Lumen Astro 4 with accessible motion, contextual GitHub issue reporting, resilient browser feedback, responsive report output, and full-glyph WOFF2 fonts that substantially reduce transfer size.
18
+
19
+ Release preparation now accepts validated `release/v<stable-semver>` branches alongside Changesets-generated branches; both undergo source/version checks, complete preflight, and a dependency audit before publication.
20
+
21
+ Release verification now retains Node 24 and Windows compatibility checks on
22
+ release pull requests, runs Linux and macOS visual baselines plus Firefox/WebKit
23
+ navigation coverage, and verifies registry artifacts, provenance, release tags,
24
+ published consumer behavior, and live documentation after deployment.
25
+
26
+ ### Patch Changes
27
+
28
+ - Center the documentation mobile menu and close icons, align utility controls, and clarify the open state with a subtle accented button surface.
29
+
30
+ - Animate documentation mobile menu opening and closing with subtle icon and drawer transitions, including a brief blur and immediate reduced-motion states.
31
+
32
+ - Fix narrow mobile header clipping, improve sidebar readability, and remove redundant spacing around the mobile table of contents.
33
+
34
+ - Resolve generated Markdown reference links to published routes across current docs and frozen archives, preserving fragments and deployment base paths.
35
+
36
+ - Clarify the v3.6.0 release candidate, upgrade verification, current troubleshooting, and shared editor and CI lint setup.
37
+ - Updated dependencies [`ddaa3c8`]:
38
+ - @santi020k/eslint-config-core@3.2.0
39
+ - @santi020k/eslint-config-typescript@3.1.6
40
+
3
41
  ## 3.5.4
4
42
 
5
43
  ### Patch Changes
package/README.md CHANGED
@@ -1,17 +1,49 @@
1
- # @santi020k/eslint-config-basic
1
+ <p align="center">
2
+ <a href="https://eslint.santi020k.com/">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/santi020k/eslint-config-basic/main/assets/readme/hero-dark.svg">
5
+ <img src="https://raw.githubusercontent.com/santi020k/eslint-config-basic/main/assets/readme/hero-light.svg" alt="ESLint Config Basic — Less setup. Clearer feedback." width="1200" height="360">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <h1 align="center">@santi020k/eslint-config-basic</h1>
11
+
12
+ <p align="center">
13
+ <strong>A lean foundation for consistent JavaScript and TypeScript quality.</strong><br>
14
+ Detect your stack. Add only what you need. Keep feedback useful.
15
+ </p>
2
16
 
3
17
  DX-first ESLint 10 flat config for JavaScript and TypeScript, with auto-detection
4
18
  and opt-in framework packages.
5
19
 
6
- [Documentation](https://eslint.santi020k.com/) ·
7
- [npm](https://www.npmjs.com/package/@santi020k/eslint-config-basic) ·
8
- [Migration to v3](https://eslint.santi020k.com/guide/migration-v2-to-v3/)
20
+ <p align="center">
21
+ <a href="https://eslint.santi020k.com/">Documentation</a> ·
22
+ <a href="https://eslint.santi020k.com/guide/config-builder/">Config builder</a> ·
23
+ <a href="https://www.npmjs.com/package/@santi020k/eslint-config-basic">npm</a> ·
24
+ <a href="https://eslint.santi020k.com/guide/migration-v2-to-v3/">Migration to v3</a> ·
25
+ <a href="https://github.com/santi020k/eslint-config-basic/issues/new/choose">Get help</a>
26
+ </p>
9
27
 
10
- [![Build](https://github.com/santi020k/eslint-config-basic/actions/workflows/build.yml/badge.svg)](https://github.com/santi020k/eslint-config-basic/actions/workflows/build.yml)
11
- [![CodeQL](https://github.com/santi020k/eslint-config-basic/actions/workflows/codeql.yml/badge.svg)](https://github.com/santi020k/eslint-config-basic/actions/workflows/codeql.yml)
12
- [![npm version](https://img.shields.io/npm/v/@santi020k/eslint-config-basic.svg)](https://www.npmjs.com/package/@santi020k/eslint-config-basic)
13
- [![npm downloads](https://img.shields.io/npm/dm/@santi020k/eslint-config-basic.svg)](https://www.npmjs.com/package/@santi020k/eslint-config-basic)
14
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/santi020k/eslint-config-basic/blob/main/LICENSE)
28
+ <p align="center">
29
+ <a href="https://github.com/santi020k/eslint-config-basic/actions/workflows/build.yml"><img src="https://github.com/santi020k/eslint-config-basic/actions/workflows/build.yml/badge.svg" alt="Build"></a>
30
+ <a href="https://github.com/santi020k/eslint-config-basic/actions/workflows/codeql.yml"><img src="https://github.com/santi020k/eslint-config-basic/actions/workflows/codeql.yml/badge.svg" alt="CodeQL"></a>
31
+ <a href="https://www.npmjs.com/package/@santi020k/eslint-config-basic"><img src="https://img.shields.io/npm/v/@santi020k/eslint-config-basic.svg" alt="npm version"></a>
32
+ <a href="https://www.npmjs.com/package/@santi020k/eslint-config-basic"><img src="https://img.shields.io/npm/dm/@santi020k/eslint-config-basic.svg" alt="npm downloads"></a>
33
+ <a href="https://github.com/santi020k/eslint-config-basic/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
34
+ </p>
35
+
36
+ **Explore:** [Quick start](#quick-start) · [Why v3 changed dependency ownership](#why-v3-changed-dependency-ownership) · [Custom configuration](#custom-configuration) · [Package choice](#package-choice) · [CLI](#cli) · [Compatibility](#compatibility)
37
+
38
+ ## Choose your path
39
+
40
+ | Your project | Start here | What stays in your control |
41
+ | --- | --- | --- |
42
+ | **JavaScript or TypeScript** | [Basic](https://eslint.santi020k.com/packages/basic/) | A lean foundation with automatic detection |
43
+ | **A framework application** | [Framework guides](https://eslint.santi020k.com/frameworks/react/) | Install the adapter for your actual stack |
44
+ | **Testing and tooling** | [Feature packs](https://eslint.santi020k.com/tooling/overview/) | Opt into the integrations you use |
45
+ | **Everything included** | [Full](https://eslint.santi020k.com/packages/full/) | One package owns the supported integrations |
46
+ | **An existing configuration** | [Interactive builder](https://eslint.santi020k.com/guide/config-builder/) | Preview choices before adopting them |
15
47
 
16
48
  ## Quick start
17
49
 
@@ -151,6 +183,31 @@ export default await defineConfig({
151
183
  TypeScript config files (`**/*.config.{ts,mts,cts}`) use this syntax fallback
152
184
  automatically. Set `untypedFiles: false` to require type information everywhere.
153
185
 
186
+ ## Formatting preferences
187
+
188
+ Choose common style preferences without repeating individual rule names:
189
+
190
+ ```js
191
+ export default await defineConfig({
192
+ formatting: {
193
+ arrowParens: 'always',
194
+ commaDangle: 'always-multiline',
195
+ quotes: 'double',
196
+ semi: true
197
+ }
198
+ })
199
+ ```
200
+
201
+ Every field is optional. `arrowParens` accepts `always` or `as-needed`,
202
+ `commaDangle` accepts `always-multiline` or `never`, `quotes` accepts `double` or
203
+ `single`, and `semi` is a boolean. Omitted fields retain existing defaults.
204
+ Preferences use warning severity; `strict: 'ci'` or `strict: 'pedantic'` promotes
205
+ them to errors. Correctness rules are unchanged. Prettier compatibility and local
206
+ flat-config overrides take precedence. Root preferences are inherited by
207
+ `projects`; each project can override individual fields. Use
208
+ `optionMergeStrategy: 'replace'` in a project to replace its inherited formatting
209
+ object instead of merging fields.
210
+
154
211
  ## Package choice
155
212
 
156
213
  | Package | Use it when | Dependency model |
@@ -161,6 +218,20 @@ automatically. Set `untypedFiles: false` to require type information everywhere.
161
218
  | `@santi020k/eslint-config-integrations` | Existing aggregate-package users | Deprecated v3 compatibility path; removed in v4 |
162
219
  | Individual framework packages | Custom composition | One framework and its plugin set |
163
220
 
221
+ ## From setup to useful feedback
222
+
223
+ ```mermaid
224
+ flowchart LR
225
+ A[Install Basic] --> B[Detect your stack]
226
+ B --> C[Compose flat config]
227
+ C --> D[Lint in editor and CI]
228
+ D --> E[Explain and improve]
229
+ ```
230
+
231
+ Detection starts from your configuration directory. Framework adapters and feature packs
232
+ stay explicit dependencies; lint feedback works in your editor and existing CI.
233
+ Use the CLI to understand a rule before changing it, and review a migration before writing it.
234
+
164
235
  ## CLI
165
236
 
166
237
  ```sh
@@ -183,11 +254,23 @@ that editor-facing ESLint config declarations stay portable. The remaining
183
254
  commands automate v3 migration, incremental strict-mode adoption, budgeted
184
255
  performance profiling, and effective-rule change review.
185
256
 
257
+ Doctor's JSON project report includes `lintOwnership.localConfig`, `lintScript`,
258
+ and `guidance` to help review overlapping workspace lint commands. Doctor also
259
+ reports potentially redundant Astro parser and rule fields passed directly to
260
+ `defineConfig()` when the installed composer and current effective config agree
261
+ on sampled files. These are review candidates, never automatic removals.
262
+
263
+ When Playwright and Testing Library are both enabled, Playwright-owned globs
264
+ restrict Testing Library's import heuristics while retaining checks for actual
265
+ Testing Library imports. Use `testingFiles.playwright` for browser tests outside
266
+ the standard Playwright directories; generic `tests/**/*.spec.ts` files are not
267
+ assumed to belong to Playwright.
268
+
186
269
  ## Compatibility
187
270
 
188
271
  - Node.js: `>=22.19.0`
189
272
  - ESLint: `^10.0.0`
190
- - TypeScript: `>=5.0.0` when enabled
273
+ - TypeScript: `>=5.0.0 <7.0.0` when enabled
191
274
  - Package managers: npm, pnpm, Yarn, and Bun
192
275
 
193
276
  Packages in the `@santi020k/eslint-config-*` family use independent minor and
@@ -201,6 +284,18 @@ for package moves and copy-paste commands. The
201
284
  [planned v4 removals](https://eslint.santi020k.com/guide/v4-removals/) page
202
285
  tracks compatibility APIs that are deprecated during v3.
203
286
 
287
+ ## Contribute and get support
288
+
289
+ - [Report a reproducible bug](https://github.com/santi020k/eslint-config-basic/issues/new/choose), ask a setup question, or propose a framework integration.
290
+ - [Read the contribution guide](https://github.com/santi020k/eslint-config-basic/blob/main/.github/CONTRIBUTING.md) for workspace setup, behavioral tests, and the release checks.
291
+ - [Report security vulnerabilities privately](https://github.com/santi020k/eslint-config-basic/blob/main/.github/SECURITY.md); keep credentials and private project data out of public issues.
292
+ - [Read the changelog](https://eslint.santi020k.com/guide/changelog/) before upgrading.
293
+
294
+ Built by [Santiago Molina](https://santi020k.com), alongside
295
+ [Lumen UI](https://github.com/santi020k/lumen),
296
+ [Quality](https://github.com/santi020k/quality), and
297
+ [Astro Doctor](https://github.com/santi020k/astro-doctor).
298
+
204
299
  ## License
205
300
 
206
301
  MIT © [santi020k](https://santi020k.com). See [LICENSE](https://github.com/santi020k/eslint-config-basic/blob/main/LICENSE).
@@ -0,0 +1,24 @@
1
+ import { FlatConfigArray } from '@santi020k/eslint-config-core';
2
+
3
+ interface EffectiveConfig {
4
+ languageOptions?: {
5
+ parserOptions?: Record<string, unknown>;
6
+ };
7
+ rules?: Record<string, unknown>;
8
+ settings?: Record<string, unknown>;
9
+ }
10
+ interface ConfigInspector {
11
+ calculateConfigForFile: (file: string) => Promise<EffectiveConfig | undefined>;
12
+ }
13
+ type InspectorConstructor = new (options: {
14
+ cwd: string;
15
+ overrideConfig: FlatConfigArray;
16
+ overrideConfigFile: true;
17
+ }) => ConfigInspector;
18
+ /** Narrow ESLint's dynamic effective-config result at the API boundary. */
19
+ declare const toEffectiveConfig: (value: unknown) => EffectiveConfig | undefined;
20
+ /** Only report fields proven redundant for a sampled file; never remove a whole override block. */
21
+ declare const compareConsumerOverrides: (cwd: string, config: FlatConfigArray, baseline: FlatConfigArray, Inspector: InspectorConstructor) => Promise<string[]>;
22
+ declare const getConsumerOverrideGuidance: (cwd: string, configPath: null | string) => Promise<string[]>;
23
+
24
+ export { compareConsumerOverrides, getConsumerOverrideGuidance, toEffectiveConfig };
@@ -0,0 +1,146 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { createRequire, findPackageJSON } from "node:module";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { flattenConfigInputs } from "@santi020k/eslint-config-core";
6
+ import { getDefineConfigMetadata } from "./define-config-metadata.js";
7
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
8
+ const isFlatConfig = (value) => Array.isArray(value) && value.every(isRecord);
9
+ const isComposer = (value) => isRecord(value) && typeof value.defineConfig === "function";
10
+ const isEslint = (value) => isRecord(value) && typeof value.ESLint === "function";
11
+ const toEffectiveConfig = (value) => {
12
+ if (!isRecord(value)) return void 0;
13
+ const languageOptions = isRecord(value.languageOptions) ? value.languageOptions : void 0;
14
+ const parserOptions = isRecord(languageOptions?.parserOptions) ? languageOptions.parserOptions : void 0;
15
+ return {
16
+ languageOptions: { parserOptions },
17
+ rules: isRecord(value.rules) ? value.rules : void 0,
18
+ settings: isRecord(value.settings) ? value.settings : void 0
19
+ };
20
+ };
21
+ const ASTRO_WORKAROUND_RULES = /* @__PURE__ */ new Set([
22
+ "@stylistic/indent",
23
+ "@stylistic/jsx-closing-tag-location",
24
+ "@stylistic/jsx-indent",
25
+ "@stylistic/jsx-indent-props",
26
+ "@typescript-eslint/no-unused-vars",
27
+ "@typescript-eslint/no-unused-expressions",
28
+ "no-undef",
29
+ "no-unused-vars",
30
+ "no-unused-expressions"
31
+ ]);
32
+ const collectAstroFiles = (root, files, depth = 0) => {
33
+ if (depth > 8) return;
34
+ const entries = readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
35
+ for (const entry of entries) {
36
+ if (files.length >= 32) break;
37
+ const path = join(root, entry.name);
38
+ if (entry.isFile() && entry.name.endsWith(".astro")) files.push(path);
39
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !["node_modules", "dist", "build", "coverage"].includes(entry.name)) collectAstroFiles(path, files, depth + 1);
40
+ }
41
+ };
42
+ const severity = (value) => {
43
+ const first = Array.isArray(value) ? value[0] : value;
44
+ return first === "off" ? 0 : first;
45
+ };
46
+ const createCandidates = (cwd, extras, baseline, Inspector) => extras.filter((entry) => entry.files?.some((pattern) => typeof pattern === "string" && pattern.includes(".astro"))).filter((entry) => entry.languageOptions?.parserOptions?.parser || Object.keys(entry.rules ?? {}).some((rule) => ASTRO_WORKAROUND_RULES.has(rule))).map((entry, index) => ({
47
+ entry,
48
+ inspector: new Inspector({
49
+ cwd,
50
+ overrideConfig: [...baseline, {
51
+ ...entry,
52
+ settings: { ...entry.settings, "eslint-config-basic/doctor-scope": true }
53
+ }],
54
+ overrideConfigFile: true
55
+ }),
56
+ key: `${index}:${entry.name ?? "override"}`
57
+ }));
58
+ const isRedundantRule = (rule, value, current, baseline) => ASTRO_WORKAROUND_RULES.has(rule) && severity(value) === 0 && severity(current.rules?.[rule]) === 0 && severity(baseline.rules?.[rule]) === 0;
59
+ const getParser = (config) => config.languageOptions?.parserOptions?.parser;
60
+ const sameParser = (left, right) => left === right || isRecord(left) && isRecord(right) && typeof left.parseForESLint === "function" && left.parseForESLint === right.parseForESLint && left.parse === right.parse;
61
+ const isRedundantParser = (entry, current, baseline) => {
62
+ const parser = getParser(entry);
63
+ return Boolean(parser) && sameParser(parser, getParser(current)) && sameParser(parser, getParser(baseline));
64
+ };
65
+ const recordCandidate = (candidate, current, baseline, file, guidance) => {
66
+ const { entry, key } = candidate;
67
+ for (const [rule, value] of Object.entries(entry.rules ?? {})) {
68
+ if (!isRedundantRule(rule, value, current, baseline)) continue;
69
+ const id = `${key}:${rule}`;
70
+ guidance.set(id, guidance.get(id) ?? `Review Astro workaround ${rule}: installed generated and current effective configs both disable it for ${file}. Remove only that field after checking all override globs; rerun lint, typecheck, and repeated autofix before removing the override.`);
71
+ }
72
+ if (isRedundantParser(entry, current, baseline)) {
73
+ const id = `${key}:parser`;
74
+ guidance.set(id, guidance.get(id) ?? `Review the explicit Astro TypeScript parser: the installed generated config already supplies the same parser for ${file}. Check every override glob and rerun lint, typecheck, and repeated autofix before removing only the parser field.`);
75
+ }
76
+ };
77
+ const inspectTarget = async (cwd, target, current, generated, candidates, guidance) => {
78
+ const [effective, defaults] = await Promise.all([
79
+ current.calculateConfigForFile(target),
80
+ generated.calculateConfigForFile(target)
81
+ ]);
82
+ if (!effective || !defaults) return;
83
+ for (const candidate of candidates) {
84
+ const scoped = await candidate.inspector.calculateConfigForFile(target);
85
+ if (scoped?.settings?.["eslint-config-basic/doctor-scope"] !== true) continue;
86
+ recordCandidate(candidate, effective, defaults, relative(cwd, target), guidance);
87
+ }
88
+ };
89
+ const compareConsumerOverrides = async (cwd, config, baseline, Inspector) => {
90
+ const metadata = getDefineConfigMetadata(config);
91
+ if (!metadata) return [];
92
+ const candidates = createCandidates(cwd, flattenConfigInputs(metadata.extraConfigs), baseline, Inspector);
93
+ if (candidates.length === 0) return [];
94
+ const current = new Inspector({ cwd, overrideConfig: config, overrideConfigFile: true });
95
+ const generated = new Inspector({ cwd, overrideConfig: baseline, overrideConfigFile: true });
96
+ const guidance = /* @__PURE__ */ new Map();
97
+ const files = [];
98
+ collectAstroFiles(cwd, files);
99
+ for (const file of files) {
100
+ await inspectTarget(cwd, file, current, generated, candidates, guidance);
101
+ await inspectTarget(cwd, `${file}/consumer-script.js`, current, generated, candidates, guidance);
102
+ }
103
+ return [...guidance.values()];
104
+ };
105
+ const resolveComposerEntry = (cwd) => {
106
+ const manifestPath = findPackageJSON(
107
+ "@santi020k/eslint-config-basic",
108
+ pathToFileURL(join(cwd, "package.json"))
109
+ );
110
+ if (!manifestPath) throw new Error("The installed Basic package manifest could not be resolved.");
111
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
112
+ const exports = isRecord(manifest) && isRecord(manifest.exports) ? manifest.exports : void 0;
113
+ const entry = isRecord(exports?.["."]) ? exports["."].import : void 0;
114
+ if (typeof entry !== "string" || !entry.startsWith("./")) {
115
+ throw new Error("The installed Basic package does not declare a supported public root import export.");
116
+ }
117
+ return pathToFileURL(join(dirname(manifestPath), entry)).href;
118
+ };
119
+ const compareInstalledComposer = async (cwd, config) => {
120
+ const metadata = getDefineConfigMetadata(config);
121
+ if (!metadata || metadata.extraConfigs.length === 0) return [];
122
+ const projectRequire = createRequire(join(cwd, "package.json"));
123
+ const composer = await import(resolveComposerEntry(cwd));
124
+ const eslint = projectRequire("eslint");
125
+ if (!isComposer(composer) || !isEslint(eslint)) return [];
126
+ const legacyRoot = Reflect.get(metadata.options ?? {}, "detectRootDir");
127
+ const root = metadata.root ?? metadata.options?.root ?? (typeof legacyRoot === "string" ? legacyRoot : cwd);
128
+ const baseline = await composer.defineConfig({ ...metadata.options, root });
129
+ return await compareConsumerOverrides(cwd, config, baseline, eslint.ESLint);
130
+ };
131
+ const getConsumerOverrideGuidance = async (cwd, configPath) => {
132
+ if (!configPath || !existsSync(configPath)) return [];
133
+ try {
134
+ const loaded = await import(pathToFileURL(configPath).href);
135
+ const config = isRecord(loaded) ? await loaded.default : void 0;
136
+ return isFlatConfig(config) ? await compareInstalledComposer(cwd, config) : [];
137
+ } catch (error) {
138
+ return [`Could not compare consumer overrides with the installed generated config: ${error instanceof Error ? error.message : String(error)}. Keep existing overrides until effective-config comparison succeeds.`];
139
+ }
140
+ };
141
+ export {
142
+ compareConsumerOverrides,
143
+ getConsumerOverrideGuidance,
144
+ toEffectiveConfig
145
+ };
146
+ //# sourceMappingURL=cli-consumer-guidance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli-consumer-guidance.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { createRequire, findPackageJSON } from 'node:module'\nimport { dirname, join, relative } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport { type FlatConfigArray, flattenConfigInputs } from '@santi020k/eslint-config-core'\n\nimport { getDefineConfigMetadata } from './define-config-metadata.js'\nimport type { defineConfig } from './index.js'\n\ninterface EffectiveConfig {\n languageOptions?: { parserOptions?: Record<string, unknown> }\n rules?: Record<string, unknown>\n settings?: Record<string, unknown>\n}\n\ninterface ConfigInspector {\n calculateConfigForFile: (file: string) => Promise<EffectiveConfig | undefined>\n}\n\ntype InspectorConstructor = new(options: {\n cwd: string\n overrideConfig: FlatConfigArray\n overrideConfigFile: true\n}) => ConfigInspector\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => (\n value !== null && typeof value === 'object' && !Array.isArray(value)\n)\n\nconst isFlatConfig = (value: unknown): value is FlatConfigArray => (\n Array.isArray(value) && value.every(isRecord)\n)\n\nconst isComposer = (value: unknown): value is { defineConfig: typeof defineConfig } => (\n isRecord(value) && typeof value.defineConfig === 'function'\n)\n\nconst isEslint = (value: unknown): value is { ESLint: InspectorConstructor } => (\n isRecord(value) && typeof value.ESLint === 'function'\n)\n\n/** Narrow ESLint's dynamic effective-config result at the API boundary. */\nexport const toEffectiveConfig = (value: unknown): EffectiveConfig | undefined => {\n if (!isRecord(value)) return undefined\n\n const languageOptions = isRecord(value.languageOptions) ? value.languageOptions : undefined\n const parserOptions = isRecord(languageOptions?.parserOptions) ? languageOptions.parserOptions : undefined\n\n return {\n languageOptions: { parserOptions },\n rules: isRecord(value.rules) ? value.rules : undefined,\n settings: isRecord(value.settings) ? value.settings : undefined\n }\n}\n\nconst ASTRO_WORKAROUND_RULES = new Set([\n '@stylistic/indent',\n '@stylistic/jsx-closing-tag-location',\n '@stylistic/jsx-indent',\n '@stylistic/jsx-indent-props',\n '@typescript-eslint/no-unused-vars',\n '@typescript-eslint/no-unused-expressions',\n 'no-undef',\n 'no-unused-vars',\n 'no-unused-expressions'\n])\n\nconst collectAstroFiles = (root: string, files: string[], depth = 0): void => {\n if (depth > 8) return\n\n const entries = readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))\n\n for (const entry of entries) {\n if (files.length >= 32) break\n\n const path = join(root, entry.name)\n\n if (entry.isFile() && entry.name.endsWith('.astro')) files.push(path)\n\n if (entry.isDirectory() && !entry.name.startsWith('.') &&\n !['node_modules', 'dist', 'build', 'coverage'].includes(entry.name)) collectAstroFiles(path, files, depth + 1)\n }\n}\n\nconst severity = (value: unknown): unknown => {\n const first: unknown = Array.isArray(value) ? value[0] : value\n\n return first === 'off' ? 0 : first\n}\n\ntype ConfigEntry = FlatConfigArray[number]\n\ninterface Candidate {\n entry: ConfigEntry\n inspector: ConfigInspector\n key: string\n}\n\nconst createCandidates = (\n cwd: string,\n extras: FlatConfigArray,\n baseline: FlatConfigArray,\n Inspector: InspectorConstructor\n): Candidate[] => (\n extras.filter(entry => entry.files?.some(pattern => typeof pattern === 'string' && pattern.includes('.astro')))\n .filter(entry => entry.languageOptions?.parserOptions?.parser ||\n Object.keys(entry.rules ?? {}).some(rule => ASTRO_WORKAROUND_RULES.has(rule)))\n .map((entry, index) => ({\n entry,\n inspector: new Inspector({\n cwd,\n overrideConfig: [...baseline, {\n ...entry,\n settings: { ...entry.settings, 'eslint-config-basic/doctor-scope': true }\n }],\n overrideConfigFile: true\n }),\n key: `${index}:${entry.name ?? 'override'}`\n }))\n)\n\nconst isRedundantRule = (\n rule: string,\n value: unknown,\n current: EffectiveConfig,\n baseline: EffectiveConfig\n): boolean => (\n ASTRO_WORKAROUND_RULES.has(rule) && severity(value) === 0 &&\n severity(current.rules?.[rule]) === 0 && severity(baseline.rules?.[rule]) === 0\n)\n\nconst getParser = (config: EffectiveConfig): unknown => config.languageOptions?.parserOptions?.parser\n\n// ESLint clones nested parser options; entry-point function identity survives that merge.\nconst sameParser = (left: unknown, right: unknown): boolean => (\n left === right || (isRecord(left) && isRecord(right) &&\n typeof left.parseForESLint === 'function' && left.parseForESLint === right.parseForESLint && left.parse === right.parse)\n)\n\nconst isRedundantParser = (entry: ConfigEntry, current: EffectiveConfig, baseline: EffectiveConfig): boolean => {\n const parser = getParser(entry)\n\n return Boolean(parser) && sameParser(parser, getParser(current)) && sameParser(parser, getParser(baseline))\n}\n\nconst recordCandidate = (\n candidate: Candidate,\n current: EffectiveConfig,\n baseline: EffectiveConfig,\n file: string,\n guidance: Map<string, string>\n): void => {\n const { entry, key } = candidate\n\n for (const [rule, value] of Object.entries(entry.rules ?? {})) {\n if (!isRedundantRule(rule, value, current, baseline)) continue\n\n const id = `${key}:${rule}`\n\n guidance.set(id, guidance.get(id) ??\n `Review Astro workaround ${rule}: installed generated and current effective configs both disable it ` +\n `for ${file}. Remove only that field after checking all override globs; ` +\n 'rerun lint, typecheck, and repeated autofix before removing the override.')\n }\n\n if (isRedundantParser(entry, current, baseline)) {\n const id = `${key}:parser`\n\n guidance.set(id, guidance.get(id) ??\n 'Review the explicit Astro TypeScript parser: the installed generated config already supplies the ' +\n `same parser for ${file}. Check every override glob and rerun lint, typecheck, ` +\n 'and repeated autofix before removing only the parser field.')\n }\n}\n\nconst inspectTarget = async (\n cwd: string,\n target: string,\n current: ConfigInspector,\n generated: ConfigInspector,\n candidates: Candidate[],\n guidance: Map<string, string>\n): Promise<void> => {\n const [effective, defaults] = await Promise.all([\n current.calculateConfigForFile(target),\n generated.calculateConfigForFile(target)\n ])\n\n if (!effective || !defaults) return\n\n for (const candidate of candidates) {\n const scoped = await candidate.inspector.calculateConfigForFile(target)\n\n if (scoped?.settings?.['eslint-config-basic/doctor-scope'] !== true) continue\n\n recordCandidate(candidate, effective, defaults, relative(cwd, target), guidance)\n }\n}\n\n/** Only report fields proven redundant for a sampled file; never remove a whole override block. */\nexport const compareConsumerOverrides = async (\n cwd: string,\n config: FlatConfigArray,\n baseline: FlatConfigArray,\n Inspector: InspectorConstructor\n): Promise<string[]> => {\n const metadata = getDefineConfigMetadata(config)\n\n if (!metadata) return []\n\n const candidates = createCandidates(cwd, flattenConfigInputs(metadata.extraConfigs), baseline, Inspector)\n\n if (candidates.length === 0) return []\n\n const current = new Inspector({ cwd, overrideConfig: config, overrideConfigFile: true })\n const generated = new Inspector({ cwd, overrideConfig: baseline, overrideConfigFile: true })\n const guidance = new Map<string, string>()\n const files: string[] = []\n\n collectAstroFiles(cwd, files)\n\n for (const file of files) {\n await inspectTarget(cwd, file, current, generated, candidates, guidance)\n\n await inspectTarget(cwd, `${file}/consumer-script.js`, current, generated, candidates, guidance)\n }\n\n return [...guidance.values()]\n}\n\nconst resolveComposerEntry = (cwd: string): string => {\n const manifestPath = findPackageJSON(\n '@santi020k/eslint-config-basic',\n pathToFileURL(join(cwd, 'package.json'))\n )\n\n if (!manifestPath) throw new Error('The installed Basic package manifest could not be resolved.')\n\n const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))\n const exports = isRecord(manifest) && isRecord(manifest.exports) ? manifest.exports : undefined\n const entry = isRecord(exports?.['.']) ? exports['.'].import : undefined\n\n if (typeof entry !== 'string' || !entry.startsWith('./')) {\n throw new Error('The installed Basic package does not declare a supported public root import export.')\n }\n\n return pathToFileURL(join(dirname(manifestPath), entry)).href\n}\n\nconst compareInstalledComposer = async (cwd: string, config: FlatConfigArray): Promise<string[]> => {\n const metadata = getDefineConfigMetadata(config)\n\n if (!metadata || metadata.extraConfigs.length === 0) return []\n\n const projectRequire = createRequire(join(cwd, 'package.json'))\n const composer: unknown = await import(resolveComposerEntry(cwd))\n const eslint: unknown = projectRequire('eslint')\n\n if (!isComposer(composer) || !isEslint(eslint)) return []\n\n const legacyRoot: unknown = Reflect.get(metadata.options ?? {}, 'detectRootDir')\n const root = metadata.root ?? metadata.options?.root ?? (typeof legacyRoot === 'string' ? legacyRoot : cwd)\n const baseline = await composer.defineConfig({ ...metadata.options, root })\n\n return await compareConsumerOverrides(cwd, config, baseline, eslint.ESLint)\n}\n\nexport const getConsumerOverrideGuidance = async (cwd: string, configPath: null | string): Promise<string[]> => {\n if (!configPath || !existsSync(configPath)) return []\n\n try {\n const loaded: unknown = await import(pathToFileURL(configPath).href)\n const config: unknown = isRecord(loaded) ? await loaded.default : undefined\n\n return isFlatConfig(config) ? await compareInstalledComposer(cwd, config) : []\n } catch (error) {\n return [`Could not compare consumer overrides with the installed generated config: ${\n error instanceof Error ? error.message : String(error)\n }. Keep existing overrides until effective-config comparison succeeds.`]\n }\n}\n"],"mappings":"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","names":[]}
@@ -11,7 +11,7 @@ import { getDefineConfigMetadata } from "./define-config-metadata.js";
11
11
  import { defineConfig } from "./index.js";
12
12
  import { isMissingRequestedPackage } from "./optional-package-errors.js";
13
13
  import { resolvePreset } from "./resolvers.js";
14
- const PRESETS = new Set(Object.values(Preset));
14
+ const PRESETS = new Map(Object.values(Preset).map((preset) => [preset, preset]));
15
15
  const CONFIG_FILENAMES = [
16
16
  "eslint.config.js",
17
17
  "eslint.config.mjs",
@@ -484,12 +484,12 @@ const createPresetReport = async (cwd, presetName, file = "eslint.config.js", op
484
484
  throw new Error("Writing preset fixes requires semanticOnly to avoid an unchecked formatting rewrite.");
485
485
  }
486
486
  const normalizedPreset = presetName.toLowerCase();
487
- if (!PRESETS.has(normalizedPreset)) {
487
+ const preset = PRESETS.get(normalizedPreset);
488
+ if (preset === void 0) {
488
489
  throw new Error(
489
- `Unknown preset "${presetName}". Use one of: ${[...PRESETS].sort().join(", ")}.`
490
+ `Unknown preset "${presetName}". Use one of: ${[...PRESETS.keys()].sort().join(", ")}.`
490
491
  );
491
492
  }
492
- const preset = normalizedPreset;
493
493
  const presetOptions = resolvePreset(preset);
494
494
  const current = await loadProjectEslint(cwd).calculateConfigForFile(file);
495
495
  const currentMetadata = await loadDefineConfigMetadata(cwd);