@effected/tsconfig-json 0.1.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/CompilerOptions.js +345 -0
- package/LICENSE +21 -0
- package/PortableTsconfig.js +138 -0
- package/README.md +96 -0
- package/ResolvedTsconfig.js +245 -0
- package/TsEnumCodec.js +203 -0
- package/TsconfigDiscovery.js +29 -0
- package/TsconfigJson.js +122 -0
- package/TsconfigLoader.js +160 -0
- package/index.d.ts +645 -0
- package/index.js +9 -0
- package/internal/extendsTarget.js +234 -0
- package/package.json +48 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
//#region src/ResolvedTsconfig.ts
|
|
2
|
+
const CONFIG_DIR_TEMPLATE = `\${configDir}`;
|
|
3
|
+
const CONFIG_DIR_TEMPLATE_LOWER = CONFIG_DIR_TEMPLATE.toLowerCase();
|
|
4
|
+
/** A value is `${configDir}`-prefixed if its leading token matches case-insensitively (E5). */
|
|
5
|
+
const startsWithConfigDir = (value) => value.slice(0, CONFIG_DIR_TEMPLATE.length).toLowerCase() === CONFIG_DIR_TEMPLATE_LOWER;
|
|
6
|
+
const normalizeSlashes = (p) => p.replace(/\\/g, "/");
|
|
7
|
+
/** POSIX `dirname` over the normalized path; a rootless path yields `"."`. */
|
|
8
|
+
const dirname = (p) => {
|
|
9
|
+
const norm = normalizeSlashes(p);
|
|
10
|
+
const idx = norm.lastIndexOf("/");
|
|
11
|
+
if (idx < 0) return ".";
|
|
12
|
+
if (idx === 0) return "/";
|
|
13
|
+
return norm.slice(0, idx);
|
|
14
|
+
};
|
|
15
|
+
/** POSIX relative path from directory `from` to directory `to`. */
|
|
16
|
+
const relative = (from, to) => {
|
|
17
|
+
const f = normalizeSlashes(from).split("/").filter((s) => s.length > 0);
|
|
18
|
+
const t = normalizeSlashes(to).split("/").filter((s) => s.length > 0);
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < f.length && i < t.length && f[i] === t[i]) i++;
|
|
21
|
+
return [...f.slice(i).map(() => ".."), ...t.slice(i)].join("/");
|
|
22
|
+
};
|
|
23
|
+
/** True for POSIX-rooted, Windows-drive-rooted, and UNC paths. */
|
|
24
|
+
const isAbsolutePath = (p) => p.startsWith("/") || p.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(p);
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild a record, mapping each value, using `defineProperty` so an own
|
|
27
|
+
* `__proto__` key is written as data rather than triggering the prototype
|
|
28
|
+
* setter. Preserves every own key (forward tolerance) without pollution.
|
|
29
|
+
*/
|
|
30
|
+
const rebuildRecord = (source, mapValue) => {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const key of Object.keys(source)) Object.defineProperty(out, key, {
|
|
33
|
+
value: mapValue(key, source[key]),
|
|
34
|
+
enumerable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
configurable: true
|
|
37
|
+
});
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
const PATH_STRING_KEYS = [
|
|
41
|
+
"outFile",
|
|
42
|
+
"outDir",
|
|
43
|
+
"rootDir",
|
|
44
|
+
"declarationDir",
|
|
45
|
+
"sourceRoot",
|
|
46
|
+
"mapRoot",
|
|
47
|
+
"tsBuildInfoFile",
|
|
48
|
+
"baseUrl",
|
|
49
|
+
"generateCpuProfile",
|
|
50
|
+
"generateTrace"
|
|
51
|
+
];
|
|
52
|
+
const PATH_LIST_KEYS = ["typeRoots", "rootDirs"];
|
|
53
|
+
/**
|
|
54
|
+
* Apply `transform` to every path-typed compilerOptions surface: the R1.4 path
|
|
55
|
+
* strings and path lists, and — when `includePathsValues` — the `paths` record's
|
|
56
|
+
* values (E5's final phase substitutes those; E5's parse phase leaves them
|
|
57
|
+
* verbatim). Non-string values pass through untouched (forward tolerance).
|
|
58
|
+
*/
|
|
59
|
+
const transformCompilerOptionPaths = (co, transform, includePathsValues) => {
|
|
60
|
+
const out = { ...co };
|
|
61
|
+
for (const key of PATH_STRING_KEYS) {
|
|
62
|
+
const value = out[key];
|
|
63
|
+
if (typeof value === "string") out[key] = transform(value);
|
|
64
|
+
}
|
|
65
|
+
for (const key of PATH_LIST_KEYS) {
|
|
66
|
+
const value = out[key];
|
|
67
|
+
if (Array.isArray(value)) out[key] = value.map((entry) => typeof entry === "string" ? transform(entry) : entry);
|
|
68
|
+
}
|
|
69
|
+
if (includePathsValues) {
|
|
70
|
+
const paths = out.paths;
|
|
71
|
+
if (paths !== null && typeof paths === "object") out.paths = rebuildRecord(paths, (_key, arr) => Array.isArray(arr) ? arr.map((entry) => typeof entry === "string" ? transform(entry) : entry) : arr);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Absolutize a config's path-typed options (E5) against its own `configDir`,
|
|
77
|
+
* using the injected `join` (`Path.Path.resolve` at the call site — so an
|
|
78
|
+
* already-absolute value is preserved). `${configDir}`-prefixed values are
|
|
79
|
+
* exempt (resolved later, in {@link (ResolvedTsconfig:variable).substituteConfigDir}), and
|
|
80
|
+
* `paths` VALUES stay verbatim. Only `compilerOptions` path surfaces are
|
|
81
|
+
* touched; `files`/`include`/`exclude` are re-rooted at merge time instead (E4).
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
const absolutize = (doc, configDir, join) => {
|
|
86
|
+
const co = doc.compilerOptions;
|
|
87
|
+
if (co === void 0) return doc;
|
|
88
|
+
const absolutizeValue = (value) => startsWithConfigDir(value) ? value : join(configDir, value);
|
|
89
|
+
return {
|
|
90
|
+
...doc,
|
|
91
|
+
compilerOptions: transformCompilerOptionPaths(co, absolutizeValue, false)
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const DERIVED_CONSUMED_KEYS = /* @__PURE__ */ new Set([
|
|
95
|
+
"compilerOptions",
|
|
96
|
+
"extends",
|
|
97
|
+
"files",
|
|
98
|
+
"include",
|
|
99
|
+
"exclude",
|
|
100
|
+
"references",
|
|
101
|
+
"watchOptions",
|
|
102
|
+
"typeAcquisition",
|
|
103
|
+
"compileOnSave"
|
|
104
|
+
]);
|
|
105
|
+
const RESOLVED_STRUCTURAL_KEYS = /* @__PURE__ */ new Set([
|
|
106
|
+
"configPath",
|
|
107
|
+
"extendedPaths",
|
|
108
|
+
"compilerOptions",
|
|
109
|
+
"files",
|
|
110
|
+
"include",
|
|
111
|
+
"exclude",
|
|
112
|
+
"references",
|
|
113
|
+
"watchOptions",
|
|
114
|
+
"typeAcquisition",
|
|
115
|
+
"compileOnSave",
|
|
116
|
+
"pathsBase",
|
|
117
|
+
"extends"
|
|
118
|
+
]);
|
|
119
|
+
const extractPassthrough = (source, consumed) => {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const key of Object.keys(source)) {
|
|
122
|
+
if (consumed.has(key)) continue;
|
|
123
|
+
Object.defineProperty(out, key, {
|
|
124
|
+
value: source[key],
|
|
125
|
+
enumerable: true,
|
|
126
|
+
writable: true,
|
|
127
|
+
configurable: true
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
/** Re-root one inherited `files`/`include`/`exclude` entry, exempting absolute and `${configDir}` entries (E4). */
|
|
133
|
+
const rerootEntry = (prefix, entry) => {
|
|
134
|
+
if (startsWithConfigDir(entry) || isAbsolutePath(entry)) return entry;
|
|
135
|
+
return prefix === "" ? entry : `${prefix}/${entry}`;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* `files`/`include`/`exclude`: the derived (own) config declaring the property
|
|
139
|
+
* wins outright — an own empty array beats an inherited value. Otherwise inherit
|
|
140
|
+
* the base's entries, re-rooted with the relative prefix.
|
|
141
|
+
*/
|
|
142
|
+
const mergeFileList = (baseList, derivedList, rerootPrefix) => {
|
|
143
|
+
if (derivedList !== void 0) return derivedList;
|
|
144
|
+
if (baseList === void 0) return void 0;
|
|
145
|
+
return baseList.map((entry) => rerootEntry(rerootPrefix, entry));
|
|
146
|
+
};
|
|
147
|
+
/** `watchOptions`: per-key shallow merge (derived wins per key). */
|
|
148
|
+
const mergeWatchOptions = (base, derived) => {
|
|
149
|
+
if (base === void 0) return derived;
|
|
150
|
+
if (derived === void 0) return base;
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
...derived
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Fold one more-derived config onto the accumulated base (E4), derived winning.
|
|
158
|
+
* The loader (Task 8) applies this across the resolution chain, own config last.
|
|
159
|
+
* `derivedPath` is the derived config's absolute normalized path, from which the
|
|
160
|
+
* re-rooting frame and `pathsBase` are computed.
|
|
161
|
+
*
|
|
162
|
+
* @public
|
|
163
|
+
*/
|
|
164
|
+
const merge = (base, derived, derivedPath) => {
|
|
165
|
+
const finalDir = dirname(derivedPath);
|
|
166
|
+
const baseDir = dirname(base.configPath);
|
|
167
|
+
const compilerOptions = {
|
|
168
|
+
...base.compilerOptions,
|
|
169
|
+
...derived.compilerOptions
|
|
170
|
+
};
|
|
171
|
+
const pathsBase = derived.compilerOptions?.paths !== void 0 ? finalDir : base.pathsBase;
|
|
172
|
+
const rerootPrefix = relative(finalDir, baseDir);
|
|
173
|
+
const files = mergeFileList(base.files, derived.files, rerootPrefix);
|
|
174
|
+
const include = mergeFileList(base.include, derived.include, rerootPrefix);
|
|
175
|
+
const exclude = mergeFileList(base.exclude, derived.exclude, rerootPrefix);
|
|
176
|
+
const references = derived.references;
|
|
177
|
+
const typeAcquisition = derived.typeAcquisition;
|
|
178
|
+
const compileOnSave = derived.compileOnSave !== void 0 ? derived.compileOnSave : base.compileOnSave === true ? true : void 0;
|
|
179
|
+
const watchOptions = mergeWatchOptions(base.watchOptions, derived.watchOptions);
|
|
180
|
+
return {
|
|
181
|
+
...extractPassthrough(base, RESOLVED_STRUCTURAL_KEYS),
|
|
182
|
+
...extractPassthrough(derived, DERIVED_CONSUMED_KEYS),
|
|
183
|
+
configPath: derivedPath,
|
|
184
|
+
extendedPaths: [...base.extendedPaths, derivedPath],
|
|
185
|
+
compilerOptions,
|
|
186
|
+
...files !== void 0 ? { files } : {},
|
|
187
|
+
...include !== void 0 ? { include } : {},
|
|
188
|
+
...exclude !== void 0 ? { exclude } : {},
|
|
189
|
+
...references !== void 0 ? { references } : {},
|
|
190
|
+
...watchOptions !== void 0 ? { watchOptions } : {},
|
|
191
|
+
...typeAcquisition !== void 0 ? { typeAcquisition } : {},
|
|
192
|
+
...compileOnSave !== void 0 ? { compileOnSave } : {},
|
|
193
|
+
...pathsBase !== void 0 ? { pathsBase } : {}
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const substituteWatchExcludes = (wo, substitute) => {
|
|
197
|
+
if (wo === void 0) return void 0;
|
|
198
|
+
const out = { ...wo };
|
|
199
|
+
for (const key of ["excludeDirectories", "excludeFiles"]) {
|
|
200
|
+
const value = out[key];
|
|
201
|
+
if (Array.isArray(value)) out[key] = value.map((entry) => typeof entry === "string" ? substitute(entry) : entry);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* The E5 final phase: replace a leading `${configDir}` token (case-insensitive,
|
|
207
|
+
* leading position only) with `finalDir` — the top-level extending config's
|
|
208
|
+
* directory — across every eligible surface: compilerOptions path options,
|
|
209
|
+
* `paths` values, `files`/`include`/`exclude`, and `watchOptions`'
|
|
210
|
+
* `excludeDirectories`/`excludeFiles`. Every other field is left untouched.
|
|
211
|
+
*
|
|
212
|
+
* @public
|
|
213
|
+
*/
|
|
214
|
+
const substituteConfigDir = (resolved, finalDir) => {
|
|
215
|
+
const substitute = (value) => startsWithConfigDir(value) ? finalDir + value.slice(CONFIG_DIR_TEMPLATE.length) : value;
|
|
216
|
+
const compilerOptions = transformCompilerOptionPaths(resolved.compilerOptions, substitute, true);
|
|
217
|
+
const files = resolved.files === void 0 ? void 0 : resolved.files.map(substitute);
|
|
218
|
+
const include = resolved.include === void 0 ? void 0 : resolved.include.map(substitute);
|
|
219
|
+
const exclude = resolved.exclude === void 0 ? void 0 : resolved.exclude.map(substitute);
|
|
220
|
+
const watchOptions = substituteWatchExcludes(resolved.watchOptions, substitute);
|
|
221
|
+
return {
|
|
222
|
+
...resolved,
|
|
223
|
+
compilerOptions,
|
|
224
|
+
...files !== void 0 ? { files } : {},
|
|
225
|
+
...include !== void 0 ? { include } : {},
|
|
226
|
+
...exclude !== void 0 ? { exclude } : {},
|
|
227
|
+
...watchOptions !== void 0 ? { watchOptions } : {}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* The pure extends-merge engine: parse-time path absolutization
|
|
232
|
+
* ({@link (ResolvedTsconfig:variable).absolutize}), the per-field merge fold
|
|
233
|
+
* ({@link (ResolvedTsconfig:variable).merge}), and the `${configDir}` final
|
|
234
|
+
* phase ({@link (ResolvedTsconfig:variable).substituteConfigDir}).
|
|
235
|
+
*
|
|
236
|
+
* @public
|
|
237
|
+
*/
|
|
238
|
+
const ResolvedTsconfig = {
|
|
239
|
+
absolutize,
|
|
240
|
+
merge,
|
|
241
|
+
substituteConfigDir
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
export { ResolvedTsconfig };
|
package/TsEnumCodec.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/TsEnumCodec.ts
|
|
4
|
+
/**
|
|
5
|
+
* Builds a family's forward/reverse maps from its R1.6 row order. The
|
|
6
|
+
* reverse map overwrites on every duplicate value, so the last-listed name
|
|
7
|
+
* for a value is canonical — exactly R1.6's alias/canonical rule.
|
|
8
|
+
*/
|
|
9
|
+
const buildTable = (rows) => {
|
|
10
|
+
const forward = /* @__PURE__ */ new Map();
|
|
11
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const [name, value] of rows) {
|
|
13
|
+
forward.set(name, value);
|
|
14
|
+
reverse.set(value, name);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
forward,
|
|
18
|
+
reverse
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const TABLES = {
|
|
22
|
+
target: buildTable([
|
|
23
|
+
["es5", 1],
|
|
24
|
+
["es6", 2],
|
|
25
|
+
["es2015", 2],
|
|
26
|
+
["es2016", 3],
|
|
27
|
+
["es2017", 4],
|
|
28
|
+
["es2018", 5],
|
|
29
|
+
["es2019", 6],
|
|
30
|
+
["es2020", 7],
|
|
31
|
+
["es2021", 8],
|
|
32
|
+
["es2022", 9],
|
|
33
|
+
["es2023", 10],
|
|
34
|
+
["es2024", 11],
|
|
35
|
+
["es2025", 12],
|
|
36
|
+
["esnext", 99]
|
|
37
|
+
]),
|
|
38
|
+
module: buildTable([
|
|
39
|
+
["none", 0],
|
|
40
|
+
["commonjs", 1],
|
|
41
|
+
["amd", 2],
|
|
42
|
+
["umd", 3],
|
|
43
|
+
["system", 4],
|
|
44
|
+
["es6", 5],
|
|
45
|
+
["es2015", 5],
|
|
46
|
+
["es2020", 6],
|
|
47
|
+
["es2022", 7],
|
|
48
|
+
["esnext", 99],
|
|
49
|
+
["node16", 100],
|
|
50
|
+
["node18", 101],
|
|
51
|
+
["node20", 102],
|
|
52
|
+
["nodenext", 199],
|
|
53
|
+
["preserve", 200]
|
|
54
|
+
]),
|
|
55
|
+
moduleResolution: buildTable([
|
|
56
|
+
["classic", 1],
|
|
57
|
+
["node", 2],
|
|
58
|
+
["node10", 2],
|
|
59
|
+
["node16", 3],
|
|
60
|
+
["nodenext", 99],
|
|
61
|
+
["bundler", 100]
|
|
62
|
+
]),
|
|
63
|
+
jsx: buildTable([
|
|
64
|
+
["preserve", 1],
|
|
65
|
+
["react", 2],
|
|
66
|
+
["react-native", 3],
|
|
67
|
+
["react-jsx", 4],
|
|
68
|
+
["react-jsxdev", 5]
|
|
69
|
+
]),
|
|
70
|
+
newLine: buildTable([["crlf", 0], ["lf", 1]]),
|
|
71
|
+
moduleDetection: buildTable([
|
|
72
|
+
["legacy", 1],
|
|
73
|
+
["auto", 2],
|
|
74
|
+
["force", 3]
|
|
75
|
+
]),
|
|
76
|
+
watchFile: buildTable([
|
|
77
|
+
["fixedpollinginterval", 0],
|
|
78
|
+
["prioritypollinginterval", 1],
|
|
79
|
+
["dynamicprioritypolling", 2],
|
|
80
|
+
["fixedchunksizepolling", 3],
|
|
81
|
+
["usefsevents", 4],
|
|
82
|
+
["usefseventsonparentdirectory", 5]
|
|
83
|
+
]),
|
|
84
|
+
watchDirectory: buildTable([
|
|
85
|
+
["usefsevents", 0],
|
|
86
|
+
["fixedpollinginterval", 1],
|
|
87
|
+
["dynamicprioritypolling", 2],
|
|
88
|
+
["fixedchunksizepolling", 3]
|
|
89
|
+
]),
|
|
90
|
+
fallbackPolling: buildTable([
|
|
91
|
+
["fixedinterval", 0],
|
|
92
|
+
["priorityinterval", 1],
|
|
93
|
+
["dynamicpriority", 2],
|
|
94
|
+
["fixedchunksize", 3]
|
|
95
|
+
])
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Encodes a family's canonical (or alias) string spelling to its numeric
|
|
99
|
+
* form. `Option.none()` for a string with no table entry — never guessed.
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
const encode = (family, value) => Option.fromNullishOr(TABLES[family].forward.get(value));
|
|
104
|
+
/**
|
|
105
|
+
* Decodes a family's numeric value to its canonical string spelling.
|
|
106
|
+
* `Option.none()` for a numeric value with no table entry (a future TS enum
|
|
107
|
+
* member) — never guessed.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
const decode = (family, value) => Option.fromNullishOr(TABLES[family].reverse.get(value));
|
|
112
|
+
/**
|
|
113
|
+
* Normalizes any spelling of a `lib` reference — the plain short name
|
|
114
|
+
* (`esnext`), the on-disk file name (`lib.esnext.d.ts`), or an absolute path
|
|
115
|
+
* to one (`/…/typescript/lib/lib.dom.iterable.d.ts`) — to the canonical
|
|
116
|
+
* lowercase short name (`esnext`, `dom.iterable`). Strips a leading
|
|
117
|
+
* directory, the `lib.` prefix and the `.d.ts` suffix; idempotent on an
|
|
118
|
+
* already-short name.
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
const normalizeLibReference = (lib) => {
|
|
123
|
+
const base = (lib.split("/").pop() ?? lib).toLowerCase();
|
|
124
|
+
const withoutPrefix = base.startsWith("lib.") ? base.slice(4) : base;
|
|
125
|
+
return withoutPrefix.endsWith(".d.ts") ? withoutPrefix.slice(0, -5) : withoutPrefix;
|
|
126
|
+
};
|
|
127
|
+
/** `compilerOptions` keys whose values are one of the R1.6 enum families. */
|
|
128
|
+
const COMPILER_OPTION_ENUM_KEYS = [
|
|
129
|
+
["target", "target"],
|
|
130
|
+
["module", "module"],
|
|
131
|
+
["moduleResolution", "moduleResolution"],
|
|
132
|
+
["jsx", "jsx"],
|
|
133
|
+
["newLine", "newLine"],
|
|
134
|
+
["moduleDetection", "moduleDetection"]
|
|
135
|
+
];
|
|
136
|
+
/**
|
|
137
|
+
* Encodes a decoded `compilerOptions` object into the numeric-enum-shaped
|
|
138
|
+
* form `ts.CompilerOptions` (and `@typescript/vfs`'s `TsEnvironment`) expect:
|
|
139
|
+
* every R1.6 enum family becomes its numeric value, and `lib` entries become
|
|
140
|
+
* the file-name form (`lib.esnext.d.ts`) — see the module banner for the
|
|
141
|
+
* evidence. Every other key (booleans, strings, arrays, unknown passthrough
|
|
142
|
+
* keys) is copied through untouched.
|
|
143
|
+
*
|
|
144
|
+
* @public
|
|
145
|
+
*/
|
|
146
|
+
const encodeCompilerOptions = (options) => {
|
|
147
|
+
const source = options;
|
|
148
|
+
const result = { ...source };
|
|
149
|
+
for (const [key, family] of COMPILER_OPTION_ENUM_KEYS) {
|
|
150
|
+
const value = source[key];
|
|
151
|
+
if (typeof value === "string") {
|
|
152
|
+
const encoded = encode(family, value);
|
|
153
|
+
if (Option.isSome(encoded)) result[key] = encoded.value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const lib = source.lib;
|
|
157
|
+
if (Array.isArray(lib)) result.lib = lib.map((entry) => typeof entry === "string" ? `lib.${normalizeLibReference(entry)}.d.ts` : entry);
|
|
158
|
+
return result;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Decodes a numeric-enum-shaped `compilerOptions` object (as produced by
|
|
162
|
+
* {@link (TsEnumCodec:variable).encodeCompilerOptions} or read off a live
|
|
163
|
+
* `ts.CompilerOptions`) back into the string-enum shape this package's
|
|
164
|
+
* schemas use: every R1.6 enum family becomes its canonical string, and
|
|
165
|
+
* `lib` entries become the short form. A numeric value with no table entry —
|
|
166
|
+
* a future TS enum member — is left as-is (passthrough, never an error) —
|
|
167
|
+
* which is why the return type stays the wider `Record<string, unknown>`
|
|
168
|
+
* rather than {@link (CompilerOptions:variable).Type}: an unmappable
|
|
169
|
+
* passthrough value would violate that narrower type's contract. Every other
|
|
170
|
+
* key is copied through untouched.
|
|
171
|
+
*
|
|
172
|
+
* @public
|
|
173
|
+
*/
|
|
174
|
+
const decodeCompilerOptions = (numeric) => {
|
|
175
|
+
const result = { ...numeric };
|
|
176
|
+
for (const [key, family] of COMPILER_OPTION_ENUM_KEYS) {
|
|
177
|
+
const value = numeric[key];
|
|
178
|
+
if (typeof value === "number") {
|
|
179
|
+
const decoded = decode(family, value);
|
|
180
|
+
if (Option.isSome(decoded)) result[key] = decoded.value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const lib = numeric.lib;
|
|
184
|
+
if (Array.isArray(lib)) result.lib = lib.map((entry) => typeof entry === "string" ? normalizeLibReference(entry) : entry);
|
|
185
|
+
return result;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* The string↔numeric enum codec for `compilerOptions` / `watchOptions`
|
|
189
|
+
* families, per R1.6. Plain data: every lookup is a synchronous map read
|
|
190
|
+
* returning `Option.Option`, never a thrown error.
|
|
191
|
+
*
|
|
192
|
+
* @public
|
|
193
|
+
*/
|
|
194
|
+
const TsEnumCodec = {
|
|
195
|
+
encode,
|
|
196
|
+
decode,
|
|
197
|
+
normalizeLibReference,
|
|
198
|
+
encodeCompilerOptions,
|
|
199
|
+
decodeCompilerOptions
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
export { TsEnumCodec };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Effect, Path } from "effect";
|
|
2
|
+
import { Walker } from "@effected/walker";
|
|
3
|
+
|
|
4
|
+
//#region src/TsconfigDiscovery.ts
|
|
5
|
+
/**
|
|
6
|
+
* Find the nearest `tsconfig.json` (or `options.filename`) at or above
|
|
7
|
+
* `start`, ascending toward the filesystem root. Absence — nowhere on the
|
|
8
|
+
* chain, or every candidate unreadable — is `Option.none()`, never an error;
|
|
9
|
+
* discovery is best-effort per `Walker.findUpward`'s absorption posture, and a
|
|
10
|
+
* permission-denied probe on one directory does not hide a config file above
|
|
11
|
+
* it.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
const findNearest = (start, options) => Effect.gen(function* () {
|
|
16
|
+
const path = yield* Path.Path;
|
|
17
|
+
const filename = options?.filename ?? "tsconfig.json";
|
|
18
|
+
const dirs = yield* Walker.ascend(start, options?.stopAt === void 0 ? {} : { stopAt: options.stopAt });
|
|
19
|
+
return yield* Walker.findUpward(dirs, (dir) => [path.join(dir, filename)]);
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Nearest-config upward discovery for `tsconfig.json`.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
const TsconfigDiscovery = { findNearest };
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { TsconfigDiscovery };
|
package/TsconfigJson.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CompilerOptions } from "./CompilerOptions.js";
|
|
2
|
+
import { Schema, SchemaTransformation } from "effect";
|
|
3
|
+
import { Jsonc } from "@effected/jsonc";
|
|
4
|
+
|
|
5
|
+
//#region src/TsconfigJson.ts
|
|
6
|
+
/**
|
|
7
|
+
* Case-insensitive literal-union decode; canonical lowercase encode.
|
|
8
|
+
* Replicated from `CompilerOptions.ts`'s module-private helper (not exported
|
|
9
|
+
* there — reused as house style, not imported, to avoid coupling this
|
|
10
|
+
* module's public schemas to CompilerOptions.ts's internals).
|
|
11
|
+
*/
|
|
12
|
+
const caseInsensitiveLiterals = (literals) => Schema.String.pipe(Schema.decodeTo(Schema.Literals(literals), SchemaTransformation.transform({
|
|
13
|
+
decode: (s) => s.toLowerCase(),
|
|
14
|
+
encode: (s) => s
|
|
15
|
+
})));
|
|
16
|
+
/** `watchOptions.watchFile`. @public */
|
|
17
|
+
const WatchFile = caseInsensitiveLiterals([
|
|
18
|
+
"fixedpollinginterval",
|
|
19
|
+
"prioritypollinginterval",
|
|
20
|
+
"dynamicprioritypolling",
|
|
21
|
+
"fixedchunksizepolling",
|
|
22
|
+
"usefsevents",
|
|
23
|
+
"usefseventsonparentdirectory"
|
|
24
|
+
]);
|
|
25
|
+
/** `watchOptions.watchDirectory`. @public */
|
|
26
|
+
const WatchDirectory = caseInsensitiveLiterals([
|
|
27
|
+
"usefsevents",
|
|
28
|
+
"fixedpollinginterval",
|
|
29
|
+
"dynamicprioritypolling",
|
|
30
|
+
"fixedchunksizepolling"
|
|
31
|
+
]);
|
|
32
|
+
/** `watchOptions.fallbackPolling`. @public */
|
|
33
|
+
const FallbackPolling = caseInsensitiveLiterals([
|
|
34
|
+
"fixedinterval",
|
|
35
|
+
"priorityinterval",
|
|
36
|
+
"dynamicpriority",
|
|
37
|
+
"fixedchunksize"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* One `references[]` entry: `path` is required and non-empty; every other key
|
|
41
|
+
* is preserved verbatim (per R1.5).
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
const Reference = Schema.StructWithRest(Schema.Struct({ path: Schema.String.check(Schema.isMinLength(1)) }), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
46
|
+
/**
|
|
47
|
+
* `watchOptions` — the three enum fields (R1.2), the two live booleans/arrays
|
|
48
|
+
* (R1.5), and a passthrough record (schemastore's `force` is phantom).
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
const WatchOptions = Schema.StructWithRest(Schema.Struct({
|
|
53
|
+
watchFile: Schema.optionalKey(WatchFile),
|
|
54
|
+
watchDirectory: Schema.optionalKey(WatchDirectory),
|
|
55
|
+
fallbackPolling: Schema.optionalKey(FallbackPolling),
|
|
56
|
+
synchronousWatchDirectory: Schema.optionalKey(Schema.Boolean),
|
|
57
|
+
excludeDirectories: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
58
|
+
excludeFiles: Schema.optionalKey(Schema.Array(Schema.String))
|
|
59
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
60
|
+
/**
|
|
61
|
+
* `typeAcquisition` — per R1.5.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
const TypeAcquisition = Schema.StructWithRest(Schema.Struct({
|
|
66
|
+
enable: Schema.optionalKey(Schema.Boolean),
|
|
67
|
+
include: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
68
|
+
exclude: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
69
|
+
disableFilenameBasedTypeAcquisition: Schema.optionalKey(Schema.Boolean)
|
|
70
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
71
|
+
/**
|
|
72
|
+
* The tsconfig.json document: every typed top-level field (R1.1) optional,
|
|
73
|
+
* plus a passthrough record so unrecognized keys (`buildOptions`, `ts-node`,
|
|
74
|
+
* …) survive decode and re-encode untouched — tsc itself silently ignores
|
|
75
|
+
* unknown top-level keys, and this schema follows suit.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
const TsconfigJson = Schema.StructWithRest(Schema.Struct({
|
|
80
|
+
compilerOptions: Schema.optionalKey(CompilerOptions),
|
|
81
|
+
extends: Schema.optionalKey(Schema.Union([Schema.String, Schema.Array(Schema.String)])),
|
|
82
|
+
files: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
83
|
+
include: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
84
|
+
exclude: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
85
|
+
references: Schema.optionalKey(Schema.Array(Reference)),
|
|
86
|
+
watchOptions: Schema.optionalKey(WatchOptions),
|
|
87
|
+
typeAcquisition: Schema.optionalKey(TypeAcquisition),
|
|
88
|
+
compileOnSave: Schema.optionalKey(Schema.Boolean),
|
|
89
|
+
$schema: Schema.optionalKey(Schema.String)
|
|
90
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
91
|
+
/**
|
|
92
|
+
* Decodes a JSONC-encoded tsconfig.json document straight into
|
|
93
|
+
* {@link (TsconfigJson:variable)}. Bound once at module top level —
|
|
94
|
+
* `Jsonc.schema` is schema-producing, and this is the shared instance (the
|
|
95
|
+
* house `FromString` idiom, R3.3; `TsconfigJson` is a `Schema.StructWithRest`
|
|
96
|
+
* value rather than a `Schema.Class`, so the codec is a sibling export, not a
|
|
97
|
+
* static).
|
|
98
|
+
*
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
const TsconfigJsonFromString = Jsonc.schema(TsconfigJson);
|
|
102
|
+
/**
|
|
103
|
+
* Raised when a tsconfig.json document fails to parse or decode. `path` is
|
|
104
|
+
* the file path when the failure is file-bound, and the empty string
|
|
105
|
+
* otherwise (e.g. decoding an in-memory string). The loader (Task 8) wraps
|
|
106
|
+
* file-bound decode failures in this error; this module only declares it.
|
|
107
|
+
*
|
|
108
|
+
* @public
|
|
109
|
+
*/
|
|
110
|
+
var TsconfigParseError = class extends Schema.TaggedErrorClass()("TsconfigParseError", {
|
|
111
|
+
/** The file path that failed to parse, or `""` when not file-bound. */
|
|
112
|
+
path: Schema.String,
|
|
113
|
+
/** The underlying decode failure. */
|
|
114
|
+
cause: Schema.Defect()
|
|
115
|
+
}) {
|
|
116
|
+
get message() {
|
|
117
|
+
return this.path.length > 0 ? `failed to parse tsconfig.json at "${this.path}"` : "failed to parse tsconfig.json";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { FallbackPolling, Reference, TsconfigJson, TsconfigJsonFromString, TsconfigParseError, TypeAcquisition, WatchDirectory, WatchFile, WatchOptions };
|