@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/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { CompilerOptions, Jsx, Lib, Module, ModuleDetection, ModuleResolution, NewLine, Target } from "./CompilerOptions.js";
2
+ import { PortableTsconfig } from "./PortableTsconfig.js";
3
+ import { ResolvedTsconfig } from "./ResolvedTsconfig.js";
4
+ import { TsconfigDiscovery } from "./TsconfigDiscovery.js";
5
+ import { FallbackPolling, Reference, TsconfigJson, TsconfigJsonFromString, TsconfigParseError, TypeAcquisition, WatchDirectory, WatchFile, WatchOptions } from "./TsconfigJson.js";
6
+ import { TsconfigExtendsError, TsconfigLoader } from "./TsconfigLoader.js";
7
+ import { TsEnumCodec } from "./TsEnumCodec.js";
8
+
9
+ export { CompilerOptions, FallbackPolling, Jsx, Lib, Module, ModuleDetection, ModuleResolution, NewLine, PortableTsconfig, Reference, ResolvedTsconfig, Target, TsEnumCodec, TsconfigDiscovery, TsconfigExtendsError, TsconfigJson, TsconfigJsonFromString, TsconfigLoader, TsconfigParseError, TypeAcquisition, WatchDirectory, WatchFile, WatchOptions };
@@ -0,0 +1,234 @@
1
+ import { Effect, FileSystem, Option, Path } from "effect";
2
+ import { Walker } from "@effected/walker";
3
+ import { Jsonc } from "@effected/jsonc";
4
+
5
+ //#region src/internal/extendsTarget.ts
6
+ /**
7
+ * The `extends` target-resolution engine (E1 relative/rooted, E2 bare-specifier
8
+ * node_modules lookup). Every input reaching {@link resolveExports} is an
9
+ * untrusted `package.json` `exports` map, so every recursive surface carries a
10
+ * depth guard, every untrusted map read goes through `Object.hasOwn`, dunder
11
+ * keys are skipped, and substituted maps are built with `Object.create(null)` —
12
+ * a JSON-parsed `{"__proto__": …}` key must never read or assign a prototype.
13
+ *
14
+ * A hostile `package.json` must never defect or fail the whole resolution: a
15
+ * malformed manifest is absorbed to "no resolution for that candidate", and
16
+ * only a genuine `PlatformError` from the underlying IO flows through the typed
17
+ * error channel.
18
+ */
19
+ /** Conditions honored in an `exports` condition object, plus the always-eligible `default`. */
20
+ const CONDITIONS = /* @__PURE__ */ new Set([
21
+ "require",
22
+ "types",
23
+ "node"
24
+ ]);
25
+ /** Keys that are never data on a plain object. Skipped on every untrusted read. */
26
+ const DUNDER_KEYS = /* @__PURE__ */ new Set([
27
+ "__proto__",
28
+ "constructor",
29
+ "prototype"
30
+ ]);
31
+ /** Depth guard for the recursive exports walk (condition nesting, fallback arrays, wildcard substitution). */
32
+ const MAX_EXPORTS_DEPTH = 32;
33
+ /** Split a bare specifier into its package name and its subpath ("" when the whole spec is the package). */
34
+ const parseSpecifier = (spec) => {
35
+ if (spec.startsWith("@")) {
36
+ const parts = spec.split("/");
37
+ return {
38
+ pkg: parts.slice(0, 2).join("/"),
39
+ subpath: parts.slice(2).join("/")
40
+ };
41
+ }
42
+ const slash = spec.indexOf("/");
43
+ return slash === -1 ? {
44
+ pkg: spec,
45
+ subpath: ""
46
+ } : {
47
+ pkg: spec.slice(0, slash),
48
+ subpath: spec.slice(slash + 1)
49
+ };
50
+ };
51
+ /** Match a single-`*` subpath pattern against a subpath, returning the captured segment or `null`. */
52
+ const matchWildcard = (pattern, subpath) => {
53
+ const star = pattern.indexOf("*");
54
+ if (star === -1 || pattern.indexOf("*", star + 1) !== -1) return null;
55
+ const prefix = pattern.slice(0, star);
56
+ const suffix = pattern.slice(star + 1);
57
+ if (subpath.length < prefix.length + suffix.length) return null;
58
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) return null;
59
+ return subpath.slice(prefix.length, subpath.length - suffix.length);
60
+ };
61
+ /** Substitute a captured wildcard segment into an export value, hardened against dunder keys and unbounded nesting. */
62
+ const substituteWildcard = (value, captured, depth) => {
63
+ if (depth > MAX_EXPORTS_DEPTH) return null;
64
+ if (typeof value === "string") return value.replace(/\*/g, captured);
65
+ if (Array.isArray(value)) return value.map((entry) => substituteWildcard(entry, captured, depth + 1));
66
+ if (typeof value === "object" && value !== null) {
67
+ const result = Object.create(null);
68
+ for (const key of Object.keys(value)) {
69
+ if (DUNDER_KEYS.has(key) || !Object.hasOwn(value, key)) continue;
70
+ result[key] = substituteWildcard(value[key], captured, depth + 1);
71
+ }
72
+ return result;
73
+ }
74
+ return value;
75
+ };
76
+ /** Find the export value for a subpath: exact key first, then a single-`*` pattern (substituted), else `undefined`. */
77
+ const matchExportKey = (exports, subpath) => {
78
+ if (typeof exports === "string") return subpath === "." ? exports : void 0;
79
+ if (Array.isArray(exports)) return subpath === "." ? exports : void 0;
80
+ if (typeof exports !== "object" || exports === null) return void 0;
81
+ const obj = exports;
82
+ const keys = Object.keys(obj);
83
+ if (!(keys.length > 0 && keys.every((key) => key.startsWith(".")))) return subpath === "." ? obj : void 0;
84
+ if (!DUNDER_KEYS.has(subpath) && Object.hasOwn(obj, subpath)) return obj[subpath];
85
+ let best;
86
+ for (const pattern of keys) {
87
+ if (DUNDER_KEYS.has(pattern) || !pattern.includes("*") || !Object.hasOwn(obj, pattern)) continue;
88
+ const captured = matchWildcard(pattern, subpath);
89
+ if (captured === null) continue;
90
+ const prefixLength = pattern.indexOf("*");
91
+ if (best === void 0 || prefixLength > best.prefixLength) best = {
92
+ pattern,
93
+ captured,
94
+ prefixLength
95
+ };
96
+ }
97
+ return best === void 0 ? void 0 : substituteWildcard(obj[best.pattern], best.captured, 0);
98
+ };
99
+ /**
100
+ * Resolve an export value to a `.json` string target: strings gate on the
101
+ * `.json` suffix, fallback arrays yield the first resolvable entry, and
102
+ * condition objects are matched in map insertion order (Node semantics) against
103
+ * the honored conditions plus `default`, under the depth guard.
104
+ */
105
+ const resolveConditionValue = (value, depth) => {
106
+ if (depth > MAX_EXPORTS_DEPTH) return void 0;
107
+ if (typeof value === "string") return value.endsWith(".json") ? value : void 0;
108
+ if (Array.isArray(value)) {
109
+ for (const entry of value) {
110
+ const resolved = resolveConditionValue(entry, depth + 1);
111
+ if (resolved !== void 0) return resolved;
112
+ }
113
+ return;
114
+ }
115
+ if (typeof value === "object" && value !== null) {
116
+ const obj = value;
117
+ for (const key of Object.keys(obj)) {
118
+ if (DUNDER_KEYS.has(key) || !Object.hasOwn(obj, key)) continue;
119
+ if (CONDITIONS.has(key) || key === "default") {
120
+ const resolved = resolveConditionValue(obj[key], depth + 1);
121
+ if (resolved !== void 0) return resolved;
122
+ }
123
+ }
124
+ }
125
+ };
126
+ /**
127
+ * Resolve a `package.json` `exports` map for a subpath key (`"."` for the
128
+ * package root, `"./sub"` for a subpath) to its `.json` target string. Pure and
129
+ * hardened; exported for direct unit testing. The returned target is verbatim
130
+ * from the manifest (still relative to the package directory) — the caller joins
131
+ * and probes it.
132
+ */
133
+ const resolveExports = (exports, subpath) => {
134
+ const matched = matchExportKey(exports, subpath);
135
+ if (matched === void 0) return Option.none();
136
+ const resolved = resolveConditionValue(matched, 0);
137
+ return resolved === void 0 ? Option.none() : Option.some(resolved);
138
+ };
139
+ /**
140
+ * Read and parse a `package.json`, coercing every failure that is not a
141
+ * genuine IO error to an EMPTY manifest — tsc's `readJson`
142
+ * (typescript.js:21176, `readJsonOrUndefined(path, host) || {}`) does exactly
143
+ * this, so a missing, unparseable or non-object manifest falls through to the
144
+ * manifest-less lookups (no exports, no tsconfig field) rather than deciding
145
+ * the candidate. A `PlatformError` from `exists` flows through.
146
+ */
147
+ const readManifest = (fs, manifestPath) => Effect.gen(function* () {
148
+ if (!(yield* fs.exists(manifestPath))) return {};
149
+ const text = yield* fs.readFileString(manifestPath);
150
+ const parsed = yield* Effect.option(Jsonc.parse(text));
151
+ if (Option.isNone(parsed)) return {};
152
+ const value = parsed.value;
153
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
154
+ });
155
+ /** Read an own property of an untrusted record, guarding dunder keys and inherited members. */
156
+ const ownProp = (record, key) => !DUNDER_KEYS.has(key) && Object.hasOwn(record, key) ? record[key] : void 0;
157
+ /**
158
+ * Resolve E1 relative/rooted targets: the exact file wins verbatim (even
159
+ * extensionless); otherwise, if the target does not already end in `.json`, the
160
+ * `.json`-appended path is tried. There is no directory fallback.
161
+ */
162
+ const resolveRelative = (fs, path, fromDir, spec) => Effect.gen(function* () {
163
+ const abs = path.resolve(fromDir, spec);
164
+ if (yield* fs.exists(abs)) return Option.some(abs);
165
+ if (!abs.endsWith(".json")) {
166
+ const withJson = `${abs}.json`;
167
+ if (yield* fs.exists(withJson)) return Option.some(withJson);
168
+ }
169
+ return Option.none();
170
+ });
171
+ /**
172
+ * Probe one candidate package under an ancestor's `node_modules`. `none` means
173
+ * "did not resolve here — keep walking": tsc's ancestor walk
174
+ * (forEachAncestorDirectoryStoppingAtGlobalCache, typescript.js:46466) only
175
+ * stops on a defined result, so a present-but-unresolved candidate does NOT
176
+ * shadow a farther ancestor's copy. There is no `package.json` presence gate —
177
+ * tsc probes `<pkg>/tsconfig.json` even when the manifest is absent
178
+ * (loadNodeModuleFromDirectoryWorker, typescript.js:45943).
179
+ */
180
+ const tryCandidate = (fs, path, dir, pkg, subpath) => Effect.gen(function* () {
181
+ const pkgDir = path.join(dir, "node_modules", pkg);
182
+ const record = yield* readManifest(fs, path.join(pkgDir, "package.json"));
183
+ const exports = ownProp(record, "exports");
184
+ if (exports !== void 0) {
185
+ const target = resolveExports(exports, subpath === "" ? "." : `./${subpath}`);
186
+ if (Option.isNone(target)) return Option.none();
187
+ const abs = path.resolve(pkgDir, target.value);
188
+ return (yield* fs.exists(abs)) ? Option.some(abs) : Option.none();
189
+ }
190
+ if (subpath !== "") {
191
+ const exact = path.resolve(pkgDir, subpath);
192
+ if (yield* fs.exists(exact)) return Option.some(exact);
193
+ if (!subpath.endsWith(".json")) {
194
+ const withJson = `${exact}.json`;
195
+ if (yield* fs.exists(withJson)) return Option.some(withJson);
196
+ }
197
+ return Option.none();
198
+ }
199
+ const tsField = ownProp(record, "tsconfig");
200
+ if (typeof tsField === "string" && tsField !== "") {
201
+ const abs = path.resolve(pkgDir, tsField);
202
+ if (yield* fs.exists(abs)) return Option.some(abs);
203
+ }
204
+ const fallback = path.join(pkgDir, "tsconfig.json");
205
+ return (yield* fs.exists(fallback)) ? Option.some(fallback) : Option.none();
206
+ });
207
+ /**
208
+ * Resolve an `extends` target to an absolute config path. Relative and rooted
209
+ * specifiers (E1) resolve against the extending config's directory; bare
210
+ * specifiers (E2) walk up the ancestor `node_modules` chain, skipping ancestors
211
+ * named `node_modules`, and resolve against the first package found.
212
+ *
213
+ * Absence is `Option.none()`; a malformed manifest is coerced to an empty one
214
+ * (tsc parity — the candidate's manifest-less lookups still run); a
215
+ * `PlatformError` from the underlying IO flows through.
216
+ */
217
+ const resolveExtendsTarget = (spec, fromConfigPath) => Effect.gen(function* () {
218
+ const fs = yield* FileSystem.FileSystem;
219
+ const path = yield* Path.Path;
220
+ const fromDir = path.dirname(fromConfigPath);
221
+ const normalized = spec.replace(/\\/g, "/");
222
+ if (normalized.startsWith("./") || normalized.startsWith("../") || path.isAbsolute(normalized)) return yield* resolveRelative(fs, path, fromDir, normalized);
223
+ const { pkg, subpath } = parseSpecifier(normalized);
224
+ const ancestors = yield* Walker.ascend(fromDir);
225
+ for (const dir of ancestors) {
226
+ if (path.basename(dir) === "node_modules") continue;
227
+ const candidate = yield* tryCandidate(fs, path, dir, pkg, subpath);
228
+ if (Option.isSome(candidate)) return candidate;
229
+ }
230
+ return Option.none();
231
+ });
232
+
233
+ //#endregion
234
+ export { resolveExports, resolveExtendsTarget };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@effected/tsconfig-json",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Composable tsconfig.json handling for Effect: schemas, extends-chain resolution, and config discovery.",
6
+ "keywords": [
7
+ "tsconfig",
8
+ "typescript",
9
+ "config",
10
+ "extends",
11
+ "resolution",
12
+ "effect",
13
+ "schema",
14
+ "effected"
15
+ ],
16
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/tsconfig-json#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/spencerbeggs/effected/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/spencerbeggs/effected.git",
23
+ "directory": "packages/tsconfig-json"
24
+ },
25
+ "license": "MIT",
26
+ "author": {
27
+ "name": "C. Spencer Beggs",
28
+ "email": "spencer@beggs.codes",
29
+ "url": "https://spencerbeg.gs"
30
+ },
31
+ "sideEffects": false,
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./index.d.ts",
36
+ "import": "./index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "peerDependencies": {
41
+ "@effected/jsonc": "0.1.0",
42
+ "@effected/walker": "0.1.0",
43
+ "effect": "4.0.0-beta.98"
44
+ },
45
+ "engines": {
46
+ "node": ">=24.11.0"
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }