@isentinel/eslint-config 6.0.0-beta.2 → 6.0.0-beta.21

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.
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,280 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { pathToFileURL } from "node:url";
6
+ //#region src/guards.ts
7
+ /**
8
+ * Internal runtime type guards. Prefer these over `as` assertions so values
9
+ * crossing untyped boundaries (`JSON.parse`, dynamic `import`, plugin objects)
10
+ * are validated at runtime rather than asserted away.
11
+ */
12
+ /**
13
+ * Whether a value is a non-null, non-array object usable as a string-keyed
14
+ * record. Narrows `unknown` without an assertion.
15
+ *
16
+ * @param value - The value to test.
17
+ * @returns Whether the value is a plain object.
18
+ */
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ /**
23
+ * Whether a value is an array whose every element is a string.
24
+ *
25
+ * @param value - The value to test.
26
+ * @returns Whether the value is a `string` array.
27
+ */
28
+ function isStringArray(value) {
29
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
30
+ }
31
+ //#endregion
32
+ //#region src/lint-cli/lib/exec/eslint-install.ts
33
+ /**
34
+ * Locating the ESLint installation a run's config is resolved against.
35
+ *
36
+ * The runner and its ignore helper both need it and must agree: the helper
37
+ * serializes the config's match patterns out of one ESLint's config loader, and
38
+ * the runner evaluates them with that same ESLint's matcher. Resolving them
39
+ * separately would let the two drift onto different installations, and a
40
+ * matcher that disagrees with the one that produced the patterns is exactly the
41
+ * failure this feature cannot have.
42
+ */
43
+ /**
44
+ * A synthetic basename for `createRequire`, which resolves relative to a file
45
+ * rather than a directory. Spelled so it can never collide with a real consumer
46
+ * module.
47
+ */
48
+ const RESOLVE_ANCHOR = "__isentinel-lint__.js";
49
+ /**
50
+ * Resolve the ESLint the consumer's config will be linted with: their own,
51
+ * resolved from `cwd`, falling back to the one resolvable from this file (the
52
+ * hoisted peer dependency) when `cwd` has no `node_modules` of its own — the
53
+ * case in the fixture-based tests.
54
+ *
55
+ * @param cwd - The consumer project root.
56
+ * @returns The package root and a require anchored in it.
57
+ * @throws {Error} When ESLint cannot be resolved from either location.
58
+ */
59
+ function resolveEslintInstall(cwd) {
60
+ let packageJson;
61
+ try {
62
+ packageJson = createRequire(path.join(cwd, RESOLVE_ANCHOR)).resolve("eslint/package.json");
63
+ } catch {
64
+ packageJson = createRequire(import.meta.url).resolve("eslint/package.json");
65
+ }
66
+ return {
67
+ requireFrom: createRequire(packageJson),
68
+ root: path.dirname(packageJson)
69
+ };
70
+ }
71
+ //#endregion
72
+ //#region src/lint-cli/ignored-child.ts
73
+ /**
74
+ * Internal helper process for {@link file://./ignored.ts}. Loads the consumer's
75
+ * resolved ESLint config once and writes back what the runner needs to classify
76
+ * lint targets: the config's match patterns when they are serializable, and
77
+ * otherwise the ignored subset of a target list.
78
+ *
79
+ * Run as a child rather than in-process for two reasons: the config-loading
80
+ * API is async while `plan` is synchronous end to end, and loading a
81
+ * consumer's flat config pulls their whole plugin tree (and jiti) into memory
82
+ * — 6-10s and several hundred MB in a large project, neither of which should
83
+ * outlive the one query the runner needs.
84
+ *
85
+ * Invoked as `node <this file> <cwd> <outFile>` with the JSON target array on
86
+ * stdin.
87
+ */
88
+ /**
89
+ * Whether a required module exposes the `ESLint` constructor this helper uses.
90
+ *
91
+ * @param value - The imported module's namespace.
92
+ * @returns Whether the namespace carries an `ESLint` constructor.
93
+ */
94
+ function isEslintModule(value) {
95
+ return isRecord(value) && typeof value["ESLint"] === "function";
96
+ }
97
+ /**
98
+ * Whether a required module exposes the `ConfigLoader` constructor this helper
99
+ * uses.
100
+ *
101
+ * @param value - The required module's exports.
102
+ * @returns Whether the exports carry a `ConfigLoader` constructor.
103
+ */
104
+ function isConfigLoaderModule(value) {
105
+ return isRecord(value) && typeof value["ConfigLoader"] === "function";
106
+ }
107
+ /**
108
+ * The config keys that leave a `files`-less config able to say something about
109
+ * a path, mirroring `META_FIELDS` in `@eslint/config-array`: a config carrying
110
+ * `ignores` and nothing else outside this set is a global ignore, and one with
111
+ * any further key only excludes files from itself.
112
+ */
113
+ const META_KEYS = /* @__PURE__ */ new Set(["basePath", "name"]);
114
+ /**
115
+ * Load the consumer's resolved config array.
116
+ *
117
+ * Reaching it means reaching past the `eslint` package's exports map: no public
118
+ * API returns it, and `calculateConfigForFile` strips exactly the
119
+ * `files`/`ignores` keys this needs. Only that coupling is caught here — a
120
+ * config that throws on load is a broken project, not a missing capability, and
121
+ * is left to fail the helper outright rather than be retried through a second,
122
+ * equally doomed config load.
123
+ *
124
+ * @param cwd - The consumer project root.
125
+ * @returns The config array, or `undefined` when the loader is unreachable.
126
+ * @rejects {Error} When the consumer's config fails to load.
127
+ */
128
+ async function loadConfigArray(cwd) {
129
+ let loader;
130
+ try {
131
+ const { requireFrom, root } = resolveEslintInstall(cwd);
132
+ const configLoaderModule = requireFrom(path.join(root, "lib", "config", "config-loader.js"));
133
+ if (!isConfigLoaderModule(configLoaderModule)) throw new Error("eslint config-loader did not export a ConfigLoader constructor");
134
+ const { ConfigLoader } = configLoaderModule;
135
+ loader = new ConfigLoader({
136
+ configFile: void 0,
137
+ cwd,
138
+ ignoreEnabled: true
139
+ });
140
+ } catch {
141
+ return;
142
+ }
143
+ return loader.loadConfigArrayForDirectory(path.join(cwd, "__placeholder__"));
144
+ }
145
+ /**
146
+ * Whether a config ignores paths for the whole run rather than just for itself.
147
+ *
148
+ * @param config - One entry of the resolved config array.
149
+ * @returns True when `ignores` is the config's only substantive key.
150
+ */
151
+ function isGlobalIgnore(config) {
152
+ return config["ignores"] !== void 0 && Object.keys(config).filter((key) => !META_KEYS.has(key)).length === 1;
153
+ }
154
+ /**
155
+ * Whether a `files`/`ignores` value is made purely of glob strings. Flat config
156
+ * also permits function matchers, and `files` may nest one level for AND
157
+ * matching; a function anywhere leaves the config with no form as data.
158
+ *
159
+ * @param value - The `files` or `ignores` value to test.
160
+ * @returns True when every matcher is a string.
161
+ */
162
+ function isGlobList(value) {
163
+ return Array.isArray(value) && value.flat(1).every((matcher) => typeof matcher === "string");
164
+ }
165
+ /**
166
+ * Reduce one config to its match keys.
167
+ *
168
+ * @param config - One entry of the resolved config array.
169
+ * @returns The entry, or `undefined` when a matcher is a function.
170
+ */
171
+ function serializeEntry({ basePath, files, ignores }) {
172
+ const entry = {};
173
+ if (files !== void 0) {
174
+ if (!isGlobList(files)) return;
175
+ entry.files = files;
176
+ }
177
+ if (ignores !== void 0) {
178
+ if (!isGlobList(ignores)) return;
179
+ entry.ignores = ignores;
180
+ }
181
+ if (typeof basePath === "string") entry.basePath = basePath;
182
+ return entry;
183
+ }
184
+ /**
185
+ * Reduce the resolved config array to the entries that decide whether ESLint
186
+ * lints a path at all, or `undefined` when a matcher is a function rather than
187
+ * a glob and so cannot cross a process boundary.
188
+ *
189
+ * Only two kinds of entry can make that decision: one with `files`, which can
190
+ * match a path, and a bare global ignore, which can veto it. A `files`-less
191
+ * config with other keys alongside its `ignores` merely narrows which configs
192
+ * merge into the one ESLint lints with, which nothing here reads — so it is
193
+ * dropped rather than carried, and with it the risk of a stripped `rules` key
194
+ * silently promoting it into a global ignore.
195
+ *
196
+ * @param configArray - The resolved config array.
197
+ * @returns The serializable entries, or `undefined` when one is a function.
198
+ */
199
+ function serializeEntries(configArray) {
200
+ const entries = [];
201
+ for (const config of configArray) {
202
+ if (config["files"] === void 0 && !isGlobalIgnore(config)) continue;
203
+ const entry = serializeEntry(config);
204
+ if (entry === void 0) return;
205
+ entries.push(entry);
206
+ }
207
+ return entries;
208
+ }
209
+ /**
210
+ * The last-resort classification: load ESLint itself and ask it per target.
211
+ * Only reached when no config array could be built, so nothing cheaper is
212
+ * already in hand.
213
+ *
214
+ * @param cwd - The consumer project root.
215
+ * @param targets - The target files to classify.
216
+ * @returns The ignored subset of `targets`.
217
+ * @rejects {Error} When ESLint cannot be resolved or its config fails to load.
218
+ */
219
+ async function queryEslint(cwd, targets) {
220
+ const { requireFrom } = resolveEslintInstall(cwd);
221
+ const eslintModule = await import(pathToFileURL(requireFrom.resolve("eslint")).href);
222
+ if (!isEslintModule(eslintModule)) throw new Error("eslint did not export an ESLint constructor");
223
+ const { ESLint } = eslintModule;
224
+ const eslint = new ESLint({ cwd });
225
+ const ignored = [];
226
+ for (const target of targets) if (await eslint.isPathIgnored(target)) ignored.push(target);
227
+ return ignored;
228
+ }
229
+ /**
230
+ * The target list, read only by the paths that classify it — the predicate
231
+ * never looks at it, and it is the larger of the two inputs.
232
+ *
233
+ * @returns The target files, absolute.
234
+ */
235
+ function readTargets() {
236
+ const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
237
+ if (!isStringArray(parsed)) throw new Error("expected a JSON array of target paths on stdin");
238
+ return parsed;
239
+ }
240
+ /**
241
+ * The best answer this helper can give: the config's patterns when they are
242
+ * data, the ignored subset of the target list when they are not.
243
+ *
244
+ * Both fallbacks reuse whatever the step before them already paid for. A config
245
+ * that holds a function matcher still classifies targets here, off the array
246
+ * already loaded, rather than loading the config a second time through `ESLint`
247
+ * — which only the case of no reachable config loader at all has to fall back
248
+ * to.
249
+ *
250
+ * @param cwd - The consumer project root.
251
+ * @returns The payload to write back.
252
+ * @rejects {Error} When the consumer's config fails to load.
253
+ */
254
+ async function resolvePayload(cwd) {
255
+ const configArray = await loadConfigArray(cwd);
256
+ if (configArray === void 0) return {
257
+ ignored: await queryEslint(cwd, readTargets()),
258
+ mode: "answers"
259
+ };
260
+ const entries = serializeEntries(configArray);
261
+ if (entries === void 0) return {
262
+ ignored: readTargets().filter((target) => configArray.getConfigStatus(target) !== "matched"),
263
+ mode: "answers"
264
+ };
265
+ return {
266
+ basePath: configArray.basePath,
267
+ entries,
268
+ mode: "predicate"
269
+ };
270
+ }
271
+ async function main() {
272
+ const [cwd, outFile] = process.argv.slice(2);
273
+ if (cwd === void 0 || outFile === void 0) throw new Error("usage: node ignored-child <cwd> <outFile> (targets on stdin)");
274
+ fs.writeFileSync(outFile, JSON.stringify(await resolvePayload(cwd)));
275
+ }
276
+ main().catch(() => {
277
+ process.exitCode = 1;
278
+ });
279
+ //#endregion
280
+ export {};