@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/LICENSE +21 -0
- package/PackageFetcher.js +241 -0
- package/PackageSpec.js +182 -0
- package/README.md +94 -0
- package/RegistryEvent.js +146 -0
- package/TsEnvironment.js +95 -0
- package/TypeCache.js +256 -0
- package/TypeRegistry.js +276 -0
- package/TypeResolver.js +183 -0
- package/Vfs.js +33 -0
- package/VirtualPackage.js +126 -0
- package/index.d.ts +990 -0
- package/index.js +11 -0
- package/internal/jsdelivr.js +62 -0
- package/internal/limits.js +16 -0
- package/internal/resolution.js +181 -0
- package/package.json +67 -0
- package/tsdoc-metadata.json +11 -0
package/TypeResolver.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { compileWildcard, extractTypesFromExport, findMainTypePath, getExportValue, isSafeRelativePath, isTypeDefinition, normalizePath } from "./internal/resolution.js";
|
|
2
|
+
import { PackageSpec } from "./PackageSpec.js";
|
|
3
|
+
import { Option, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/TypeResolver.ts
|
|
6
|
+
/**
|
|
7
|
+
* A resolved module: the declaration file a specifier resolves to within a
|
|
8
|
+
* package.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
var ResolvedModule = class extends Schema.Class("ResolvedModule")({
|
|
13
|
+
/** The file path relative to the package root (no `./` prefix). */
|
|
14
|
+
filePath: Schema.String,
|
|
15
|
+
/** Whether the path names a TypeScript declaration file. */
|
|
16
|
+
isTypeDefinition: Schema.Boolean,
|
|
17
|
+
/** The package the path belongs to. */
|
|
18
|
+
package: PackageSpec
|
|
19
|
+
}) {};
|
|
20
|
+
const DUNDER_KEYS = /* @__PURE__ */ new Set([
|
|
21
|
+
"__proto__",
|
|
22
|
+
"constructor",
|
|
23
|
+
"prototype"
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Build a `ResolvedModule` only when the manifest-supplied path stays inside
|
|
27
|
+
* the package: resolved paths reach `PackageFetcher.downloadFile`'s URL and
|
|
28
|
+
* the cache's join, so an absolute or `..`-bearing path from a hostile
|
|
29
|
+
* manifest must not survive resolution.
|
|
30
|
+
*/
|
|
31
|
+
const safeResolved = (filePath, pkg) => {
|
|
32
|
+
const normalized = normalizePath(filePath.replace(/^\.\//, ""));
|
|
33
|
+
if (!isSafeRelativePath(normalized)) return Option.none();
|
|
34
|
+
return Option.some(ResolvedModule.make({
|
|
35
|
+
filePath: normalized,
|
|
36
|
+
isTypeDefinition: isTypeDefinition(normalized),
|
|
37
|
+
package: pkg
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Pure `package.json` → declaration-file resolution.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Stateless pure functions — no service, no layer (v3's `Layer.succeed` over
|
|
45
|
+
* stateless functions was ceremony), and no fictional error channel: v3
|
|
46
|
+
* declared a `ResolutionError` its total implementation could never raise.
|
|
47
|
+
* Here {@link TypeResolver.resolveImport} is honest in the other direction
|
|
48
|
+
* too — it returns `Option.none()` where v3 fabricated a guessed fallback
|
|
49
|
+
* path, leaving fallback policy to the caller.
|
|
50
|
+
*
|
|
51
|
+
* All map inputs are untrusted CDN data; the machinery underneath is depth-
|
|
52
|
+
* guarded, wildcard-bounded and prototype-pollution-safe (see
|
|
53
|
+
* `internal/resolution.ts`), and every resolved path is validated to stay
|
|
54
|
+
* inside the package before a `ResolvedModule` is constructed.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
var TypeResolver = class TypeResolver {
|
|
59
|
+
constructor() {}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an import specifier (`"zod"`, `"zod/lib/types"`) against a
|
|
62
|
+
* manifest.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Resolution order: the `exports` map (`types` condition, then
|
|
66
|
+
* `import`/`default`, fallback arrays in order), then `typesVersions["*"]`
|
|
67
|
+
* (exact, then bounded wildcards), then — for the root specifier only —
|
|
68
|
+
* the top-level `types`/`typings` fields. `Option.none()` when the
|
|
69
|
+
* manifest offers no evidence for the subpath, or when the evidence names
|
|
70
|
+
* a path outside the package (hostile manifest — fails closed).
|
|
71
|
+
*/
|
|
72
|
+
static resolveImport(specifier, manifest, pkg) {
|
|
73
|
+
let subpath;
|
|
74
|
+
if (specifier === pkg.name) subpath = ".";
|
|
75
|
+
else if (specifier.startsWith(`${pkg.name}/`)) subpath = `./${specifier.slice(pkg.name.length + 1)}`;
|
|
76
|
+
else {
|
|
77
|
+
const bare = specifier.replace(/^\/+/, "");
|
|
78
|
+
subpath = bare === "" || bare === "." ? "." : bare.startsWith("./") ? bare : `./${bare}`;
|
|
79
|
+
}
|
|
80
|
+
if (manifest.exports !== void 0) {
|
|
81
|
+
const exportValue = getExportValue(manifest.exports, subpath);
|
|
82
|
+
const typesPath = extractTypesFromExport(exportValue);
|
|
83
|
+
if (typesPath !== null) return safeResolved(typesPath, pkg);
|
|
84
|
+
}
|
|
85
|
+
if (manifest.typesVersions !== void 0 && Object.hasOwn(manifest.typesVersions, "*")) {
|
|
86
|
+
const versionMap = manifest.typesVersions["*"];
|
|
87
|
+
if (versionMap !== void 0) {
|
|
88
|
+
const lookupPath = subpath === "." ? "." : subpath.replace(/^\.\//, "");
|
|
89
|
+
if (!DUNDER_KEYS.has(lookupPath) && Object.hasOwn(versionMap, lookupPath)) {
|
|
90
|
+
const mapped = versionMap[lookupPath];
|
|
91
|
+
const first = Array.isArray(mapped) ? mapped[0] : mapped;
|
|
92
|
+
if (typeof first === "string") return safeResolved(first, pkg);
|
|
93
|
+
}
|
|
94
|
+
for (const pattern of Object.keys(versionMap)) {
|
|
95
|
+
if (DUNDER_KEYS.has(pattern) || !pattern.includes("*")) continue;
|
|
96
|
+
if (!Object.hasOwn(versionMap, pattern)) continue;
|
|
97
|
+
const regex = compileWildcard(pattern);
|
|
98
|
+
if (regex === null) continue;
|
|
99
|
+
const match = regex.exec(lookupPath);
|
|
100
|
+
if (match === null) continue;
|
|
101
|
+
const mapped = versionMap[pattern];
|
|
102
|
+
const first = Array.isArray(mapped) ? mapped[0] : mapped;
|
|
103
|
+
if (typeof first === "string") return safeResolved(first.replace(/\*/g, match[1] ?? ""), pkg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (subpath === ".") {
|
|
108
|
+
if (manifest.types !== void 0) return safeResolved(manifest.types, pkg);
|
|
109
|
+
if (manifest.typings !== void 0) return safeResolved(manifest.typings, pkg);
|
|
110
|
+
}
|
|
111
|
+
return Option.none();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the manifest's main type entry.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* Total by the documented `index.d.ts` convention floor: `types`/`typings`,
|
|
118
|
+
* then the root export's types condition, then a declaration-extension
|
|
119
|
+
* swap of `main`, then `index.d.ts`. A main path that escapes the package
|
|
120
|
+
* (hostile manifest) also falls to the floor rather than surviving.
|
|
121
|
+
*/
|
|
122
|
+
static resolveMainEntry(manifest, pkg) {
|
|
123
|
+
return Option.getOrElse(safeResolved(findMainTypePath(manifest), pkg), () => ResolvedModule.make({
|
|
124
|
+
filePath: "index.d.ts",
|
|
125
|
+
isTypeDefinition: true,
|
|
126
|
+
package: pkg
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Enumerate every entry point that exposes type definitions: the main
|
|
131
|
+
* entry plus each `exports` subpath with a types-bearing condition,
|
|
132
|
+
* deduplicated by file path.
|
|
133
|
+
*
|
|
134
|
+
* @remarks
|
|
135
|
+
* Wildcard export keys (`"./*"`) are skipped: enumeration has no captured
|
|
136
|
+
* segment to substitute, so a pattern entry would emit a literal
|
|
137
|
+
* `dist/*.d.ts`. Pattern subpaths resolve through
|
|
138
|
+
* {@link TypeResolver.resolveImport}, which has the concrete specifier.
|
|
139
|
+
* Entries whose paths escape the package are skipped.
|
|
140
|
+
*/
|
|
141
|
+
static resolveTypeEntries(manifest, pkg) {
|
|
142
|
+
const entries = [TypeResolver.resolveMainEntry(manifest, pkg)];
|
|
143
|
+
if (manifest.exports !== void 0 && typeof manifest.exports === "object" && !Array.isArray(manifest.exports)) for (const key of Object.keys(manifest.exports)) {
|
|
144
|
+
if (DUNDER_KEYS.has(key)) continue;
|
|
145
|
+
if (!key.startsWith(".")) continue;
|
|
146
|
+
if (key.includes("*")) continue;
|
|
147
|
+
if (!Object.hasOwn(manifest.exports, key)) continue;
|
|
148
|
+
const typesPath = extractTypesFromExport(manifest.exports[key]);
|
|
149
|
+
if (typesPath !== null) {
|
|
150
|
+
const entry = safeResolved(typesPath, pkg);
|
|
151
|
+
if (Option.isSome(entry)) entries.push(entry.value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
155
|
+
return entries.filter((entry) => {
|
|
156
|
+
if (!entry.isTypeDefinition) return false;
|
|
157
|
+
if (seen.has(entry.filePath)) return false;
|
|
158
|
+
seen.add(entry.filePath);
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The conventional declaration-file path for a JavaScript file path
|
|
164
|
+
* (`lib/index.js` → `lib/index.d.ts`, `.mjs` → `.d.mts`, `.cjs` →
|
|
165
|
+
* `.d.cts`).
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* The input is a tree path from the CDN — untrusted — so a path that is
|
|
169
|
+
* absolute or escapes the package yields `Option.none()` instead of a
|
|
170
|
+
* `ResolvedModule` that could reach a download URL.
|
|
171
|
+
*/
|
|
172
|
+
static findTypeDefinition(jsFilePath, pkg) {
|
|
173
|
+
let typePath;
|
|
174
|
+
if (jsFilePath.endsWith(".mjs")) typePath = jsFilePath.replace(/\.mjs$/, ".d.mts");
|
|
175
|
+
else if (jsFilePath.endsWith(".cjs")) typePath = jsFilePath.replace(/\.cjs$/, ".d.cts");
|
|
176
|
+
else if (jsFilePath.endsWith(".js")) typePath = jsFilePath.replace(/\.js$/, ".d.ts");
|
|
177
|
+
else typePath = `${jsFilePath.replace(/\.(m?js|cjs)$/, "")}.d.ts`;
|
|
178
|
+
return safeResolved(typePath, pkg);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
export { ResolvedModule, TypeResolver };
|
package/Vfs.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/Vfs.ts
|
|
2
|
+
/**
|
|
3
|
+
* Merge VFS maps left to right into a new map; later entries win on path
|
|
4
|
+
* collisions.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { mergeVfs } from "@tsdoctor/registry";
|
|
9
|
+
*
|
|
10
|
+
* const combined = mergeVfs(vfsA, vfsB);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
const mergeVfs = (...maps) => {
|
|
16
|
+
const out = /* @__PURE__ */ new Map();
|
|
17
|
+
for (const map of maps) for (const [path, content] of map) out.set(path, content);
|
|
18
|
+
return out;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Prefix every path in `entries` with `node_modules/<name>/`, normalizing
|
|
22
|
+
* away leading slashes.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
const prefixVfs = (name, entries) => {
|
|
27
|
+
const out = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const [path, content] of entries) out.set(`node_modules/${name}/${path.replace(/^\/+/, "")}`, content);
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { mergeVfs, prefixVfs };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Effect, FileSystem, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/VirtualPackage.ts
|
|
4
|
+
/**
|
|
5
|
+
* A synthetic npm package built from locally supplied TypeScript declaration
|
|
6
|
+
* content, for inclusion in a {@link Vfs} without fetching from the CDN.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Useful when you have locally generated `.d.ts` files — API Extractor
|
|
10
|
+
* output, hand-written ambient declarations — and want them in the same VFS
|
|
11
|
+
* `TypeRegistry` builds from remote packages. Instances are transient: they
|
|
12
|
+
* are never persisted to the disk cache.
|
|
13
|
+
*
|
|
14
|
+
* The class is deliberately subclass-friendly (the rspress consumer extends
|
|
15
|
+
* it): construct via `VirtualPackage.make(...)` or the statics, and extend
|
|
16
|
+
* with `class Mine extends VirtualPackage { ... }`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { VirtualPackage } from "@tsdoctor/registry";
|
|
21
|
+
*
|
|
22
|
+
* const pkg = VirtualPackage.create("@my-org/api-types", "1.0.0", "export interface User { id: string }");
|
|
23
|
+
* const vfs = pkg.toVfs();
|
|
24
|
+
* // node_modules/@my-org/api-types/package.json, node_modules/@my-org/api-types/index.d.ts
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
var VirtualPackage = class VirtualPackage extends Schema.Class("VirtualPackage")({
|
|
30
|
+
/** The package name (e.g. `"@my-org/api-types"`). */
|
|
31
|
+
name: Schema.String,
|
|
32
|
+
/** The package version. */
|
|
33
|
+
version: Schema.String,
|
|
34
|
+
/** Entry file names (e.g. `"index.d.ts"`) mapped to declaration source. */
|
|
35
|
+
entries: Schema.ReadonlyMap(Schema.String, Schema.String)
|
|
36
|
+
}) {
|
|
37
|
+
/**
|
|
38
|
+
* Single-entry factory: a virtual package whose sole entry point is
|
|
39
|
+
* `index.d.ts`.
|
|
40
|
+
*/
|
|
41
|
+
static create(name, version, declarations) {
|
|
42
|
+
return VirtualPackage.make({
|
|
43
|
+
name,
|
|
44
|
+
version,
|
|
45
|
+
entries: /* @__PURE__ */ new Map([["index.d.ts", declarations]])
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Multi-entry factory: one `.d.ts` per entry point, exposed through a
|
|
50
|
+
* synthetic `exports` map.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* An empty entries map is developer wiring, not input — it would produce a
|
|
54
|
+
* package whose `types` points at a file that does not exist — so it
|
|
55
|
+
* throws at construction (defect posture), as does an entry set whose
|
|
56
|
+
* names collide after extension normalization (see
|
|
57
|
+
* {@link VirtualPackage.toVfs}).
|
|
58
|
+
*/
|
|
59
|
+
static createMultiEntry(name, version, entries) {
|
|
60
|
+
if (entries.size === 0) throw new Error(`VirtualPackage.createMultiEntry: "${name}" needs at least one entry file`);
|
|
61
|
+
return VirtualPackage.make({
|
|
62
|
+
name,
|
|
63
|
+
version,
|
|
64
|
+
entries
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load a single `.d.ts` file from disk as a virtual package with one
|
|
69
|
+
* `index.d.ts` entry.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* Reads through the platform-agnostic `FileSystem` service; the
|
|
73
|
+
* `PlatformError` surfaces typed.
|
|
74
|
+
*/
|
|
75
|
+
static fromFile(name, version, filePath) {
|
|
76
|
+
return Effect.gen(function* () {
|
|
77
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(filePath);
|
|
78
|
+
return VirtualPackage.create(name, version, content);
|
|
79
|
+
}).pipe(Effect.withSpan("VirtualPackage.fromFile"));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The package's {@link Vfs}: a synthetic `package.json` plus every entry
|
|
83
|
+
* file, each path prefixed `node_modules/<name>/`.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* The `package.json` uses `types` for a single entry and an `exports` map
|
|
87
|
+
* for multiple entries, so TypeScript module resolution works against the
|
|
88
|
+
* generated VFS.
|
|
89
|
+
*/
|
|
90
|
+
toVfs() {
|
|
91
|
+
const vfs = /* @__PURE__ */ new Map();
|
|
92
|
+
const prefix = `node_modules/${this.name}`;
|
|
93
|
+
vfs.set(`${prefix}/package.json`, this.toPackageJson());
|
|
94
|
+
for (const [fileName, content] of this.entries) {
|
|
95
|
+
if (fileName === "package.json") throw new Error(`VirtualPackage: "${this.name}" cannot define package.json as an entry`);
|
|
96
|
+
vfs.set(`${prefix}/${fileName}`, content);
|
|
97
|
+
}
|
|
98
|
+
return vfs;
|
|
99
|
+
}
|
|
100
|
+
toPackageJson() {
|
|
101
|
+
if (this.entries.size === 0) throw new Error(`VirtualPackage: "${this.name}" has no entry files — nothing to point types at`);
|
|
102
|
+
const manifest = {
|
|
103
|
+
name: this.name,
|
|
104
|
+
version: this.version
|
|
105
|
+
};
|
|
106
|
+
if (this.entries.size === 1) {
|
|
107
|
+
const [only] = this.entries.keys();
|
|
108
|
+
manifest.types = only ?? "index.d.ts";
|
|
109
|
+
} else {
|
|
110
|
+
manifest.exports = {};
|
|
111
|
+
const sources = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const fileName of this.entries.keys()) {
|
|
113
|
+
const baseName = fileName.replace(/\.d\.(m|c)?ts$/, "");
|
|
114
|
+
const key = baseName === "index" ? "." : `./${baseName}`;
|
|
115
|
+
const previous = sources.get(key);
|
|
116
|
+
if (previous !== void 0) throw new Error(`VirtualPackage: "${this.name}" entries "${previous}" and "${fileName}" both normalize to the export key "${key}"`);
|
|
117
|
+
sources.set(key, fileName);
|
|
118
|
+
manifest.exports[key] = { types: `./${fileName}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return JSON.stringify(manifest, null, 2);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { VirtualPackage };
|