@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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * go-stdlib — reading `net/http`, `fmt` and the rest of Go's standard library.
3
+ *
4
+ * The standard library is a large share of what anyone asks about Go and it is
5
+ * not on the module proxy: `net/http` and `std` both 404, and
6
+ * `github.com/golang/go` resolves to an archive with ZERO entries under `src/`,
7
+ * because a nested `go.mod` excludes the whole tree.
8
+ *
9
+ * It does ship inside the `golang.org/toolchain` module, which is 83 MB of
10
+ * mostly prebuilt binaries. Downloading that to read one package would be
11
+ * absurd, so this reads the archive's central directory over HTTP Range and then
12
+ * pulls only the entries it wants — 2.2 MB for `net/http`, against 83 MB for the
13
+ * archive. `src/` is identical across GOOS and GOARCH, so the platform is
14
+ * pinned rather than detected.
15
+ *
16
+ * A local toolchain, where there is one, costs nothing at all and matches the
17
+ * version the project actually builds with, so it is tried first.
18
+ */
19
+ /** Where a sliced stdlib is filed, alongside the extracted modules. */
20
+ export declare function stdlibDirName(goVersion: string): string;
21
+ export interface StdlibLocation {
22
+ dir: string;
23
+ version: string;
24
+ }
25
+ /**
26
+ * A local Go installation's copy of the package, if there is one.
27
+ *
28
+ * Free, and exactly the version the project builds against. `GOROOT` is read
29
+ * from the environment rather than from `go env`, so this stays synchronous and
30
+ * usable from `resolve`; discovering an unset `GOROOT` needs a spawn and belongs
31
+ * in the acquire path.
32
+ */
33
+ export declare function findInGoroot(importPath: string, goroot: string | undefined): StdlibLocation | null;
34
+ /** A stdlib package this tool sliced out of a toolchain archive earlier. */
35
+ export declare function findSliced(importPath: string, roots: readonly string[]): StdlibLocation | null;
36
+ interface ToolchainVersion {
37
+ /** The module version, e.g. `v0.0.1-go1.24.12.linux-amd64`. */
38
+ moduleVersion: string;
39
+ /** The Go release it carries, e.g. `go1.24.12`. */
40
+ goVersion: string;
41
+ }
42
+ /**
43
+ * Which toolchain to read, given what the project's `go` directive asks for.
44
+ *
45
+ * The newest patch of the project's own minor version, so the answer describes
46
+ * the API the project compiles against. With no directive, or a version older
47
+ * than any release still published, the newest available stands in.
48
+ */
49
+ export declare function chooseToolchain(available: readonly ToolchainVersion[], goDirective: string | null): ToolchainVersion | null;
50
+ export declare function listToolchains(fetchFn: typeof fetch, signal?: AbortSignal): Promise<ToolchainVersion[]>;
51
+ export interface AcquireStdlibInput {
52
+ importPath: string;
53
+ /** The project's `go` directive, so the answer matches what it builds with. */
54
+ goDirective: string | null;
55
+ /** Where the sliced package tree is written. */
56
+ installDir: string;
57
+ fetchFn: typeof fetch;
58
+ signal?: AbortSignal | undefined;
59
+ }
60
+ /**
61
+ * Slice one standard-library package out of a toolchain archive.
62
+ *
63
+ * Subdirectories come too — `net/http/httptest` and `net/http/httputil` are what
64
+ * a question about `net/http` half the time turns out to be about, and they are
65
+ * adjacent in the archive, so they cost almost nothing on top.
66
+ */
67
+ export declare function acquireStdlibPackage(input: AcquireStdlibInput): Promise<{
68
+ success: boolean;
69
+ installDir: string;
70
+ stderr: string;
71
+ }>;
72
+ export {};
@@ -0,0 +1,210 @@
1
+ /**
2
+ * go-stdlib — reading `net/http`, `fmt` and the rest of Go's standard library.
3
+ *
4
+ * The standard library is a large share of what anyone asks about Go and it is
5
+ * not on the module proxy: `net/http` and `std` both 404, and
6
+ * `github.com/golang/go` resolves to an archive with ZERO entries under `src/`,
7
+ * because a nested `go.mod` excludes the whole tree.
8
+ *
9
+ * It does ship inside the `golang.org/toolchain` module, which is 83 MB of
10
+ * mostly prebuilt binaries. Downloading that to read one package would be
11
+ * absurd, so this reads the archive's central directory over HTTP Range and then
12
+ * pulls only the entries it wants — 2.2 MB for `net/http`, against 83 MB for the
13
+ * archive. `src/` is identical across GOOS and GOARCH, so the platform is
14
+ * pinned rather than detected.
15
+ *
16
+ * A local toolchain, where there is one, costs nothing at all and matches the
17
+ * version the project actually builds with, so it is tried first.
18
+ */
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { readRemoteZip, readRemoteEntries } from '../shared/zip.js';
22
+ import { isGoFile } from './eco-go.js';
23
+ const TOOLCHAIN = 'https://proxy.golang.org/golang.org/toolchain/@v';
24
+ /** `src/` is byte-identical across platforms, so one is as good as another. */
25
+ const PLATFORM = 'linux-amd64';
26
+ const USER_AGENT = 'pi-task (github.com/mjasnikovs/pi-task)';
27
+ /** Where a sliced stdlib is filed, alongside the extracted modules. */
28
+ export function stdlibDirName(goVersion) {
29
+ return `std@${goVersion}`;
30
+ }
31
+ /**
32
+ * A local Go installation's copy of the package, if there is one.
33
+ *
34
+ * Free, and exactly the version the project builds against. `GOROOT` is read
35
+ * from the environment rather than from `go env`, so this stays synchronous and
36
+ * usable from `resolve`; discovering an unset `GOROOT` needs a spawn and belongs
37
+ * in the acquire path.
38
+ */
39
+ export function findInGoroot(importPath, goroot) {
40
+ const root = goroot?.trim();
41
+ if (!root)
42
+ return null;
43
+ const dir = path.join(root, 'src', ...importPath.split('/'));
44
+ if (!fs.existsSync(dir))
45
+ return null;
46
+ const version = fs.readFileSync(path.join(root, 'VERSION'), 'utf8').split('\n')[0].trim();
47
+ return { dir, version: version || 'unknown' };
48
+ }
49
+ /** A stdlib package this tool sliced out of a toolchain archive earlier. */
50
+ export function findSliced(importPath, roots) {
51
+ for (const root of roots) {
52
+ let entries;
53
+ try {
54
+ entries = fs.readdirSync(root).filter(e => e.startsWith('std@'));
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ for (const entry of entries.sort().reverse()) {
60
+ const dir = path.join(root, entry, ...importPath.split('/'));
61
+ if (fs.existsSync(dir))
62
+ return { dir, version: entry.slice('std@'.length) };
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function parseToolchainList(body) {
68
+ const out = [];
69
+ for (const line of body.split('\n')) {
70
+ const match = new RegExp(`^(v[\\d.]+-(go[\\d.]+)\\.${PLATFORM})$`).exec(line.trim());
71
+ if (match)
72
+ out.push({ moduleVersion: match[1], goVersion: match[2] });
73
+ }
74
+ return out.sort((a, b) => compareGoVersions(a.goVersion, b.goVersion));
75
+ }
76
+ /** Numeric ordering, because `go1.9` sorts above `go1.24` as a string. */
77
+ function compareGoVersions(a, b) {
78
+ const parts = (v) => v.replace(/^go/, '').split('.').map(Number);
79
+ const [x, y] = [parts(a), parts(b)];
80
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
81
+ const diff = (x[i] ?? 0) - (y[i] ?? 0);
82
+ if (diff !== 0)
83
+ return diff;
84
+ }
85
+ return 0;
86
+ }
87
+ /**
88
+ * Which toolchain to read, given what the project's `go` directive asks for.
89
+ *
90
+ * The newest patch of the project's own minor version, so the answer describes
91
+ * the API the project compiles against. With no directive, or a version older
92
+ * than any release still published, the newest available stands in.
93
+ */
94
+ export function chooseToolchain(available, goDirective) {
95
+ if (available.length === 0)
96
+ return null;
97
+ const wanted = goDirective?.trim().replace(/^go/, '');
98
+ if (wanted) {
99
+ const minor = wanted.split('.').slice(0, 2).join('.');
100
+ const matching = available.filter(v => v.goVersion.replace(/^go/, '').startsWith(`${minor}.`));
101
+ if (matching.length > 0)
102
+ return matching[matching.length - 1];
103
+ }
104
+ return available[available.length - 1];
105
+ }
106
+ export async function listToolchains(fetchFn, signal) {
107
+ const response = await fetchFn(`${TOOLCHAIN}/list`, {
108
+ headers: { 'user-agent': USER_AGENT },
109
+ ...(signal ? { signal } : {})
110
+ });
111
+ if (!response.ok)
112
+ return [];
113
+ return parseToolchainList(await response.text());
114
+ }
115
+ function toolchainUrl(moduleVersion) {
116
+ return `${TOOLCHAIN}/${moduleVersion}.zip`;
117
+ }
118
+ /**
119
+ * The archive's entry list, cached on disk.
120
+ *
121
+ * Reading it costs 1.6 MB, and every research child that asks about a second
122
+ * stdlib package would pay it again — the list is the same bytes for a given
123
+ * toolchain, so it is written once beside the source it describes.
124
+ */
125
+ async function toolchainEntries(moduleVersion, cacheFile, fetchFn, signal) {
126
+ const cached = readCachedEntries(cacheFile);
127
+ if (cached)
128
+ return cached;
129
+ const url = toolchainUrl(moduleVersion);
130
+ const head = await fetchFn(url, {
131
+ method: 'HEAD',
132
+ redirect: 'follow',
133
+ headers: { 'user-agent': USER_AGENT },
134
+ ...(signal ? { signal } : {})
135
+ });
136
+ const size = Number(head.headers.get('content-length'));
137
+ if (!Number.isFinite(size) || size <= 0) {
138
+ throw new Error(`toolchain ${moduleVersion}: no content length to range over`);
139
+ }
140
+ const entries = await readRemoteZip(size, rangeFetch(url, fetchFn, signal));
141
+ // Only `src` is ever read, and dropping the rest takes the cache from
142
+ // 10,751 entries to a fraction of that.
143
+ const wanted = entries.filter(e => /\/src\//.test(e.name) && isGoFile(e.name));
144
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
145
+ fs.writeFileSync(cacheFile, JSON.stringify({ size, entries: wanted }));
146
+ return wanted;
147
+ }
148
+ function readCachedEntries(cacheFile) {
149
+ try {
150
+ const parsed = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
151
+ return Array.isArray(parsed.entries) && parsed.entries.length > 0 ? parsed.entries : null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function rangeFetch(url, fetchFn, signal) {
158
+ return async (start, end) => {
159
+ const response = await fetchFn(url, {
160
+ headers: { 'user-agent': USER_AGENT, range: `bytes=${start}-${end}` },
161
+ redirect: 'follow',
162
+ ...(signal ? { signal } : {})
163
+ });
164
+ if (!response.ok)
165
+ throw new Error(`range ${start}-${end}: HTTP ${response.status}`);
166
+ return Buffer.from(await response.arrayBuffer());
167
+ };
168
+ }
169
+ /**
170
+ * Slice one standard-library package out of a toolchain archive.
171
+ *
172
+ * Subdirectories come too — `net/http/httptest` and `net/http/httputil` are what
173
+ * a question about `net/http` half the time turns out to be about, and they are
174
+ * adjacent in the archive, so they cost almost nothing on top.
175
+ */
176
+ export async function acquireStdlibPackage(input) {
177
+ const { importPath, installDir, fetchFn, signal } = input;
178
+ try {
179
+ const chosen = chooseToolchain(await listToolchains(fetchFn, signal), input.goDirective);
180
+ if (!chosen) {
181
+ return { success: false, installDir, stderr: 'No Go toolchain is published to slice.' };
182
+ }
183
+ const dest = path.join(installDir, stdlibDirName(chosen.goVersion));
184
+ const entries = await toolchainEntries(chosen.moduleVersion, path.join(installDir, `.${chosen.moduleVersion}.entries.json`), fetchFn, signal);
185
+ const prefix = `/src/${importPath}/`;
186
+ const wanted = entries.filter(e => e.name.includes(prefix));
187
+ if (wanted.length === 0) {
188
+ return {
189
+ success: false,
190
+ installDir,
191
+ stderr: `"${importPath}" is not a package in ${chosen.goVersion}.`
192
+ };
193
+ }
194
+ const bytes = await readRemoteEntries(wanted, rangeFetch(toolchainUrl(chosen.moduleVersion), fetchFn, signal));
195
+ for (const [name, content] of bytes) {
196
+ const rel = name.slice(name.indexOf('/src/') + '/src/'.length);
197
+ const target = path.join(dest, ...rel.split('/'));
198
+ fs.mkdirSync(path.dirname(target), { recursive: true });
199
+ fs.writeFileSync(target, content);
200
+ }
201
+ return { success: true, installDir, stderr: '' };
202
+ }
203
+ catch (err) {
204
+ return {
205
+ success: false,
206
+ installDir,
207
+ stderr: err instanceof Error ? err.message : String(err)
208
+ };
209
+ }
210
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * go-surface — reducing Go source to the API a caller outside the package can
3
+ * reach.
4
+ *
5
+ * Go ships no declarations file, so this is the `.d.ts` a Go package does not
6
+ * have. It is the same job `rustSurface` does in eco-cargo.ts, and it needs the
7
+ * same care, but three things about Go make a straight port wrong.
8
+ *
9
+ * There are no semicolons. A declaration ends where Go's lexer would insert one,
10
+ * which is a property of the last token on the line, so the scanner has to track
11
+ * that token rather than look for a terminator.
12
+ *
13
+ * A signature can hold a balanced brace pair before its body:
14
+ * `func Bind(v interface{}) HandlerFunc {`. Splitting the head at the first `{`,
15
+ * the way the Rust version does, cuts that one at `interface`. The body is the
16
+ * group whose closer ENDS the declaration, not the first group that opens.
17
+ *
18
+ * And a doc comment has no marker at all. Adjacency is the marker: the comment
19
+ * paragraph on the lines directly above, with no blank line between. Every Go
20
+ * file also opens with a licence header, so "keep what came before" reprints the
21
+ * licence into every chunk of every file.
22
+ */
23
+ /**
24
+ * Where a declaration begins. Column 0 only: `goSurface` indents struct fields
25
+ * and interface methods, and an `^\s*` anchor would cut every field into its own
26
+ * chunk. eco-cargo.ts records the same trap.
27
+ */
28
+ export declare const GO_DECL_SPLIT_RE: RegExp;
29
+ /**
30
+ * The same heads indented, plus the keyword-less members only Go has: a struct
31
+ * field is `Name Type`, an interface method is `Name(args) ret`, a grouped const
32
+ * is `Name Type = value`. Reached only when one declaration does not fit a
33
+ * chunk, which `Context` and `IRoutes` in gin both fail to.
34
+ */
35
+ export declare const GO_MEMBER_SPLIT_RE: RegExp;
36
+ export interface GoItem {
37
+ /** Comments and directives between the previous item and this one. */
38
+ pending: string;
39
+ text: string;
40
+ }
41
+ /**
42
+ * Split source into declarations. Works unchanged on a struct body or a const
43
+ * group, whose members obey the same semicolon rule with no keyword in front.
44
+ */
45
+ export declare function splitGoItems(src: string): GoItem[];
46
+ /** True when the first rune of `name` is an uppercase letter. */
47
+ export declare function isExported(name: string): boolean;
48
+ /** The base type of a method's receiver, or null for a plain function. */
49
+ export declare function receiverType(text: string): string | null;
50
+ /**
51
+ * The comment paragraph attached to this declaration, or nothing.
52
+ *
53
+ * Only the last paragraph, and only when it ends on the line directly above —
54
+ * the blank line before a licence header is what separates it from the code.
55
+ */
56
+ export declare function keptPreamble(pending: string): string;
57
+ /**
58
+ * The file's `//go:build` constraint, which applies to every declaration in it.
59
+ *
60
+ * `binding.go` and `binding_nomsgpack.go` declare the same twelve names under
61
+ * opposite constraints, so a reader shown one without the banner cannot tell
62
+ * which build they are reading.
63
+ */
64
+ export declare function buildConstraint(src: string): string | null;
65
+ /**
66
+ * One Go file reduced to the declarations a caller outside the package can use.
67
+ */
68
+ export declare function goSurface(src: string): string;
69
+ /**
70
+ * Everything reachable from {@link goSurface}, by source.
71
+ *
72
+ * `String(fn)` sees one level, and a helper left out of a list like this has
73
+ * three times frozen every cached package on the rule it replaced.
74
+ */
75
+ export declare function goContentFingerprint(): string;