@williamthorsen/tsconfig 0.2.1 → 0.4.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/.readyup/kits/default.js +308 -0
- package/.readyup/manifest.json +18 -0
- package/README.md +70 -15
- package/package.json +7 -5
- package/tsconfig.base.json +2 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const __readyupVersion = "0.25.0";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// .readyup/kits/default.ts
|
|
7
|
+
import { defineRdyKit } from "readyup";
|
|
8
|
+
import {
|
|
9
|
+
discoverWorkspaces,
|
|
10
|
+
esYearForNodeMajor,
|
|
11
|
+
fileExists,
|
|
12
|
+
readEnginesNodeFloor,
|
|
13
|
+
readJsonFile,
|
|
14
|
+
readToolVersionsNode,
|
|
15
|
+
readTsconfigChain
|
|
16
|
+
} from "readyup/check-utils";
|
|
17
|
+
|
|
18
|
+
// src/readiness/base-chain.ts
|
|
19
|
+
var BASE_PACKAGE = "@williamthorsen/tsconfig";
|
|
20
|
+
var PATH_SPECIFIER = /^(?:\.{1,2}(?:[/\\]|$)|[/\\]|[A-Za-z]:)/;
|
|
21
|
+
function findBaseIndex(entries) {
|
|
22
|
+
const index = entries.findIndex((entry) => entry.specifier !== void 0 && isBaseSpecifier(entry.specifier));
|
|
23
|
+
return index === -1 ? void 0 : index;
|
|
24
|
+
}
|
|
25
|
+
function hasExternalBase(entries) {
|
|
26
|
+
return entries.some((entry) => isExternalBase(entry));
|
|
27
|
+
}
|
|
28
|
+
function isBaseSpecifier(specifier) {
|
|
29
|
+
return specifier === BASE_PACKAGE || specifier.startsWith(`${BASE_PACKAGE}/`);
|
|
30
|
+
}
|
|
31
|
+
function isConsumerOwnedConfig(path) {
|
|
32
|
+
return !path.startsWith("../") && !path.split("/").includes("node_modules");
|
|
33
|
+
}
|
|
34
|
+
function isExternalBase(entry) {
|
|
35
|
+
const { specifier } = entry;
|
|
36
|
+
if (specifier === void 0) return false;
|
|
37
|
+
return !PATH_SPECIFIER.test(specifier) && !isBaseSpecifier(specifier);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/readiness/classifyChain.ts
|
|
41
|
+
function classifyChain(chain) {
|
|
42
|
+
if (chain.unresolvedExtends.some((unresolved) => isBaseSpecifier(unresolved.specifier))) {
|
|
43
|
+
return { kind: "uninstalled" };
|
|
44
|
+
}
|
|
45
|
+
const baseIndex = findBaseIndex(chain.entries);
|
|
46
|
+
if (baseIndex !== void 0) return { baseIndex, kind: "adopted" };
|
|
47
|
+
return hasExternalBase(chain.entries) ? { kind: "external-base" } : { kind: "unadopted" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/readiness/es-year.ts
|
|
51
|
+
var ES_YEAR = /^es\d{4}$/;
|
|
52
|
+
function isEsYearAtLeast(candidate, floor) {
|
|
53
|
+
return candidate >= floor;
|
|
54
|
+
}
|
|
55
|
+
function parseEsYear(value) {
|
|
56
|
+
const normalized = value.trim().toLowerCase();
|
|
57
|
+
return ES_YEAR.test(normalized) ? normalized : void 0;
|
|
58
|
+
}
|
|
59
|
+
function readDeclaredEsYear(compilerOptions) {
|
|
60
|
+
const target = compilerOptions["target"];
|
|
61
|
+
const targetYear = typeof target === "string" ? parseEsYear(target) : void 0;
|
|
62
|
+
if (targetYear !== void 0) return targetYear;
|
|
63
|
+
const libYears = toStringArray(compilerOptions["lib"]).flatMap((entry) => {
|
|
64
|
+
const year = parseEsYear(entry);
|
|
65
|
+
return year === void 0 ? [] : [year];
|
|
66
|
+
});
|
|
67
|
+
return libYears.toSorted().at(-1);
|
|
68
|
+
}
|
|
69
|
+
function toStringArray(value) {
|
|
70
|
+
if (!Array.isArray(value)) return [];
|
|
71
|
+
return value.filter((entry) => typeof entry === "string");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/readiness/findRedundantOptions.ts
|
|
75
|
+
import { isDeepStrictEqual } from "node:util";
|
|
76
|
+
function findRedundantOptions(entries, baseIndex) {
|
|
77
|
+
const baseOptions = entries[baseIndex]?.compilerOptions;
|
|
78
|
+
if (baseOptions === void 0) return [];
|
|
79
|
+
const exempt = dependencyDeclaredKeys(entries, baseIndex);
|
|
80
|
+
const redundant = [];
|
|
81
|
+
for (const entry of entries.slice(0, baseIndex)) {
|
|
82
|
+
if (!isConsumerOwnedConfig(entry.path)) continue;
|
|
83
|
+
for (const [key, value] of Object.entries(entry.compilerOptions)) {
|
|
84
|
+
if (exempt.has(key) || !Object.hasOwn(baseOptions, key)) continue;
|
|
85
|
+
if (isDeepStrictEqual(value, baseOptions[key])) redundant.push({ key, path: entry.path });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return redundant;
|
|
89
|
+
}
|
|
90
|
+
function dependencyDeclaredKeys(entries, baseIndex) {
|
|
91
|
+
const keys = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const [index, entry] of entries.entries()) {
|
|
93
|
+
if (index === baseIndex || isConsumerOwnedConfig(entry.path)) continue;
|
|
94
|
+
for (const key of Object.keys(entry.compilerOptions)) keys.add(key);
|
|
95
|
+
}
|
|
96
|
+
return keys;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/readiness/node-floor.ts
|
|
100
|
+
var NODE_MAJOR = /^v?(\d+)(?:[.\s]|$)/;
|
|
101
|
+
var NODE_MAJOR_CEILING = 99;
|
|
102
|
+
function classifyNodeEsYear(major, esYearForMajor) {
|
|
103
|
+
const esYear = esYearForMajor(major);
|
|
104
|
+
if (esYear !== void 0) return { esYear, kind: "exact" };
|
|
105
|
+
const lowestKnown = findLowestKnownMajor(esYearForMajor);
|
|
106
|
+
if (lowestKnown === void 0 || major > lowestKnown) return { kind: "unknown" };
|
|
107
|
+
const lowestEsYear = esYearForMajor(lowestKnown);
|
|
108
|
+
return lowestEsYear === void 0 ? { kind: "unknown" } : { esYear: lowestEsYear, kind: "under" };
|
|
109
|
+
}
|
|
110
|
+
function lowestNodeMajor(floors) {
|
|
111
|
+
const majors = floors.flatMap((floor) => {
|
|
112
|
+
const major = readNodeMajor(floor);
|
|
113
|
+
return major === void 0 ? [] : [major];
|
|
114
|
+
});
|
|
115
|
+
return majors.length === 0 ? void 0 : Math.min(...majors);
|
|
116
|
+
}
|
|
117
|
+
function readNodeMajor(floor) {
|
|
118
|
+
const major = NODE_MAJOR.exec(floor.trim())?.[1];
|
|
119
|
+
return major === void 0 ? void 0 : Number(major);
|
|
120
|
+
}
|
|
121
|
+
function findLowestKnownMajor(esYearForMajor) {
|
|
122
|
+
for (let candidate = 1; candidate <= NODE_MAJOR_CEILING; candidate += 1) {
|
|
123
|
+
if (esYearForMajor(candidate) !== void 0) return candidate;
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// .readyup/kits/default.ts
|
|
129
|
+
var PACKAGE_NAME = "@williamthorsen/tsconfig";
|
|
130
|
+
var BASE_SPECIFIER = `${PACKAGE_NAME}/tsconfig.base.json`;
|
|
131
|
+
var ADOPTION_URL = "https://github.com/williamthorsen/eslint-config/tree/main/packages/tsconfig#adopting-the-base";
|
|
132
|
+
var NO_BASE_ES_YEAR = `no tsconfig reaches a ${PACKAGE_NAME} base declaring an ES year`;
|
|
133
|
+
var NO_NODE_ES_YEAR = "no ES year is known for the declared Node floor";
|
|
134
|
+
var cache = {
|
|
135
|
+
chains: /* @__PURE__ */ new Map()
|
|
136
|
+
};
|
|
137
|
+
var default_default = defineRdyKit({
|
|
138
|
+
description: `Adoption and alignment checks for a project consuming ${PACKAGE_NAME}`,
|
|
139
|
+
defaultSeverity: "warn",
|
|
140
|
+
checklists: [
|
|
141
|
+
{
|
|
142
|
+
name: "adoption",
|
|
143
|
+
checks: [
|
|
144
|
+
{
|
|
145
|
+
name: `Every workspace tsconfig extends ${PACKAGE_NAME}`,
|
|
146
|
+
skip: skipUnlessSomeTsconfigIsAccountable,
|
|
147
|
+
check: everyTsconfigAdoptsTheBase,
|
|
148
|
+
fix: `Extend ${BASE_SPECIFIER} from each tsconfig named above, and declare ${PACKAGE_NAME} as a devDependency of each package that names it but cannot resolve it. Adoption: ${ADOPTION_URL}`
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "No tsconfig re-declares an option the base already supplies",
|
|
152
|
+
severity: "recommend",
|
|
153
|
+
check: noRedundantOptions,
|
|
154
|
+
fix: "Delete each option named above: the base already supplies it with the same value"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "alignment",
|
|
160
|
+
checks: [
|
|
161
|
+
{
|
|
162
|
+
name: `No tsconfig declares an ES year other than the one ${PACKAGE_NAME} sets`,
|
|
163
|
+
skip: skipUnlessBaseEsYearKnown,
|
|
164
|
+
check: everyEsYearMatchesTheBase,
|
|
165
|
+
fix: "Delete the target or lib declaration named above, so the ES year the base sets applies"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "The declared Node floor supports the base's ES year",
|
|
169
|
+
severity: "error",
|
|
170
|
+
skip: skipUnlessNodeFloorComparable,
|
|
171
|
+
check: nodeFloorSupportsBaseEsYear,
|
|
172
|
+
fix: "Raise engines.node to a major implementing the base's ES year: below it, code that typechecks fails at runtime"
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
function adoptedTsconfigs() {
|
|
179
|
+
return classifyAdoptions().filter((adoption) => adoption.kind === "adopted");
|
|
180
|
+
}
|
|
181
|
+
function classifyAdoptions() {
|
|
182
|
+
cache.adoptions ??= findTsconfigs().map((path) => classifyTsconfig(path));
|
|
183
|
+
return cache.adoptions;
|
|
184
|
+
}
|
|
185
|
+
function classifyTsconfig(path) {
|
|
186
|
+
const chain = readChain(path);
|
|
187
|
+
if (chain === void 0) return { kind: "unadopted", path };
|
|
188
|
+
const classification = classifyChain(chain);
|
|
189
|
+
return classification.kind === "adopted" ? { baseIndex: classification.baseIndex, chain, kind: "adopted", path } : { kind: classification.kind, path };
|
|
190
|
+
}
|
|
191
|
+
function declaredNodeFloors() {
|
|
192
|
+
const engineFloors = tsconfigSearchDirs().flatMap((dir) => {
|
|
193
|
+
const manifest = readJsonFile(dirPath(dir, "package.json"));
|
|
194
|
+
if (manifest === void 0) return [];
|
|
195
|
+
const floor = readEnginesNodeFloor(manifest);
|
|
196
|
+
return floor.kind === "found" ? [floor.floor] : [];
|
|
197
|
+
});
|
|
198
|
+
if (engineFloors.length > 0) return engineFloors;
|
|
199
|
+
const toolVersionsFloor = readToolVersionsNode();
|
|
200
|
+
return toolVersionsFloor === void 0 ? [] : [toolVersionsFloor];
|
|
201
|
+
}
|
|
202
|
+
function dedupe(values) {
|
|
203
|
+
return [...new Set(values)];
|
|
204
|
+
}
|
|
205
|
+
function dirPath(dir, basename) {
|
|
206
|
+
return dir === "." ? basename : `${dir}/${basename}`;
|
|
207
|
+
}
|
|
208
|
+
function everyEsYearMatchesTheBase() {
|
|
209
|
+
const baseEsYear = readBaseEsYear();
|
|
210
|
+
if (baseEsYear === void 0) return true;
|
|
211
|
+
const offenders = [];
|
|
212
|
+
for (const adoption of adoptedTsconfigs()) {
|
|
213
|
+
for (const entry of adoption.chain.entries.slice(0, adoption.baseIndex)) {
|
|
214
|
+
if (!isConsumerOwnedConfig(entry.path)) continue;
|
|
215
|
+
const declared = readDeclaredEsYear(entry.compilerOptions);
|
|
216
|
+
if (declared !== void 0 && declared !== baseEsYear) offenders.push(`${entry.path} (${declared})`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (offenders.length === 0) return { ok: true, detail: `every tsconfig extending the base is at ${baseEsYear}` };
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
detail: `the base sets ${baseEsYear}; declared otherwise in: ${dedupe(offenders).join(", ")}`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function everyTsconfigAdoptsTheBase() {
|
|
226
|
+
const accountable = classifyAdoptions().filter((adoption) => adoption.kind !== "external-base");
|
|
227
|
+
const adopted = accountable.filter((adoption) => adoption.kind === "adopted");
|
|
228
|
+
const progress = { type: "fraction", passedCount: adopted.length, count: accountable.length };
|
|
229
|
+
const uninstalled = pathsOfKind(accountable, "uninstalled");
|
|
230
|
+
const unadopted = pathsOfKind(accountable, "unadopted");
|
|
231
|
+
if (uninstalled.length === 0 && unadopted.length === 0) return { ok: true, progress };
|
|
232
|
+
const details = [
|
|
233
|
+
uninstalled.length > 0 ? `${PACKAGE_NAME} is named but not installed in: ${uninstalled.join(", ")}` : void 0,
|
|
234
|
+
unadopted.length > 0 ? `no extends chain reaches the base from: ${unadopted.join(", ")}` : void 0
|
|
235
|
+
].filter((detail) => detail !== void 0);
|
|
236
|
+
return { ok: false, detail: details.join("; "), progress };
|
|
237
|
+
}
|
|
238
|
+
function findTsconfigs() {
|
|
239
|
+
return tsconfigSearchDirs().map((dir) => dirPath(dir, "tsconfig.json")).filter((path) => fileExists(path));
|
|
240
|
+
}
|
|
241
|
+
function judgeNodeFloor() {
|
|
242
|
+
const baseEsYear = readBaseEsYear();
|
|
243
|
+
if (baseEsYear === void 0) return { kind: "skip", reason: NO_BASE_ES_YEAR };
|
|
244
|
+
const nodeEsYear = readNodeEsYear();
|
|
245
|
+
if (nodeEsYear === void 0) return { kind: "skip", reason: "no declared Node floor names a major" };
|
|
246
|
+
if (nodeEsYear.kind === "unknown") return { kind: "skip", reason: NO_NODE_ES_YEAR };
|
|
247
|
+
if (nodeEsYear.kind === "under") {
|
|
248
|
+
return isEsYearAtLeast(baseEsYear, nodeEsYear.esYear) ? {
|
|
249
|
+
detail: `the declared Node floor implements less than ${nodeEsYear.esYear}, below the base's ${baseEsYear}`,
|
|
250
|
+
kind: "fail"
|
|
251
|
+
} : { kind: "skip", reason: NO_NODE_ES_YEAR };
|
|
252
|
+
}
|
|
253
|
+
return isEsYearAtLeast(nodeEsYear.esYear, baseEsYear) ? { detail: `the declared Node floor implements ${nodeEsYear.esYear}`, kind: "pass" } : {
|
|
254
|
+
detail: `the declared Node floor implements only ${nodeEsYear.esYear}, below the base's ${baseEsYear}`,
|
|
255
|
+
kind: "fail"
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function nodeFloorSupportsBaseEsYear() {
|
|
259
|
+
const verdict = judgeNodeFloor();
|
|
260
|
+
return verdict.kind === "skip" ? true : { ok: verdict.kind === "pass", detail: verdict.detail };
|
|
261
|
+
}
|
|
262
|
+
function noRedundantOptions() {
|
|
263
|
+
const offenders = adoptedTsconfigs().flatMap(
|
|
264
|
+
(adoption) => findRedundantOptions(adoption.chain.entries, adoption.baseIndex).map(
|
|
265
|
+
(redundant) => `${redundant.path} (${redundant.key})`
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
if (offenders.length === 0) return true;
|
|
269
|
+
return { ok: false, detail: `the base already supplies: ${dedupe(offenders).join(", ")}` };
|
|
270
|
+
}
|
|
271
|
+
function pathsOfKind(classified, kind) {
|
|
272
|
+
return classified.filter((adoption) => adoption.kind === kind).map((adoption) => adoption.path);
|
|
273
|
+
}
|
|
274
|
+
function readBaseEsYear() {
|
|
275
|
+
for (const adoption of adoptedTsconfigs()) {
|
|
276
|
+
const baseOptions = adoption.chain.entries[adoption.baseIndex]?.compilerOptions;
|
|
277
|
+
const esYear = baseOptions === void 0 ? void 0 : readDeclaredEsYear(baseOptions);
|
|
278
|
+
if (esYear !== void 0) return esYear;
|
|
279
|
+
}
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
function readChain(path) {
|
|
283
|
+
if (!cache.chains.has(path)) cache.chains.set(path, readTsconfigChain(path));
|
|
284
|
+
return cache.chains.get(path);
|
|
285
|
+
}
|
|
286
|
+
function readNodeEsYear() {
|
|
287
|
+
const major = lowestNodeMajor(declaredNodeFloors());
|
|
288
|
+
return major === void 0 ? void 0 : classifyNodeEsYear(major, esYearForNodeMajor);
|
|
289
|
+
}
|
|
290
|
+
function skipUnlessBaseEsYearKnown() {
|
|
291
|
+
return readBaseEsYear() === void 0 ? NO_BASE_ES_YEAR : false;
|
|
292
|
+
}
|
|
293
|
+
function skipUnlessNodeFloorComparable() {
|
|
294
|
+
const verdict = judgeNodeFloor();
|
|
295
|
+
return verdict.kind === "skip" ? verdict.reason : false;
|
|
296
|
+
}
|
|
297
|
+
function skipUnlessSomeTsconfigIsAccountable() {
|
|
298
|
+
const classified = classifyAdoptions();
|
|
299
|
+
if (classified.length === 0) return "no workspace tsconfig was found";
|
|
300
|
+
return classified.some((adoption) => adoption.kind !== "external-base") ? false : "every workspace tsconfig extends a base belonging to another package";
|
|
301
|
+
}
|
|
302
|
+
function tsconfigSearchDirs() {
|
|
303
|
+
cache.searchDirs ??= [.../* @__PURE__ */ new Set([".", ...discoverWorkspaces().map((workspace) => workspace.dir)])];
|
|
304
|
+
return cache.searchDirs;
|
|
305
|
+
}
|
|
306
|
+
export {
|
|
307
|
+
default_default as default
|
|
308
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kits": [
|
|
4
|
+
{
|
|
5
|
+
"checklists": [
|
|
6
|
+
"adoption",
|
|
7
|
+
"alignment"
|
|
8
|
+
],
|
|
9
|
+
"description": "Adoption and alignment checks for a project consuming @williamthorsen/tsconfig",
|
|
10
|
+
"name": "default",
|
|
11
|
+
"path": "kits/default.js",
|
|
12
|
+
"readyupVersion": "0.25.0",
|
|
13
|
+
"source": "kits/default.ts",
|
|
14
|
+
"sourceHash": "4d0a555c",
|
|
15
|
+
"targetHash": "4c59a48d"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/README.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Shared TypeScript base config for Node-only projects. Inlines the settings of [`@tsconfig/strictest`](https://www.npmjs.com/package/@tsconfig/strictest) and adds the Node and build options it leaves out, so a consuming repo declares only what is genuinely its own.
|
|
4
4
|
|
|
5
|
-
<!-- section:release-notes
|
|
5
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v0.4.0 (2026-08-09)
|
|
7
|
+
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- Add a bundled readyup kit that checks tsconfig adoption and alignment (#152)
|
|
11
|
+
|
|
12
|
+
Adds a readiness check to `@williamthorsen/tsconfig` that reports, package by package, whether a repo has adopted the base config and stayed aligned with it. A package that extends a framework's own base is reported as having opted out. The Node floor is the only finding that fails the check; the rest is advisory. The check is run with `pnpm exec rdy run --from npm:@williamthorsen/tsconfig`.
|
|
13
|
+
<!-- /section:release-notes -->
|
|
6
14
|
|
|
7
15
|
## Installation
|
|
8
16
|
|
|
@@ -22,35 +30,82 @@ pnpm add -D @williamthorsen/tsconfig
|
|
|
22
30
|
}
|
|
23
31
|
```
|
|
24
32
|
|
|
33
|
+
## Adopting the base
|
|
34
|
+
|
|
35
|
+
Extend the base from every tsconfig in the repo, not from the root alone. A workspace config reaches it either directly or through a config that does:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
// packages/api/tsconfig.json
|
|
39
|
+
{
|
|
40
|
+
"extends": "../../tsconfig.json",
|
|
41
|
+
"include": ["src/"],
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every package whose tsconfig names the base by package specifier must also declare it as a devDependency: TypeScript resolves `extends` from the directory of the config that declares it, so a workspace naming a package it does not depend on resolves nothing.
|
|
46
|
+
|
|
47
|
+
Once the base is in the chain, delete the options it already supplies with the same value. The inherited value applies either way, and the copy left behind stops tracking the base the next time it changes. An option set to a _different_ value is an override and stays.
|
|
48
|
+
|
|
49
|
+
A package that extends a framework base such as `astro/tsconfigs/base` or `@tsconfig/svelte` has opted out of this one, which is a supported choice.
|
|
50
|
+
|
|
25
51
|
## What the base sets
|
|
26
52
|
|
|
27
53
|
Everything `@tsconfig/strictest` sets, plus the Node and build options it omits:
|
|
28
54
|
|
|
29
|
-
| Option | Value
|
|
30
|
-
| ---------------------------- |
|
|
31
|
-
| `allowImportingTsExtensions` | `true`
|
|
32
|
-
| `lib` | `["ES2025"]` |
|
|
33
|
-
| `module` | `"NodeNext"`
|
|
34
|
-
| `moduleDetection` | `"force"`
|
|
35
|
-
| `noEmit` | `true`
|
|
36
|
-
| `removeComments` | `true`
|
|
37
|
-
| `target` | `"ES2025"`
|
|
55
|
+
| Option | Value |
|
|
56
|
+
| ---------------------------- | --------------------------------- |
|
|
57
|
+
| `allowImportingTsExtensions` | `true` |
|
|
58
|
+
| `lib` | `["ES2025", "ESNext.Disposable"]` |
|
|
59
|
+
| `module` | `"NodeNext"` |
|
|
60
|
+
| `moduleDetection` | `"force"` |
|
|
61
|
+
| `noEmit` | `true` |
|
|
62
|
+
| `removeComments` | `true` |
|
|
63
|
+
| `target` | `"ES2025"` |
|
|
38
64
|
|
|
39
|
-
`target` and `lib` are pinned to a Node 24 floor.
|
|
65
|
+
`target` and `lib` are pinned to a Node 24 floor. `ESNext.Disposable` supplies the explicit-resource-management declarations (`using`, `Disposable`, `DisposableStack`), which Node 24 implements and no numbered lib yet carries.
|
|
40
66
|
|
|
41
67
|
## What the consumer owns
|
|
42
68
|
|
|
43
69
|
The base declares none of these:
|
|
44
70
|
|
|
45
|
-
- **`types
|
|
46
|
-
- **`paths
|
|
47
|
-
- **`jsx
|
|
71
|
+
- **`types`**: The ambient type packages in scope. Most Node projects want `["node"]`; declaring it narrows the default, which would otherwise pull in every `@types/*` package installed.
|
|
72
|
+
- **`paths`**: TypeScript resolves `baseUrl`-less `paths` relative to the file that _declares_ them, so a path alias set in this package would resolve inside `node_modules`. Aliases must be declared by the config that owns the source tree.
|
|
73
|
+
- **`jsx`**: Meaningless for the many consumers with no JSX at all.
|
|
48
74
|
|
|
49
75
|
`include` and `exclude` are likewise the consumer's, since only it knows its own layout.
|
|
50
76
|
|
|
51
77
|
## Emitting
|
|
52
78
|
|
|
53
|
-
The base sets `noEmit: true`, on the assumption that a separate build step owns emit. A consumer that builds with this config directly must override `noEmit`
|
|
79
|
+
The base sets `noEmit: true`, on the assumption that a separate build step owns emit. A consumer that builds with this config directly must override `noEmit` and, if it keeps `allowImportingTsExtensions`, pair that with `rewriteRelativeImportExtensions` so `./foo.ts` specifiers survive the rewrite.
|
|
80
|
+
|
|
81
|
+
## Checking your configuration
|
|
82
|
+
|
|
83
|
+
This package ships a [ReadyUp](https://www.npmjs.com/package/readyup) kit that checks whether your tsconfigs are wired for the version you have installed. It is a migration aid rather than a CI gate: only a Node floor too old to run the base's ES year is reported as an error, and everything else caps at a warning.
|
|
84
|
+
|
|
85
|
+
Run it once:
|
|
86
|
+
|
|
87
|
+
```shell
|
|
88
|
+
pnpm exec rdy run --from npm:@williamthorsen/tsconfig
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or list it in `.config/readyup.config.ts` to include it whenever you run `rdy run --packages`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { defineRdyConfig } from 'readyup';
|
|
95
|
+
|
|
96
|
+
export default defineRdyConfig({
|
|
97
|
+
packages: ['@williamthorsen/tsconfig'],
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The kit walks each workspace tsconfig's `extends` chain and reports four things:
|
|
102
|
+
|
|
103
|
+
- **Adoption**: a tsconfig reaching neither this base nor a framework base. A tsconfig naming this base through a specifier that does not resolve is reported separately, as declared but not installed.
|
|
104
|
+
- **Re-declaration**: an option restated with the base's own value. A key that a framework base in the same chain also declares is exempt, since restating it is how a config wins against that base.
|
|
105
|
+
- **ES year**: a `target` or `lib` declaring a year other than the base's, read from the base you extend rather than from a constant compiled into the kit.
|
|
106
|
+
- **Node floor**: an `engines.node` below the major that implements that ES year. This is the one failure `tsc` cannot surface on its own, which is why it alone is an error.
|
|
107
|
+
|
|
108
|
+
The kit runs at the version resolved from your `node_modules`, so it reports whether your configuration matches that version. It never reports whether that version is current.
|
|
54
109
|
|
|
55
110
|
## License
|
|
56
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/tsconfig",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared TypeScript base config for Node-only projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tsconfig",
|
|
@@ -23,10 +23,14 @@
|
|
|
23
23
|
"./tsconfig.base.json": "./tsconfig.base.json"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
+
".readyup/kits/*.js",
|
|
27
|
+
".readyup/manifest.json",
|
|
26
28
|
"tsconfig.base.json"
|
|
27
29
|
],
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@tsconfig/strictest": "2.0.8"
|
|
31
|
+
"@tsconfig/strictest": "2.0.8",
|
|
32
|
+
"esbuild": "0.28.1",
|
|
33
|
+
"readyup": "0.25.0"
|
|
30
34
|
},
|
|
31
35
|
"engines": {
|
|
32
36
|
"node": ">=24"
|
|
@@ -37,8 +41,6 @@
|
|
|
37
41
|
},
|
|
38
42
|
"scripts": {
|
|
39
43
|
"build": ":",
|
|
40
|
-
"check": "nmr lint:strict"
|
|
41
|
-
"test": ":",
|
|
42
|
-
"test:coverage": ":"
|
|
44
|
+
"check": "nmr lint:strict"
|
|
43
45
|
}
|
|
44
46
|
}
|
package/tsconfig.base.json
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
// Permit `./foo.ts` specifiers; the build step rewrites them on emit.
|
|
30
30
|
"allowImportingTsExtensions": true,
|
|
31
31
|
// Without this, `lib` follows `target` into the `.full` variant, admitting DOM globals.
|
|
32
|
-
|
|
32
|
+
// `ESNext.Disposable` carries the explicit-resource-management declarations; no numbered lib has them.
|
|
33
|
+
"lib": ["ES2025", "ESNext.Disposable"],
|
|
33
34
|
// Default is `ES2022` with `Bundler` resolution — wrong for published Node packages.
|
|
34
35
|
"module": "NodeNext",
|
|
35
36
|
// Files with no import or export would otherwise be scripts sharing global scope.
|