@prometheus-ai/utils 0.5.4 → 0.5.8
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/CHANGELOG.md +14 -0
- package/README.md +36 -0
- package/dist/types/abortable.d.ts +5 -0
- package/dist/types/dirs.d.ts +4 -0
- package/dist/types/env.d.ts +4 -0
- package/dist/types/fetch-retry.d.ts +8 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/logger.d.ts +22 -5
- package/dist/types/loop-phase.d.ts +10 -0
- package/dist/types/module-timer.d.ts +1 -0
- package/dist/types/path-tree.d.ts +76 -0
- package/dist/types/procmgr.d.ts +4 -0
- package/dist/types/runtime-install.d.ts +68 -0
- package/dist/types/tab-spacing.d.ts +16 -0
- package/dist/types/timing-buffer.d.ts +22 -0
- package/package.json +4 -2
- package/src/abortable.ts +86 -1
- package/src/dirs.ts +10 -0
- package/src/env.ts +17 -0
- package/src/fetch-retry.ts +8 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +320 -73
- package/src/loop-phase.ts +49 -0
- package/src/module-timer.ts +148 -0
- package/src/path-tree.ts +147 -0
- package/src/procmgr.ts +1 -1
- package/src/runtime-install.ts +372 -0
- package/src/tab-spacing.ts +51 -0
- package/src/temp.ts +57 -4
- package/src/timing-buffer.ts +47 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-load timing preload.
|
|
3
|
+
*
|
|
4
|
+
* `bun --preload .../module-timer.ts <entry>` installs Bun plugin hooks (only
|
|
5
|
+
* when `PROMETHEUS_TIMING` is set) that record an inclusive module window plus resolved
|
|
6
|
+
* static child edges:
|
|
7
|
+
*
|
|
8
|
+
* onLoad start → appended end marker after the module's top-level body
|
|
9
|
+
*
|
|
10
|
+
* Events are pushed into a process-global buffer that {@link logger.printTimings}
|
|
11
|
+
* drains and renders as a module DAG/tree. Each module row can therefore show
|
|
12
|
+
* both total time and `self` time after subtracting child module intervals.
|
|
13
|
+
*
|
|
14
|
+
* Why a preload (and not a normal import): Bun reads the *entire* statically
|
|
15
|
+
* reachable graph before evaluating any module, so hooks installed from inside
|
|
16
|
+
* that graph cannot observe its own loading — they only catch later dynamically
|
|
17
|
+
* loaded modules. A preload runs first, so it sees the static-import phase that
|
|
18
|
+
* dominates startup.
|
|
19
|
+
*
|
|
20
|
+
* Kept dependency-free on purpose: the sole import is Bun's `plugin`, so this is
|
|
21
|
+
* cheap to preload before pi-utils (and winston) exist. The buffer is shared with
|
|
22
|
+
* the logger via a registry Symbol so neither side needs to import the other.
|
|
23
|
+
*
|
|
24
|
+
* **What is measured:** an inclusive per-module window. `onLoad` stamps the
|
|
25
|
+
* start before reading source; the returned source has a tiny marker appended at
|
|
26
|
+
* the end of the module. That marker runs after Bun parses/transpiles the module
|
|
27
|
+
* and after any top-level await in that module completes, so the duration
|
|
28
|
+
* includes read + parse/transpile + dependency wait + top-level execution/TLA.
|
|
29
|
+
* If a module throws before its final statement, no end marker is recorded.
|
|
30
|
+
*
|
|
31
|
+
* **Tree shape:** `onResolve` observes importer → specifier edges and resolves
|
|
32
|
+
* them with `Bun.resolveSync` without taking over Bun's real resolution. The
|
|
33
|
+
* logger renders these edges as a DAG/tree and computes module `self` time by
|
|
34
|
+
* subtracting the union of child intervals, avoiding misleading flat inclusive
|
|
35
|
+
* totals.
|
|
36
|
+
*
|
|
37
|
+
* **Coverage limits:**
|
|
38
|
+
* - TS/TSX only — intercepting `node_modules` CJS `.js`/`.cjs` and forcing ESM
|
|
39
|
+
* breaks their default-export detection, so they are left to Bun's default path.
|
|
40
|
+
* - **Dev runs only.** In the compiled `prometheus` binary every module is pre-bundled
|
|
41
|
+
* into bunfs, so `onLoad` never fires; profile with a `bun --preload` dev run.
|
|
42
|
+
*/
|
|
43
|
+
import { plugin } from "bun";
|
|
44
|
+
import { moduleLoadBuffer } from "./timing-buffer";
|
|
45
|
+
|
|
46
|
+
// Restrict to TS/TSX only. node_modules ships CommonJS `.js`/`.cjs` that Bun
|
|
47
|
+
// auto-detects when loaded via its default path; if we intercept and return
|
|
48
|
+
// `{ contents, loader: "js" }`, Bun forces ESM and CJS modules fail to load
|
|
49
|
+
// (e.g. `Missing 'default' export`). Our own source tree (where the interesting
|
|
50
|
+
// timing lives) is uniformly TypeScript, so a TS-only filter is both safe and
|
|
51
|
+
// sufficient.
|
|
52
|
+
const MODULE_LOADER_FILTER = /\.[mc]?tsx?$/;
|
|
53
|
+
const MODULE_COMPLETE_KEY: symbol = Symbol.for("prometheus.moduleLoadComplete");
|
|
54
|
+
const MODULE_BODY_START_KEY: symbol = Symbol.for("prometheus.moduleBodyStart");
|
|
55
|
+
const STATIC_IMPORT_PATTERN =
|
|
56
|
+
/\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
57
|
+
|
|
58
|
+
type CompleteStore = Record<symbol, ((path: string) => void) | undefined>;
|
|
59
|
+
|
|
60
|
+
function bodyStartMarker(path: string): string {
|
|
61
|
+
return `;globalThis[Symbol.for("prometheus.moduleBodyStart")]?.(${JSON.stringify(path)});\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function completionMarker(path: string): string {
|
|
65
|
+
return `\n;globalThis[Symbol.for("prometheus.moduleLoadComplete")]?.(${JSON.stringify(path)});\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function instrumentContents(path: string, contents: string): string {
|
|
69
|
+
const start = bodyStartMarker(path);
|
|
70
|
+
const end = completionMarker(path);
|
|
71
|
+
if (!contents.startsWith("#!")) return `${start}${contents}${end}`;
|
|
72
|
+
const newline = contents.indexOf("\n");
|
|
73
|
+
if (newline === -1) return `${contents}\n${start}${end}`;
|
|
74
|
+
return `${contents.slice(0, newline + 1)}${start}${contents.slice(newline + 1)}${end}`;
|
|
75
|
+
}
|
|
76
|
+
function importerDir(importer: string): string {
|
|
77
|
+
const slash = importer.lastIndexOf("/");
|
|
78
|
+
if (slash === -1) return ".";
|
|
79
|
+
return importer.slice(0, slash);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function childSetFor(importsByPath: Map<string, Set<string>>, path: string): Set<string> {
|
|
83
|
+
let children = importsByPath.get(path);
|
|
84
|
+
if (!children) {
|
|
85
|
+
children = new Set<string>();
|
|
86
|
+
importsByPath.set(path, children);
|
|
87
|
+
}
|
|
88
|
+
return children;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addImportEdges(importsByPath: Map<string, Set<string>>, importer: string, contents: string): void {
|
|
92
|
+
STATIC_IMPORT_PATTERN.lastIndex = 0;
|
|
93
|
+
for (const match of contents.matchAll(STATIC_IMPORT_PATTERN)) {
|
|
94
|
+
const specifier = match[1] ?? match[2];
|
|
95
|
+
if (!specifier) continue;
|
|
96
|
+
try {
|
|
97
|
+
const resolved = Bun.resolveSync(specifier, importerDir(importer));
|
|
98
|
+
if (MODULE_LOADER_FILTER.test(resolved) && resolved !== importer) {
|
|
99
|
+
childSetFor(importsByPath, importer).add(resolved);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Leave Bun's real resolver/runtime to surface any error. This scanner is only an observer.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (process.env.PROMETHEUS_TIMING) {
|
|
108
|
+
const buffer = moduleLoadBuffer();
|
|
109
|
+
const starts = new Map<string, number>();
|
|
110
|
+
const bodyStarts = new Map<string, number>();
|
|
111
|
+
const importsByPath = new Map<string, Set<string>>();
|
|
112
|
+
const store = globalThis as unknown as CompleteStore;
|
|
113
|
+
store[MODULE_BODY_START_KEY] = (path: string): void => {
|
|
114
|
+
bodyStarts.set(path, performance.now());
|
|
115
|
+
};
|
|
116
|
+
store[MODULE_COMPLETE_KEY] = (path: string): void => {
|
|
117
|
+
const start = starts.get(path);
|
|
118
|
+
if (start === undefined) return;
|
|
119
|
+
starts.delete(path);
|
|
120
|
+
const end = performance.now();
|
|
121
|
+
const bodyStart = bodyStarts.get(path);
|
|
122
|
+
bodyStarts.delete(path);
|
|
123
|
+
const imports = importsByPath.get(path);
|
|
124
|
+
buffer.push({
|
|
125
|
+
path,
|
|
126
|
+
start,
|
|
127
|
+
durationMs: end - start,
|
|
128
|
+
bodyMs: bodyStart === undefined ? undefined : end - bodyStart,
|
|
129
|
+
imports: imports ? [...imports] : [],
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
plugin({
|
|
134
|
+
name: "pi-module-load-timer",
|
|
135
|
+
setup(build) {
|
|
136
|
+
build.onLoad({ filter: MODULE_LOADER_FILTER }, async args => {
|
|
137
|
+
starts.set(args.path, performance.now());
|
|
138
|
+
childSetFor(importsByPath, args.path);
|
|
139
|
+
const contents = await Bun.file(args.path).text();
|
|
140
|
+
addImportEdges(importsByPath, args.path, contents);
|
|
141
|
+
return {
|
|
142
|
+
contents: instrumentContents(args.path, contents),
|
|
143
|
+
loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
package/src/path-tree.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-level path tree shared by grouped file listings (find / grep / ast tools)
|
|
3
|
+
* and compaction file-operation lists.
|
|
4
|
+
*
|
|
5
|
+
* Flat path lists used to group by the *immediate* parent directory and print the
|
|
6
|
+
* full directory path in every header. For results spread across a deep tree — or
|
|
7
|
+
* rooted outside cwd, where paths stay absolute — that repeated the shared prefix
|
|
8
|
+
* on every line. The tree below folds single-child directory chains (so the common
|
|
9
|
+
* prefix collapses into one header) and nests the rest, charging the model one
|
|
10
|
+
* token per path segment instead of one per file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
14
|
+
|
|
15
|
+
/** True for `scheme://…` entries that have no meaningful directory structure. */
|
|
16
|
+
export function isUrlLikePath(filePath: string): boolean {
|
|
17
|
+
return URL_LIKE_PATH_RE.test(filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PathTreeNode {
|
|
21
|
+
/** Direct file leaves, in first-seen order. */
|
|
22
|
+
files: Array<{ name: string; key: string }>;
|
|
23
|
+
/** Dedup set for `files` (a glob can surface the same path twice on retry). */
|
|
24
|
+
fileNames: Set<string>;
|
|
25
|
+
/** Child directories, in first-seen order. */
|
|
26
|
+
subdirs: Array<{ name: string; node: PathTreeNode }>;
|
|
27
|
+
/** Dedup index for `subdirs`. */
|
|
28
|
+
dirIndex: Map<string, PathTreeNode>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PathTreeInput {
|
|
32
|
+
/** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Whether the leaf itself is a directory (trailing-slash match from find). */
|
|
35
|
+
isDir: boolean;
|
|
36
|
+
/** Opaque key carried onto file events for section lookup. Defaults to `path`. */
|
|
37
|
+
key?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One node emitted while walking the tree: a folded directory or a file leaf. */
|
|
41
|
+
export interface GroupedTreeEvent {
|
|
42
|
+
kind: "dir" | "file";
|
|
43
|
+
/** 0-based nesting depth (root children are depth 0). */
|
|
44
|
+
depth: number;
|
|
45
|
+
/** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** File key for `kind === "file"`; empty string for directories. */
|
|
48
|
+
key: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createNode(): PathTreeNode {
|
|
52
|
+
return { files: [], fileNames: new Set(), subdirs: [], dirIndex: new Map() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addFile(node: PathTreeNode, name: string, key: string): void {
|
|
56
|
+
if (node.fileNames.has(name)) return;
|
|
57
|
+
node.fileNames.add(name);
|
|
58
|
+
node.files.push({ name, key });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a directory tree from a flat list of paths. URL-like entries are kept
|
|
63
|
+
* whole as root-level file leaves (they have no meaningful directory structure).
|
|
64
|
+
* Absolute paths carry a leading empty segment so they share a common `/` root
|
|
65
|
+
* and fold like any other prefix.
|
|
66
|
+
*/
|
|
67
|
+
export function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode {
|
|
68
|
+
const root = createNode();
|
|
69
|
+
for (const { path: rawPath, isDir, key } of entries) {
|
|
70
|
+
const normalized = rawPath.replace(/\\/g, "/");
|
|
71
|
+
const fileKey = key ?? rawPath;
|
|
72
|
+
if (isUrlLikePath(normalized)) {
|
|
73
|
+
addFile(root, normalized, fileKey);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
77
|
+
if (trimmed.length === 0) continue;
|
|
78
|
+
const segments = trimmed.split("/");
|
|
79
|
+
const dirCount = isDir ? segments.length : segments.length - 1;
|
|
80
|
+
let node = root;
|
|
81
|
+
for (let i = 0; i < dirCount; i++) {
|
|
82
|
+
const segment = segments[i]!;
|
|
83
|
+
let child = node.dirIndex.get(segment);
|
|
84
|
+
if (!child) {
|
|
85
|
+
child = createNode();
|
|
86
|
+
node.dirIndex.set(segment, child);
|
|
87
|
+
node.subdirs.push({ name: segment, node: child });
|
|
88
|
+
}
|
|
89
|
+
node = child;
|
|
90
|
+
}
|
|
91
|
+
if (!isDir) {
|
|
92
|
+
addFile(node, segments[segments.length - 1]!, fileKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Depth-first walk yielding directory and file events. Directories collapse their
|
|
100
|
+
* single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
|
|
101
|
+
* header. Each node's direct files are emitted before its subdirectories, keeping
|
|
102
|
+
* a file unambiguously attached to the header above it.
|
|
103
|
+
*/
|
|
104
|
+
export function* walkPathTree(node: PathTreeNode, depth = 0): Generator<GroupedTreeEvent> {
|
|
105
|
+
for (const file of node.files) {
|
|
106
|
+
yield { kind: "file", depth, name: file.name, key: file.key };
|
|
107
|
+
}
|
|
108
|
+
for (const subdir of node.subdirs) {
|
|
109
|
+
let dirNode = subdir.node;
|
|
110
|
+
const parts = [subdir.name];
|
|
111
|
+
while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
|
|
112
|
+
const only = dirNode.subdirs[0]!;
|
|
113
|
+
parts.push(only.name);
|
|
114
|
+
dirNode = only.node;
|
|
115
|
+
}
|
|
116
|
+
yield { kind: "dir", depth, name: parts.join("/"), key: "" };
|
|
117
|
+
yield* walkPathTree(dirNode, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a flat path list as a grouped, prefix-folded directory tree without
|
|
123
|
+
* per-file bodies (find-tool output shape, also used by compaction `<files>`
|
|
124
|
+
* lists). Single-child directory chains fold into one header (`# a/b/c/`),
|
|
125
|
+
* each level adds one `#`, and files are listed bare under the deepest
|
|
126
|
+
* directory header that owns them. Trailing-slash entries are directory
|
|
127
|
+
* leaves and keep their slash in the header.
|
|
128
|
+
*
|
|
129
|
+
* `annotate` receives each file's full original path and its return value is
|
|
130
|
+
* appended verbatim to the file line (e.g. ` (RW)`).
|
|
131
|
+
*
|
|
132
|
+
* Order follows the input: a directory appears when its first member is
|
|
133
|
+
* emitted, and a node's own files precede its subdirectories.
|
|
134
|
+
*/
|
|
135
|
+
export function formatGroupedPaths(paths: readonly string[], annotate?: (path: string) => string): string {
|
|
136
|
+
if (paths.length === 0) return "";
|
|
137
|
+
const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
for (const event of walkPathTree(tree)) {
|
|
140
|
+
if (event.kind === "dir") {
|
|
141
|
+
lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
|
|
142
|
+
} else {
|
|
143
|
+
lines.push(annotate ? `${event.name}${annotate(event.key)}` : event.name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
package/src/procmgr.ts
CHANGED
|
@@ -16,7 +16,7 @@ let cachedShellConfig: ShellConfig | null = null;
|
|
|
16
16
|
/**
|
|
17
17
|
* Check if a shell binary is executable.
|
|
18
18
|
*/
|
|
19
|
-
function isExecutable(path: string): boolean {
|
|
19
|
+
export function isExecutable(path: string): boolean {
|
|
20
20
|
try {
|
|
21
21
|
fs.accessSync(path, fs.constants.X_OK);
|
|
22
22
|
return true;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import * as Module from "node:module";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* On-demand runtime dependency support for native-heavy optional packages
|
|
8
|
+
* (Transformers.js, fastembed) that are never bundled into the CLI or the
|
|
9
|
+
* compiled binary. Consumers `bun install` a pinned dependency set into a
|
|
10
|
+
* cache directory on first use ({@link ensureRuntimeInstalled}) and load the
|
|
11
|
+
* entrypoint via `createRequire`.
|
|
12
|
+
*
|
|
13
|
+
* Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
|
|
14
|
+
* specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
|
|
15
|
+
* (issue #1763). Runtime-installed graphs (`@huggingface/transformers` →
|
|
16
|
+
* `onnxruntime-node` → `onnxruntime-common`, `fastembed` →
|
|
17
|
+
* `@anush008/tokenizers` → platform binding) all point `main`/`exports` at
|
|
18
|
+
* nested files, so the stock resolver cannot load any of them. We patch
|
|
19
|
+
* `Module._resolveFilename` to resolve those bare specifiers against the
|
|
20
|
+
* registered runtime caches ourselves, honoring `main`/`exports`.
|
|
21
|
+
*
|
|
22
|
+
* This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
|
|
23
|
+
* mutating the `node:module` resolver, so the resolution logic is unit-testable
|
|
24
|
+
* without a compiled binary.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
|
|
28
|
+
const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
|
|
29
|
+
|
|
30
|
+
/** Extension probes appended to a `main`/`exports` target that lacks one. */
|
|
31
|
+
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walk a conditional `exports` target (string, array of fallbacks, or a
|
|
39
|
+
* condition object) and return the first relative path that matches a runtime
|
|
40
|
+
* condition in declaration order. Returns `null` when nothing applies (e.g.
|
|
41
|
+
* an `import`-only entry).
|
|
42
|
+
*/
|
|
43
|
+
export function selectConditionalTarget(target: unknown): string | null {
|
|
44
|
+
if (typeof target === "string") return target;
|
|
45
|
+
if (Array.isArray(target)) {
|
|
46
|
+
for (const entry of target) {
|
|
47
|
+
const resolved = selectConditionalTarget(entry);
|
|
48
|
+
if (resolved) return resolved;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (isRecord(target)) {
|
|
53
|
+
for (const condition in target) {
|
|
54
|
+
if (!RUNTIME_CONDITIONS[condition]) continue;
|
|
55
|
+
const resolved = selectConditionalTarget(target[condition]);
|
|
56
|
+
if (resolved) return resolved;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
|
|
63
|
+
function resolveFileTarget(pkgDir: string, relative: string): string | null {
|
|
64
|
+
const base = path.join(pkgDir, relative);
|
|
65
|
+
const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(candidate);
|
|
69
|
+
if (stat.isFile()) return candidate;
|
|
70
|
+
if (stat.isDirectory()) {
|
|
71
|
+
const indexed = resolveFileTarget(candidate, "index");
|
|
72
|
+
if (indexed) return indexed;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// missing candidate — keep probing
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveExportsEntry(
|
|
82
|
+
pkgDir: string,
|
|
83
|
+
exports: Record<string, unknown>,
|
|
84
|
+
subpath: string | undefined,
|
|
85
|
+
): string | null {
|
|
86
|
+
let subpathMap = false;
|
|
87
|
+
for (const key in exports) {
|
|
88
|
+
subpathMap = key === "." || key.startsWith("./");
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (subpathMap) {
|
|
92
|
+
const key = subpath ? `./${subpath}` : ".";
|
|
93
|
+
if (!(key in exports)) return null;
|
|
94
|
+
const target = selectConditionalTarget(exports[key]);
|
|
95
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
96
|
+
}
|
|
97
|
+
// A bare condition map only describes the package root, so a subpath
|
|
98
|
+
// request falls through to plain path joining at the call site.
|
|
99
|
+
if (subpath) return null;
|
|
100
|
+
const target = selectConditionalTarget(exports);
|
|
101
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Split a bare specifier into its package name and optional subpath, handling
|
|
106
|
+
* scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
|
|
107
|
+
*/
|
|
108
|
+
export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
|
|
109
|
+
const segments = specifier.split("/");
|
|
110
|
+
const take = specifier.startsWith("@") ? 2 : 1;
|
|
111
|
+
const packageName = segments.slice(0, take).join("/");
|
|
112
|
+
const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
|
|
113
|
+
return { packageName, subpath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a bare specifier against an installed `node_modules` directory,
|
|
118
|
+
* honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
|
|
119
|
+
* Returns an absolute file path, or `null` when the package/entry is absent.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
|
|
122
|
+
const { packageName, subpath } = splitBareSpecifier(specifier);
|
|
123
|
+
const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
|
|
124
|
+
const manifest = readManifest(pkgDir);
|
|
125
|
+
if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
|
|
126
|
+
|
|
127
|
+
const { exports } = manifest;
|
|
128
|
+
if (typeof exports === "string" || isRecord(exports)) {
|
|
129
|
+
const map = typeof exports === "string" ? { ".": exports } : exports;
|
|
130
|
+
const resolved = resolveExportsEntry(pkgDir, map, subpath);
|
|
131
|
+
if (resolved) return resolved;
|
|
132
|
+
}
|
|
133
|
+
if (subpath) return resolveFileTarget(pkgDir, subpath);
|
|
134
|
+
if (typeof manifest.main === "string") {
|
|
135
|
+
const resolved = resolveFileTarget(pkgDir, manifest.main);
|
|
136
|
+
if (resolved) return resolved;
|
|
137
|
+
}
|
|
138
|
+
return resolveFileTarget(pkgDir, "index.js");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readManifest(pkgDir: string): Record<string, unknown> | null {
|
|
142
|
+
try {
|
|
143
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
144
|
+
return isRecord(parsed) ? parsed : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ModuleResolver {
|
|
151
|
+
_resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface ResolverRegistration {
|
|
155
|
+
runtimeNodeModules: string;
|
|
156
|
+
stubs: Record<string, string>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const REGISTRY = Symbol.for("prometheus.runtimeModuleResolver.registry");
|
|
160
|
+
const PATCHED = Symbol.for("prometheus.runtimeModuleResolver.patched");
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The registration list lives on `globalThis` so a bundled copy and a
|
|
164
|
+
* source copy of this module in one process share the same registry — the
|
|
165
|
+
* resolver is patched once per process, and the patched closure must see
|
|
166
|
+
* every registration.
|
|
167
|
+
*/
|
|
168
|
+
function resolverRegistry(): ResolverRegistration[] {
|
|
169
|
+
const holder = globalThis as { [REGISTRY]?: ResolverRegistration[] };
|
|
170
|
+
holder[REGISTRY] ??= [];
|
|
171
|
+
return holder[REGISTRY];
|
|
172
|
+
}
|
|
173
|
+
function pathContains(root: string, candidate: string): boolean {
|
|
174
|
+
const relative = path.relative(root, candidate);
|
|
175
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parentFilename(parent: unknown): string | null {
|
|
179
|
+
if (!isRecord(parent)) return null;
|
|
180
|
+
const filename = parent.filename;
|
|
181
|
+
return typeof filename === "string" ? filename : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RuntimeResolverOptions {
|
|
185
|
+
/** Absolute path to the runtime cache's `node_modules`. */
|
|
186
|
+
runtimeNodeModules: string;
|
|
187
|
+
/** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
|
|
188
|
+
stubs?: Record<string, string>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Patch `node:module`'s resolver (idempotently) so bare specifiers that the
|
|
193
|
+
* stock compiled-binary resolver cannot find fall back to the registered
|
|
194
|
+
* runtime caches. Stock resolution is tried first and kept for anything
|
|
195
|
+
* outside the registered roots (bundled imports, node builtins, host or
|
|
196
|
+
* extension trees). Multiple runtime roots may register; they are consulted
|
|
197
|
+
* in registration order.
|
|
198
|
+
*
|
|
199
|
+
* One stock "success" is distrusted: the compiled-binary resolver ignores
|
|
200
|
+
* `main`/`exports` for real-FS packages (Bun #1763), so a package shipping
|
|
201
|
+
* its TS source next to `dist/` (e.g. `@huggingface/hub`'s root `index.ts`)
|
|
202
|
+
* resolves to the wrong file. When the stock hit lands inside a registered
|
|
203
|
+
* runtime root, the manifest-aware resolution wins.
|
|
204
|
+
*/
|
|
205
|
+
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): void {
|
|
206
|
+
const registry = resolverRegistry();
|
|
207
|
+
const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
|
|
208
|
+
if (existing) Object.assign(existing.stubs, stubs);
|
|
209
|
+
else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
|
|
210
|
+
|
|
211
|
+
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
|
|
212
|
+
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
|
|
213
|
+
if (target[PATCHED]) return;
|
|
214
|
+
const original = target._resolveFilename.bind(target);
|
|
215
|
+
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
|
|
216
|
+
let stockResolved: string | null = null;
|
|
217
|
+
let stockError: unknown;
|
|
218
|
+
try {
|
|
219
|
+
stockResolved = original(request, parent, isMain, options);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
stockError = error;
|
|
222
|
+
}
|
|
223
|
+
const bare = !request.startsWith(".") && !request.startsWith("node:") && !path.isAbsolute(request);
|
|
224
|
+
if (bare) {
|
|
225
|
+
const parentFile = parentFilename(parent);
|
|
226
|
+
for (const registration of resolverRegistry()) {
|
|
227
|
+
const parentInRuntime = parentFile !== null && pathContains(registration.runtimeNodeModules, parentFile);
|
|
228
|
+
if (parentInRuntime) {
|
|
229
|
+
const stub = registration.stubs[request];
|
|
230
|
+
if (stub) return stub;
|
|
231
|
+
if (!stockResolved || !pathContains(registration.runtimeNodeModules, stockResolved)) {
|
|
232
|
+
const fallback = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
233
|
+
if (fallback) return fallback;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (stockResolved) {
|
|
237
|
+
// Correct a stock hit only inside the top-level package the
|
|
238
|
+
// request names. A hit in a nested node_modules (e.g. tar's
|
|
239
|
+
// minizlib resolving its own minipass@3 under
|
|
240
|
+
// <root>/minizlib/node_modules/) is version-correct — overriding
|
|
241
|
+
// it with the top-level instance would cross major versions.
|
|
242
|
+
const { packageName } = splitBareSpecifier(request);
|
|
243
|
+
const pkgDir = path.join(registration.runtimeNodeModules, ...packageName.split("/"));
|
|
244
|
+
if (!stockResolved.startsWith(pkgDir + path.sep)) continue;
|
|
245
|
+
if (path.relative(pkgDir, stockResolved).split(path.sep).includes("node_modules")) continue;
|
|
246
|
+
const expected = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
247
|
+
if (expected) return expected;
|
|
248
|
+
} else {
|
|
249
|
+
const stub = registration.stubs[request];
|
|
250
|
+
if (stub) return stub;
|
|
251
|
+
const fallback = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
252
|
+
if (fallback) return fallback;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (stockResolved) return stockResolved;
|
|
257
|
+
throw stockError;
|
|
258
|
+
};
|
|
259
|
+
target[PATCHED] = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Pinned dependency set materialized into a runtime cache directory. */
|
|
263
|
+
export interface RuntimeInstallSpec {
|
|
264
|
+
dependencies: Record<string, string>;
|
|
265
|
+
/** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
|
|
266
|
+
overrides?: Record<string, string>;
|
|
267
|
+
/** Packages whose lifecycle scripts bun may run during the install. */
|
|
268
|
+
trustedDependencies?: string[];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type RuntimeInstallPhase = "initiate" | "download" | "done";
|
|
272
|
+
|
|
273
|
+
export interface EnsureRuntimeInstalledOptions {
|
|
274
|
+
/** Directory owning the runtime `package.json` + `node_modules`. */
|
|
275
|
+
runtimeDir: string;
|
|
276
|
+
install: RuntimeInstallSpec;
|
|
277
|
+
/** Package whose installed manifest marks the runtime complete; defaults to the first dependency. */
|
|
278
|
+
probePackage?: string;
|
|
279
|
+
/** Phase notifications (progress UI); not emitted when already installed. */
|
|
280
|
+
onPhase?: (phase: RuntimeInstallPhase) => void;
|
|
281
|
+
lockAttempts?: number;
|
|
282
|
+
lockSleepMs?: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isErrnoCode(error: unknown, code: string): boolean {
|
|
286
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs: number): Promise<() => Promise<void>> {
|
|
290
|
+
const lockDir = `${runtimeDir}.lock`;
|
|
291
|
+
await fsp.mkdir(path.dirname(lockDir), { recursive: true });
|
|
292
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
293
|
+
try {
|
|
294
|
+
await fsp.mkdir(lockDir);
|
|
295
|
+
return async () => {
|
|
296
|
+
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (!isErrnoCode(error, "EEXIST")) throw error;
|
|
300
|
+
await Bun.sleep(sleepMs);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
307
|
+
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
308
|
+
const manifest: Record<string, unknown> = {
|
|
309
|
+
private: true,
|
|
310
|
+
type: "module",
|
|
311
|
+
dependencies: install.dependencies,
|
|
312
|
+
};
|
|
313
|
+
if (install.overrides && Object.keys(install.overrides).length) manifest.overrides = install.overrides;
|
|
314
|
+
if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
|
|
315
|
+
await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
319
|
+
if (!stream) return "";
|
|
320
|
+
return new Response(stream).text();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
324
|
+
// `process.execPath` is plain bun in source/bundle mode and the compiled
|
|
325
|
+
// binary otherwise; BUN_BE_BUN makes the compiled binary act as bun.
|
|
326
|
+
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
|
|
327
|
+
env: { ...Bun.env, BUN_BE_BUN: "1" },
|
|
328
|
+
stdout: "pipe",
|
|
329
|
+
stderr: "pipe",
|
|
330
|
+
});
|
|
331
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
332
|
+
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
333
|
+
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
334
|
+
proc.exited,
|
|
335
|
+
]);
|
|
336
|
+
if (exitCode === 0) return;
|
|
337
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Failed to install runtime at ${runtimeDir} with ${process.execPath} install (exit ${exitCode}): ${output}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
345
|
+
* cross-process safe via a lock directory). Returns `runtimeDir`.
|
|
346
|
+
*/
|
|
347
|
+
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
|
|
348
|
+
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
|
|
349
|
+
let probePackage = options.probePackage;
|
|
350
|
+
if (!probePackage) {
|
|
351
|
+
for (const name in install.dependencies) {
|
|
352
|
+
probePackage = name;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!probePackage) throw new Error(`Runtime install at ${runtimeDir} declares no dependencies`);
|
|
357
|
+
const probeManifest = Bun.file(path.join(runtimeDir, "node_modules", ...probePackage.split("/"), "package.json"));
|
|
358
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
359
|
+
|
|
360
|
+
onPhase?.("initiate");
|
|
361
|
+
const releaseLock = await acquireInstallLock(runtimeDir, lockAttempts, lockSleepMs);
|
|
362
|
+
try {
|
|
363
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
364
|
+
await writeRuntimeManifest(runtimeDir, install);
|
|
365
|
+
onPhase?.("download");
|
|
366
|
+
await runRuntimeInstall(runtimeDir);
|
|
367
|
+
onPhase?.("done");
|
|
368
|
+
return runtimeDir;
|
|
369
|
+
} finally {
|
|
370
|
+
await releaseLock();
|
|
371
|
+
}
|
|
372
|
+
}
|