@mjasnikovs/pi-task 0.40.28 → 0.40.30
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/README.md +6 -3
- package/dist/shared/zip.d.ts +86 -0
- package/dist/shared/zip.js +251 -0
- package/dist/workers/docs-core.js +1 -1
- package/dist/workers/docs-ecosystems.d.ts +23 -3
- package/dist/workers/docs-ecosystems.js +59 -6
- package/dist/workers/docs-index.d.ts +13 -0
- package/dist/workers/docs-index.js +3 -3
- package/dist/workers/eco-go.d.ts +213 -0
- package/dist/workers/eco-go.js +754 -0
- package/dist/workers/go-stdlib.d.ts +72 -0
- package/dist/workers/go-stdlib.js +210 -0
- package/dist/workers/go-surface.d.ts +75 -0
- package/dist/workers/go-surface.js +571 -0
- package/dist/workers/pi-worker-docs.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
|
-
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
@@ -201,11 +201,14 @@ Resolves an installed package, indexes its API surface and README into a local S
|
|
|
201
201
|
| `npm` | `package.json`, or a `node_modules/` directory | the `.d.ts` files the package ships, plus README | the installed `package.json` |
|
|
202
202
|
| `cargo` | `Cargo.toml`, at the directory or one level below it | `.rs` source reduced to public item heads, doc comments and attributes | `Cargo.lock` |
|
|
203
203
|
| `hackage` | `*.cabal`, `cabal.project`, `stack.yaml` or `package.yaml` | `.hs` source reduced to the export list, signatures and type declarations | `dist-newstyle/cache/plan.json`, then `cabal.project.freeze`, then `stack.yaml.lock` |
|
|
204
|
+
| `go` | `go.mod` or `go.work`, at the directory, above it or one level below | `.go` source reduced to exported declarations, struct fields, interface methods and doc comments | `go.mod`, which since Go 1.17 carries the resolved closure |
|
|
204
205
|
|
|
205
206
|
- **No manifest, no lookup.** In a directory with none of the above the tool refuses, spawns nothing and installs nothing, and points you at `pi-worker-search` / `pi-worker-fetch` instead.
|
|
206
207
|
- **Two manifests** (a Tauri app, say) are resolved by whichever registry already has the package on disk. If neither does, the call is refused as ambiguous and you pass `ecosystem: "cargo"` to say which.
|
|
207
|
-
- A package the project does not have is fetched once into a dedicated cache dir: `npm install --ignore-scripts` for npm, the `.crate` tarball for cargo, the Hackage tarball (or cabal's own cached copy) for hackage.
|
|
208
|
+
- A package the project does not have is fetched once into a dedicated cache dir: `npm install --ignore-scripts` for npm, the `.crate` tarball for cargo, the Hackage tarball (or cabal's own cached copy) for hackage, the module zip from `proxy.golang.org` for go.
|
|
208
209
|
- A Haskell **module** name is refused by name — `Data.Aeson` is not a package, `aeson` is.
|
|
210
|
+
- For **go you pass the import path**, `github.com/gin-gonic/gin/binding` or `net/http`, because that is what a Go file names. The module serving it is worked out by asking the proxy for the longest prefix that resolves — `gin/binding` belongs to `gin`, while `aws-sdk-go-v2/service/s3` is its own module. Vendored source and the local module cache are read first and cost nothing.
|
|
211
|
+
- The **Go standard library** is not on the proxy. It is read from a local `GOROOT` where there is one, and otherwise sliced out of the `golang.org/toolchain` archive over HTTP range requests — about 2 MB for `net/http` against 83 MB for the archive.
|
|
209
212
|
- The first call for a `(ecosystem, package, version)` triple pays a one-time ingestion cost; later calls are FTS-only.
|
|
210
213
|
- Cache lives at `${XDG_CACHE_HOME:-~/.cache}/pi-worker/docs.sqlite` — delete it to reset.
|
|
211
214
|
|
|
@@ -260,7 +263,7 @@ them checked in.
|
|
|
260
263
|
|
|
261
264
|
```sh
|
|
262
265
|
bun install
|
|
263
|
-
bun run test #
|
|
266
|
+
bun run test # 4631 tests pass across 247 files (1 skip)
|
|
264
267
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
265
268
|
bun run build # tsc → dist/
|
|
266
269
|
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zip — reading a zip archive in-process, whole or a slice at a time.
|
|
3
|
+
*
|
|
4
|
+
* Go module zips are why this exists. `unzip` is absent from a default Windows
|
|
5
|
+
* install and GNU tar cannot read zip at all, so shelling out behaves
|
|
6
|
+
* differently on every machine the agent runs on.
|
|
7
|
+
*
|
|
8
|
+
* The slice half is what makes the Go standard library affordable. It ships
|
|
9
|
+
* inside an 83 MB toolchain archive of which any one package is under a
|
|
10
|
+
* megabyte, and a reader that can pull the central directory and then only the
|
|
11
|
+
* entries it wants leaves the other 82 MB on the server.
|
|
12
|
+
*/
|
|
13
|
+
/** The comment is a 16-bit length, so the EOCD starts no further back than this. */
|
|
14
|
+
export declare const EOCD_SEARCH_SPAN: number;
|
|
15
|
+
export interface ZipEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
method: number;
|
|
18
|
+
compressedSize: number;
|
|
19
|
+
uncompressedSize: number;
|
|
20
|
+
/** Offset of the entry's LOCAL header, which is not where its data begins. */
|
|
21
|
+
localHeaderOffset: number;
|
|
22
|
+
}
|
|
23
|
+
interface CentralDirectoryLocation {
|
|
24
|
+
offset: number;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where the central directory is, given the tail of the file.
|
|
29
|
+
*
|
|
30
|
+
* `tailStart` is that tail's offset in the whole archive, so a caller that
|
|
31
|
+
* fetched only the last few kilobytes still gets absolute offsets back.
|
|
32
|
+
*/
|
|
33
|
+
export declare function findCentralDirectory(tail: Buffer, tailStart?: number): CentralDirectoryLocation;
|
|
34
|
+
/**
|
|
35
|
+
* The entries a central directory declares.
|
|
36
|
+
*
|
|
37
|
+
* Sizes come from HERE and never from a local header: every Go module zip sets
|
|
38
|
+
* the data-descriptor flag, which zeroes the size and CRC fields in the local
|
|
39
|
+
* header and moves the real values to a trailer after the data.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseCentralDirectory(cd: Buffer): ZipEntry[];
|
|
42
|
+
/**
|
|
43
|
+
* Where an entry's compressed bytes start, read from its LOCAL header.
|
|
44
|
+
*
|
|
45
|
+
* The local header's name and extra lengths are its own and may differ from the
|
|
46
|
+
* central directory's — deriving this from the central record produces an offset
|
|
47
|
+
* that is silently a few bytes wrong.
|
|
48
|
+
*/
|
|
49
|
+
export declare function dataOffset(local: Buffer, at: number): number;
|
|
50
|
+
/** Decompress one entry's raw bytes. */
|
|
51
|
+
export declare function inflateEntry(raw: Buffer, method: number): Buffer;
|
|
52
|
+
/** Every entry of an archive held whole in memory. */
|
|
53
|
+
export declare function readZip(archive: Buffer): ZipEntry[];
|
|
54
|
+
/** One entry's decompressed content, from an archive held whole in memory. */
|
|
55
|
+
export declare function readEntry(archive: Buffer, entry: ZipEntry): Buffer;
|
|
56
|
+
/**
|
|
57
|
+
* True for an entry path that would write outside its destination directory.
|
|
58
|
+
* A module zip has never carried one; a malicious archive is the reason to look.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isUnsafeEntryName(name: string): boolean;
|
|
61
|
+
export interface ExtractOptions {
|
|
62
|
+
/** Which entries to write. Directory entries are absent from a module zip. */
|
|
63
|
+
filter?: (entry: ZipEntry) => boolean;
|
|
64
|
+
/** Path segments to drop from the front of every name, for a prefixed archive. */
|
|
65
|
+
strip?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Write an in-memory archive's entries under `dest`. Returns what was written. */
|
|
68
|
+
export declare function extractZip(archive: Buffer, dest: string, options?: ExtractOptions): string[];
|
|
69
|
+
/** Fetch `[start, end]` inclusive, the way an HTTP Range request is written. */
|
|
70
|
+
export type RangeFetch = (start: number, end: number) => Promise<Buffer>;
|
|
71
|
+
/**
|
|
72
|
+
* The entries of a remote archive, read from its tail alone.
|
|
73
|
+
*
|
|
74
|
+
* Two requests: the tail, then the central directory. `size` is the archive's
|
|
75
|
+
* total length, which a HEAD or the first ranged response reports.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readRemoteZip(size: number, fetchRange: RangeFetch): Promise<ZipEntry[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Fetch some entries of a remote archive, coalescing neighbours into one request.
|
|
80
|
+
*
|
|
81
|
+
* Zip entries are laid out in central-directory order, and the entries of one
|
|
82
|
+
* directory are almost always adjacent, so a package's files come back as a
|
|
83
|
+
* single contiguous read.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readRemoteEntries(entries: readonly ZipEntry[], fetchRange: RangeFetch): Promise<Map<string, Buffer>>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zip — reading a zip archive in-process, whole or a slice at a time.
|
|
3
|
+
*
|
|
4
|
+
* Go module zips are why this exists. `unzip` is absent from a default Windows
|
|
5
|
+
* install and GNU tar cannot read zip at all, so shelling out behaves
|
|
6
|
+
* differently on every machine the agent runs on.
|
|
7
|
+
*
|
|
8
|
+
* The slice half is what makes the Go standard library affordable. It ships
|
|
9
|
+
* inside an 83 MB toolchain archive of which any one package is under a
|
|
10
|
+
* megabyte, and a reader that can pull the central directory and then only the
|
|
11
|
+
* entries it wants leaves the other 82 MB on the server.
|
|
12
|
+
*/
|
|
13
|
+
import { inflateRawSync } from 'node:zlib';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
const EOCD_SIG = 0x06054b50;
|
|
17
|
+
const ZIP64_EOCD_SIG = 0x06064b50;
|
|
18
|
+
const ZIP64_LOCATOR_SIG = 0x07064b50;
|
|
19
|
+
const CDFH_SIG = 0x02014b50;
|
|
20
|
+
const EOCD_SIZE = 22;
|
|
21
|
+
const ZIP64_LOCATOR_SIZE = 20;
|
|
22
|
+
const LOCAL_HEADER_FIXED = 30;
|
|
23
|
+
/** A 32-bit field carrying this asks to be read from the zip64 extra field. */
|
|
24
|
+
const ZIP64_SENTINEL = 0xffffffff;
|
|
25
|
+
/** The comment is a 16-bit length, so the EOCD starts no further back than this. */
|
|
26
|
+
export const EOCD_SEARCH_SPAN = 0xffff + EOCD_SIZE;
|
|
27
|
+
const STORED = 0;
|
|
28
|
+
const DEFLATED = 8;
|
|
29
|
+
/**
|
|
30
|
+
* Where the central directory is, given the tail of the file.
|
|
31
|
+
*
|
|
32
|
+
* `tailStart` is that tail's offset in the whole archive, so a caller that
|
|
33
|
+
* fetched only the last few kilobytes still gets absolute offsets back.
|
|
34
|
+
*/
|
|
35
|
+
export function findCentralDirectory(tail, tailStart = 0) {
|
|
36
|
+
const eocd = lastIndexOfSignature(tail, EOCD_SIG);
|
|
37
|
+
if (eocd < 0)
|
|
38
|
+
throw new Error('not a zip archive: no end-of-central-directory record');
|
|
39
|
+
const size = tail.readUInt32LE(eocd + 12);
|
|
40
|
+
const offset = tail.readUInt32LE(eocd + 16);
|
|
41
|
+
if (size !== ZIP64_SENTINEL && offset !== ZIP64_SENTINEL)
|
|
42
|
+
return { offset, size };
|
|
43
|
+
const locator = eocd - ZIP64_LOCATOR_SIZE;
|
|
44
|
+
if (locator < 0 || tail.readUInt32LE(locator) !== ZIP64_LOCATOR_SIG) {
|
|
45
|
+
throw new Error('zip64 archive without a zip64 locator');
|
|
46
|
+
}
|
|
47
|
+
const zip64Eocd = Number(tail.readBigUInt64LE(locator + 8)) - tailStart;
|
|
48
|
+
if (zip64Eocd < 0 || tail.readUInt32LE(zip64Eocd) !== ZIP64_EOCD_SIG) {
|
|
49
|
+
throw new Error('zip64 end-of-central-directory record is outside the read span');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
size: Number(tail.readBigUInt64LE(zip64Eocd + 40)),
|
|
53
|
+
offset: Number(tail.readBigUInt64LE(zip64Eocd + 48))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function lastIndexOfSignature(buf, sig) {
|
|
57
|
+
for (let i = buf.length - 4; i >= 0; i--) {
|
|
58
|
+
if (buf.readUInt32LE(i) === sig)
|
|
59
|
+
return i;
|
|
60
|
+
}
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The entries a central directory declares.
|
|
65
|
+
*
|
|
66
|
+
* Sizes come from HERE and never from a local header: every Go module zip sets
|
|
67
|
+
* the data-descriptor flag, which zeroes the size and CRC fields in the local
|
|
68
|
+
* header and moves the real values to a trailer after the data.
|
|
69
|
+
*/
|
|
70
|
+
export function parseCentralDirectory(cd) {
|
|
71
|
+
const entries = [];
|
|
72
|
+
let at = 0;
|
|
73
|
+
while (at + 46 <= cd.length && cd.readUInt32LE(at) === CDFH_SIG) {
|
|
74
|
+
const nameLen = cd.readUInt16LE(at + 28);
|
|
75
|
+
const extraLen = cd.readUInt16LE(at + 30);
|
|
76
|
+
const commentLen = cd.readUInt16LE(at + 32);
|
|
77
|
+
const entry = {
|
|
78
|
+
name: cd.toString('utf8', at + 46, at + 46 + nameLen),
|
|
79
|
+
method: cd.readUInt16LE(at + 10),
|
|
80
|
+
compressedSize: cd.readUInt32LE(at + 20),
|
|
81
|
+
uncompressedSize: cd.readUInt32LE(at + 24),
|
|
82
|
+
localHeaderOffset: cd.readUInt32LE(at + 42)
|
|
83
|
+
};
|
|
84
|
+
applyZip64Extra(entry, cd.subarray(at + 46 + nameLen, at + 46 + nameLen + extraLen));
|
|
85
|
+
entries.push(entry);
|
|
86
|
+
at += 46 + nameLen + extraLen + commentLen;
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Zip64 widens only the fields that overflowed, and packs them in that order.
|
|
92
|
+
* Reading a fixed layout would misplace every value on an entry that overflowed
|
|
93
|
+
* one field but not the one before it.
|
|
94
|
+
*/
|
|
95
|
+
function applyZip64Extra(entry, extra) {
|
|
96
|
+
let at = 0;
|
|
97
|
+
while (at + 4 <= extra.length) {
|
|
98
|
+
const id = extra.readUInt16LE(at);
|
|
99
|
+
const size = extra.readUInt16LE(at + 2);
|
|
100
|
+
if (id !== 0x0001) {
|
|
101
|
+
at += 4 + size;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let field = at + 4;
|
|
105
|
+
const next = () => {
|
|
106
|
+
const v = Number(extra.readBigUInt64LE(field));
|
|
107
|
+
field += 8;
|
|
108
|
+
return v;
|
|
109
|
+
};
|
|
110
|
+
if (entry.uncompressedSize === ZIP64_SENTINEL)
|
|
111
|
+
entry.uncompressedSize = next();
|
|
112
|
+
if (entry.compressedSize === ZIP64_SENTINEL)
|
|
113
|
+
entry.compressedSize = next();
|
|
114
|
+
if (entry.localHeaderOffset === ZIP64_SENTINEL)
|
|
115
|
+
entry.localHeaderOffset = next();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Where an entry's compressed bytes start, read from its LOCAL header.
|
|
121
|
+
*
|
|
122
|
+
* The local header's name and extra lengths are its own and may differ from the
|
|
123
|
+
* central directory's — deriving this from the central record produces an offset
|
|
124
|
+
* that is silently a few bytes wrong.
|
|
125
|
+
*/
|
|
126
|
+
export function dataOffset(local, at) {
|
|
127
|
+
return at + LOCAL_HEADER_FIXED + local.readUInt16LE(at + 26) + local.readUInt16LE(at + 28);
|
|
128
|
+
}
|
|
129
|
+
/** Decompress one entry's raw bytes. */
|
|
130
|
+
export function inflateEntry(raw, method) {
|
|
131
|
+
if (method === STORED)
|
|
132
|
+
return raw;
|
|
133
|
+
if (method === DEFLATED)
|
|
134
|
+
return inflateRawSync(raw);
|
|
135
|
+
throw new Error(`unsupported zip compression method ${method}`);
|
|
136
|
+
}
|
|
137
|
+
/** Every entry of an archive held whole in memory. */
|
|
138
|
+
export function readZip(archive) {
|
|
139
|
+
const tailStart = Math.max(0, archive.length - EOCD_SEARCH_SPAN);
|
|
140
|
+
const { offset, size } = findCentralDirectory(archive.subarray(tailStart), tailStart);
|
|
141
|
+
return parseCentralDirectory(archive.subarray(offset, offset + size));
|
|
142
|
+
}
|
|
143
|
+
/** One entry's decompressed content, from an archive held whole in memory. */
|
|
144
|
+
export function readEntry(archive, entry) {
|
|
145
|
+
const start = dataOffset(archive, entry.localHeaderOffset);
|
|
146
|
+
return inflateEntry(archive.subarray(start, start + entry.compressedSize), entry.method);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* True for an entry path that would write outside its destination directory.
|
|
150
|
+
* A module zip has never carried one; a malicious archive is the reason to look.
|
|
151
|
+
*/
|
|
152
|
+
export function isUnsafeEntryName(name) {
|
|
153
|
+
if (name === '' || path.isAbsolute(name) || /^[A-Za-z]:/.test(name))
|
|
154
|
+
return true;
|
|
155
|
+
return name.split(/[/\\]/).some(seg => seg === '..');
|
|
156
|
+
}
|
|
157
|
+
/** Write an in-memory archive's entries under `dest`. Returns what was written. */
|
|
158
|
+
export function extractZip(archive, dest, options = {}) {
|
|
159
|
+
const written = [];
|
|
160
|
+
for (const entry of readZip(archive)) {
|
|
161
|
+
if (entry.name.endsWith('/'))
|
|
162
|
+
continue;
|
|
163
|
+
if (options.filter && !options.filter(entry))
|
|
164
|
+
continue;
|
|
165
|
+
if (isUnsafeEntryName(entry.name))
|
|
166
|
+
continue;
|
|
167
|
+
const rel = stripSegments(entry.name, options.strip ?? 0);
|
|
168
|
+
if (rel === null)
|
|
169
|
+
continue;
|
|
170
|
+
const target = path.join(dest, rel);
|
|
171
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
172
|
+
fs.writeFileSync(target, readEntry(archive, entry));
|
|
173
|
+
written.push(target);
|
|
174
|
+
}
|
|
175
|
+
return written;
|
|
176
|
+
}
|
|
177
|
+
function stripSegments(name, count) {
|
|
178
|
+
if (count === 0)
|
|
179
|
+
return name;
|
|
180
|
+
const parts = name.split('/');
|
|
181
|
+
return parts.length > count ? parts.slice(count).join('/') : null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The entries of a remote archive, read from its tail alone.
|
|
185
|
+
*
|
|
186
|
+
* Two requests: the tail, then the central directory. `size` is the archive's
|
|
187
|
+
* total length, which a HEAD or the first ranged response reports.
|
|
188
|
+
*/
|
|
189
|
+
export async function readRemoteZip(size, fetchRange) {
|
|
190
|
+
const tailStart = Math.max(0, size - EOCD_SEARCH_SPAN);
|
|
191
|
+
const tail = await fetchRange(tailStart, size - 1);
|
|
192
|
+
const cd = findCentralDirectory(tail, tailStart);
|
|
193
|
+
const held = cd.offset - tailStart;
|
|
194
|
+
const bytes = held >= 0 && held + cd.size <= tail.length ?
|
|
195
|
+
tail.subarray(held, held + cd.size)
|
|
196
|
+
: await fetchRange(cd.offset, cd.offset + cd.size - 1);
|
|
197
|
+
return parseCentralDirectory(bytes);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Room for a local header whose name and extra fields we have not read yet.
|
|
201
|
+
* Go's zip writer emits no local extra field, so this is slack, not a guess at a
|
|
202
|
+
* real value — an entry that overruns it is re-fetched exactly.
|
|
203
|
+
*/
|
|
204
|
+
const LOCAL_HEADER_SLACK = 4096;
|
|
205
|
+
/**
|
|
206
|
+
* A gap worth paying for rather than opening a second connection. Well under the
|
|
207
|
+
* bytes a TCP slow-start ramp wastes on an extra round trip.
|
|
208
|
+
*/
|
|
209
|
+
const COALESCE_GAP = 64 * 1024;
|
|
210
|
+
/**
|
|
211
|
+
* Fetch some entries of a remote archive, coalescing neighbours into one request.
|
|
212
|
+
*
|
|
213
|
+
* Zip entries are laid out in central-directory order, and the entries of one
|
|
214
|
+
* directory are almost always adjacent, so a package's files come back as a
|
|
215
|
+
* single contiguous read.
|
|
216
|
+
*/
|
|
217
|
+
export async function readRemoteEntries(entries, fetchRange) {
|
|
218
|
+
const out = new Map();
|
|
219
|
+
const ordered = [...entries].sort((a, b) => a.localHeaderOffset - b.localHeaderOffset);
|
|
220
|
+
for (const span of coalesce(ordered)) {
|
|
221
|
+
const buf = await fetchRange(span.start, span.end);
|
|
222
|
+
for (const entry of span.entries) {
|
|
223
|
+
const at = entry.localHeaderOffset - span.start;
|
|
224
|
+
const from = dataOffset(buf, at);
|
|
225
|
+
const to = from + entry.compressedSize;
|
|
226
|
+
const raw = to <= buf.length ? buf.subarray(from, to) : await refetch(entry, fetchRange);
|
|
227
|
+
out.set(entry.name, inflateEntry(raw, entry.method));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
async function refetch(entry, fetchRange) {
|
|
233
|
+
const head = await fetchRange(entry.localHeaderOffset, entry.localHeaderOffset + LOCAL_HEADER_FIXED - 1);
|
|
234
|
+
const start = dataOffset(head, 0) + entry.localHeaderOffset;
|
|
235
|
+
return fetchRange(start, start + entry.compressedSize - 1);
|
|
236
|
+
}
|
|
237
|
+
function coalesce(ordered) {
|
|
238
|
+
const spans = [];
|
|
239
|
+
for (const entry of ordered) {
|
|
240
|
+
const start = entry.localHeaderOffset;
|
|
241
|
+
const end = start + LOCAL_HEADER_FIXED + LOCAL_HEADER_SLACK + entry.compressedSize - 1;
|
|
242
|
+
const last = spans[spans.length - 1];
|
|
243
|
+
if (last && start - last.end <= COALESCE_GAP) {
|
|
244
|
+
last.end = Math.max(last.end, end);
|
|
245
|
+
last.entries.push(entry);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
spans.push({ start, end, entries: [entry] });
|
|
249
|
+
}
|
|
250
|
+
return spans;
|
|
251
|
+
}
|
|
@@ -283,7 +283,7 @@ export async function acquirePackage(input) {
|
|
|
283
283
|
const pin = declaredRange ?
|
|
284
284
|
{ source: 'declared-range', range: declaredRange, asked }
|
|
285
285
|
: { source: 'npm-latest', asked };
|
|
286
|
-
const install = await profile.acquire(asked, declaredRange, io);
|
|
286
|
+
const install = await profile.acquire(asked, declaredRange, cwd, io);
|
|
287
287
|
if (!install.success) {
|
|
288
288
|
return { ok: false, stage: 'install', stderr: install.stderr, pin, asked };
|
|
289
289
|
}
|
|
@@ -18,7 +18,7 @@ import { resolvePackage, type ResolvedPackage } from './docs-resolve.js';
|
|
|
18
18
|
import { npmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
19
19
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
20
20
|
import type { ExportGap } from './export-gap.js';
|
|
21
|
-
export type EcosystemId = 'npm' | 'cargo' | 'hackage';
|
|
21
|
+
export type EcosystemId = 'npm' | 'cargo' | 'hackage' | 'go';
|
|
22
22
|
/**
|
|
23
23
|
* Every filesystem, process and network reach a row is allowed. Rows read no
|
|
24
24
|
* environment and call no global directly, so a test injects a fake registry and
|
|
@@ -33,6 +33,10 @@ export interface EcosystemIo {
|
|
|
33
33
|
cargoHome: string;
|
|
34
34
|
/** Every directory cabal may have filed a downloaded tarball under. */
|
|
35
35
|
cabalPackageDirs: readonly string[];
|
|
36
|
+
/** Root of the Go module cache — `GOMODCACHE`, or its default. */
|
|
37
|
+
goModCache: string;
|
|
38
|
+
/** A local Go installation, whose `src/` is the standard library. */
|
|
39
|
+
goroot: string | undefined;
|
|
36
40
|
signal?: AbortSignal | undefined;
|
|
37
41
|
}
|
|
38
42
|
/**
|
|
@@ -63,8 +67,14 @@ export interface EcosystemProfile {
|
|
|
63
67
|
resolve: (name: string, cwd: string, io: EcosystemIo) => ResolvedPackage;
|
|
64
68
|
/** The version range the project pins this package to, if it pins one. */
|
|
65
69
|
declaredRange: (name: string, cwd: string) => string | null;
|
|
66
|
-
/**
|
|
67
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Fetch a package that is not on disk into `io.modulesDir`.
|
|
72
|
+
*
|
|
73
|
+
* `cwd` is the project, not the destination: Go reads its `go` directive to
|
|
74
|
+
* pick which toolchain's standard library an answer should describe, and to
|
|
75
|
+
* tell `net/http` from a dotless module's own `myapp/internal/db`.
|
|
76
|
+
*/
|
|
77
|
+
acquire: (name: string, range: string | null, cwd: string, io: EcosystemIo) => Promise<AcquireResult>;
|
|
68
78
|
/**
|
|
69
79
|
* A second resolution hop, for ecosystems where the package that ships the
|
|
70
80
|
* documented surface is not the one that was asked for.
|
|
@@ -122,6 +132,15 @@ export interface EcosystemProfile {
|
|
|
122
132
|
commentPrefix: string;
|
|
123
133
|
/** Directories the surface walk never descends into: tests, build output. */
|
|
124
134
|
skipDirs: readonly string[];
|
|
135
|
+
/**
|
|
136
|
+
* Narrow the walked files to one copy of each declaration.
|
|
137
|
+
*
|
|
138
|
+
* Two ecosystems ship the same API twice in one package: npm as `.d.ts` and
|
|
139
|
+
* `.d.cts` twins, Go as build-tag variants of one file. Both cost the
|
|
140
|
+
* retrieval budget for text the reader already has, and neither is
|
|
141
|
+
* separable downstream — same identifiers, same package, same version.
|
|
142
|
+
*/
|
|
143
|
+
selectFiles?: (files: readonly string[], pkg: ResolvedPackage) => string[];
|
|
125
144
|
/** What this ecosystem's packages ship, for a "there is nothing to read" answer. */
|
|
126
145
|
surfaceLabel: string;
|
|
127
146
|
/**
|
|
@@ -167,6 +186,7 @@ export declare const ECOSYSTEMS: {
|
|
|
167
186
|
readonly npm: EcosystemProfile;
|
|
168
187
|
readonly cargo: EcosystemProfile;
|
|
169
188
|
readonly hackage: EcosystemProfile;
|
|
189
|
+
readonly go: EcosystemProfile;
|
|
170
190
|
};
|
|
171
191
|
/** Which ecosystems `cwd` looks like a project of, in roster order. */
|
|
172
192
|
export declare function detectEcosystems(cwd: string, roster?: readonly EcosystemProfile[]): EcosystemId[];
|
|
@@ -20,9 +20,12 @@ import * as path from 'node:path';
|
|
|
20
20
|
import { runAutoInstall, findDeclaredRange, extractParentPackage, resolveTypeSourceForDocs, getDocsModulesDir } from './docs-core.js';
|
|
21
21
|
import { resolvePackage, isDtsFile, isValidModuleName } from './docs-resolve.js';
|
|
22
22
|
import { DECL_SPLIT_RE, MEMBER_SPLIT_RE } from './docs-chunk.js';
|
|
23
|
+
import { dropParallelDeclarations } from './docs-index.js';
|
|
23
24
|
import { npmVersionLookup } from './npm-version.js';
|
|
24
25
|
import { resolveCrate, cratesLatest, crateTarballUrl, crateOf, isValidCrateName, isRustFile, lockedVersion, rustSurface, cargoProjectName, childDirs, lockedDeps, manifestCrates, cargoExportGap, cargoContentFingerprint, cargoSupplementCandidates, CARGO_DECL_SPLIT_RE, CARGO_MEMBER_SPLIT_RE } from './eco-cargo.js';
|
|
25
26
|
import { resolveHackage, hackageLatest, hackageVersion, hackageTarballUrl, hackageExtractDir, hackageProjectName, supplementCandidates, hackageExportGap, hackageContentFingerprint, findCabalTarball, cachedVersions, resolvedVersions, manifestPackages, isValidHackageName, isHaskellFile, haskellSurface, HACKAGE_DECL_SPLIT_RE, HACKAGE_SKIP_DIRS, HACKAGE_MEMBER_SPLIT_RE } from './eco-hackage.js';
|
|
27
|
+
import { detectGo, isValidImportPath, resolveGoPackage, acquireGoModule, goDeclaredVersion, goLatest, goProjectName, goDeclaredDeps, goManifestDeps, isGoFile, selectBuildVariants, selectOwnPackage, defaultGoModCache, goContentFingerprintParts } from './eco-go.js';
|
|
28
|
+
import { goSurface, goContentFingerprint, GO_DECL_SPLIT_RE, GO_MEMBER_SPLIT_RE } from './go-surface.js';
|
|
26
29
|
import { runChild } from '../shared/child-process.js';
|
|
27
30
|
/**
|
|
28
31
|
* Is any of `names` present at `cwd` or above it?
|
|
@@ -76,6 +79,8 @@ export function defaultEcosystemIo(overrides = {}) {
|
|
|
76
79
|
modulesDir: getDocsModulesDir(),
|
|
77
80
|
cargoHome: process.env.CARGO_HOME?.trim() || path.join(os.homedir(), '.cargo'),
|
|
78
81
|
cabalPackageDirs: defaultCabalPackageDirs(),
|
|
82
|
+
goModCache: defaultGoModCache(),
|
|
83
|
+
goroot: process.env.GOROOT?.trim() || undefined,
|
|
79
84
|
...overrides
|
|
80
85
|
};
|
|
81
86
|
}
|
|
@@ -122,7 +127,7 @@ export function npmProfile(hooks = {}) {
|
|
|
122
127
|
parentPackage: extractParentPackage,
|
|
123
128
|
resolve: (name, cwd) => resolve(name, cwd),
|
|
124
129
|
declaredRange: findDeclaredRange,
|
|
125
|
-
acquire: (name, range, io) => runAutoInstall(io.spawn, name, {
|
|
130
|
+
acquire: (name, range, _cwd, io) => runAutoInstall(io.spawn, name, {
|
|
126
131
|
signal: io.signal,
|
|
127
132
|
versionRange: range ?? undefined
|
|
128
133
|
}),
|
|
@@ -139,6 +144,7 @@ export function npmProfile(hooks = {}) {
|
|
|
139
144
|
commentPrefix: '//',
|
|
140
145
|
// A nested node_modules is another package's surface, never this one's.
|
|
141
146
|
skipDirs: ['node_modules'],
|
|
147
|
+
selectFiles: dropParallelDeclarations,
|
|
142
148
|
surfaceLabel: '.d.ts files or README',
|
|
143
149
|
packageSubject: 'an npm package',
|
|
144
150
|
projectGlobs: ['*.ts', '*.tsx'],
|
|
@@ -259,7 +265,7 @@ const cargoProfile = {
|
|
|
259
265
|
resolve: (name, cwd, io) => resolveCrate(name, cwd, { cargoHome: io.cargoHome, modulesDir: io.modulesDir }),
|
|
260
266
|
// Cargo has already resolved every version; the lock IS the pin.
|
|
261
267
|
declaredRange: (name, cwd) => lockedVersion(name, cwd),
|
|
262
|
-
acquire: async (name, range, io) => {
|
|
268
|
+
acquire: async (name, range, _cwd, io) => {
|
|
263
269
|
// Asked even when the range is known: the download host wants the name as
|
|
264
270
|
// PUBLISHED, and only the API knows whether that is `tokio-util` or
|
|
265
271
|
// `tokio_util`. A null answer falls back to the caller's spelling.
|
|
@@ -293,7 +299,7 @@ const cargoProfile = {
|
|
|
293
299
|
// Not unpacked here. `acquire` reads crates.io for the published
|
|
294
300
|
// spelling, so `tokio-util` and `tokio_util` both land.
|
|
295
301
|
}
|
|
296
|
-
const got = await cargoProfile.acquire(c.name, c.version, io);
|
|
302
|
+
const got = await cargoProfile.acquire(c.name, c.version, cwd, io);
|
|
297
303
|
if (!got.success)
|
|
298
304
|
continue;
|
|
299
305
|
try {
|
|
@@ -359,7 +365,7 @@ const hackageProfile = {
|
|
|
359
365
|
parentPackage: name => name,
|
|
360
366
|
resolve: (name, cwd, io) => resolveHackage(name, cwd, { modulesDir: io.modulesDir }),
|
|
361
367
|
declaredRange: (name, cwd) => hackageVersion(name, cwd),
|
|
362
|
-
acquire: async (name, range, io) => {
|
|
368
|
+
acquire: async (name, range, _cwd, io) => {
|
|
363
369
|
const dir = hackageExtractDir(io.modulesDir);
|
|
364
370
|
const version = range
|
|
365
371
|
?? cachedVersions(name, io.cabalPackageDirs).pop()
|
|
@@ -413,7 +419,7 @@ const hackageProfile = {
|
|
|
413
419
|
// Not unpacked yet. `acquire` prefers the tarball cabal already
|
|
414
420
|
// downloaded as a dependency, so this is local work, not a fetch.
|
|
415
421
|
}
|
|
416
|
-
const got = await hackageProfile.acquire(c.name, c.version, io);
|
|
422
|
+
const got = await hackageProfile.acquire(c.name, c.version, cwd, io);
|
|
417
423
|
if (!got.success)
|
|
418
424
|
continue;
|
|
419
425
|
try {
|
|
@@ -459,10 +465,57 @@ function hasCabalManifest(cwd) {
|
|
|
459
465
|
return false;
|
|
460
466
|
}
|
|
461
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* A Go package's surface is cut out of `.go` source, and the row keeps `internal`
|
|
470
|
+
* out of it: Go's compiler enforces that no package outside the module may
|
|
471
|
+
* import one, so an `internal` tree is not API however exported its names are.
|
|
472
|
+
*/
|
|
473
|
+
const goProfile = {
|
|
474
|
+
id: 'go',
|
|
475
|
+
why: 'A Go IMPORT PATH is not a module — gin/binding is served by gin, while '
|
|
476
|
+
+ 'aws-sdk-go-v2/service/s3 is its own module — so the boundary is found by '
|
|
477
|
+
+ 'asking the proxy for the longest prefix that resolves, longest first. '
|
|
478
|
+
+ 'Versions come from go.mod, which since Go 1.17 IS the lockfile: its '
|
|
479
|
+
+ 'indirect block is the resolved closure, so nothing walks a graph and '
|
|
480
|
+
+ 'go.sum is never read. The standard library is not on the proxy at all, '
|
|
481
|
+
+ 'and is read from a local GOROOT or sliced out of the toolchain archive.',
|
|
482
|
+
registryLabel: 'proxy.golang.org',
|
|
483
|
+
manifestLabel: 'go.mod',
|
|
484
|
+
detect: detectGo,
|
|
485
|
+
isValidName: isValidImportPath,
|
|
486
|
+
// Identity, because the module a path belongs to cannot be found without the
|
|
487
|
+
// network. Everything that needs the real module — resolve, acquire, latest —
|
|
488
|
+
// is handed `io` and works it out there.
|
|
489
|
+
parentPackage: name => name,
|
|
490
|
+
resolve: (name, cwd, io) => resolveGoPackage(name, cwd, {
|
|
491
|
+
goModCache: io.goModCache,
|
|
492
|
+
modulesDir: io.modulesDir,
|
|
493
|
+
goroot: io.goroot
|
|
494
|
+
}),
|
|
495
|
+
declaredRange: goDeclaredVersion,
|
|
496
|
+
acquire: (name, range, cwd, io) => acquireGoModule(name, range, cwd, { goModCache: io.goModCache, modulesDir: io.modulesDir, goroot: io.goroot }, io.fetch, io.signal),
|
|
497
|
+
latest: (name, io) => goLatest(name, io.fetch, io.signal),
|
|
498
|
+
isSurfaceFile: isGoFile,
|
|
499
|
+
surface: content => goSurface(content),
|
|
500
|
+
contentFingerprint: () => [goContentFingerprint(), ...goContentFingerprintParts()].join('\u0000'),
|
|
501
|
+
declSplitRe: GO_DECL_SPLIT_RE,
|
|
502
|
+
memberSplitRe: GO_MEMBER_SPLIT_RE,
|
|
503
|
+
typeKeywords: ['type', 'struct', 'interface', 'func'],
|
|
504
|
+
commentPrefix: '//',
|
|
505
|
+
skipDirs: ['testdata', 'examples', 'vendor', 'internal', '.git'],
|
|
506
|
+
selectFiles: (files, pkg) => selectBuildVariants(selectOwnPackage(files, pkg.root, pkg.name, pkg.version)),
|
|
507
|
+
surfaceLabel: '.go source or README',
|
|
508
|
+
packageSubject: 'a Go package',
|
|
509
|
+
projectGlobs: ['*.go'],
|
|
510
|
+
projectName: goProjectName,
|
|
511
|
+
declaredDeps: goDeclaredDeps,
|
|
512
|
+
manifestDeps: goManifestDeps
|
|
513
|
+
};
|
|
462
514
|
export const ECOSYSTEMS = {
|
|
463
515
|
npm: npmProfile(),
|
|
464
516
|
cargo: cargoProfile,
|
|
465
|
-
hackage: hackageProfile
|
|
517
|
+
hackage: hackageProfile,
|
|
518
|
+
go: goProfile
|
|
466
519
|
};
|
|
467
520
|
/** Which ecosystems `cwd` looks like a project of, in roster order. */
|
|
468
521
|
export function detectEcosystems(cwd, roster = Object.values(ECOSYSTEMS)) {
|
|
@@ -35,4 +35,17 @@ export interface IndexResult {
|
|
|
35
35
|
* have said so.
|
|
36
36
|
*/
|
|
37
37
|
export declare function chunkerFingerprint(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Drop a `.d.cts` / `.d.mts` that sits beside a `.d.ts` of the same name.
|
|
40
|
+
*
|
|
41
|
+
* Modern npm packages ship parallel declarations for ESM and CJS: the same API
|
|
42
|
+
* written twice. zod 4.5.4 indexed to 2565 chunks over 1215 distinct bodies,
|
|
43
|
+
* 1280 of them from `.d.cts`; hono, which ships none, had 704 distinct of 708.
|
|
44
|
+
* The cost is the eight-chunk retrieval budget — half of it can go to text the
|
|
45
|
+
* reader already has.
|
|
46
|
+
*
|
|
47
|
+
* The sibling test, not a blanket ban on the extensions: a package shipping only
|
|
48
|
+
* `.d.cts` still has to be readable, and all 123 of zod's had a `.d.ts` twin.
|
|
49
|
+
*/
|
|
50
|
+
export declare function dropParallelDeclarations(files: readonly string[]): string[];
|
|
38
51
|
export declare function ensureIndexed(cache: CacheHandle, pkg: ResolvedPackage, profile?: EcosystemProfile, supplements?: readonly ResolvedPackage[]): IndexResult;
|
|
@@ -59,7 +59,7 @@ function computeContentHash(pkg, profile, supplements = []) {
|
|
|
59
59
|
hash.update(ZERO_SEP);
|
|
60
60
|
// Source text, the same trick as `declSplitRe.source`: the fingerprint moves
|
|
61
61
|
// whenever the selection rule does, with nothing to remember to bump.
|
|
62
|
-
hash.update(Buffer.from(`${String(profile.isSurfaceFile)}\u0000${String(
|
|
62
|
+
hash.update(Buffer.from(`${String(profile.isSurfaceFile)}\u0000${String(profile.selectFiles)}`
|
|
63
63
|
+ `\u0000${String(dropDeadMajors)}`, 'utf8'));
|
|
64
64
|
hash.update(ZERO_SEP);
|
|
65
65
|
// The extractor and the writer, by source. Surfacing only `pkg.entry` below
|
|
@@ -144,7 +144,7 @@ function walkSurface(root, profile) {
|
|
|
144
144
|
* The sibling test, not a blanket ban on the extensions: a package shipping only
|
|
145
145
|
* `.d.cts` still has to be readable, and all 123 of zod's had a `.d.ts` twin.
|
|
146
146
|
*/
|
|
147
|
-
function dropParallelDeclarations(files) {
|
|
147
|
+
export function dropParallelDeclarations(files) {
|
|
148
148
|
const esm = new Set(files.filter(f => f.endsWith('.d.ts')).map(f => f.slice(0, -'.d.ts'.length)));
|
|
149
149
|
return files.filter(f => {
|
|
150
150
|
const base = /\.d\.[cm]ts$/.exec(f) ? f.slice(0, -'.d.cts'.length) : null;
|
|
@@ -181,7 +181,7 @@ function collectFiles(pkg, profile) {
|
|
|
181
181
|
const walked = walkSurface(pkg.root, profile);
|
|
182
182
|
const surface = dropDeadMajors(walked, pkg.root, pkg.version);
|
|
183
183
|
return {
|
|
184
|
-
surface: profile.
|
|
184
|
+
surface: profile.selectFiles ? profile.selectFiles(surface, pkg) : surface,
|
|
185
185
|
readme: pkg.readme
|
|
186
186
|
};
|
|
187
187
|
}
|