@tsdoctor/registry 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,11 @@
1
+ import { RegistryEvent, RegistryObserver } from "./RegistryEvent.js";
2
+ import { FetchError, PackageFetcher, PackageManifest, PackageNotFoundError, VersionNotFoundError } from "./PackageFetcher.js";
3
+ import { PackageSpec } from "./PackageSpec.js";
4
+ import { TsEnvironment, TsEnvironmentError } from "./TsEnvironment.js";
5
+ import { TypeCache, TypeCacheError, TypeCacheMetadata } from "./TypeCache.js";
6
+ import { ResolvedModule, TypeResolver } from "./TypeResolver.js";
7
+ import { mergeVfs, prefixVfs } from "./Vfs.js";
8
+ import { BatchLoadError, TypeRegistry } from "./TypeRegistry.js";
9
+ import { VirtualPackage } from "./VirtualPackage.js";
10
+
11
+ export { BatchLoadError, FetchError, PackageFetcher, PackageManifest, PackageNotFoundError, PackageSpec, RegistryEvent, RegistryObserver, ResolvedModule, TsEnvironment, TsEnvironmentError, TypeCache, TypeCacheError, TypeCacheMetadata, TypeRegistry, TypeResolver, VersionNotFoundError, VirtualPackage, mergeVfs, prefixVfs };
@@ -0,0 +1,62 @@
1
+ import { isSafeRelativePath } from "./resolution.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/internal/jsdelivr.ts
5
+ /** Base URL for the jsDelivr data/metadata API. */
6
+ const DATA_API = "https://data.jsdelivr.com/v1";
7
+ /** Base URL for the jsDelivr file-serving CDN. */
8
+ const CDN = "https://cdn.jsdelivr.net";
9
+ /**
10
+ * Matches TypeScript declaration file names: the standard `.d.ts`, `.d.mts`
11
+ * and `.d.cts` suffixes, plus the arbitrary-extension form `.d.<ext>.ts`
12
+ * (TS 5 `allowArbitraryExtensions`, e.g. `styles.d.css.ts`). The middle
13
+ * segment is only valid before a plain `.ts` — `.d.<ext>.mts`/`.cts` are not
14
+ * declaration forms — and cannot span path separators, so a directory named
15
+ * `assets.d.css` does not make `assets.d.css/index.ts` a declaration file.
16
+ */
17
+ const TYPE_FILE_PATTERN = /(\.d\.[cm]?ts|\.d\.[^./\\]+\.ts)$/i;
18
+ /** The package metadata endpoint: versions and dist-tags. */
19
+ const versionsUrl = (name) => `${DATA_API}/package/npm/${name}`;
20
+ /** The flat file-tree endpoint for a pinned package version. */
21
+ const fileTreeUrl = (pkg) => `${DATA_API}/package/npm/${pkg.name}@${pkg.version}/flat`;
22
+ /**
23
+ * The CDN URL for one file of a pinned package version. Each path segment is
24
+ * percent-encoded so a file name containing `?`, `#` or `%` cannot rewrite
25
+ * the URL's query, fragment or escaping, and a path that escapes the pinned
26
+ * package (`..` segments, absolute forms) throws before a URL exists —
27
+ * `encodeURIComponent` leaves dots active, so encoding alone cannot stop
28
+ * URL normalization from resolving a `..` outside the package. Service
29
+ * callers wrap the throw into a typed failure.
30
+ */
31
+ const fileUrl = (pkg, filePath) => {
32
+ const relative = filePath.replace(/^\/+/, "");
33
+ if (!isSafeRelativePath(relative)) throw new Error(`file path escapes the pinned package: ${filePath}`);
34
+ const encoded = relative.split("/").filter((segment) => segment !== "" && segment !== ".").map(encodeURIComponent).join("/");
35
+ return `${CDN}/npm/${pkg.name}@${pkg.version}/${encoded}`;
36
+ };
37
+ /** The CDN URL for a pinned package version's `package.json`. */
38
+ const packageJsonUrl = (pkg) => fileUrl(pkg, "package.json");
39
+ /**
40
+ * The `/package/npm/:name` response: published versions plus dist-tags.
41
+ * Lenient — only the two fields the resolver reads.
42
+ */
43
+ const VersionsResponse = Schema.Struct({
44
+ versions: Schema.Array(Schema.String),
45
+ tags: Schema.Record(Schema.String, Schema.String)
46
+ });
47
+ /**
48
+ * The `/package/npm/:pkg@:version/flat` response. `default` is metadata only
49
+ * (`null` for packages that declare none, e.g. `ink`); the loader consumes
50
+ * `files`, never `default`. `size` (bytes) is used to pre-check the
51
+ * type-file download budget before any request is made.
52
+ */
53
+ const FileTreeResponse = Schema.Struct({
54
+ default: Schema.NullOr(Schema.String),
55
+ files: Schema.Array(Schema.Struct({
56
+ name: Schema.String,
57
+ size: Schema.optionalKey(Schema.Number)
58
+ }))
59
+ });
60
+
61
+ //#endregion
62
+ export { CDN, DATA_API, FileTreeResponse, TYPE_FILE_PATTERN, VersionsResponse, fileTreeUrl, fileUrl, packageJsonUrl, versionsUrl };
@@ -0,0 +1,16 @@
1
+ //#region src/internal/limits.ts
2
+ /**
3
+ * Cap on declaration files materialized per package by `getTypeFiles`. A
4
+ * pathological or hostile file tree naming more declaration files than this
5
+ * fails typed rather than exhausting memory.
6
+ */
7
+ const MAX_TYPE_FILES_PER_PACKAGE = 5e3;
8
+ /**
9
+ * Cap on total downloaded declaration bytes per package, checked as the
10
+ * downloads accumulate. The yaml alias-budget lesson applied to downloads:
11
+ * budget the materialization, not just the input's static size claims.
12
+ */
13
+ const MAX_TYPE_BYTES_PER_PACKAGE = 67108864;
14
+
15
+ //#endregion
16
+ export { MAX_TYPE_BYTES_PER_PACKAGE, MAX_TYPE_FILES_PER_PACKAGE };
@@ -0,0 +1,181 @@
1
+ //#region src/internal/resolution.ts
2
+ /**
3
+ * Exports-map resolution machinery. Every input here — `exports` values,
4
+ * `typesVersions` maps, wildcard patterns — is untrusted JSON fetched from a
5
+ * CDN, so every recursive surface carries a depth guard, wildcard patterns
6
+ * are bounded before regex compilation, and untrusted keys are only read
7
+ * through `Object.hasOwn` (a JSON-parsed `{"__proto__": …}` key would
8
+ * otherwise read or, worse, assign the prototype).
9
+ */
10
+ /** Keys that are never data on a plain object. Skipped everywhere. */
11
+ const DUNDER_KEYS = /* @__PURE__ */ new Set([
12
+ "__proto__",
13
+ "constructor",
14
+ "prototype"
15
+ ]);
16
+ /** Whether a path names a TypeScript declaration file. */
17
+ const isTypeDefinition = (filePath) => filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts");
18
+ /** Normalize backslashes to forward slashes. */
19
+ const normalizePath = (path) => path.replace(/\\/g, "/");
20
+ /**
21
+ * Reject paths that are absolute or contain `..` segments. Shared by the
22
+ * cache (paths from CDN file trees joined under the cache root) and the
23
+ * resolver (paths from untrusted manifests that reach the CDN download URL).
24
+ */
25
+ const isSafeRelativePath = (filePath) => {
26
+ if (filePath.length === 0) return false;
27
+ if (filePath.startsWith("/") || filePath.startsWith("\\")) return false;
28
+ if (/^[A-Za-z]:/.test(filePath)) return false;
29
+ return !filePath.split(/[/\\]+/).some((segment) => segment === "..");
30
+ };
31
+ const escapeRegex = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
32
+ /**
33
+ * Compile an exports/typesVersions wildcard pattern to a regex, or `null`
34
+ * when the pattern exceeds the wildcard bound. npm semantics use exactly one
35
+ * `*`; a hostile pattern with many wildcards would compile to a
36
+ * catastrophic-backtracking regex, so past the bound it simply does not
37
+ * match.
38
+ */
39
+ const compileWildcard = (pattern) => {
40
+ let stars = 0;
41
+ for (const char of pattern) if (char === "*") stars += 1;
42
+ if (stars === 0 || stars > 1) return null;
43
+ return new RegExp(`^${escapeRegex(pattern).replace(/\\?\*/g, "(.*)")}$`);
44
+ };
45
+ /**
46
+ * Substitute a captured wildcard segment into an exports value.
47
+ *
48
+ * @remarks
49
+ * This is where v3 had a live prototype-pollution defect: it copied untrusted
50
+ * keys into a plain object literal, so an `exports` map containing a
51
+ * `"__proto__"` key assigned the prototype of the result. Substituted maps
52
+ * are built with `Object.create(null)` and dunder keys are skipped. Past the
53
+ * depth guard nothing resolves (`null`).
54
+ */
55
+ const substituteWildcard = (value, captured, depth = 0) => {
56
+ if (depth > 256) return null;
57
+ if (typeof value === "string") return value.replace(/\*/g, captured);
58
+ if (Array.isArray(value)) return value.map((entry) => substituteWildcard(entry, captured, depth + 1));
59
+ if (typeof value === "object" && value !== null) {
60
+ const result = Object.create(null);
61
+ for (const key of Object.keys(value)) {
62
+ if (DUNDER_KEYS.has(key)) continue;
63
+ if (!Object.hasOwn(value, key)) continue;
64
+ const entry = value[key];
65
+ result[key] = typeof entry === "string" || typeof entry === "object" && entry !== null ? substituteWildcard(entry, captured, depth + 1) : entry;
66
+ }
67
+ return result;
68
+ }
69
+ return value;
70
+ };
71
+ /**
72
+ * Look up a subpath in an `exports` map: the root-conditions sugar form
73
+ * first, then exact keys (with and without the `./` prefix), then bounded
74
+ * wildcard patterns resolved by Node's specificity rules. Returns the export
75
+ * value (wildcards substituted) or `null`.
76
+ */
77
+ const getExportValue = (exports, subpath) => {
78
+ if (exports === void 0 || exports === null) return null;
79
+ if (typeof exports === "string") return subpath === "." ? exports : null;
80
+ if (Array.isArray(exports)) return subpath === "." ? exports : null;
81
+ if (typeof exports !== "object") return null;
82
+ const exportsObj = exports;
83
+ const keys = Object.keys(exportsObj);
84
+ if (!keys.some((key) => key.startsWith("."))) return subpath === "." ? exportsObj : null;
85
+ const withoutDot = subpath.replace(/^\.\//, "");
86
+ for (const key of [subpath, withoutDot]) if (!DUNDER_KEYS.has(key) && Object.hasOwn(exportsObj, key)) return exportsObj[key];
87
+ let best = null;
88
+ for (const pattern of keys) {
89
+ if (DUNDER_KEYS.has(pattern) || !pattern.includes("*")) continue;
90
+ if (!Object.hasOwn(exportsObj, pattern)) continue;
91
+ const regex = compileWildcard(pattern);
92
+ if (regex === null) continue;
93
+ const match = regex.exec(subpath) ?? regex.exec(withoutDot);
94
+ if (match === null) continue;
95
+ const base = pattern.indexOf("*");
96
+ if (best === null || base > best.base || base === best.base && pattern.length > best.pattern.length) best = {
97
+ pattern,
98
+ captured: match[1] ?? "",
99
+ base
100
+ };
101
+ }
102
+ if (best !== null) return substituteWildcard(exportsObj[best.pattern], best.captured);
103
+ return null;
104
+ };
105
+ /**
106
+ * Extract a types-bearing path from an export value: `types` first, then
107
+ * `import` / `default`, recursing into nested condition objects under the
108
+ * depth guard. A fallback array (Node semantics) is walked in order and the
109
+ * first entry yielding a types path wins.
110
+ */
111
+ const extractTypesFromExport = (exportValue, depth = 0) => {
112
+ if (depth > 256) return null;
113
+ if (exportValue === void 0 || exportValue === null) return null;
114
+ if (typeof exportValue === "string") return exportValue;
115
+ if (Array.isArray(exportValue)) {
116
+ for (const entry of exportValue) {
117
+ const found = extractTypesFromExport(entry, depth + 1);
118
+ if (found !== null) return found;
119
+ }
120
+ return null;
121
+ }
122
+ if (typeof exportValue !== "object") return null;
123
+ const conditions = exportValue;
124
+ for (const condition of [
125
+ "types",
126
+ "import",
127
+ "default"
128
+ ]) {
129
+ if (!Object.hasOwn(conditions, condition)) continue;
130
+ const value = conditions[condition];
131
+ if (typeof value === "string") return value;
132
+ if (typeof value === "object" && value !== null) {
133
+ const nested = extractTypesFromExport(value, depth + 1);
134
+ if (nested !== null) return nested;
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+ /** The conventional lookup candidates for a bare subpath, most specific first. */
140
+ const tryExtensions = (basePath) => [
141
+ basePath,
142
+ `${basePath}.d.ts`,
143
+ `${basePath}.d.mts`,
144
+ `${basePath}.d.cts`,
145
+ `${basePath}.ts`,
146
+ `${basePath}.mts`,
147
+ `${basePath}.cts`,
148
+ `${basePath}.js`,
149
+ `${basePath}.mjs`,
150
+ `${basePath}.cjs`,
151
+ `${basePath}/index.d.ts`,
152
+ `${basePath}/index.d.mts`,
153
+ `${basePath}/index.d.cts`,
154
+ `${basePath}/index.ts`,
155
+ `${basePath}/index.js`
156
+ ].map(normalizePath);
157
+ /**
158
+ * The main type entry for a manifest: `types`/`typings`, then the root
159
+ * export's types condition, then a declaration-extension swap of `main`,
160
+ * with the documented `index.d.ts` convention floor — which is what makes
161
+ * `TypeResolver.resolveMainEntry` genuinely total.
162
+ */
163
+ const findMainTypePath = (manifest) => {
164
+ if (manifest.types !== void 0) return manifest.types;
165
+ if (manifest.typings !== void 0) return manifest.typings;
166
+ if (manifest.exports !== void 0) {
167
+ const rootExport = getExportValue(manifest.exports, ".");
168
+ const typesPath = extractTypesFromExport(rootExport);
169
+ if (typesPath !== null) return typesPath;
170
+ }
171
+ if (manifest.main !== void 0) {
172
+ const mainWithoutExt = manifest.main.replace(/\.(m?[jt]s|cjs)$/, "");
173
+ const found = tryExtensions(mainWithoutExt).find(isTypeDefinition);
174
+ if (found !== void 0) return found;
175
+ return manifest.main;
176
+ }
177
+ return "index.d.ts";
178
+ };
179
+
180
+ //#endregion
181
+ export { compileWildcard, extractTypesFromExport, findMainTypePath, getExportValue, isSafeRelativePath, isTypeDefinition, normalizePath, substituteWildcard, tryExtensions };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@tsdoctor/registry",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "TypeScript virtual file systems for Effect: fetch, cache and resolve type definitions from npm via the jsDelivr CDN, and build @typescript/vfs environments for Twoslash-style documentation tooling.",
6
+ "keywords": [
7
+ "typescript",
8
+ "vfs",
9
+ "twoslash",
10
+ "types",
11
+ "jsdelivr",
12
+ "cache",
13
+ "effect"
14
+ ],
15
+ "homepage": "https://github.com/spencerbeggs/tsdoctor#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/spencerbeggs/tsdoctor/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/spencerbeggs/tsdoctor.git",
22
+ "directory": "packages/registry"
23
+ },
24
+ "license": "MIT",
25
+ "author": {
26
+ "name": "C. Spencer Beggs",
27
+ "email": "spencer@beggs.codes",
28
+ "url": "https://spencerbeg.gs"
29
+ },
30
+ "sideEffects": false,
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./index.d.ts",
35
+ "import": "./index.js",
36
+ "default": "./index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "peerDependencies": {
41
+ "@effect/platform-node": "4.0.0-rc.109",
42
+ "@effected/semver": "^0.5.0",
43
+ "@effected/store": "^0.4.0",
44
+ "@effected/tsconfig-json": "^0.6.0",
45
+ "@effected/xdg": "^0.3.0",
46
+ "@typescript/vfs": "^1.6.4",
47
+ "effect": "4.0.0-rc.109",
48
+ "typescript": "^6.0.3"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@effected/tsconfig-json": {
52
+ "optional": true
53
+ },
54
+ "@effected/xdg": {
55
+ "optional": true
56
+ },
57
+ "@typescript/vfs": {
58
+ "optional": true
59
+ },
60
+ "typescript": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "engines": {
65
+ "node": ">=24.11.0"
66
+ }
67
+ }
@@ -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.59.0"
9
+ }
10
+ ]
11
+ }