1tube 0.1.54 → 0.2.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/edge.js +10 -3
- package/package.json +21 -11
- package/scripts/npm-cli.js +34 -33
- package/src/backends/bun/dep-graph.ts +310 -0
- package/src/backends/bun/function-host.ts +1190 -0
- package/src/backends/bun/hot-reloader.ts +385 -0
- package/src/backends/bun/shared-runtime.ts +176 -0
- package/src/backends/env-hot-reloader.ts +10 -9
- package/src/backends/vercel/bundle-profile.ts +14 -11
- package/src/backends/vercel/capture-config.ts +95 -5
- package/src/backends/vercel/wrapper.ts +6 -4
- package/src/backends/workerd/backend.ts +114 -78
- package/src/backends/workerd/bundle-profile.ts +13 -13
- package/src/backends/workerd/hot-reloader.ts +10 -10
- package/src/backends/workerd/prebuilt.ts +1 -1
- package/src/backends/workerd/proc-stats.ts +35 -23
- package/src/backends/workerd/process.ts +67 -53
- package/src/backends/workerd/runtime-overrides.ts +34 -23
- package/src/backends/workerd/shared-runtime.ts +85 -42
- package/src/boot-progress.ts +11 -7
- package/src/bundler/core.ts +154 -53
- package/src/bundler/profile.ts +5 -5
- package/src/cli/build.ts +48 -85
- package/src/cli/deno-config.ts +16 -25
- package/src/cli/package.ts +65 -55
- package/src/cli/vercel-build.ts +40 -25
- package/src/cli.ts +1 -1
- package/src/compat/deno-shim.ts +540 -0
- package/src/compat/deps.ts +359 -0
- package/src/compat/environment.ts +65 -0
- package/src/compat/esbuild-plugin.ts +109 -0
- package/src/compat/globals.ts +158 -0
- package/src/compat/import-map.ts +183 -0
- package/src/compat/plugin.ts +416 -0
- package/src/compat/remote.ts +145 -0
- package/src/compat/shared-deps.ts +178 -0
- package/src/compat/specifiers.ts +103 -0
- package/src/discovery.ts +6 -7
- package/src/edge.ts +13 -4
- package/src/env-file.ts +21 -9
- package/src/firmware-content-hash.ts +2 -1
- package/src/fs-watch.ts +78 -0
- package/src/gateway/auth.ts +2 -2
- package/src/gateway/body-watchdog.ts +1 -1
- package/src/gateway/cors.ts +8 -8
- package/src/gateway/logging.ts +3 -3
- package/src/gateway/rate-limit.ts +4 -4
- package/src/health.ts +2 -2
- package/src/launch.ts +134 -450
- package/src/log-buffer.ts +12 -11
- package/src/logs/api.ts +1 -1
- package/src/logs/db.ts +2 -2
- package/src/logs/schema.ts +1 -1
- package/src/logs/writer.ts +1 -1
- package/src/manifest.ts +4 -5
- package/src/registry.ts +41 -4
- package/src/server.ts +452 -558
- package/src/stdio.ts +37 -0
- package/src/types/bun-module-graph.d.ts +26 -0
- package/src/version.ts +2 -3
- package/tsconfig.json +17 -0
- package/deno.json +0 -38
- package/deno.lock +0 -572
- package/src/backends/deno/dep-graph.ts +0 -404
- package/src/backends/deno/hot-reloader.ts +0 -471
- package/src/backends/deno/shared-runtime.ts +0 -218
- package/src/backends/deno/source-rewriter.ts +0 -637
- package/src/backends/deno/worker-entry-colocated.ts +0 -406
- package/src/backends/deno/worker-entry.ts +0 -375
- package/src/backends/deno/worker-host.ts +0 -2166
- package/src/cli/dep-pin.ts +0 -126
- package/src/env-scope.ts +0 -124
package/dist/edge.js
CHANGED
|
@@ -152,9 +152,16 @@ export function normalizeRateLimit(rl) {
|
|
|
152
152
|
export function getStandaloneServe() {
|
|
153
153
|
const runtime = globalThis;
|
|
154
154
|
const standaloneServe = runtime.Deno?.serve;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
if (typeof standaloneServe === "function")
|
|
156
|
+
return standaloneServe.bind(runtime.Deno);
|
|
157
|
+
// `bun supabase/functions/<name>/index.ts` outside the gateway.
|
|
158
|
+
const bunServe = runtime.Bun?.serve;
|
|
159
|
+
if (typeof bunServe === "function") {
|
|
160
|
+
return (handler) => {
|
|
161
|
+
bunServe.call(runtime.Bun, { fetch: handler });
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
158
165
|
}
|
|
159
166
|
export function normalizeRoutePath(path) {
|
|
160
167
|
const trimmed = path.trim();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "1tube",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Self-hosted Supabase Edge Functions gateway
|
|
5
|
+
"description": "Self-hosted Supabase Edge Functions gateway on Bun, plus an ESLint plugin for consumers",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
"drizzle",
|
|
42
42
|
"scripts/npm-cli.js",
|
|
43
43
|
"src",
|
|
44
|
-
"
|
|
45
|
-
"deno.lock",
|
|
44
|
+
"tsconfig.json",
|
|
46
45
|
"README.md",
|
|
47
46
|
"LICENSE",
|
|
48
47
|
".env.example"
|
|
@@ -51,20 +50,31 @@
|
|
|
51
50
|
"access": "public"
|
|
52
51
|
},
|
|
53
52
|
"scripts": {
|
|
54
|
-
"dev": "
|
|
55
|
-
"serve": "
|
|
56
|
-
"
|
|
53
|
+
"dev": "bun --watch src/cli.ts",
|
|
54
|
+
"serve": "bun src/cli.ts",
|
|
55
|
+
"play": "bun src/cli.ts --dev --hmr --functions ./playground --port 3199",
|
|
56
|
+
"play:workerd": "bun src/cli.ts --backend workerd --dev --hmr --functions ./playground --port 3199",
|
|
57
|
+
"build": "bun src/cli.ts build",
|
|
58
|
+
"test": "bun test --timeout 60000",
|
|
59
|
+
"build:edge": "bunx tsc --ignoreConfig --target ES2022 --module ESNext --moduleResolution Bundler --declaration --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts",
|
|
57
60
|
"prepack": "bun run build:edge",
|
|
58
|
-
"typecheck": "
|
|
59
|
-
"check": "deno cache --allow-scripts=npm:protobufjs src/server.ts",
|
|
61
|
+
"typecheck": "bun check",
|
|
60
62
|
"smoke:bin": "node ./scripts/npm-cli.js --version",
|
|
61
|
-
"prepublishOnly": "bun run build:edge && bun run typecheck && bun run
|
|
63
|
+
"prepublishOnly": "bun run build:edge && bun run typecheck && bun run smoke:bin"
|
|
62
64
|
},
|
|
63
65
|
"dependencies": {
|
|
66
|
+
"@zip.js/zip.js": "^2.8.0",
|
|
64
67
|
"drizzle-orm": "1.0.0-rc.4",
|
|
68
|
+
"es-module-lexer": "^2",
|
|
69
|
+
"esbuild": "^0.28.1",
|
|
65
70
|
"hono": "^4.12.30"
|
|
66
71
|
},
|
|
67
72
|
"devDependencies": {
|
|
68
|
-
"drizzle-kit": "1.0.0-rc.4"
|
|
73
|
+
"drizzle-kit": "1.0.0-rc.4",
|
|
74
|
+
"@types/bun": "latest",
|
|
75
|
+
"typescript": "^6"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"bun": ">=1.4.3"
|
|
69
79
|
}
|
|
70
80
|
}
|
package/scripts/npm-cli.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `1tube` npm bin — a tiny Node shim that locates
|
|
5
|
-
* launcher (`src/launch.ts`). All argument handling, the managed
|
|
6
|
-
* dependency pinning, minimum-dependency-age, and live reload live in
|
|
7
|
-
* launcher itself, so this shim stays trivial: find
|
|
8
|
-
* up signals + exit code.
|
|
4
|
+
* `1tube` npm bin — a tiny Node shim that locates Bun and runs the single
|
|
5
|
+
* launcher (`src/launch.ts`). All argument handling, the managed dependency
|
|
6
|
+
* store, dependency pinning, minimum-dependency-age, and live reload live in
|
|
7
|
+
* the launcher itself, so this shim stays trivial: find bun, forward argv,
|
|
8
|
+
* wire up signals + exit code.
|
|
9
9
|
*
|
|
10
|
-
* Machines using this need
|
|
11
|
-
* forwarded verbatim — the launcher recognizes its own flags (`--
|
|
12
|
-
* `--config`, `--env-file`, `--
|
|
13
|
-
* `--no-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* Machines using this need Bun on PATH (or set `BUN_BIN`). Arguments are
|
|
11
|
+
* forwarded verbatim — the launcher recognizes its own flags (`--deps-dir`,
|
|
12
|
+
* `--refresh-deps`, `--config`, `--env-file`, `--minimum-dependency-age`,
|
|
13
|
+
* `--no-pin`, `--no-dep-watch`, …) and forwards everything else to the
|
|
14
|
+
* gateway. `1TUBE_BUN_ARGS` (space-separated) is appended for projects that
|
|
15
|
+
* prefer to keep flags out of the command line; the Deno-era
|
|
16
|
+
* `1TUBE_DENO_ARGS` is still honoured.
|
|
17
|
+
*
|
|
18
|
+
* The package's own dependencies (hono, esbuild, …) are installed next to it
|
|
19
|
+
* by the host's package manager, so Bun resolves them from node_modules.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { spawn } from "node:child_process";
|
|
@@ -23,43 +27,40 @@ import { fileURLToPath } from "node:url";
|
|
|
23
27
|
|
|
24
28
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
29
|
const launcherEntrypoint = resolve(packageRoot, "src", "launch.ts");
|
|
26
|
-
const
|
|
30
|
+
const bunBin = process.env.BUN_BIN || "bun";
|
|
27
31
|
|
|
28
|
-
// The launcher
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
const denoArgs = [
|
|
32
|
-
"run",
|
|
33
|
-
"--quiet",
|
|
34
|
-
"-A",
|
|
35
|
-
"--node-modules-dir=false",
|
|
32
|
+
// The launcher imports only node: builtins + relative source, and it owns the
|
|
33
|
+
// gateway child's dependency store and env — so the outer `bun` stays minimal.
|
|
34
|
+
const bunArgs = [
|
|
36
35
|
launcherEntrypoint,
|
|
37
36
|
...tokenizeArgs(
|
|
38
|
-
process.env.
|
|
37
|
+
process.env.ONETUBE_BUN_ARGS ?? process.env["1TUBE_BUN_ARGS"] ??
|
|
38
|
+
process.env.ONETUBE_DENO_ARGS ?? process.env["1TUBE_DENO_ARGS"] ?? "",
|
|
39
39
|
),
|
|
40
40
|
...process.argv.slice(2),
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
// Resolve the real
|
|
44
|
-
// `.cmd`/`.bat` shim without a shell, but
|
|
45
|
-
// binary on POSIX) with `shell:false` lets
|
|
46
|
-
// important since we forward arbitrary
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
43
|
+
// Resolve the real bun binary up front. Node ≥18.20 refuses to spawn a
|
|
44
|
+
// `.cmd`/`.bat` shim (e.g. npm's global `bun.cmd`) without a shell, but
|
|
45
|
+
// spawning a real `.exe` (or a bare binary on POSIX) with `shell:false` lets
|
|
46
|
+
// Node escape arguments properly — important since we forward arbitrary
|
|
47
|
+
// flags, some carrying paths with spaces.
|
|
48
|
+
const resolvedBun = resolveExecutable(bunBin);
|
|
49
|
+
const needsShell = resolvedBun
|
|
50
|
+
? /\.(cmd|bat)$/i.test(resolvedBun)
|
|
50
51
|
: process.platform === "win32";
|
|
51
|
-
const spawnBin =
|
|
52
|
+
const spawnBin = resolvedBun ?? bunBin;
|
|
52
53
|
|
|
53
54
|
let child;
|
|
54
55
|
if (needsShell) {
|
|
55
|
-
const command = [spawnBin, ...
|
|
56
|
+
const command = [spawnBin, ...bunArgs].map(quoteForCmd).join(" ");
|
|
56
57
|
child = spawn(command, {
|
|
57
58
|
stdio: "inherit",
|
|
58
59
|
shell: true,
|
|
59
60
|
windowsHide: false,
|
|
60
61
|
});
|
|
61
62
|
} else {
|
|
62
|
-
child = spawn(spawnBin,
|
|
63
|
+
child = spawn(spawnBin, bunArgs, {
|
|
63
64
|
stdio: "inherit",
|
|
64
65
|
shell: false,
|
|
65
66
|
windowsHide: false,
|
|
@@ -75,11 +76,11 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
75
76
|
child.on("error", (err) => {
|
|
76
77
|
if (err && "code" in err && err.code === "ENOENT") {
|
|
77
78
|
console.error(
|
|
78
|
-
"[1tube]
|
|
79
|
+
"[1tube] Bun was not found on PATH. Install Bun (https://bun.sh) or set BUN_BIN",
|
|
79
80
|
);
|
|
80
81
|
process.exit(127);
|
|
81
82
|
}
|
|
82
|
-
console.error(`[1tube] failed to start
|
|
83
|
+
console.error(`[1tube] failed to start Bun: ${err.message}`);
|
|
83
84
|
process.exit(1);
|
|
84
85
|
});
|
|
85
86
|
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-function dependency graph for the Bun backend.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs:
|
|
5
|
+
*
|
|
6
|
+
* 1. HMR precision. A file change maps to exactly the functions that
|
|
7
|
+
* transitively import it, so touching `_shared/x.ts` reloads only its
|
|
8
|
+
* importers instead of everything.
|
|
9
|
+
* 2. Dependency pre-install. While walking, every `npm:` / `jsr:` request
|
|
10
|
+
* reachable from a function's local files is collected, so the host can
|
|
11
|
+
* install them in one batch BEFORE importing (the compat plugin's
|
|
12
|
+
* resolver is synchronous and would otherwise block per package).
|
|
13
|
+
*
|
|
14
|
+
* Only local files are walked. Imports are discovered with
|
|
15
|
+
* `Bun.Transpiler#scanImports` (the same scanner Bun's bundler uses), and
|
|
16
|
+
* resolved with Bun's resolver after import-map rewriting. Anything under
|
|
17
|
+
* `node_modules`, `node:`/`bun:` builtins and remote URLs is a leaf: none of
|
|
18
|
+
* them can fire a local fs event.
|
|
19
|
+
*
|
|
20
|
+
* Parsed imports are cached per file keyed by mtime + size, so building
|
|
21
|
+
* graphs for 100 functions that share `_shared/*` parses each shared file
|
|
22
|
+
* once, and an HMR refresh only re-reads what changed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { statSync } from "node:fs";
|
|
26
|
+
import { readFile } from "node:fs/promises";
|
|
27
|
+
import { dirname, extname } from "node:path";
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
import { type ImportMap, resolveImportMap } from "../../compat/import-map.ts";
|
|
30
|
+
import {
|
|
31
|
+
isRemoteSpecifier,
|
|
32
|
+
type PackageRequest,
|
|
33
|
+
packageKey,
|
|
34
|
+
parsePackageSpecifier,
|
|
35
|
+
} from "../../compat/specifiers.ts";
|
|
36
|
+
|
|
37
|
+
export { fileURLToPath, pathToFileURL };
|
|
38
|
+
|
|
39
|
+
export interface DepGraphOptions {
|
|
40
|
+
/** The project's deno.json import map, if any. */
|
|
41
|
+
importMap?: ImportMap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DepGraph {
|
|
45
|
+
/**
|
|
46
|
+
* Build (or rebuild) the graph for one function entry (path or file URL).
|
|
47
|
+
* Unreadable / half-written files are tolerated: they become leaves.
|
|
48
|
+
*/
|
|
49
|
+
refresh(name: string, entry: string): Promise<void>;
|
|
50
|
+
/** {@link refresh} for many entries, sharing the parse cache. */
|
|
51
|
+
refreshMany(entries: Iterable<{ name: string; entry: string }>): Promise<void>;
|
|
52
|
+
/** Drop a function's graph (its entry was deleted). */
|
|
53
|
+
forget(name: string): void;
|
|
54
|
+
/** Names whose graph contains any of `changedFiles` (paths or file URLs). */
|
|
55
|
+
affected(changedFiles: Iterable<string>): Set<string>;
|
|
56
|
+
/** Absolute paths of every local file in `name`'s graph, entry included. */
|
|
57
|
+
filesFor(name: string): readonly string[];
|
|
58
|
+
/** `npm:`/`jsr:` requests reachable from `name`'s local files. */
|
|
59
|
+
packagesFor(name: string): readonly PackageRequest[];
|
|
60
|
+
/** Remote (`https:`) modules imported directly from local files. */
|
|
61
|
+
remotesFor(name: string): readonly string[];
|
|
62
|
+
/** Total number of distinct local files tracked. */
|
|
63
|
+
readonly size: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ParsedFile {
|
|
67
|
+
mtimeMs: number;
|
|
68
|
+
size: number;
|
|
69
|
+
/** Resolved local dependency paths. */
|
|
70
|
+
files: string[];
|
|
71
|
+
packages: PackageRequest[];
|
|
72
|
+
remotes: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface FunctionGraph {
|
|
76
|
+
files: Set<string>;
|
|
77
|
+
packages: Map<string, PackageRequest>;
|
|
78
|
+
remotes: Set<string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const LOADERS: Record<string, "ts" | "tsx" | "js" | "jsx"> = {
|
|
82
|
+
".ts": "ts",
|
|
83
|
+
".mts": "ts",
|
|
84
|
+
".cts": "ts",
|
|
85
|
+
".tsx": "tsx",
|
|
86
|
+
".js": "js",
|
|
87
|
+
".mjs": "js",
|
|
88
|
+
".cjs": "js",
|
|
89
|
+
".jsx": "jsx",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const transpilers = new Map<string, Bun.Transpiler>();
|
|
93
|
+
function transpilerFor(path: string): Bun.Transpiler | null {
|
|
94
|
+
const loader = LOADERS[extname(path).toLowerCase()];
|
|
95
|
+
if (!loader) return null;
|
|
96
|
+
let t = transpilers.get(loader);
|
|
97
|
+
if (!t) {
|
|
98
|
+
t = new Bun.Transpiler({ loader });
|
|
99
|
+
transpilers.set(loader, t);
|
|
100
|
+
}
|
|
101
|
+
return t;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toPath(pathOrUrl: string): string {
|
|
105
|
+
if (pathOrUrl.startsWith("file://")) return fileURLToPath(pathOrUrl);
|
|
106
|
+
return pathOrUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Reverse-index key. Watcher paths and resolver output can disagree on
|
|
111
|
+
* case and separators on Windows; elsewhere paths are compared verbatim.
|
|
112
|
+
*/
|
|
113
|
+
const fileKey = process.platform === "win32"
|
|
114
|
+
? (p: string) => p.replace(/\\/g, "/").toLowerCase()
|
|
115
|
+
: (p: string) => p;
|
|
116
|
+
|
|
117
|
+
function isDependencyPath(p: string): boolean {
|
|
118
|
+
return /[\\/]node_modules[\\/]/.test(p);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createDepGraph(options: DepGraphOptions = {}): DepGraph {
|
|
122
|
+
const perName = new Map<string, FunctionGraph>();
|
|
123
|
+
const reverse = new Map<string, Set<string>>();
|
|
124
|
+
const parsed = new Map<string, ParsedFile>();
|
|
125
|
+
|
|
126
|
+
/** Classify + resolve one specifier imported from `importer`. */
|
|
127
|
+
function classify(
|
|
128
|
+
spec: string,
|
|
129
|
+
importer: string,
|
|
130
|
+
out: { files: string[]; packages: PackageRequest[]; remotes: string[] },
|
|
131
|
+
depth = 0,
|
|
132
|
+
): void {
|
|
133
|
+
if (depth > 4) return;
|
|
134
|
+
if (spec.startsWith("node:") || spec.startsWith("bun:")) return;
|
|
135
|
+
const pkg = parsePackageSpecifier(spec);
|
|
136
|
+
if (pkg) {
|
|
137
|
+
out.packages.push(pkg);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (isRemoteSpecifier(spec)) {
|
|
141
|
+
out.remotes.push(spec);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (spec.startsWith("file://")) {
|
|
145
|
+
out.files.push(fileURLToPath(spec));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const pathLike = spec.startsWith("./") || spec.startsWith("../") ||
|
|
149
|
+
spec.startsWith("/");
|
|
150
|
+
if (!pathLike) {
|
|
151
|
+
const mapped = resolveImportMap(
|
|
152
|
+
spec,
|
|
153
|
+
pathToFileURL(importer).href,
|
|
154
|
+
options.importMap,
|
|
155
|
+
);
|
|
156
|
+
if (mapped !== null) {
|
|
157
|
+
classify(mapped, importer, out, depth + 1);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const resolved = Bun.resolveSync(spec, dirname(importer));
|
|
163
|
+
if (!isDependencyPath(resolved)) out.files.push(resolved);
|
|
164
|
+
} catch {
|
|
165
|
+
// Bare package or a not-yet-written file: a leaf either way. A file
|
|
166
|
+
// that appears later is picked up by the next refresh.
|
|
167
|
+
if (pathLike) {
|
|
168
|
+
out.files.push(fileURLToPath(new URL(spec, pathToFileURL(importer))));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function parseFile(path: string): Promise<ParsedFile | null> {
|
|
174
|
+
let st;
|
|
175
|
+
try {
|
|
176
|
+
st = statSync(path);
|
|
177
|
+
} catch {
|
|
178
|
+
parsed.delete(path);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const cached = parsed.get(path);
|
|
182
|
+
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
|
|
183
|
+
return cached;
|
|
184
|
+
}
|
|
185
|
+
const transpiler = transpilerFor(path);
|
|
186
|
+
const entry: ParsedFile = {
|
|
187
|
+
mtimeMs: st.mtimeMs,
|
|
188
|
+
size: st.size,
|
|
189
|
+
files: [],
|
|
190
|
+
packages: [],
|
|
191
|
+
remotes: [],
|
|
192
|
+
};
|
|
193
|
+
if (transpiler) {
|
|
194
|
+
try {
|
|
195
|
+
const code = await readFile(path, "utf8");
|
|
196
|
+
for (const imp of transpiler.scanImports(code)) {
|
|
197
|
+
classify(imp.path, path, entry);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Syntax error mid-edit: no edges until the next clean save.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
parsed.set(path, entry);
|
|
204
|
+
return entry;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Per-refresh memo: within one refresh every file is stat-validated (and,
|
|
209
|
+
* on a miss, parsed) once, however many functions share it. Without it a
|
|
210
|
+
* 200-function project with a common `_shared` tree pays one stat per
|
|
211
|
+
* file per function — tens of seconds on a slow disk.
|
|
212
|
+
*/
|
|
213
|
+
type Batch = Map<string, Promise<ParsedFile | null>>;
|
|
214
|
+
|
|
215
|
+
function parseOnce(batch: Batch, path: string): Promise<ParsedFile | null> {
|
|
216
|
+
let p = batch.get(path);
|
|
217
|
+
if (!p) batch.set(path, p = parseFile(path));
|
|
218
|
+
return p;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function walk(entry: string, batch: Batch = new Map()): Promise<FunctionGraph> {
|
|
222
|
+
const g: FunctionGraph = {
|
|
223
|
+
files: new Set([entry]),
|
|
224
|
+
packages: new Map(),
|
|
225
|
+
remotes: new Set(),
|
|
226
|
+
};
|
|
227
|
+
let frontier = [entry];
|
|
228
|
+
while (frontier.length > 0) {
|
|
229
|
+
const results = await Promise.all(frontier.map((f) => parseOnce(batch, f)));
|
|
230
|
+
const next: string[] = [];
|
|
231
|
+
for (const r of results) {
|
|
232
|
+
if (!r) continue;
|
|
233
|
+
for (const pkg of r.packages) g.packages.set(packageKey(pkg), pkg);
|
|
234
|
+
for (const url of r.remotes) g.remotes.add(url);
|
|
235
|
+
for (const f of r.files) {
|
|
236
|
+
if (g.files.has(f)) continue;
|
|
237
|
+
g.files.add(f);
|
|
238
|
+
next.push(f);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
frontier = next;
|
|
242
|
+
}
|
|
243
|
+
return g;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function indexAdd(name: string, file: string): void {
|
|
247
|
+
const k = fileKey(file);
|
|
248
|
+
let set = reverse.get(k);
|
|
249
|
+
if (!set) reverse.set(k, set = new Set());
|
|
250
|
+
set.add(name);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function indexRemove(name: string, file: string): void {
|
|
254
|
+
const k = fileKey(file);
|
|
255
|
+
const set = reverse.get(k);
|
|
256
|
+
if (!set) return;
|
|
257
|
+
set.delete(name);
|
|
258
|
+
if (set.size === 0) reverse.delete(k);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function replace(name: string, fresh: FunctionGraph): void {
|
|
262
|
+
const old = perName.get(name);
|
|
263
|
+
if (old) {
|
|
264
|
+
for (const f of old.files) if (!fresh.files.has(f)) indexRemove(name, f);
|
|
265
|
+
}
|
|
266
|
+
for (const f of fresh.files) {
|
|
267
|
+
if (!old || !old.files.has(f)) indexAdd(name, f);
|
|
268
|
+
}
|
|
269
|
+
perName.set(name, fresh);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
async refresh(name, entry) {
|
|
274
|
+
replace(name, await walk(toPath(entry)));
|
|
275
|
+
},
|
|
276
|
+
async refreshMany(entries) {
|
|
277
|
+
const batch: Batch = new Map();
|
|
278
|
+
await Promise.all(
|
|
279
|
+
[...entries].map(async (e) => replace(e.name, await walk(toPath(e.entry), batch))),
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
forget(name) {
|
|
283
|
+
const old = perName.get(name);
|
|
284
|
+
if (!old) return;
|
|
285
|
+
for (const f of old.files) indexRemove(name, f);
|
|
286
|
+
perName.delete(name);
|
|
287
|
+
},
|
|
288
|
+
affected(changedFiles) {
|
|
289
|
+
const out = new Set<string>();
|
|
290
|
+
for (const raw of changedFiles) {
|
|
291
|
+
const owners = reverse.get(fileKey(toPath(raw)));
|
|
292
|
+
if (!owners) continue;
|
|
293
|
+
for (const n of owners) out.add(n);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
},
|
|
297
|
+
filesFor(name) {
|
|
298
|
+
return [...(perName.get(name)?.files ?? [])];
|
|
299
|
+
},
|
|
300
|
+
packagesFor(name) {
|
|
301
|
+
return [...(perName.get(name)?.packages.values() ?? [])];
|
|
302
|
+
},
|
|
303
|
+
remotesFor(name) {
|
|
304
|
+
return [...(perName.get(name)?.remotes ?? [])];
|
|
305
|
+
},
|
|
306
|
+
get size() {
|
|
307
|
+
return reverse.size;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|