@effected/tsconfig-json 0.3.1 → 0.3.3

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.
@@ -101,16 +101,6 @@ const PRESERVED_OPTIONS = [
101
101
  * is filtered, which is the safe outcome.)
102
102
  */
103
103
  const isResolvedTsconfig = (input) => typeof input.configPath === "string" && Array.isArray(input.extendedPaths) && typeof input.compilerOptions === "object" && input.compilerOptions !== null;
104
- /**
105
- * Project a {@link (ResolvedTsconfig:interface)} or a bare
106
- * `CompilerOptions.Type` down to a {@link (PortableTsconfig:interface)}: copy
107
- * only the allow-listed type-semantics options, force `composite: false` and
108
- * `noEmit: true` regardless of what the source declared, and stamp `$schema`.
109
- * Every other key — including every unknown passthrough key the source
110
- * preserved for forward tolerance — is dropped; this is an allow-list, not a
111
- * deny-list, so an option this package does not yet classify never leaks onto
112
- * the portable shape by accident.
113
- */
114
104
  const make = (input) => {
115
105
  const source = isResolvedTsconfig(input) ? input.compilerOptions : input;
116
106
  const compilerOptions = {};
@@ -126,13 +116,27 @@ const make = (input) => {
126
116
  };
127
117
  };
128
118
  /**
129
- * The portable-tsconfig filter: {@link (PortableTsconfig:variable).make}
119
+ * The portable-tsconfig filter: {@link (PortableTsconfig:class).make}
130
120
  * narrows a resolved or bare compiler-options object to the allow-listed,
131
121
  * machine-independent subset described on {@link (PortableTsconfig:interface)}.
132
122
  *
133
123
  * @public
134
124
  */
135
- const PortableTsconfig = { make };
125
+ var PortableTsconfig = class {
126
+ constructor() {}
127
+ /**
128
+ * Project a {@link (ResolvedTsconfig:interface)} or a bare
129
+ * `CompilerOptions.Type` down to a {@link (PortableTsconfig:interface)}:
130
+ * copy only the allow-listed type-semantics options, force
131
+ * `composite: false` and `noEmit: true` regardless of what the source
132
+ * declared, and stamp `$schema`. Every other key — including every
133
+ * unknown passthrough key the source preserved for forward tolerance —
134
+ * is dropped; this is an allow-list, not a deny-list, so an option this
135
+ * package does not yet classify never leaks onto the portable shape by
136
+ * accident.
137
+ */
138
+ static make = make;
139
+ };
136
140
 
137
141
  //#endregion
138
142
  export { PortableTsconfig };
package/README.md CHANGED
@@ -104,6 +104,47 @@ const compilerOptions = TsconfigLoaderSync.compilerOptions("./tsconfig.json", op
104
104
 
105
105
  `load` and `resolve` have the same synchronous forms. Failures are the async pipeline's own typed errors thrown as themselves — `TsconfigParseError`, `TsconfigExtendsError` or a `PlatformError` wrapping whatever your `readFile` threw — never a fiber-failure wrapper.
106
106
 
107
+ ## TypeScript 7 and the classic compiler API
108
+
109
+ TypeScript 7's native `tsc` ships a version-only stub as its `.` export — there is no JS compiler API behind it. A package that drives the typechecker as a library (`@typescript/vfs`, the language service, the Program API) can neither type against nor runtime-load a TypeScript 7 install, and the obvious fix of pinning the dev `typescript` back to 6 forfeits the native `tsc` and leaves toolchain peers on `^7` unmet. The recipe that keeps both starts here, because this package is what removes the compile-time half of the dependency.
110
+
111
+ 1. Type against the tsconfig JSON form, not the compiler. Public option types use `CompilerOptions.Type` — `{ target: "es2022" }`, strings all the way down — so nothing in `src` or the tests imports `typescript`, not even as a type. The dev `typescript` stays on 7, native `tsc --noEmit` still runs the typecheck, and the toolchain's `^7` peer stays satisfied.
112
+ 2. Convert at a single runtime seam. `TsEnumCodec.encodeCompilerOptions` maps the string form to the numeric enums a real compiler expects and returns `ProgrammaticCompilerOptions`, the shape a `ts.CompilerOptions`-typed API takes without a cast. Only that one call site knows a compiler exists.
113
+ 3. Alias a classic install for the test runtime, where the JS API is genuinely needed: a dev-only `"typescript-classic": "npm:typescript@^6.0.3"` plus a vitest `resolve.alias` mapping `typescript` to it. The consumer-facing peer stays an optional `^6` — runtime-only, for callers of the compiler-touching module.
114
+ 4. Pass `tsLibDirectory` explicitly when you drive `@typescript/vfs`. It locates `lib.*.d.ts` through `require.resolve("typescript")`, which under this setup resolves the TypeScript 7 install, and that install has no lib directory: the map comes back **empty**, silently, with no error to follow. Derive the directory from the module you actually loaded.
115
+
116
+ ```json
117
+ {
118
+ "devDependencies": {
119
+ "typescript-classic": "npm:typescript@^6.0.3"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ```ts
125
+ import { defineConfig } from "vitest/config";
126
+
127
+ export default defineConfig({
128
+ resolve: { alias: { typescript: "typescript-classic" } },
129
+ });
130
+ ```
131
+
132
+ ```ts
133
+ import { dirname } from "node:path";
134
+ import { TsEnumCodec } from "@effected/tsconfig-json";
135
+ import { createDefaultMapFromNodeModules } from "@typescript/vfs";
136
+ import ts from "typescript";
137
+
138
+ const fsMap = createDefaultMapFromNodeModules(
139
+ TsEnumCodec.encodeCompilerOptions({ target: "es2023", lib: ["esnext"] }),
140
+ ts,
141
+ dirname(ts.sys.getExecutingFilePath()),
142
+ );
143
+
144
+ console.log(fsMap.size);
145
+ // the lib files the loaded compiler ships; drop the third argument and this is 0
146
+ ```
147
+
107
148
  ## Features
108
149
 
109
150
  - `TsconfigJson` / `TsconfigJsonFromString` — the document schema and its JSONC string codec. Comments and trailing commas are legal in every parse; there is no JSON-strict path.
@@ -72,16 +72,6 @@ const transformCompilerOptionPaths = (co, transform, includePathsValues) => {
72
72
  }
73
73
  return out;
74
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
75
  const absolutize = (doc, configDir, join) => {
86
76
  const co = doc.compilerOptions;
87
77
  if (co === void 0) return doc;
@@ -153,14 +143,6 @@ const mergeWatchOptions = (base, derived) => {
153
143
  ...derived
154
144
  };
155
145
  };
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
146
  const merge = (base, derived, derivedPath) => {
165
147
  const finalDir = dirname(derivedPath);
166
148
  const baseDir = dirname(base.configPath);
@@ -202,15 +184,6 @@ const substituteWatchExcludes = (wo, substitute) => {
202
184
  }
203
185
  return out;
204
186
  };
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
187
  const substituteConfigDir = (resolved, finalDir) => {
215
188
  const substitute = (value) => startsWithConfigDir(value) ? finalDir + value.slice(CONFIG_DIR_TEMPLATE.length) : value;
216
189
  const compilerOptions = transformCompilerOptionPaths(resolved.compilerOptions, substitute, true);
@@ -229,16 +202,42 @@ const substituteConfigDir = (resolved, finalDir) => {
229
202
  };
230
203
  /**
231
204
  * 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}).
205
+ * ({@link (ResolvedTsconfig:class).absolutize}), the per-field merge fold
206
+ * ({@link (ResolvedTsconfig:class).merge}), and the `${configDir}` final
207
+ * phase ({@link (ResolvedTsconfig:class).substituteConfigDir}).
235
208
  *
236
209
  * @public
237
210
  */
238
- const ResolvedTsconfig = {
239
- absolutize,
240
- merge,
241
- substituteConfigDir
211
+ var ResolvedTsconfig = class {
212
+ constructor() {}
213
+ /**
214
+ * Absolutize a config's path-typed options (E5) against its own
215
+ * `configDir`, using the injected `join` (`Path.Path.resolve` at the call
216
+ * site — so an already-absolute value is preserved). `${configDir}`-prefixed
217
+ * values are exempt (resolved later, in
218
+ * {@link (ResolvedTsconfig:class).substituteConfigDir}), and `paths`
219
+ * VALUES stay verbatim. Only `compilerOptions` path surfaces are touched;
220
+ * `files`/`include`/`exclude` are re-rooted at merge time instead (E4).
221
+ */
222
+ static absolutize = absolutize;
223
+ /**
224
+ * Fold one more-derived config onto the accumulated base (E4), derived
225
+ * winning. The loader (Task 8) applies this across the resolution chain,
226
+ * own config last. `derivedPath` is the derived config's absolute
227
+ * normalized path, from which the re-rooting frame and `pathsBase` are
228
+ * computed.
229
+ */
230
+ static merge = merge;
231
+ /**
232
+ * The E5 final phase: replace a leading `${configDir}` token
233
+ * (case-insensitive, leading position only) with `finalDir` — the
234
+ * top-level extending config's directory — across every eligible
235
+ * surface: compilerOptions path options, `paths` values,
236
+ * `files`/`include`/`exclude`, and `watchOptions`'
237
+ * `excludeDirectories`/`excludeFiles`. Every other field is left
238
+ * untouched.
239
+ */
240
+ static substituteConfigDir = substituteConfigDir;
242
241
  };
243
242
 
244
243
  //#endregion
package/TsEnumCodec.js CHANGED
@@ -94,31 +94,8 @@ const TABLES = {
94
94
  ["fixedchunksize", 3]
95
95
  ])
96
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
97
  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
98
  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
99
  const normalizeLibReference = (lib) => {
123
100
  const base = (lib.split("/").pop() ?? lib).toLowerCase();
124
101
  const withoutPrefix = base.startsWith("lib.") ? base.slice(4) : base;
@@ -133,24 +110,6 @@ const COMPILER_OPTION_ENUM_KEYS = [
133
110
  ["newLine", "newLine"],
134
111
  ["moduleDetection", "moduleDetection"]
135
112
  ];
136
- /**
137
- * Encodes a decoded `compilerOptions` object into the numeric-enum-shaped
138
- * {@link ProgrammaticCompilerOptions} form `ts.CompilerOptions` (and
139
- * `@typescript/vfs`'s `TsEnvironment`) expect: every R1.6 enum family becomes
140
- * its numeric value, and `lib` entries become the file-name form
141
- * (`lib.esnext.d.ts`) — see the module banner for the evidence. Every other key
142
- * (booleans, strings, arrays, unknown passthrough keys) is copied through
143
- * untouched.
144
- *
145
- * The return carries this package's single narrowing from the codec's internal
146
- * `Record<string, unknown>` (whose values include the schema's `unknown`
147
- * passthrough and its `readonly` arrays) to {@link ProgrammaticCompilerOptions}
148
- * — see that type's docs for why the package owns this one assertion instead of
149
- * leaving every consumer to cast. Runtime behavior is unchanged; only the
150
- * declared return type narrows.
151
- *
152
- * @public
153
- */
154
113
  const encodeCompilerOptions = (options) => {
155
114
  const source = options;
156
115
  const result = { ...source };
@@ -165,20 +124,6 @@ const encodeCompilerOptions = (options) => {
165
124
  if (Array.isArray(lib)) result.lib = lib.map((entry) => typeof entry === "string" ? `lib.${normalizeLibReference(entry)}.d.ts` : entry);
166
125
  return result;
167
126
  };
168
- /**
169
- * Decodes a numeric-enum-shaped `compilerOptions` object (as produced by
170
- * {@link (TsEnumCodec:variable).encodeCompilerOptions} or read off a live
171
- * `ts.CompilerOptions`) back into the string-enum shape this package's
172
- * schemas use: every R1.6 enum family becomes its canonical string, and
173
- * `lib` entries become the short form. A numeric value with no table entry —
174
- * a future TS enum member — is left as-is (passthrough, never an error) —
175
- * which is why the return type stays the wider `Record<string, unknown>`
176
- * rather than {@link (CompilerOptions:variable).Type}: an unmappable
177
- * passthrough value would violate that narrower type's contract. Every other
178
- * key is copied through untouched.
179
- *
180
- * @public
181
- */
182
127
  const decodeCompilerOptions = (numeric) => {
183
128
  const result = { ...numeric };
184
129
  for (const [key, family] of COMPILER_OPTION_ENUM_KEYS) {
@@ -199,12 +144,61 @@ const decodeCompilerOptions = (numeric) => {
199
144
  *
200
145
  * @public
201
146
  */
202
- const TsEnumCodec = {
203
- encode,
204
- decode,
205
- normalizeLibReference,
206
- encodeCompilerOptions,
207
- decodeCompilerOptions
147
+ var TsEnumCodec = class {
148
+ constructor() {}
149
+ /**
150
+ * Encodes a family's canonical (or alias) string spelling to its numeric
151
+ * form. `Option.none()` for a string with no table entry — never guessed.
152
+ */
153
+ static encode = encode;
154
+ /**
155
+ * Decodes a family's numeric value to its canonical string spelling.
156
+ * `Option.none()` for a numeric value with no table entry (a future TS
157
+ * enum member) — never guessed.
158
+ */
159
+ static decode = decode;
160
+ /**
161
+ * Normalizes any spelling of a `lib` reference — the plain short name
162
+ * (`esnext`), the on-disk file name (`lib.esnext.d.ts`), or an absolute
163
+ * path to one (`/…/typescript/lib/lib.dom.iterable.d.ts`) — to the
164
+ * canonical lowercase short name (`esnext`, `dom.iterable`). Strips a
165
+ * leading directory, the `lib.` prefix and the `.d.ts` suffix; idempotent
166
+ * on an already-short name.
167
+ */
168
+ static normalizeLibReference = normalizeLibReference;
169
+ /**
170
+ * Encodes a decoded `compilerOptions` object into the
171
+ * numeric-enum-shaped {@link ProgrammaticCompilerOptions} form
172
+ * `ts.CompilerOptions` (and `@typescript/vfs`'s `TsEnvironment`) expect:
173
+ * every R1.6 enum family becomes its numeric value, and `lib` entries
174
+ * become the file-name form (`lib.esnext.d.ts`) — see the module banner
175
+ * for the evidence. Every other key (booleans, strings, arrays, unknown
176
+ * passthrough keys) is copied through untouched.
177
+ *
178
+ * @remarks
179
+ * The return carries this package's single narrowing from the codec's
180
+ * internal `Record<string, unknown>` (whose values include the schema's
181
+ * `unknown` passthrough and its `readonly` arrays) to
182
+ * {@link ProgrammaticCompilerOptions} — see that type's docs for why the
183
+ * package owns this one assertion instead of leaving every consumer to
184
+ * cast. Runtime behavior is unchanged; only the declared return type
185
+ * narrows.
186
+ */
187
+ static encodeCompilerOptions = encodeCompilerOptions;
188
+ /**
189
+ * Decodes a numeric-enum-shaped `compilerOptions` object (as produced by
190
+ * {@link TsEnumCodec.encodeCompilerOptions} or read off a live
191
+ * `ts.CompilerOptions`) back into the string-enum shape this package's
192
+ * schemas use: every R1.6 enum family becomes its canonical string, and
193
+ * `lib` entries become the short form. A numeric value with no table
194
+ * entry — a future TS enum member — is left as-is (passthrough, never an
195
+ * error) — which is why the return type stays the wider
196
+ * `Record<string, unknown>` rather than
197
+ * {@link (CompilerOptions:namespace).Type}: an unmappable passthrough
198
+ * value would violate that narrower type's contract. Every other key is
199
+ * copied through untouched.
200
+ */
201
+ static decodeCompilerOptions = decodeCompilerOptions;
208
202
  };
209
203
 
210
204
  //#endregion
@@ -2,16 +2,6 @@ import { Effect, Path } from "effect";
2
2
  import { Walker } from "@effected/walker";
3
3
 
4
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
5
  const findNearest = (start, options) => Effect.gen(function* () {
16
6
  const path = yield* Path.Path;
17
7
  const filename = options?.filename ?? "tsconfig.json";
@@ -23,7 +13,18 @@ const findNearest = (start, options) => Effect.gen(function* () {
23
13
  *
24
14
  * @public
25
15
  */
26
- const TsconfigDiscovery = { findNearest };
16
+ var TsconfigDiscovery = class {
17
+ constructor() {}
18
+ /**
19
+ * Find the nearest `tsconfig.json` (or `options.filename`) at or above
20
+ * `start`, ascending toward the filesystem root. Absence — nowhere on the
21
+ * chain, or every candidate unreadable — is `Option.none()`, never an
22
+ * error; discovery is best-effort per `Walker.findUpward`'s absorption
23
+ * posture, and a permission-denied probe on one directory does not hide a
24
+ * config file above it.
25
+ */
26
+ static findNearest = findNearest;
27
+ };
27
28
 
28
29
  //#endregion
29
30
  export { TsconfigDiscovery };
package/TsconfigLoader.js CHANGED
@@ -58,14 +58,6 @@ const loadAbs = (abs) => Effect.gen(function* () {
58
58
  cause
59
59
  })));
60
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
61
  const load = Effect.fn("TsconfigLoader.load")(function* (configPath) {
70
62
  const path = yield* Path.Path;
71
63
  return yield* loadAbs(normalizeSlashes(path.resolve(configPath)));
@@ -119,19 +111,6 @@ const collect = (configPath, chain) => Effect.gen(function* () {
119
111
  });
120
112
  return layers;
121
113
  });
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
114
  const resolve = Effect.fn("TsconfigLoader.resolve")(function* (configPath) {
136
115
  const path = yield* Path.Path;
137
116
  const topAbs = normalizeSlashes(path.resolve(configPath));
@@ -144,14 +123,6 @@ const resolve = Effect.fn("TsconfigLoader.resolve")(function* (configPath) {
144
123
  for (const entry of layers) acc = ResolvedTsconfig.merge(acc, entry.doc, entry.path);
145
124
  return ResolvedTsconfig.substituteConfigDir(acc, path.dirname(topAbs));
146
125
  });
147
- /**
148
- * Resolve a tsconfig.json's full `extends` chain and project out the merged
149
- * `compilerOptions` — a thin projection of {@link TsconfigLoader.resolve} for
150
- * the common "just give me the effective options" query. Same pipeline, same
151
- * typed failures.
152
- *
153
- * @public
154
- */
155
126
  const compilerOptions = Effect.fn("TsconfigLoader.compilerOptions")(function* (configPath) {
156
127
  return (yield* resolve(configPath)).compilerOptions;
157
128
  });
@@ -163,10 +134,36 @@ const compilerOptions = Effect.fn("TsconfigLoader.compilerOptions")(function* (c
163
134
  *
164
135
  * @public
165
136
  */
166
- const TsconfigLoader = {
167
- load,
168
- resolve,
169
- compilerOptions
137
+ var TsconfigLoader = class {
138
+ constructor() {}
139
+ /**
140
+ * Read one config file and decode it through {@link TsconfigJsonFromString}.
141
+ * A decode failure is wrapped in a {@link TsconfigParseError} carrying the
142
+ * file's absolute path; a `PlatformError` from the read flows through
143
+ * untranslated. No `extends` resolution — {@link TsconfigLoader.resolve}
144
+ * drives that.
145
+ */
146
+ static load = load;
147
+ /**
148
+ * Resolve a tsconfig.json and its full `extends` chain into a
149
+ * {@link (ResolvedTsconfig:interface)}: load and decode each config,
150
+ * absolutize its path options (E5), resolve `extends` depth-first with
151
+ * per-branch cycle and depth guards (E1-E3, E6), fold the chain
152
+ * own-config-last (E4), then substitute a leading `${configDir}` once
153
+ * against the top config's directory (E5 final phase). `configPath` +
154
+ * `extendedPaths` come back base-most first, own config last. Every
155
+ * failure is a typed error — `TsconfigParseError` (a malformed file,
156
+ * carrying that file's path), `TsconfigExtendsError` (a broken chain), or
157
+ * a `PlatformError` from IO — never a defect.
158
+ */
159
+ static resolve = resolve;
160
+ /**
161
+ * Resolve a tsconfig.json's full `extends` chain and project out the
162
+ * merged `compilerOptions` — a thin projection of
163
+ * {@link TsconfigLoader.resolve} for the common "just give me the
164
+ * effective options" query. Same pipeline, same typed failures.
165
+ */
166
+ static compilerOptions = compilerOptions;
170
167
  };
171
168
 
172
169
  //#endregion
@@ -67,30 +67,8 @@ const runWith = (options, effect) => {
67
67
  if (Result.isSuccess(defect)) throw defect.success;
68
68
  throw Cause.squash(exit.cause);
69
69
  };
70
- /**
71
- * {@link TsconfigLoader.load}, synchronously: read and decode one config file
72
- * through the consumer-supplied operations. Throws `TsconfigParseError` or a
73
- * `PlatformError` — the async pipeline's exact typed failures.
74
- *
75
- * @public
76
- */
77
70
  const load = (configPath, options) => runWith(options, TsconfigLoader.load(configPath));
78
- /**
79
- * {@link TsconfigLoader.resolve}, synchronously: the full load -\> extends -\>
80
- * merge -\> `${configDir}` pipeline through the consumer-supplied operations.
81
- * Throws `TsconfigParseError`, `TsconfigExtendsError` or a `PlatformError` —
82
- * the async pipeline's exact typed failures.
83
- *
84
- * @public
85
- */
86
71
  const resolve = (configPath, options) => runWith(options, TsconfigLoader.resolve(configPath));
87
- /**
88
- * {@link TsconfigLoader.compilerOptions}, synchronously: resolve the full
89
- * `extends` chain and project out the merged `compilerOptions`. Throws the
90
- * same typed failures as {@link TsconfigLoaderSync.resolve}.
91
- *
92
- * @public
93
- */
94
72
  const compilerOptions = (configPath, options) => resolve(configPath, options).compilerOptions;
95
73
  /**
96
74
  * The synchronous tsconfig.json loader facade: the {@link TsconfigLoader}
@@ -121,10 +99,29 @@ const compilerOptions = (configPath, options) => resolve(configPath, options).co
121
99
  *
122
100
  * @public
123
101
  */
124
- const TsconfigLoaderSync = {
125
- load,
126
- resolve,
127
- compilerOptions
102
+ var TsconfigLoaderSync = class {
103
+ constructor() {}
104
+ /**
105
+ * {@link TsconfigLoader.load}, synchronously: read and decode one config
106
+ * file through the consumer-supplied operations. Throws
107
+ * `TsconfigParseError` or a `PlatformError` — the async pipeline's exact
108
+ * typed failures.
109
+ */
110
+ static load = load;
111
+ /**
112
+ * {@link TsconfigLoader.resolve}, synchronously: the full load -\>
113
+ * extends -\> merge -\> `${configDir}` pipeline through the
114
+ * consumer-supplied operations. Throws `TsconfigParseError`,
115
+ * `TsconfigExtendsError` or a `PlatformError` — the async pipeline's exact
116
+ * typed failures.
117
+ */
118
+ static resolve = resolve;
119
+ /**
120
+ * {@link TsconfigLoader.compilerOptions}, synchronously: resolve the full
121
+ * `extends` chain and project out the merged `compilerOptions`. Throws
122
+ * the same typed failures as {@link TsconfigLoaderSync.resolve}.
123
+ */
124
+ static compilerOptions = compilerOptions;
128
125
  };
129
126
 
130
127
  //#endregion
package/index.d.ts CHANGED
@@ -550,17 +550,43 @@ interface ResolvedTsconfig {
550
550
  }
551
551
  /**
552
552
  * The pure extends-merge engine: parse-time path absolutization
553
- * ({@link (ResolvedTsconfig:variable).absolutize}), the per-field merge fold
554
- * ({@link (ResolvedTsconfig:variable).merge}), and the `${configDir}` final
555
- * phase ({@link (ResolvedTsconfig:variable).substituteConfigDir}).
553
+ * ({@link (ResolvedTsconfig:class).absolutize}), the per-field merge fold
554
+ * ({@link (ResolvedTsconfig:class).merge}), and the `${configDir}` final
555
+ * phase ({@link (ResolvedTsconfig:class).substituteConfigDir}).
556
556
  *
557
557
  * @public
558
558
  */
559
- declare const ResolvedTsconfig: {
560
- readonly absolutize: (doc: TsconfigJson.Type, configDir: string, join: (a: string, b: string) => string) => TsconfigJson.Type;
561
- readonly merge: (base: ResolvedTsconfig, derived: TsconfigJson.Type, derivedPath: string) => ResolvedTsconfig;
562
- readonly substituteConfigDir: (resolved: ResolvedTsconfig, finalDir: string) => ResolvedTsconfig;
563
- };
559
+ declare class ResolvedTsconfig {
560
+ private constructor();
561
+ /**
562
+ * Absolutize a config's path-typed options (E5) against its own
563
+ * `configDir`, using the injected `join` (`Path.Path.resolve` at the call
564
+ * site — so an already-absolute value is preserved). `${configDir}`-prefixed
565
+ * values are exempt (resolved later, in
566
+ * {@link (ResolvedTsconfig:class).substituteConfigDir}), and `paths`
567
+ * VALUES stay verbatim. Only `compilerOptions` path surfaces are touched;
568
+ * `files`/`include`/`exclude` are re-rooted at merge time instead (E4).
569
+ */
570
+ static readonly absolutize: (doc: TsconfigJson.Type, configDir: string, join: (a: string, b: string) => string) => TsconfigJson.Type;
571
+ /**
572
+ * Fold one more-derived config onto the accumulated base (E4), derived
573
+ * winning. The loader (Task 8) applies this across the resolution chain,
574
+ * own config last. `derivedPath` is the derived config's absolute
575
+ * normalized path, from which the re-rooting frame and `pathsBase` are
576
+ * computed.
577
+ */
578
+ static readonly merge: (base: ResolvedTsconfig, derived: TsconfigJson.Type, derivedPath: string) => ResolvedTsconfig;
579
+ /**
580
+ * The E5 final phase: replace a leading `${configDir}` token
581
+ * (case-insensitive, leading position only) with `finalDir` — the
582
+ * top-level extending config's directory — across every eligible
583
+ * surface: compilerOptions path options, `paths` values,
584
+ * `files`/`include`/`exclude`, and `watchOptions`'
585
+ * `excludeDirectories`/`excludeFiles`. Every other field is left
586
+ * untouched.
587
+ */
588
+ static readonly substituteConfigDir: (resolved: ResolvedTsconfig, finalDir: string) => ResolvedTsconfig;
589
+ }
564
590
  //#endregion
565
591
  //#region src/PortableTsconfig.d.ts
566
592
  /**
@@ -578,15 +604,27 @@ interface PortableTsconfig {
578
604
  readonly compilerOptions: Record<string, unknown>;
579
605
  }
580
606
  /**
581
- * The portable-tsconfig filter: {@link (PortableTsconfig:variable).make}
607
+ * The portable-tsconfig filter: {@link (PortableTsconfig:class).make}
582
608
  * narrows a resolved or bare compiler-options object to the allow-listed,
583
609
  * machine-independent subset described on {@link (PortableTsconfig:interface)}.
584
610
  *
585
611
  * @public
586
612
  */
587
- declare const PortableTsconfig: {
588
- readonly make: (input: ResolvedTsconfig | CompilerOptions.Type) => PortableTsconfig;
589
- };
613
+ declare class PortableTsconfig {
614
+ private constructor();
615
+ /**
616
+ * Project a {@link (ResolvedTsconfig:interface)} or a bare
617
+ * `CompilerOptions.Type` down to a {@link (PortableTsconfig:interface)}:
618
+ * copy only the allow-listed type-semantics options, force
619
+ * `composite: false` and `noEmit: true` regardless of what the source
620
+ * declared, and stamp `$schema`. Every other key — including every
621
+ * unknown passthrough key the source preserved for forward tolerance —
622
+ * is dropped; this is an allow-list, not a deny-list, so an option this
623
+ * package does not yet classify never leaks onto the portable shape by
624
+ * accident.
625
+ */
626
+ static readonly make: (input: ResolvedTsconfig | CompilerOptions.Type) => PortableTsconfig;
627
+ }
590
628
  //#endregion
591
629
  //#region src/TsconfigDiscovery.d.ts
592
630
  /**
@@ -605,9 +643,18 @@ interface FindNearestOptions {
605
643
  *
606
644
  * @public
607
645
  */
608
- declare const TsconfigDiscovery: {
609
- readonly findNearest: (start: string, options?: FindNearestOptions) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem | Path.Path>;
610
- };
646
+ declare class TsconfigDiscovery {
647
+ private constructor();
648
+ /**
649
+ * Find the nearest `tsconfig.json` (or `options.filename`) at or above
650
+ * `start`, ascending toward the filesystem root. Absence — nowhere on the
651
+ * chain, or every candidate unreadable — is `Option.none()`, never an
652
+ * error; discovery is best-effort per `Walker.findUpward`'s absorption
653
+ * posture, and a permission-denied probe on one directory does not hide a
654
+ * config file above it.
655
+ */
656
+ static readonly findNearest: (start: string, options?: FindNearestOptions) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem | Path.Path>;
657
+ }
611
658
  //#endregion
612
659
  //#region src/TsconfigLoader.d.ts
613
660
  declare const TsconfigExtendsError_base: Schema.Class<TsconfigExtendsError, Schema.TaggedStruct<"TsconfigExtendsError", {
@@ -642,8 +689,16 @@ declare class TsconfigExtendsError extends TsconfigExtendsError_base {
642
689
  *
643
690
  * @public
644
691
  */
645
- declare const TsconfigLoader: {
646
- readonly load: (configPath: string) => Effect.Effect<{
692
+ declare class TsconfigLoader {
693
+ private constructor();
694
+ /**
695
+ * Read one config file and decode it through {@link TsconfigJsonFromString}.
696
+ * A decode failure is wrapped in a {@link TsconfigParseError} carrying the
697
+ * file's absolute path; a `PlatformError` from the read flows through
698
+ * untranslated. No `extends` resolution — {@link TsconfigLoader.resolve}
699
+ * drives that.
700
+ */
701
+ static readonly load: (configPath: string) => Effect.Effect<{
647
702
  readonly [x: string]: unknown;
648
703
  readonly compilerOptions?: {
649
704
  readonly [x: string]: unknown;
@@ -790,8 +845,26 @@ declare const TsconfigLoader: {
790
845
  readonly compileOnSave?: boolean | undefined;
791
846
  readonly $schema?: string | undefined;
792
847
  }, PlatformError.PlatformError | TsconfigParseError, FileSystem.FileSystem | Path.Path>;
793
- readonly resolve: (configPath: string) => Effect.Effect<ResolvedTsconfig, PlatformError.PlatformError | TsconfigExtendsError | TsconfigParseError, FileSystem.FileSystem | Path.Path>;
794
- readonly compilerOptions: (configPath: string) => Effect.Effect<{
848
+ /**
849
+ * Resolve a tsconfig.json and its full `extends` chain into a
850
+ * {@link (ResolvedTsconfig:interface)}: load and decode each config,
851
+ * absolutize its path options (E5), resolve `extends` depth-first with
852
+ * per-branch cycle and depth guards (E1-E3, E6), fold the chain
853
+ * own-config-last (E4), then substitute a leading `${configDir}` once
854
+ * against the top config's directory (E5 final phase). `configPath` +
855
+ * `extendedPaths` come back base-most first, own config last. Every
856
+ * failure is a typed error — `TsconfigParseError` (a malformed file,
857
+ * carrying that file's path), `TsconfigExtendsError` (a broken chain), or
858
+ * a `PlatformError` from IO — never a defect.
859
+ */
860
+ static readonly resolve: (configPath: string) => Effect.Effect<ResolvedTsconfig, PlatformError.PlatformError | TsconfigExtendsError | TsconfigParseError, FileSystem.FileSystem | Path.Path>;
861
+ /**
862
+ * Resolve a tsconfig.json's full `extends` chain and project out the
863
+ * merged `compilerOptions` — a thin projection of
864
+ * {@link TsconfigLoader.resolve} for the common "just give me the
865
+ * effective options" query. Same pipeline, same typed failures.
866
+ */
867
+ static readonly compilerOptions: (configPath: string) => Effect.Effect<{
795
868
  readonly [x: string]: unknown;
796
869
  readonly target?: "es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "es2023" | "es2024" | "es2025" | "es5" | "es6" | "esnext" | undefined;
797
870
  readonly module?: "amd" | "commonjs" | "es2015" | "es2020" | "es2022" | "es6" | "esnext" | "node16" | "node18" | "node20" | "nodenext" | "none" | "preserve" | "system" | "umd" | undefined;
@@ -909,7 +982,7 @@ declare const TsconfigLoader: {
909
982
  }[] | undefined;
910
983
  readonly maxNodeModuleJsDepth?: number | undefined;
911
984
  }, PlatformError.PlatformError | TsconfigExtendsError | TsconfigParseError, FileSystem.FileSystem | Path.Path>;
912
- };
985
+ }
913
986
  //#endregion
914
987
  //#region src/TsconfigLoaderSync.d.ts
915
988
  /**
@@ -1012,11 +1085,30 @@ interface TsconfigLoaderSyncOptions {
1012
1085
  *
1013
1086
  * @public
1014
1087
  */
1015
- declare const TsconfigLoaderSync: {
1016
- readonly load: (configPath: string, options: TsconfigLoaderSyncOptions) => TsconfigJson.Type;
1017
- readonly resolve: (configPath: string, options: TsconfigLoaderSyncOptions) => ResolvedTsconfig;
1018
- readonly compilerOptions: (configPath: string, options: TsconfigLoaderSyncOptions) => CompilerOptions.Type;
1019
- };
1088
+ declare class TsconfigLoaderSync {
1089
+ private constructor();
1090
+ /**
1091
+ * {@link TsconfigLoader.load}, synchronously: read and decode one config
1092
+ * file through the consumer-supplied operations. Throws
1093
+ * `TsconfigParseError` or a `PlatformError` — the async pipeline's exact
1094
+ * typed failures.
1095
+ */
1096
+ static readonly load: (configPath: string, options: TsconfigLoaderSyncOptions) => TsconfigJson.Type;
1097
+ /**
1098
+ * {@link TsconfigLoader.resolve}, synchronously: the full load -\>
1099
+ * extends -\> merge -\> `${configDir}` pipeline through the
1100
+ * consumer-supplied operations. Throws `TsconfigParseError`,
1101
+ * `TsconfigExtendsError` or a `PlatformError` — the async pipeline's exact
1102
+ * typed failures.
1103
+ */
1104
+ static readonly resolve: (configPath: string, options: TsconfigLoaderSyncOptions) => ResolvedTsconfig;
1105
+ /**
1106
+ * {@link TsconfigLoader.compilerOptions}, synchronously: resolve the full
1107
+ * `extends` chain and project out the merged `compilerOptions`. Throws
1108
+ * the same typed failures as {@link TsconfigLoaderSync.resolve}.
1109
+ */
1110
+ static readonly compilerOptions: (configPath: string, options: TsconfigLoaderSyncOptions) => CompilerOptions.Type;
1111
+ }
1020
1112
  //#endregion
1021
1113
  //#region src/TsEnumCodec.d.ts
1022
1114
  /**
@@ -1068,7 +1160,7 @@ type ProgrammaticCompilerOptionsValue = string | number | boolean | (string | nu
1068
1160
  readonly circular?: boolean;
1069
1161
  }[] | null | undefined;
1070
1162
  /**
1071
- * The shape {@link (TsEnumCodec:variable).encodeCompilerOptions} returns: the
1163
+ * The shape {@link TsEnumCodec.encodeCompilerOptions} returns: the
1072
1164
  * numeric-enum-encoded `compilerOptions` a virtual-TS environment and the
1073
1165
  * TypeScript compiler API consume programmatically.
1074
1166
  *
@@ -1115,13 +1207,62 @@ interface ProgrammaticCompilerOptions {
1115
1207
  *
1116
1208
  * @public
1117
1209
  */
1118
- declare const TsEnumCodec: {
1119
- readonly encode: (family: EnumFamily, value: string) => Option.Option<number>;
1120
- readonly decode: (family: EnumFamily, value: number) => Option.Option<string>;
1121
- readonly normalizeLibReference: (lib: string) => string;
1122
- readonly encodeCompilerOptions: (options: CompilerOptions.Type) => ProgrammaticCompilerOptions;
1123
- readonly decodeCompilerOptions: (numeric: Readonly<Record<string, unknown>>) => Record<string, unknown>;
1124
- };
1210
+ declare class TsEnumCodec {
1211
+ private constructor();
1212
+ /**
1213
+ * Encodes a family's canonical (or alias) string spelling to its numeric
1214
+ * form. `Option.none()` for a string with no table entry — never guessed.
1215
+ */
1216
+ static readonly encode: (family: EnumFamily, value: string) => Option.Option<number>;
1217
+ /**
1218
+ * Decodes a family's numeric value to its canonical string spelling.
1219
+ * `Option.none()` for a numeric value with no table entry (a future TS
1220
+ * enum member) — never guessed.
1221
+ */
1222
+ static readonly decode: (family: EnumFamily, value: number) => Option.Option<string>;
1223
+ /**
1224
+ * Normalizes any spelling of a `lib` reference — the plain short name
1225
+ * (`esnext`), the on-disk file name (`lib.esnext.d.ts`), or an absolute
1226
+ * path to one (`/…/typescript/lib/lib.dom.iterable.d.ts`) — to the
1227
+ * canonical lowercase short name (`esnext`, `dom.iterable`). Strips a
1228
+ * leading directory, the `lib.` prefix and the `.d.ts` suffix; idempotent
1229
+ * on an already-short name.
1230
+ */
1231
+ static readonly normalizeLibReference: (lib: string) => string;
1232
+ /**
1233
+ * Encodes a decoded `compilerOptions` object into the
1234
+ * numeric-enum-shaped {@link ProgrammaticCompilerOptions} form
1235
+ * `ts.CompilerOptions` (and `@typescript/vfs`'s `TsEnvironment`) expect:
1236
+ * every R1.6 enum family becomes its numeric value, and `lib` entries
1237
+ * become the file-name form (`lib.esnext.d.ts`) — see the module banner
1238
+ * for the evidence. Every other key (booleans, strings, arrays, unknown
1239
+ * passthrough keys) is copied through untouched.
1240
+ *
1241
+ * @remarks
1242
+ * The return carries this package's single narrowing from the codec's
1243
+ * internal `Record<string, unknown>` (whose values include the schema's
1244
+ * `unknown` passthrough and its `readonly` arrays) to
1245
+ * {@link ProgrammaticCompilerOptions} — see that type's docs for why the
1246
+ * package owns this one assertion instead of leaving every consumer to
1247
+ * cast. Runtime behavior is unchanged; only the declared return type
1248
+ * narrows.
1249
+ */
1250
+ static readonly encodeCompilerOptions: (options: CompilerOptions.Type) => ProgrammaticCompilerOptions;
1251
+ /**
1252
+ * Decodes a numeric-enum-shaped `compilerOptions` object (as produced by
1253
+ * {@link TsEnumCodec.encodeCompilerOptions} or read off a live
1254
+ * `ts.CompilerOptions`) back into the string-enum shape this package's
1255
+ * schemas use: every R1.6 enum family becomes its canonical string, and
1256
+ * `lib` entries become the short form. A numeric value with no table
1257
+ * entry — a future TS enum member — is left as-is (passthrough, never an
1258
+ * error) — which is why the return type stays the wider
1259
+ * `Record<string, unknown>` rather than
1260
+ * {@link (CompilerOptions:namespace).Type}: an unmappable passthrough
1261
+ * value would violate that narrower type's contract. Every other key is
1262
+ * copied through untouched.
1263
+ */
1264
+ static readonly decodeCompilerOptions: (numeric: Readonly<Record<string, unknown>>) => Record<string, unknown>;
1265
+ }
1125
1266
  //#endregion
1126
1267
  export { CompilerOptions, type EnumFamily, FallbackPolling, type FindNearestOptions, Jsx, JsxConfig, Lib, Module, ModuleDetection, ModuleResolution, NewLine, PortableTsconfig, type ProgrammaticCompilerOptions, type ProgrammaticCompilerOptionsValue, Reference, ResolvedTsconfig, type SyncFileSystem, type SyncPath, Target, TsEnumCodec, TsconfigDiscovery, TsconfigExtendsError, TsconfigJson, TsconfigJsonFromString, TsconfigLoader, TsconfigLoaderSync, type TsconfigLoaderSyncOptions, TsconfigParseError, TypeAcquisition, WatchDirectory, WatchFile, WatchOptions };
1127
1268
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/tsconfig-json",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "Composable tsconfig.json handling for Effect: schemas, extends-chain resolution, and config discovery.",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@effected/jsonc": "~0.5.1",
43
- "@effected/walker": "~0.3.2",
43
+ "@effected/walker": "~0.3.3",
44
44
  "effect": "4.0.0-beta.101"
45
45
  },
46
46
  "engines": {