@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.
@@ -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 };