@isentinel/pnpm-plugin-eslint-config 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-PRESENT Anthony Fu<https://github.com/antfu>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/extensions.mjs ADDED
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Package extensions for dependencies whose published type declarations import
3
+ * packages they never declare.
4
+ *
5
+ * Under `enableGlobalVirtualStore: true` a package's real location leaves the
6
+ * project tree entirely, so the parent-directory walk TypeScript performs no
7
+ * longer passes through pnpm's hidden hoist store or the project root. A
8
+ * dependency then sees only its own declared dependencies, and every
9
+ * under-declared type import silently degrades to `any` (`skipLibCheck` hides
10
+ * the `TS2307` but the error type remains).
11
+ *
12
+ * This table is the sole source of truth, read by three consumers:
13
+ *
14
+ * - This package's `pnpmfile.mjs`, which pnpm auto-loads for anyone who
15
+ * installs it as a config dependency. It applies {@link publishedExtensions}
16
+ * rather than the whole table — see `consumerFacing` below. Consumers need
17
+ * the hook at all because `packageExtensions` is project-local and cannot be
18
+ * published.
19
+ * - `scripts/gen-package-extensions.ts`, which renders the table into the
20
+ * `packageExtensions` block of this repository's own `pnpm-workspace.yaml`.
21
+ * Our install therefore needs no pnpmfile: pnpm applies the same repairs
22
+ * natively.
23
+ * - `scripts/check-package-extensions.ts`, which fails when an installed
24
+ * package gains an under-declared import that no entry covers.
25
+ *
26
+ * Plain JavaScript rather than TypeScript because pnpm's config-dependency
27
+ * loader only probes `pnpmfile.mjs`/`.cjs`, and Node refuses to strip types
28
+ * from files under `node_modules`. JSDoc carries the types instead.
29
+ */
30
+ /**
31
+ * @typedef {object} PackageExtension
32
+ * @property {boolean} [consumerFacing] Whether a consumer of
33
+ * `@isentinel/eslint-config` loads this package's declarations. Only the
34
+ * preset's published type surface counts: every other entry repairs a package
35
+ * that ESLint loads at runtime, where resolution is not broken, so injecting
36
+ * it downstream would add `@types` packages nobody's program reads.
37
+ * `scripts/check-published-extensions.ts` derives the true set from the built
38
+ * declarations and fails when a flag disagrees.
39
+ * @property {Record<string, string>} [dependencies] Declarations to add to
40
+ * `dependencies`.
41
+ * @property {string} [fixedIn] The first version to declare these itself. Later
42
+ * versions are left alone.
43
+ * @property {Array<string>} [ignore] Specifiers accounted for without being
44
+ * injected, because they are not part of the package's type surface. Read
45
+ * only by the check script; nothing is injected for them.
46
+ * @property {string} name The package to extend.
47
+ * @property {Record<string, string>} [peerDependencies] Declarations to add to
48
+ * `peerDependencies`, each marked optional.
49
+ */
50
+ /**
51
+ * The parts of a package manifest this table reads and writes. Every dependency
52
+ * field arrives as an object, since pnpm fills them in before calling the hook,
53
+ * but a manifest built by hand — in tests, say — need not carry them.
54
+ *
55
+ * @typedef {object} PackageManifest
56
+ * @property {Record<string, string>} [dependencies] What the package declares.
57
+ * @property {string} [name] The package's name, matched against an entry.
58
+ * @property {Record<string, string>} [peerDependencies] What the package
59
+ * expects its consumer to provide.
60
+ * @property {Record<string, { optional?: boolean }>} [peerDependenciesMeta]
61
+ * Which of those peers are optional.
62
+ * @property {string} [version] The installed version, tested against `fixedIn`.
63
+ */
64
+ /**
65
+ * A tsconfig template variable that leaked into two plugins' published
66
+ * declarations. Unresolvable under any layout; tracked in
67
+ * christopher-buss/eslint-config#640.
68
+ */
69
+ // oxlint-disable-next-line no-template-curly-in-string -- The literal text those declarations ship.
70
+ const LEAKED_CONFIG_DIR = "${configDir}";
71
+ /** The release portion of a version, up to any prerelease or build metadata. */
72
+ const RELEASE_SEPARATOR = /[-+]/u;
73
+ /** @type {ReadonlyArray<PackageExtension>} */
74
+ export const packageExtensions = [
75
+ {
76
+ name: "@cspell/eslint-plugin",
77
+ dependencies: {
78
+ "@types/estree": "^1",
79
+ },
80
+ },
81
+ {
82
+ name: "@e18e/eslint-plugin",
83
+ dependencies: {
84
+ "@eslint/core": "^1",
85
+ "@types/estree": "^1",
86
+ },
87
+ peerDependencies: {
88
+ "@typescript-eslint/typescript-estree": "*",
89
+ "@typescript-eslint/utils": "*",
90
+ "typescript": "*",
91
+ },
92
+ },
93
+ {
94
+ name: "@es-joy/jsdoccomment",
95
+ peerDependencies: {
96
+ eslint: "*",
97
+ },
98
+ },
99
+ {
100
+ name: "@eslint-community/eslint-utils",
101
+ dependencies: {
102
+ "@types/estree": "^1",
103
+ },
104
+ },
105
+ {
106
+ name: "@eslint/markdown",
107
+ dependencies: {
108
+ "@types/mdast": "^4",
109
+ "@types/unist": "^3",
110
+ },
111
+ },
112
+ {
113
+ name: "@stylistic/eslint-plugin",
114
+ // `define-config-support.d.ts` augments a package that is deprecated
115
+ // and only present for consumers who opted into it themselves.
116
+ ignore: ["eslint-define-config"],
117
+ },
118
+ {
119
+ name: "@typescript-eslint/types",
120
+ consumerFacing: true,
121
+ peerDependencies: {
122
+ typescript: "*",
123
+ },
124
+ },
125
+ {
126
+ name: "editorconfig",
127
+ // Test declarations ship inside the tarball.
128
+ ignore: ["chai"],
129
+ },
130
+ {
131
+ name: "eslint-flat-config-utils",
132
+ consumerFacing: true,
133
+ peerDependencies: {
134
+ eslint: "*",
135
+ },
136
+ },
137
+ {
138
+ name: "eslint-plugin-comment-length",
139
+ // Rule-tester declarations ship inside the tarball.
140
+ ignore: ["@typescript-eslint/rule-tester"],
141
+ },
142
+ {
143
+ name: "eslint-plugin-de-morgan",
144
+ dependencies: {
145
+ "@types/estree": "^1",
146
+ },
147
+ },
148
+ {
149
+ name: "eslint-plugin-flawless",
150
+ ignore: [LEAKED_CONFIG_DIR],
151
+ },
152
+ {
153
+ name: "eslint-plugin-format-lua",
154
+ ignore: ["eslint-define-config"],
155
+ },
156
+ {
157
+ name: "eslint-plugin-jsdoc",
158
+ dependencies: {
159
+ "@eslint/core": "^1",
160
+ "@types/estree": "^1",
161
+ "@types/json-schema": "^7",
162
+ "jsdoc-type-pratt-parser": "^7",
163
+ },
164
+ peerDependencies: {
165
+ "@typescript-eslint/types": "*",
166
+ },
167
+ },
168
+ {
169
+ name: "eslint-plugin-jsonc",
170
+ dependencies: {
171
+ "@types/estree": "^1",
172
+ },
173
+ },
174
+ {
175
+ name: "eslint-plugin-n",
176
+ dependencies: {
177
+ "@eslint/core": "^1",
178
+ "@types/estree": "^1",
179
+ },
180
+ },
181
+ {
182
+ name: "eslint-plugin-perfectionist",
183
+ peerDependencies: {
184
+ "@typescript-eslint/types": "*",
185
+ "typescript": "*",
186
+ },
187
+ },
188
+ {
189
+ name: "eslint-plugin-roblox-ts",
190
+ ignore: [LEAKED_CONFIG_DIR],
191
+ },
192
+ {
193
+ name: "eslint-plugin-sentinel",
194
+ peerDependencies: {
195
+ "@typescript-eslint/utils": "*",
196
+ },
197
+ },
198
+ {
199
+ name: "eslint-plugin-sonarjs",
200
+ dependencies: {
201
+ "@eslint/core": "^1",
202
+ "@types/estree": "^1",
203
+ "@types/estree-jsx": "^1",
204
+ "@types/functional-red-black-tree": "^1",
205
+ "@types/semver": "^7",
206
+ "type-fest": "^5",
207
+ },
208
+ peerDependencies: {
209
+ "@typescript-eslint/utils": "*",
210
+ },
211
+ },
212
+ {
213
+ name: "fdir",
214
+ dependencies: {
215
+ "@types/picomatch": "^4",
216
+ },
217
+ },
218
+ {
219
+ name: "generate-differences",
220
+ dependencies: {
221
+ "diff-match-patch-es": "^1",
222
+ },
223
+ },
224
+ {
225
+ name: "jsdoc-type-pratt-parser",
226
+ dependencies: {
227
+ "@types/estree": "^1",
228
+ },
229
+ // Test and build declarations ship inside the tarball but are not
230
+ // reachable from the package's entry points.
231
+ ignore: ["mocha", "tsup"],
232
+ },
233
+ {
234
+ name: "jsonc-eslint-parser",
235
+ dependencies: {
236
+ "@types/estree": "^1",
237
+ },
238
+ peerDependencies: {
239
+ eslint: "*",
240
+ },
241
+ },
242
+ {
243
+ name: "mdast-util-gfm-autolink-literal",
244
+ dependencies: {
245
+ "mdast-util-from-markdown": "^2",
246
+ "mdast-util-to-markdown": "^2",
247
+ },
248
+ },
249
+ {
250
+ name: "pkg-types",
251
+ peerDependencies: {
252
+ typescript: "*",
253
+ },
254
+ },
255
+ {
256
+ name: "toml-eslint-parser",
257
+ peerDependencies: {
258
+ eslint: "*",
259
+ },
260
+ },
261
+ {
262
+ name: "yaml-eslint-parser",
263
+ peerDependencies: {
264
+ eslint: "*",
265
+ },
266
+ },
267
+ ];
268
+
269
+ /**
270
+ * The entries a consumer's program actually loads, which is all the shipped
271
+ * hook applies. The rest repair packages ESLint only ever loads at runtime, so
272
+ * injecting them downstream would install `@types` packages no consumer's
273
+ * program reads.
274
+ *
275
+ * @type {ReadonlyArray<PackageExtension>}
276
+ */
277
+ export const publishedExtensions = packageExtensions.filter(
278
+ (extension) => extension.consumerFacing === true,
279
+ );
280
+
281
+ /**
282
+ * Injects the declarations `extensions` lists for this manifest. Existing
283
+ * declarations always win, so an upstream fix takes effect without the table
284
+ * having to be updated first.
285
+ *
286
+ * @param {PackageManifest} manifest - The manifest pnpm read from the package.
287
+ * @param {ReadonlyArray<PackageExtension>} extensions - The table to apply.
288
+ * @returns {PackageManifest} The same manifest, extended in place.
289
+ */
290
+ export function extendManifest(manifest, extensions) {
291
+ for (const extension of extensions) {
292
+ if (extension.name !== manifest.name || isFixedUpstream(manifest, extension)) {
293
+ continue;
294
+ }
295
+
296
+ if (extension.dependencies !== undefined) {
297
+ injectDependencies(manifest, extension.dependencies);
298
+ }
299
+
300
+ if (extension.peerDependencies !== undefined) {
301
+ injectPeerDependencies(manifest, extension.peerDependencies);
302
+ }
303
+ }
304
+
305
+ return manifest;
306
+ }
307
+ /**
308
+ * The `readPackage` hook itself, applying {@link publishedExtensions}. Takes
309
+ * only the manifest: pnpm passes a context object as its second argument, so
310
+ * the table must not be a parameter here.
311
+ *
312
+ * @param {PackageManifest} manifest - The manifest pnpm read from the package.
313
+ * @returns {PackageManifest} The same manifest, extended in place.
314
+ */
315
+ export function applyExtensions(manifest) {
316
+ return extendManifest(manifest, publishedExtensions);
317
+ }
318
+
319
+ /**
320
+ * Collects the entries of `additions` that `manifest` does not already declare
321
+ * in any of its dependency fields.
322
+ *
323
+ * @param {PackageManifest} manifest - The manifest being extended.
324
+ * @param {Record<string, string>} additions - The entries to add.
325
+ * @returns {Record<string, string>} The entries that are genuinely missing.
326
+ */
327
+ function missingFrom(manifest, additions) {
328
+ const missing = {};
329
+ for (const [name, range] of Object.entries(additions)) {
330
+ if (
331
+ manifest.dependencies?.[name] === undefined &&
332
+ manifest.peerDependencies?.[name] === undefined
333
+ ) {
334
+ missing[name] = range;
335
+ }
336
+ }
337
+
338
+ return missing;
339
+ }
340
+
341
+ /**
342
+ * Adds `additions` to the manifest's `dependencies`.
343
+ *
344
+ * @param {PackageManifest} manifest - The manifest being extended.
345
+ * @param {Record<string, string>} additions - The declarations to inject.
346
+ */
347
+ function injectDependencies(manifest, additions) {
348
+ const missing = missingFrom(manifest, additions);
349
+ if (Object.keys(missing).length > 0) {
350
+ manifest.dependencies = { ...manifest.dependencies, ...missing };
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Adds `additions` to the manifest's `peerDependencies`, each marked optional
356
+ * so an absent provider never fails the install.
357
+ *
358
+ * @param {PackageManifest} manifest - The manifest being extended.
359
+ * @param {Record<string, string>} additions - The declarations to inject.
360
+ */
361
+ function injectPeerDependencies(manifest, additions) {
362
+ const missing = missingFrom(manifest, additions);
363
+ const names = Object.keys(missing);
364
+ if (names.length === 0) {
365
+ return;
366
+ }
367
+
368
+ manifest.peerDependencies = { ...manifest.peerDependencies, ...missing };
369
+ const meta = { ...manifest.peerDependenciesMeta };
370
+ for (const name of names) {
371
+ meta[name] = { optional: true };
372
+ }
373
+
374
+ manifest.peerDependenciesMeta = meta;
375
+ }
376
+
377
+ /**
378
+ * Splits a version into its numeric release components.
379
+ *
380
+ * @param {string} version - The version to split.
381
+ * @returns {Array<number>} Its major, minor and patch numbers.
382
+ */
383
+ function releaseParts(version) {
384
+ const [release = version] = version.split(RELEASE_SEPARATOR, 1);
385
+ return release.split(".").map((part) => Number.parseInt(part, 10) || 0);
386
+ }
387
+
388
+ /**
389
+ * Compares the release portion of two versions, ignoring any prerelease or
390
+ * build metadata.
391
+ *
392
+ * @param {string} version - The version to test.
393
+ * @param {string} other - The version to test it against.
394
+ * @returns {boolean} Whether `version` sorts before `other`.
395
+ */
396
+ function isBefore(version, other) {
397
+ const left = releaseParts(version);
398
+ const right = releaseParts(other);
399
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
400
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
401
+ if (difference !== 0) {
402
+ return difference < 0;
403
+ }
404
+ }
405
+
406
+ return false;
407
+ }
408
+
409
+ /**
410
+ * Whether the installed version already declares what the entry would inject.
411
+ *
412
+ * @param {PackageManifest} manifest - The manifest pnpm read from the package.
413
+ * @param {PackageExtension} extension - The entry matching it.
414
+ * @returns {boolean} Whether to leave the manifest alone.
415
+ */
416
+ function isFixedUpstream(manifest, extension) {
417
+ if (extension.fixedIn === undefined || manifest.version === undefined) {
418
+ return false;
419
+ }
420
+
421
+ return !isBefore(manifest.version, extension.fixedIn);
422
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@isentinel/pnpm-plugin-eslint-config",
3
+ "version": "1.0.0",
4
+ "description": "pnpm plugin that declares the type-only imports @isentinel/eslint-config's dependencies are missing",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm-plugin",
8
+ "pnpmfile",
9
+ "eslint-config"
10
+ ],
11
+ "homepage": "https://github.com/christopher-buss/eslint-config/tree/main/pnpm-plugin",
12
+ "bugs": {
13
+ "url": "https://github.com/christopher-buss/eslint-config/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/christopher-buss/eslint-config.git",
18
+ "directory": "pnpm-plugin"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Christopher Buss <christopher.buss@pm.me> (https://github.com/christopher-buss)",
22
+ "sideEffects": false,
23
+ "type": "module",
24
+ "exports": {
25
+ "./extensions": "./extensions.mjs",
26
+ "./package.json": "./package.json",
27
+ "./pnpmfile": "./pnpmfile.mjs"
28
+ },
29
+ "files": [
30
+ "extensions.mjs",
31
+ "pnpmfile.mjs"
32
+ ],
33
+ "engines": {
34
+ "node": ">=24.12.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
package/pnpmfile.mjs ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The entry point pnpm auto-loads for config dependencies whose name matches
3
+ * its plugin pattern (`pnpm-plugin-`, prefixed by any scope). All of the logic
4
+ * lives in `extensions.mjs` so the repository's own `.pnpmfile.mjs` can share
5
+ * it.
6
+ */
7
+ import { applyExtensions } from "./extensions.mjs";
8
+
9
+ export const hooks = {
10
+ readPackage: applyExtensions,
11
+ };