docspack 0.3.0 → 1.0.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/dist/agent.d.ts +48 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +243 -0
- package/dist/agent.js.map +1 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +78 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +75 -9
- package/dist/build.js.map +1 -1
- package/dist/changed.d.ts +31 -0
- package/dist/changed.d.ts.map +1 -0
- package/dist/changed.js +71 -0
- package/dist/changed.js.map +1 -0
- package/dist/cli.js +131 -10
- package/dist/cli.js.map +1 -1
- package/dist/coverage.d.ts +35 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +64 -0
- package/dist/coverage.js.map +1 -0
- package/dist/db.d.ts +38 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +109 -6
- package/dist/db.js.map +1 -1
- package/dist/discovery.d.ts +14 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +31 -6
- package/dist/discovery.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +90 -3
- package/dist/doctor.js.map +1 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +7 -3
- package/dist/document.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +65 -4
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +3 -0
- package/dist/mcp.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +1 -0
- package/dist/preview.js.map +1 -1
- package/dist/search.d.ts +17 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +116 -21
- package/dist/search.js.map +1 -1
- package/dist/spec.d.ts +6 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +12 -3
- package/dist/spec.js.map +1 -1
- package/dist/surface.d.ts +45 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +208 -0
- package/dist/surface.js.map +1 -0
- package/dist/sync.d.ts +8 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +50 -1
- package/dist/sync.js.map +1 -1
- package/dist/verify.d.ts +9 -0
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +55 -27
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +308 -0
- package/src/artifact.ts +110 -0
- package/src/build.ts +103 -10
- package/src/changed.ts +99 -0
- package/src/cli.ts +167 -10
- package/src/coverage.ts +96 -0
- package/src/db.ts +168 -7
- package/src/discovery.ts +40 -5
- package/src/doctor.ts +100 -4
- package/src/document.ts +13 -4
- package/src/help.ts +65 -4
- package/src/index.ts +30 -0
- package/src/mcp.ts +3 -0
- package/src/preview.ts +1 -0
- package/src/search.ts +158 -24
- package/src/spec.ts +21 -3
- package/src/surface.ts +265 -0
- package/src/sync.ts +66 -2
- package/src/verify.ts +57 -27
package/src/surface.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One name a library publishes, and how a caller reaches it.
|
|
7
|
+
*
|
|
8
|
+
* This is a narrower reading of a package than [`readExportSurface`](./exports.ts), which
|
|
9
|
+
* collects every identifier in every `.d.ts` so that `verify` accuses as little as possible.
|
|
10
|
+
* Here the opposite is wanted: only what an entry point actually exports, because these names
|
|
11
|
+
* become answers, and an answer about a library's private type helps nobody.
|
|
12
|
+
*/
|
|
13
|
+
export interface ExportedSymbol {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** What a caller writes to import it, e.g. `hono/jwt`. */
|
|
16
|
+
readonly from: string;
|
|
17
|
+
/** Declaration file the name was read from, relative to the package root. */
|
|
18
|
+
readonly file: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PublicSurface {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
/** Subpaths of the `exports` map that resolve to a declaration file. */
|
|
25
|
+
readonly entryPoints: number;
|
|
26
|
+
readonly symbols: ReadonlyMap<string, ExportedSymbol>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How far `export … from` chains are followed out of an entry point. */
|
|
30
|
+
const MAX_DEPTH = 6;
|
|
31
|
+
|
|
32
|
+
/** A package this large is generated, and reading all of it would dominate a sync. */
|
|
33
|
+
const MAX_FILES = 600;
|
|
34
|
+
|
|
35
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
36
|
+
|
|
37
|
+
const DECLARATION =
|
|
38
|
+
/export\s+(?:declare\s+)?(?:abstract\s+)?(?:class|function|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
39
|
+
/**
|
|
40
|
+
* The same declaration, with the JSDoc comment above it captured as well.
|
|
41
|
+
*
|
|
42
|
+
* The comment body is written as "anything that is not the comment's end" rather than as a lazy
|
|
43
|
+
* run of any character. A lazy run expands across dozens of declarations to reach a distant
|
|
44
|
+
* comment terminator, and in a single global pass that swallows everything in between: on hono's
|
|
45
|
+
* 75kB type file it found 28 declarations where there are hundreds.
|
|
46
|
+
*/
|
|
47
|
+
const DECLARED =
|
|
48
|
+
/(\/\*\*(?:[^*]|\*(?!\/))*\*\/\s*)?export\s+(?:declare\s+)?(?:abstract\s+)?(?:class|function|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
49
|
+
|
|
50
|
+
/** How much of a declaration is carried: long enough for a signature, short enough to rank. */
|
|
51
|
+
const DECLARATION_LIMIT = 400;
|
|
52
|
+
const EXPORT_LIST = /export\s*\{([^}]*)\}/g;
|
|
53
|
+
const REEXPORT =
|
|
54
|
+
/export\s+(?:\*|\{[^}]*\})\s*(?:as\s+[A-Za-z_$][\w$]*\s*)?from\s+['"]([^'"]+)['"]/g;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reads what a library exports, starting from its `exports` map. Returns nothing when the
|
|
58
|
+
* package ships no type declarations, which is not an error: most of a project's dependencies
|
|
59
|
+
* are not the ones anybody asks questions about.
|
|
60
|
+
*/
|
|
61
|
+
export async function readPublicSurface(dir: string): Promise<PublicSurface | undefined> {
|
|
62
|
+
const manifest = await readJson(join(dir, "package.json"));
|
|
63
|
+
if (manifest === undefined) return undefined;
|
|
64
|
+
|
|
65
|
+
const symbols = new Map<string, ExportedSymbol>();
|
|
66
|
+
const name = typeof manifest.name === "string" ? manifest.name : "";
|
|
67
|
+
const budget = { files: MAX_FILES };
|
|
68
|
+
let entryPoints = 0;
|
|
69
|
+
|
|
70
|
+
for (const entry of entryPointsOf(manifest, dir, name)) {
|
|
71
|
+
entryPoints += 1;
|
|
72
|
+
await collect(entry.file, entry.specifier, dir, 0, new Set<string>(), symbols, budget);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entryPoints === 0) return undefined;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
version: typeof manifest.version === "string" ? manifest.version : "",
|
|
80
|
+
entryPoints,
|
|
81
|
+
symbols,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every declaration in one file, with the JSDoc comment above it when there is one, keyed by the
|
|
87
|
+
* name it declares.
|
|
88
|
+
*
|
|
89
|
+
* One pass over the file rather than one per name. A generated declaration file can be megabytes
|
|
90
|
+
* and export hundreds of names, and scanning it once per name made indexing TypeScript itself
|
|
91
|
+
* take longer than everything else in a sync put together.
|
|
92
|
+
*/
|
|
93
|
+
export function declarationsIn(source: string, limit = DECLARATION_LIMIT): Map<string, string> {
|
|
94
|
+
const found = new Map<string, string>();
|
|
95
|
+
DECLARED.lastIndex = 0;
|
|
96
|
+
for (const match of source.matchAll(DECLARED)) {
|
|
97
|
+
const name = match[2];
|
|
98
|
+
if (name === undefined || found.has(name) || match.index === undefined) continue;
|
|
99
|
+
const head = match[0];
|
|
100
|
+
const start = match.index + head.length;
|
|
101
|
+
const body = source.slice(start, start + limit);
|
|
102
|
+
found.set(name, head + trimToDeclaration(body, body.length === limit));
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Cuts a declaration's body where the declaration ends.
|
|
109
|
+
*
|
|
110
|
+
* A fixed window runs into whatever follows it, so the body is cut at the next thing that starts
|
|
111
|
+
* at the left margin — another declaration, or the comment above one. What is left is a
|
|
112
|
+
* signature rather than a signature plus the first half of its neighbour.
|
|
113
|
+
*/
|
|
114
|
+
function trimToDeclaration(body: string, truncated: boolean): string {
|
|
115
|
+
// A declaration's continuation lines are indented, so an unindented keyword is a new statement.
|
|
116
|
+
const next = body.search(
|
|
117
|
+
/\n(?:export|declare|type|interface|function|class|const|let|var|enum)\s|\n\/\*\*/,
|
|
118
|
+
);
|
|
119
|
+
if (next !== -1) return body.slice(0, next);
|
|
120
|
+
if (!truncated) return body;
|
|
121
|
+
// The window ran out mid-declaration. Ending on a whole line beats ending mid-identifier.
|
|
122
|
+
const lastLine = body.lastIndexOf("\n");
|
|
123
|
+
return lastLine === -1 ? body : `${body.slice(0, lastLine)}\n…`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The declaration of one name, with the JSDoc comment above it when there is one. This is the
|
|
128
|
+
* text that answers a question the prose never covered.
|
|
129
|
+
*/
|
|
130
|
+
export function declarationOf(
|
|
131
|
+
source: string,
|
|
132
|
+
name: string,
|
|
133
|
+
limit = DECLARATION_LIMIT,
|
|
134
|
+
): string | undefined {
|
|
135
|
+
return declarationsIn(source, limit).get(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** True when the declaration `declarationOf` returned carries a JSDoc comment. */
|
|
139
|
+
export function hasJsdoc(declaration: string): boolean {
|
|
140
|
+
return declaration.startsWith("/**");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface EntryPoint {
|
|
144
|
+
readonly file: string;
|
|
145
|
+
/** The specifier a caller imports, e.g. `hono` or `hono/jwt`. */
|
|
146
|
+
readonly specifier: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function* entryPointsOf(
|
|
150
|
+
manifest: Record<string, unknown>,
|
|
151
|
+
dir: string,
|
|
152
|
+
name: string,
|
|
153
|
+
): Generator<EntryPoint> {
|
|
154
|
+
const exports = manifest.exports;
|
|
155
|
+
const entries: [string, unknown][] =
|
|
156
|
+
typeof exports === "object" && exports !== null
|
|
157
|
+
? Object.entries(exports as Record<string, unknown>)
|
|
158
|
+
: [[".", { types: manifest.types ?? manifest.typings }]];
|
|
159
|
+
|
|
160
|
+
for (const [subpath, entry] of entries) {
|
|
161
|
+
const types = declarationTarget(entry);
|
|
162
|
+
if (types === undefined) continue;
|
|
163
|
+
// A wildcard subpath names no single module a caller could import.
|
|
164
|
+
if (subpath.includes("*")) continue;
|
|
165
|
+
const file = resolve(dir, types);
|
|
166
|
+
if (!existsSync(file)) continue;
|
|
167
|
+
yield { file, specifier: specifierFor(name, subpath) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function specifierFor(name: string, subpath: string): string {
|
|
172
|
+
if (subpath === "." || subpath === "") return name;
|
|
173
|
+
return `${name}/${subpath.replace(/^\.\//, "")}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function declarationTarget(entry: unknown): string | undefined {
|
|
177
|
+
if (typeof entry !== "object" || entry === null) return undefined;
|
|
178
|
+
const record = entry as Record<string, unknown>;
|
|
179
|
+
if (typeof record.types === "string") return record.types;
|
|
180
|
+
for (const key of ["import", "require", "default"] as const) {
|
|
181
|
+
const nested = record[key];
|
|
182
|
+
if (typeof nested === "object" && nested !== null) {
|
|
183
|
+
const types = (nested as Record<string, unknown>).types;
|
|
184
|
+
if (typeof types === "string") return types;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function collect(
|
|
191
|
+
file: string,
|
|
192
|
+
specifier: string,
|
|
193
|
+
root: string,
|
|
194
|
+
depth: number,
|
|
195
|
+
seen: Set<string>,
|
|
196
|
+
into: Map<string, ExportedSymbol>,
|
|
197
|
+
budget: { files: number },
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
if (depth > MAX_DEPTH || budget.files <= 0 || seen.has(file) || !existsSync(file)) return;
|
|
200
|
+
seen.add(file);
|
|
201
|
+
budget.files -= 1;
|
|
202
|
+
|
|
203
|
+
let source: string;
|
|
204
|
+
try {
|
|
205
|
+
source = await readFile(file, "utf8");
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The first entry point that publishes a name wins, so a name re-exported from several
|
|
211
|
+
// subpaths is attributed to the one listed first — which is the package's own ordering.
|
|
212
|
+
const record = (name: string): void => {
|
|
213
|
+
if (!IDENTIFIER.test(name) || into.has(name)) return;
|
|
214
|
+
into.set(name, { name, from: specifier, file: relativePath(root, file) });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const match of source.matchAll(DECLARATION)) {
|
|
218
|
+
if (match[1] !== undefined) record(match[1]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const match of source.matchAll(EXPORT_LIST)) {
|
|
222
|
+
for (const entry of (match[1] ?? "").split(",")) {
|
|
223
|
+
// `export { internal as public }` publishes the second name.
|
|
224
|
+
const parts = entry.split(/\s+as\s+/);
|
|
225
|
+
record((parts[parts.length - 1] ?? "").trim().replace(/^type\s+/, ""));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const match of source.matchAll(REEXPORT)) {
|
|
230
|
+
const target = match[1];
|
|
231
|
+
// A bare specifier points into another package, whose names are not this one's to publish.
|
|
232
|
+
if (target === undefined || !target.startsWith(".")) continue;
|
|
233
|
+
const next = resolveDeclaration(resolve(dirname(file), target));
|
|
234
|
+
if (next !== undefined) await collect(next, specifier, root, depth + 1, seen, into, budget);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `./external.cjs` inside a declaration file means `./external.d.cts`, not a JavaScript file. */
|
|
239
|
+
function resolveDeclaration(specifier: string): string | undefined {
|
|
240
|
+
const base = specifier.replace(/\.(js|cjs|mjs)$/, "");
|
|
241
|
+
for (const candidate of [
|
|
242
|
+
`${base}.d.ts`,
|
|
243
|
+
`${base}.d.cts`,
|
|
244
|
+
`${base}.d.mts`,
|
|
245
|
+
join(base, "index.d.ts"),
|
|
246
|
+
join(base, "index.d.cts"),
|
|
247
|
+
]) {
|
|
248
|
+
if (existsSync(candidate)) return candidate;
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function relativePath(root: string, file: string): string {
|
|
254
|
+
return relative(root, file).split(sep).join("/");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function readJson(file: string): Promise<Record<string, unknown> | undefined> {
|
|
258
|
+
try {
|
|
259
|
+
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
|
|
260
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
261
|
+
return parsed as Record<string, unknown>;
|
|
262
|
+
} catch {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { readArtifact } from "./artifact.js";
|
|
3
|
+
import type { IndexedChunk, PackageKind, Store } from "./db.js";
|
|
4
|
+
import { type DiscoveredPackage, discoverLibraries, discoverPackages } from "./discovery.js";
|
|
4
5
|
import { chunkId, estimateTokens, resolveChunkFile } from "./spec.js";
|
|
5
6
|
|
|
6
7
|
export interface SyncOptions {
|
|
@@ -8,6 +9,12 @@ export interface SyncOptions {
|
|
|
8
9
|
readonly store: Store;
|
|
9
10
|
/** Re-index packages that are already in the store. */
|
|
10
11
|
readonly force?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Also derive an index from each installed library's own type declarations. On by default:
|
|
14
|
+
* half of a library's exported names appear in no documentation anyone published, and the
|
|
15
|
+
* declarations are the only local answer for those.
|
|
16
|
+
*/
|
|
17
|
+
readonly artifacts?: boolean;
|
|
11
18
|
readonly onProgress?: (message: string) => void;
|
|
12
19
|
}
|
|
13
20
|
|
|
@@ -21,6 +28,7 @@ export interface SyncedPackage {
|
|
|
21
28
|
readonly tokens: number;
|
|
22
29
|
readonly status: SyncStatus;
|
|
23
30
|
readonly trusted: boolean;
|
|
31
|
+
readonly kind: PackageKind;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export interface SyncResult {
|
|
@@ -48,6 +56,7 @@ export async function syncProject(options: SyncOptions): Promise<SyncResult> {
|
|
|
48
56
|
tokens: 0,
|
|
49
57
|
status: "cached",
|
|
50
58
|
trusted: pkg.trusted,
|
|
59
|
+
kind: "docs",
|
|
51
60
|
});
|
|
52
61
|
continue;
|
|
53
62
|
}
|
|
@@ -70,12 +79,67 @@ export async function syncProject(options: SyncOptions): Promise<SyncResult> {
|
|
|
70
79
|
tokens: chunks.reduce((total, chunk) => total + chunk.tokens, 0),
|
|
71
80
|
status: "indexed",
|
|
72
81
|
trusted: pkg.trusted,
|
|
82
|
+
kind: "docs",
|
|
73
83
|
});
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
if (options.artifacts !== false) synced.push(...(await syncArtifacts(options)));
|
|
87
|
+
|
|
76
88
|
return { packages: synced, problems };
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Indexes each installed library's exported declarations.
|
|
93
|
+
*
|
|
94
|
+
* These are not ranked alongside prose. They are addressed by name, so a question that names a
|
|
95
|
+
* symbol can be answered from the installed build even when nobody documented it — and a corpus
|
|
96
|
+
* of generated type machinery cannot crowd out the documentation that does exist.
|
|
97
|
+
*/
|
|
98
|
+
async function syncArtifacts(options: SyncOptions): Promise<SyncedPackage[]> {
|
|
99
|
+
const synced: SyncedPackage[] = [];
|
|
100
|
+
|
|
101
|
+
for (const library of await discoverLibraries(options.cwd)) {
|
|
102
|
+
const id = `${library.name}@${library.version}`;
|
|
103
|
+
if (options.force !== true && options.store.hasPackage(id)) {
|
|
104
|
+
synced.push({
|
|
105
|
+
id,
|
|
106
|
+
name: library.name,
|
|
107
|
+
version: library.version,
|
|
108
|
+
chunks: options.store.countChunks(id),
|
|
109
|
+
tokens: 0,
|
|
110
|
+
status: "cached",
|
|
111
|
+
trusted: true,
|
|
112
|
+
kind: "artifact",
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
options.onProgress?.(`reading ${id}`);
|
|
118
|
+
const artifact = await readArtifact(library.dir);
|
|
119
|
+
// A dependency that ships no type declarations has nothing to derive, which is ordinary
|
|
120
|
+
// rather than a problem worth reporting on every sync.
|
|
121
|
+
if (artifact === undefined) continue;
|
|
122
|
+
|
|
123
|
+
options.store.indexPackage(
|
|
124
|
+
{ id: artifact.id, name: artifact.name, version: artifact.version, kind: "artifact" },
|
|
125
|
+
artifact.chunks,
|
|
126
|
+
artifact.symbols,
|
|
127
|
+
);
|
|
128
|
+
synced.push({
|
|
129
|
+
id: artifact.id,
|
|
130
|
+
name: artifact.name,
|
|
131
|
+
version: artifact.version,
|
|
132
|
+
chunks: artifact.chunks.length,
|
|
133
|
+
tokens: artifact.chunks.reduce((total, chunk) => total + chunk.tokens, 0),
|
|
134
|
+
status: "indexed",
|
|
135
|
+
trusted: true,
|
|
136
|
+
kind: "artifact",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return synced;
|
|
141
|
+
}
|
|
142
|
+
|
|
79
143
|
async function readChunks(
|
|
80
144
|
pkg: DiscoveredPackage,
|
|
81
145
|
): Promise<{ chunks: IndexedChunk[]; problems: string[] }> {
|
package/src/verify.ts
CHANGED
|
@@ -196,8 +196,12 @@ async function verifyPackage(
|
|
|
196
196
|
): Promise<VerifiedPackage> {
|
|
197
197
|
const empty = { id, checked: 0, matched: 0, unrecognised: 0, findings: [] };
|
|
198
198
|
|
|
199
|
-
const
|
|
200
|
-
|
|
199
|
+
const packageLibraries = await documentedLibraries(packageDir, manifest);
|
|
200
|
+
// A chunk may name its own libraries, which is how a monorepo says that one chunk describes
|
|
201
|
+
// `@acme/api@1.2.0` and its neighbour describes `@acme/cli@3.0.0`. Those are what that chunk
|
|
202
|
+
// is checked against; the package's list covers every chunk that names none.
|
|
203
|
+
const scopes = manifest.chunks.map((chunk) => chunk.documents ?? packageLibraries);
|
|
204
|
+
if (packageLibraries.length === 0 && scopes.every((scope) => scope.length === 0)) {
|
|
201
205
|
return {
|
|
202
206
|
...empty,
|
|
203
207
|
status: "skipped",
|
|
@@ -205,39 +209,52 @@ async function verifyPackage(
|
|
|
205
209
|
};
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
const documents =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const documents = [
|
|
213
|
+
...new Set([...packageLibraries, ...scopes.flat()].map(formatDocumentedLibrary)),
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// One resolution per library, however many chunks name it: reading a type surface is the
|
|
217
|
+
// expensive part, and a monorepo pack names the same eighteen libraries throughout.
|
|
218
|
+
const surfaces = new Map<string, ReadonlySet<string>>();
|
|
219
|
+
const resolve = async (library: DocumentedLibrary): Promise<ReadonlySet<string>> => {
|
|
220
|
+
const cached = surfaces.get(library.name);
|
|
221
|
+
if (cached !== undefined) return cached;
|
|
222
|
+
|
|
214
223
|
const libraryDir =
|
|
215
224
|
(await resolvePackageDir(library.name, packageDir)) ??
|
|
216
225
|
(await resolvePackageDir(library.name, cwd));
|
|
217
226
|
const surface = libraryDir === undefined ? undefined : await readExportSurface(libraryDir);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (const name of surface.names) names.add(name);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (names.size === 0) {
|
|
226
|
-
return {
|
|
227
|
-
...empty,
|
|
228
|
-
status: "skipped",
|
|
229
|
-
documents,
|
|
230
|
-
reason: `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not installed, or ship no type declarations`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
227
|
+
const names = surface?.names ?? new Set<string>();
|
|
228
|
+
surfaces.set(library.name, names);
|
|
229
|
+
return names;
|
|
230
|
+
};
|
|
233
231
|
|
|
234
|
-
const declared = new Set([...names].map(normalize));
|
|
235
232
|
const findings: DriftFinding[] = [];
|
|
233
|
+
const missing = new Set<string>();
|
|
236
234
|
let checked = 0;
|
|
237
235
|
let matched = 0;
|
|
238
236
|
let unrecognised = 0;
|
|
237
|
+
let readAny = false;
|
|
238
|
+
|
|
239
|
+
for (const [index, chunk] of manifest.chunks.entries()) {
|
|
240
|
+
const scope = scopes[index] ?? packageLibraries;
|
|
241
|
+
if (scope.length === 0) continue;
|
|
242
|
+
|
|
243
|
+
// An entity is fine if any library in scope declares it: a monorepo documents eighteen
|
|
244
|
+
// packages from one surface, and a name belongs to whichever of them exports it.
|
|
245
|
+
const names = new Set<string>();
|
|
246
|
+
for (const library of scope) {
|
|
247
|
+
const surface = await resolve(library);
|
|
248
|
+
if (surface.size === 0) {
|
|
249
|
+
missing.add(library.name);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const name of surface) names.add(name);
|
|
253
|
+
}
|
|
254
|
+
if (names.size === 0) continue;
|
|
255
|
+
readAny = true;
|
|
239
256
|
|
|
240
|
-
|
|
257
|
+
const declared = new Set([...names].map(normalize));
|
|
241
258
|
for (const entity of chunk.entities) {
|
|
242
259
|
const symbol = checkableSymbol(entity);
|
|
243
260
|
if (symbol === undefined) continue;
|
|
@@ -266,11 +283,24 @@ async function verifyPackage(
|
|
|
266
283
|
}
|
|
267
284
|
}
|
|
268
285
|
|
|
286
|
+
if (!readAny) {
|
|
287
|
+
const unread = [...missing];
|
|
288
|
+
return {
|
|
289
|
+
...empty,
|
|
290
|
+
status: "skipped",
|
|
291
|
+
documents,
|
|
292
|
+
reason:
|
|
293
|
+
unread.length === 0
|
|
294
|
+
? "has no chunk naming a library to check against"
|
|
295
|
+
: `${unread.join(", ")} ${unread.length === 1 ? "is" : "are"} not installed, or ship no type declarations`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
269
299
|
return {
|
|
270
300
|
id,
|
|
271
301
|
status: "verified",
|
|
272
302
|
documents,
|
|
273
|
-
...(missing.
|
|
303
|
+
...(missing.size === 0 ? {} : { unchecked: [...missing] }),
|
|
274
304
|
checked,
|
|
275
305
|
matched,
|
|
276
306
|
unrecognised,
|
|
@@ -285,7 +315,7 @@ async function verifyPackage(
|
|
|
285
315
|
* installed package can see. The `docspack` key is read as a fallback: it is build configuration
|
|
286
316
|
* rather than payload, and a package built before `documents` reached the manifest has only that.
|
|
287
317
|
*/
|
|
288
|
-
async function documentedLibraries(
|
|
318
|
+
export async function documentedLibraries(
|
|
289
319
|
packageDir: string,
|
|
290
320
|
manifest: PackageManifest,
|
|
291
321
|
): Promise<readonly DocumentedLibrary[]> {
|