@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,160 @@
|
|
|
1
|
+
import { ResolvedTsconfig } from "./ResolvedTsconfig.js";
|
|
2
|
+
import { TsconfigJsonFromString, TsconfigParseError } from "./TsconfigJson.js";
|
|
3
|
+
import { resolveExtendsTarget } from "./internal/extendsTarget.js";
|
|
4
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/TsconfigLoader.ts
|
|
7
|
+
/**
|
|
8
|
+
* The maximum number of `extends` levels the loader will descend before failing
|
|
9
|
+
* with {@link TsconfigExtendsError} `reason: "depth"`. A guard on the recursive
|
|
10
|
+
* walk over untrusted config files, not a tsc limit.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_EXTENDS_DEPTH = 32;
|
|
13
|
+
/**
|
|
14
|
+
* Raised when a config's `extends` chain cannot be resolved: an unresolvable
|
|
15
|
+
* target (`"not-found"`), a re-entrant chain (`"cycle"`), a chain deeper than
|
|
16
|
+
* `MAX_EXTENDS_DEPTH` (`"depth"`), or an empty target string (`"empty"`).
|
|
17
|
+
* `path` is the config whose `extends` failed, `target` is what it tried to
|
|
18
|
+
* extend (the offending spec for `"not-found"`/`"empty"`, the re-entered config
|
|
19
|
+
* path for `"cycle"`, the refused config for `"depth"`), and `chain` is the full
|
|
20
|
+
* resolution chain of normalized absolute paths.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var TsconfigExtendsError = class extends Schema.TaggedErrorClass()("TsconfigExtendsError", {
|
|
25
|
+
/** The config whose `extends` could not be resolved. */
|
|
26
|
+
path: Schema.String,
|
|
27
|
+
/** The target that failed: the spec, the re-entered path, or the refused path. */
|
|
28
|
+
target: Schema.String,
|
|
29
|
+
/** Why resolution failed. */
|
|
30
|
+
reason: Schema.Literals([
|
|
31
|
+
"not-found",
|
|
32
|
+
"cycle",
|
|
33
|
+
"depth",
|
|
34
|
+
"empty"
|
|
35
|
+
]),
|
|
36
|
+
/** The full resolution chain of normalized absolute config paths. */
|
|
37
|
+
chain: Schema.Array(Schema.String)
|
|
38
|
+
}) {
|
|
39
|
+
get message() {
|
|
40
|
+
return this.reason === "not-found" ? `cannot resolve extends target "${this.target}" from "${this.path}"` : this.reason === "cycle" ? `extends cycle re-entering "${this.target}" via ${this.chain.join(" -> ")}` : this.reason === "depth" ? `extends chain exceeded depth ${MAX_EXTENDS_DEPTH} at "${this.path}"` : `empty extends target in "${this.path}"`;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** tsc normalizes to forward slashes throughout; the merge engine and the cycle keys assume that convention. */
|
|
44
|
+
const normalizeSlashes = (p) => p.replace(/\\/g, "/");
|
|
45
|
+
/** The shared JSONC decode entrypoint (`TsconfigJsonFromString` is already the shared codec instance). */
|
|
46
|
+
const decodeConfig = Schema.decodeEffect(TsconfigJsonFromString);
|
|
47
|
+
/** Normalize a config's `extends` field (absent / string / array) to an ordered spec list. */
|
|
48
|
+
const extendsSpecs = (doc) => {
|
|
49
|
+
const ext = doc.extends;
|
|
50
|
+
if (ext === void 0) return [];
|
|
51
|
+
return typeof ext === "string" ? [ext] : ext;
|
|
52
|
+
};
|
|
53
|
+
/** Read + decode one config at an already-absolute, normalized path; wrap decode failures with that path. */
|
|
54
|
+
const loadAbs = (abs) => Effect.gen(function* () {
|
|
55
|
+
const text = yield* (yield* FileSystem.FileSystem).readFileString(abs);
|
|
56
|
+
return yield* decodeConfig(text).pipe(Effect.mapError((cause) => TsconfigParseError.make({
|
|
57
|
+
path: abs,
|
|
58
|
+
cause
|
|
59
|
+
})));
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Read one config file and decode it through {@link TsconfigJsonFromString}. A
|
|
63
|
+
* decode failure is wrapped in a {@link TsconfigParseError} carrying the file's
|
|
64
|
+
* absolute path; a `PlatformError` from the read flows through untranslated. No
|
|
65
|
+
* `extends` resolution — {@link TsconfigLoader.resolve} drives that.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
const load = (configPath) => Effect.gen(function* () {
|
|
70
|
+
const path = yield* Path.Path;
|
|
71
|
+
return yield* loadAbs(normalizeSlashes(path.resolve(configPath)));
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Collect the flattened, base-most-first list of absolutized config documents
|
|
75
|
+
* for `configPath` and its full `extends` chain. `chain` is the stack of
|
|
76
|
+
* already-visited normalized absolute paths on THIS branch (copied per branch,
|
|
77
|
+
* so a diamond is legal); it excludes `configPath` itself until it is admitted.
|
|
78
|
+
*/
|
|
79
|
+
const collect = (configPath, chain) => Effect.gen(function* () {
|
|
80
|
+
const path = yield* Path.Path;
|
|
81
|
+
const abs = normalizeSlashes(path.resolve(configPath));
|
|
82
|
+
if (chain.length >= MAX_EXTENDS_DEPTH) return yield* Effect.fail(TsconfigExtendsError.make({
|
|
83
|
+
path: abs,
|
|
84
|
+
target: abs,
|
|
85
|
+
reason: "depth",
|
|
86
|
+
chain: [...chain, abs]
|
|
87
|
+
}));
|
|
88
|
+
const doc = yield* loadAbs(abs);
|
|
89
|
+
const absolutized = ResolvedTsconfig.absolutize(doc, path.dirname(abs), path.resolve);
|
|
90
|
+
const newChain = [...chain, abs];
|
|
91
|
+
const layers = [];
|
|
92
|
+
for (const spec of extendsSpecs(absolutized)) {
|
|
93
|
+
if (spec === "") return yield* Effect.fail(TsconfigExtendsError.make({
|
|
94
|
+
path: abs,
|
|
95
|
+
target: "",
|
|
96
|
+
reason: "empty",
|
|
97
|
+
chain: newChain
|
|
98
|
+
}));
|
|
99
|
+
const target = yield* resolveExtendsTarget(spec, abs);
|
|
100
|
+
if (Option.isNone(target)) return yield* Effect.fail(TsconfigExtendsError.make({
|
|
101
|
+
path: abs,
|
|
102
|
+
target: spec,
|
|
103
|
+
reason: "not-found",
|
|
104
|
+
chain: newChain
|
|
105
|
+
}));
|
|
106
|
+
const normTarget = normalizeSlashes(target.value);
|
|
107
|
+
if (newChain.includes(normTarget)) return yield* Effect.fail(TsconfigExtendsError.make({
|
|
108
|
+
path: abs,
|
|
109
|
+
target: normTarget,
|
|
110
|
+
reason: "cycle",
|
|
111
|
+
chain: [...newChain, normTarget]
|
|
112
|
+
}));
|
|
113
|
+
const subLayers = yield* collect(normTarget, newChain);
|
|
114
|
+
for (const sub of subLayers) layers.push(sub);
|
|
115
|
+
}
|
|
116
|
+
layers.push({
|
|
117
|
+
doc: absolutized,
|
|
118
|
+
path: abs
|
|
119
|
+
});
|
|
120
|
+
return layers;
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a tsconfig.json and its full `extends` chain into a
|
|
124
|
+
* {@link (ResolvedTsconfig:interface)}: load and decode each config, absolutize its path
|
|
125
|
+
* options (E5), resolve `extends` depth-first with per-branch cycle and depth
|
|
126
|
+
* guards (E1-E3, E6), fold the chain own-config-last (E4), then substitute a
|
|
127
|
+
* leading `${configDir}` once against the top config's directory (E5 final
|
|
128
|
+
* phase). `configPath` + `extendedPaths` come back base-most first, own config
|
|
129
|
+
* last. Every failure is a typed error — `TsconfigParseError` (a malformed file,
|
|
130
|
+
* carrying that file's path), `TsconfigExtendsError` (a broken chain), or a
|
|
131
|
+
* `PlatformError` from IO — never a defect.
|
|
132
|
+
*
|
|
133
|
+
* @public
|
|
134
|
+
*/
|
|
135
|
+
const resolve = (configPath) => Effect.gen(function* () {
|
|
136
|
+
const path = yield* Path.Path;
|
|
137
|
+
const topAbs = normalizeSlashes(path.resolve(configPath));
|
|
138
|
+
const layers = yield* collect(topAbs, []);
|
|
139
|
+
let acc = {
|
|
140
|
+
configPath: topAbs,
|
|
141
|
+
extendedPaths: [],
|
|
142
|
+
compilerOptions: {}
|
|
143
|
+
};
|
|
144
|
+
for (const entry of layers) acc = ResolvedTsconfig.merge(acc, entry.doc, entry.path);
|
|
145
|
+
return ResolvedTsconfig.substituteConfigDir(acc, path.dirname(topAbs));
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* The tsconfig.json loader: {@link TsconfigLoader.load} reads and decodes one
|
|
149
|
+
* config file, {@link TsconfigLoader.resolve} runs the full load -\> extends -\>
|
|
150
|
+
* merge -\> `${configDir}` pipeline.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
const TsconfigLoader = {
|
|
155
|
+
load,
|
|
156
|
+
resolve
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
export { TsconfigExtendsError, TsconfigLoader };
|