1tube 0.1.46 → 0.1.48
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 +44 -12
- package/deno.lock +24 -14
- package/package.json +5 -5
- package/scripts/npm-cli.js +37 -165
- package/src/cli/dep-pin.ts +126 -0
- package/src/cli.ts +7 -46
- package/src/launch.ts +677 -0
- package/src/server.ts +113 -8
package/README.md
CHANGED
|
@@ -62,25 +62,57 @@ npx 1tube package --functions supabase/functions --in dist --out fw.1tube --sign
|
|
|
62
62
|
This replaces local-source invocations such as
|
|
63
63
|
`deno run -A ../1tube/src/cli.ts ...` in downstream CI pipelines.
|
|
64
64
|
|
|
65
|
-
###
|
|
65
|
+
### One launcher, one path
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
There is a single launcher (`src/launch.ts`): the npm bin, the deno tasks, and
|
|
68
|
+
host projects all funnel through it, and `serve` **always** spawns the gateway
|
|
69
|
+
as a child `deno run`. That child process is what lets the launcher reconcile
|
|
70
|
+
the lockfile with your installed dependencies _before_ Deno resolves any npm
|
|
71
|
+
specifier — and fully reload it when a dependency changes (see below).
|
|
72
|
+
|
|
73
|
+
The launcher recognizes its own flags and forwards everything else to the
|
|
74
|
+
gateway, so it fully replaces a bespoke `deno run … src/server.ts` line — no
|
|
75
|
+
`--` split needed:
|
|
72
76
|
|
|
73
77
|
```bash
|
|
74
78
|
npx 1tube serve --functions ./supabase/functions --dev --hmr \
|
|
75
|
-
--
|
|
79
|
+
--config ./supabase/functions/deno.json --env-file=.env --node-modules-dir=false
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
Launcher flags: `--lock <path>` (managed lockfile, default
|
|
83
|
+
`<cwd>/.1tube/deno.lock`), `--no-lock`, `--refresh-lock`, `--config <path>`,
|
|
84
|
+
`--env-file[=<path>]` (repeatable), `--minimum-dependency-age <n>`,
|
|
85
|
+
`--node-modules-dir <v>`, `--no-pin`, `--no-dep-watch`. Anything after a `--`
|
|
86
|
+
is forwarded verbatim to the gateway.
|
|
87
|
+
|
|
88
|
+
- If you don't pass `--config`, the launcher falls back to 1tube's bundled
|
|
89
|
+
`deno.json`; the gateway's own sources use fully-qualified `npm:`/`jsr:`
|
|
90
|
+
specifiers, so they resolve either way.
|
|
81
91
|
- Prefer keeping the command line clean? Set the same flags in
|
|
82
|
-
`1TUBE_DENO_ARGS` (space-separated)
|
|
83
|
-
|
|
92
|
+
`1TUBE_DENO_ARGS` (space-separated); they're appended to the launcher args.
|
|
93
|
+
|
|
94
|
+
### Dependency pinning & live reload
|
|
95
|
+
|
|
96
|
+
The edge runtime runs with `--node-modules-dir=false`, so Deno resolves npm
|
|
97
|
+
specifiers from its own cache + lockfile, not from `node_modules`. An import map
|
|
98
|
+
that maps a dependency with an **unconstrained** specifier (`npm:1tube/edge`,
|
|
99
|
+
i.e. `@*`) is satisfied by _any_ locked version — so bumping that dependency in
|
|
100
|
+
`package.json` would silently never reach the edge runtime, and the gateway
|
|
101
|
+
could end up a different version than the edge surface its functions import.
|
|
102
|
+
|
|
103
|
+
The launcher closes this gap: before spawning the gateway it rewrites every
|
|
104
|
+
unconstrained `npm:` specifier in your `--config` import map to the **exact
|
|
105
|
+
version installed in `node_modules`** (the source of truth your package manager
|
|
106
|
+
controls), writing the result to `<.1tube>/deno.gen.json`. Now the specifier's
|
|
107
|
+
version tracks the installed dependency, so Deno's native lockfile auto-update
|
|
108
|
+
recomputes just that entry — every already-pinned dependency keeps its locked
|
|
109
|
+
version (and the warm-boot speed a lockfile buys).
|
|
110
|
+
|
|
111
|
+
While serving, the launcher watches your deno config and each pinned
|
|
112
|
+
dependency's `package.json`. When an install or a config edit changes the
|
|
113
|
+
effective version set, it prints `dependency change detected — reloading
|
|
114
|
+
gateway` and fully restarts the gateway so Deno re-resolves. Disable with
|
|
115
|
+
`--no-pin` / `--no-dep-watch`.
|
|
84
116
|
|
|
85
117
|
### Minimum dependency age
|
|
86
118
|
|
package/deno.lock
CHANGED
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
"npm:@ai-sdk/google@2": "2.0.74_zod@3.25.76",
|
|
16
16
|
"npm:@ai-sdk/openai@2": "2.0.106_zod@3.25.76",
|
|
17
17
|
"npm:ai@6": "6.0.193_zod@3.25.76",
|
|
18
|
-
"npm:drizzle-kit@1.0.0-rc.
|
|
18
|
+
"npm:drizzle-kit@1.0.0-rc.4": "1.0.0-rc.4",
|
|
19
19
|
"npm:drizzle-orm@1.0.0-rc.3": "1.0.0-rc.3_zod@3.25.76",
|
|
20
|
+
"npm:drizzle-orm@1.0.0-rc.4": "1.0.0-rc.4_zod@3.25.76",
|
|
20
21
|
"npm:es-module-lexer@2": "2.1.0",
|
|
21
22
|
"npm:esbuild@~0.25.5": "0.25.12",
|
|
22
23
|
"npm:esbuild@~0.28.1": "0.28.1",
|
|
23
|
-
"npm:hono@4": "4.12.
|
|
24
|
-
"npm:hono@^4.12.
|
|
24
|
+
"npm:hono@4": "4.12.30",
|
|
25
|
+
"npm:hono@^4.12.30": "4.12.30",
|
|
25
26
|
"npm:typescript@latest": "6.0.3",
|
|
26
27
|
"npm:zod@^3.23.0": "3.25.76"
|
|
27
28
|
},
|
|
@@ -129,8 +130,8 @@
|
|
|
129
130
|
"json-schema"
|
|
130
131
|
]
|
|
131
132
|
},
|
|
132
|
-
"@drizzle-team/brocli@0.
|
|
133
|
-
"integrity": "sha512-
|
|
133
|
+
"@drizzle-team/brocli@0.12.0": {
|
|
134
|
+
"integrity": "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="
|
|
134
135
|
},
|
|
135
136
|
"@esbuild/aix-ppc64@0.25.12": {
|
|
136
137
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
|
@@ -417,8 +418,8 @@
|
|
|
417
418
|
"zod"
|
|
418
419
|
]
|
|
419
420
|
},
|
|
420
|
-
"drizzle-kit@1.0.0-rc.
|
|
421
|
-
"integrity": "sha512-
|
|
421
|
+
"drizzle-kit@1.0.0-rc.4": {
|
|
422
|
+
"integrity": "sha512-KZCpjRyu+oYHLj/UJogfFlOkWhHVkaEI2EOT1U3NDVXUzLoTyPjqwFxwOrlQxsY6jzRyxcz4EacqboGfhEeYrA==",
|
|
422
423
|
"dependencies": [
|
|
423
424
|
"@drizzle-team/brocli",
|
|
424
425
|
"@js-temporal/polyfill",
|
|
@@ -437,6 +438,15 @@
|
|
|
437
438
|
"zod"
|
|
438
439
|
]
|
|
439
440
|
},
|
|
441
|
+
"drizzle-orm@1.0.0-rc.4_zod@3.25.76": {
|
|
442
|
+
"integrity": "sha512-BT+pf+qoiYHqltoA88Jmf6ilGMXPlpfE0hEJKc2adRtMCAl25Swk/t5gXcWxZNAwdtf3F5gCd2FpeOyP/pT0Hw==",
|
|
443
|
+
"dependencies": [
|
|
444
|
+
"zod"
|
|
445
|
+
],
|
|
446
|
+
"optionalPeers": [
|
|
447
|
+
"zod"
|
|
448
|
+
]
|
|
449
|
+
},
|
|
440
450
|
"es-module-lexer@2.1.0": {
|
|
441
451
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="
|
|
442
452
|
},
|
|
@@ -515,11 +525,11 @@
|
|
|
515
525
|
"resolve-pkg-maps"
|
|
516
526
|
]
|
|
517
527
|
},
|
|
518
|
-
"hono@4.12.
|
|
519
|
-
"integrity": "sha512-
|
|
528
|
+
"hono@4.12.30": {
|
|
529
|
+
"integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="
|
|
520
530
|
},
|
|
521
|
-
"jiti@2.
|
|
522
|
-
"integrity": "sha512-
|
|
531
|
+
"jiti@2.7.0": {
|
|
532
|
+
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
|
523
533
|
"bin": true
|
|
524
534
|
},
|
|
525
535
|
"jsbi@4.3.2": {
|
|
@@ -553,9 +563,9 @@
|
|
|
553
563
|
],
|
|
554
564
|
"packageJson": {
|
|
555
565
|
"dependencies": [
|
|
556
|
-
"npm:drizzle-kit@1.0.0-rc.
|
|
557
|
-
"npm:drizzle-orm@1.0.0-rc.
|
|
558
|
-
"npm:hono@^4.12.
|
|
566
|
+
"npm:drizzle-kit@1.0.0-rc.4",
|
|
567
|
+
"npm:drizzle-orm@1.0.0-rc.4",
|
|
568
|
+
"npm:hono@^4.12.30"
|
|
559
569
|
]
|
|
560
570
|
}
|
|
561
571
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "1tube",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
"serve": "deno task serve",
|
|
56
56
|
"build:edge": "deno run -A npm:typescript@latest/tsc --target ES2022 --module ESNext --moduleResolution Bundler --declaration --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts",
|
|
57
57
|
"prepack": "bun run build:edge",
|
|
58
|
-
"typecheck": "deno check src/server.ts src/cli.ts src/edge.ts src/edge-caller.ts src/warmup-client.ts eslint-plugin.js",
|
|
58
|
+
"typecheck": "deno check src/server.ts src/cli.ts src/launch.ts src/edge.ts src/edge-caller.ts src/warmup-client.ts eslint-plugin.js",
|
|
59
59
|
"check": "deno cache --allow-scripts=npm:protobufjs src/server.ts",
|
|
60
60
|
"smoke:bin": "node ./scripts/npm-cli.js --version",
|
|
61
61
|
"prepublishOnly": "bun run build:edge && bun run typecheck && bun run check && bun run smoke:bin"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"drizzle-orm": "1.0.0-rc.
|
|
65
|
-
"hono": "^4.12.
|
|
64
|
+
"drizzle-orm": "1.0.0-rc.4",
|
|
65
|
+
"hono": "^4.12.30"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"drizzle-kit": "1.0.0-rc.
|
|
68
|
+
"drizzle-kit": "1.0.0-rc.4"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/scripts/npm-cli.js
CHANGED
|
@@ -1,113 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* `1tube` npm bin — a tiny Node shim that locates Deno and runs the single
|
|
5
|
+
* launcher (`src/launch.ts`). All argument handling, the managed lockfile,
|
|
6
|
+
* dependency pinning, minimum-dependency-age, and live reload live in the
|
|
7
|
+
* launcher itself, so this shim stays trivial: find deno, forward argv, wire
|
|
8
|
+
* up signals + exit code.
|
|
9
|
+
*
|
|
10
|
+
* Machines using this need Deno on PATH (or set `DENO_BIN`). Arguments are
|
|
11
|
+
* forwarded verbatim — the launcher recognizes its own flags (`--lock`,
|
|
12
|
+
* `--config`, `--env-file`, `--node-modules-dir`, `--minimum-dependency-age`,
|
|
13
|
+
* `--no-lock`, `--no-pin`, `--no-dep-watch`, …) and forwards everything else
|
|
14
|
+
* to the gateway. `1TUBE_DENO_ARGS` (space-separated) is appended for projects
|
|
15
|
+
* that prefer to keep flags out of the command line.
|
|
16
|
+
*/
|
|
17
|
+
|
|
3
18
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
5
20
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
21
|
import process from "node:process";
|
|
7
22
|
import { fileURLToPath } from "node:url";
|
|
8
23
|
|
|
9
24
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
const
|
|
11
|
-
const cliEntrypoint = resolve(packageRoot, "src", "cli.ts");
|
|
25
|
+
const launcherEntrypoint = resolve(packageRoot, "src", "launch.ts");
|
|
12
26
|
const denoBin = process.env.DENO_BIN || "deno";
|
|
13
27
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
// Projects that would rather not thread flags through the command line
|
|
25
|
-
// can set `1TUBE_DENO_ARGS` instead (same flags, space-separated).
|
|
26
|
-
const rawArgs = process.argv.slice(2);
|
|
27
|
-
const sepIndex = rawArgs.indexOf("--");
|
|
28
|
-
const tubeArgs = sepIndex === -1 ? rawArgs : rawArgs.slice(0, sepIndex);
|
|
29
|
-
const cliDenoArgs = sepIndex === -1 ? [] : rawArgs.slice(sepIndex + 1);
|
|
30
|
-
|
|
31
|
-
// CLI passthrough is appended last so it wins on any duplicate flag
|
|
32
|
-
// (Deno applies last-one-wins for repeated flags).
|
|
33
|
-
const denoPassthrough = [
|
|
28
|
+
// The launcher needs no import map or lockfile of its own (it imports only
|
|
29
|
+
// node: builtins + relative source), and it owns the gateway child's lock,
|
|
30
|
+
// config and npm-config — so the outer `deno run` stays minimal and fast.
|
|
31
|
+
const denoArgs = [
|
|
32
|
+
"run",
|
|
33
|
+
"--quiet",
|
|
34
|
+
"-A",
|
|
35
|
+
"--node-modules-dir=false",
|
|
36
|
+
launcherEntrypoint,
|
|
34
37
|
...tokenizeArgs(
|
|
35
38
|
process.env.ONETUBE_DENO_ARGS ?? process.env["1TUBE_DENO_ARGS"] ?? "",
|
|
36
39
|
),
|
|
37
|
-
...
|
|
40
|
+
...process.argv.slice(2),
|
|
38
41
|
];
|
|
39
42
|
|
|
40
|
-
// Default to --help only when the user gave us nothing at all; if they
|
|
41
|
-
// supplied deno flags but no 1tube subcommand, let cli.ts fall through to
|
|
42
|
-
// its default `serve` path.
|
|
43
|
-
const forwardedTubeArgs = tubeArgs.length === 0 && denoPassthrough.length === 0
|
|
44
|
-
? ["--help"]
|
|
45
|
-
: tubeArgs;
|
|
46
|
-
|
|
47
|
-
// 1tube ships its own deno.json (import map for the gateway's jsr:/npm:
|
|
48
|
-
// dependencies). Only inject it when the project didn't supply its own
|
|
49
|
-
// --config — a Supabase project points --config at
|
|
50
|
-
// supabase/functions/deno.json so its function workers resolve the
|
|
51
|
-
// project's bare specifiers. The gateway's own sources use fully
|
|
52
|
-
// qualified npm:/jsr: specifiers, so they resolve without our config.
|
|
53
|
-
const userSuppliedConfig = denoPassthrough.some((a) =>
|
|
54
|
-
a === "--config" || a === "-c" ||
|
|
55
|
-
a.startsWith("--config=") || a.startsWith("-c=")
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Managed lockfile
|
|
59
|
-
// ----------------
|
|
60
|
-
// A lockfile is what makes a big functions project boot fast: without one
|
|
61
|
-
// (`--no-lock`) Deno re-solves the entire npm/jsr version graph from
|
|
62
|
-
// scratch on every start, and a 100-function project pays that as a
|
|
63
|
-
// multi-second floor every boot. People reach for `--no-lock` to dodge
|
|
64
|
-
// "the lock went stale when I bumped a dep" pain — but in dev Deno's
|
|
65
|
-
// lockfile is ADDITIVE and non-frozen: a changed dependency is appended
|
|
66
|
-
// without error. So the right default is a lockfile we own, not none.
|
|
67
|
-
//
|
|
68
|
-
// We point `--lock` at `.1tube/deno.lock` — a gateway-owned scratch path
|
|
69
|
-
// (already gitignored alongside `.1tube/logs.db`) so it never adds git
|
|
70
|
-
// churn and never collides with a project's own committed `deno.lock`.
|
|
71
|
-
// We never pass `--frozen`, so dep changes self-heal. `1TUBE_REFRESH_LOCK=1`
|
|
72
|
-
// drops the managed lock for a clean rebuild. Any explicit `--lock`,
|
|
73
|
-
// `--no-lock`, or `--frozen` the user passes wins — we don't second-guess.
|
|
74
|
-
const managedLock = resolveManagedLock(denoPassthrough);
|
|
75
|
-
|
|
76
|
-
const denoArgs = ["run", "--quiet", "-A"];
|
|
77
|
-
if (!userSuppliedConfig) denoArgs.push("--config", denoConfig);
|
|
78
|
-
if (managedLock) denoArgs.push("--lock", managedLock);
|
|
79
|
-
denoArgs.push(...denoPassthrough, cliEntrypoint, ...forwardedTubeArgs);
|
|
80
|
-
|
|
81
|
-
// Deno 2.9 enables a 24h "minimum dependency age" by default, which makes
|
|
82
|
-
// a function that pins a freshly published npm version fail to resolve in
|
|
83
|
-
// local dev ("…newer than the specified minimum dependency date"). This
|
|
84
|
-
// setting cannot be changed from inside the process (Deno snapshots npm
|
|
85
|
-
// config at startup), so the launcher is the place to neutralize it.
|
|
86
|
-
//
|
|
87
|
-
// NPM_CONFIG_MIN_RELEASE_AGE is the LOWEST explicit tier in Deno's
|
|
88
|
-
// precedence chain (CLI flag > deno.json > .npmrc > this env var > the
|
|
89
|
-
// 24h built-in default). Setting it to 0 cancels ONLY the built-in
|
|
90
|
-
// default — any .npmrc `min-release-age`, deno.json `minimumDependencyAge`,
|
|
91
|
-
// or `--minimum-dependency-age` the project sets still wins. We never
|
|
92
|
-
// override a value the user already exported.
|
|
93
|
-
const childEnv = { ...process.env };
|
|
94
|
-
if (
|
|
95
|
-
childEnv.NPM_CONFIG_MIN_RELEASE_AGE === undefined &&
|
|
96
|
-
childEnv.npm_config_min_release_age === undefined
|
|
97
|
-
) {
|
|
98
|
-
childEnv.NPM_CONFIG_MIN_RELEASE_AGE = "0";
|
|
99
|
-
}
|
|
100
|
-
// Let the gateway print which lockfile is in effect (it can't read its own
|
|
101
|
-
// process's `--lock` flag back out of Deno).
|
|
102
|
-
if (managedLock) childEnv.ONETUBE_LOCK = managedLock;
|
|
103
|
-
|
|
104
43
|
// Resolve the real deno binary up front. Node ≥18.20 refuses to spawn a
|
|
105
|
-
// `.cmd`/`.bat` shim without a shell, but spawning a real `.exe` (or a
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// which carry paths with spaces (`--config "C:\My Project\deno.json"`).
|
|
109
|
-
// We only fall back to a shell when we must (a `.cmd`/`.bat` shim, or we
|
|
110
|
-
// couldn't locate the binary on a Windows PATH at all).
|
|
44
|
+
// `.cmd`/`.bat` shim without a shell, but spawning a real `.exe` (or a bare
|
|
45
|
+
// binary on POSIX) with `shell:false` lets Node escape arguments properly —
|
|
46
|
+
// important since we forward arbitrary flags, some carrying paths with spaces.
|
|
111
47
|
const resolvedDeno = resolveExecutable(denoBin);
|
|
112
48
|
const needsShell = resolvedDeno
|
|
113
49
|
? /\.(cmd|bat)$/i.test(resolvedDeno)
|
|
@@ -116,26 +52,17 @@ const spawnBin = resolvedDeno ?? denoBin;
|
|
|
116
52
|
|
|
117
53
|
let child;
|
|
118
54
|
if (needsShell) {
|
|
119
|
-
// A `.cmd`/`.bat` shim (the npm-installed deno) — or an unresolved name
|
|
120
|
-
// on Windows — must go through a shell. We pass a single, manually
|
|
121
|
-
// quoted command string instead of an args array so that (a) paths
|
|
122
|
-
// with spaces survive and (b) we sidestep Node's DEP0190 "args are not
|
|
123
|
-
// escaped" deprecation, which fires only for the shell-plus-array form.
|
|
124
55
|
const command = [spawnBin, ...denoArgs].map(quoteForCmd).join(" ");
|
|
125
56
|
child = spawn(command, {
|
|
126
57
|
stdio: "inherit",
|
|
127
58
|
shell: true,
|
|
128
59
|
windowsHide: false,
|
|
129
|
-
env: childEnv,
|
|
130
60
|
});
|
|
131
61
|
} else {
|
|
132
|
-
// Real executable (deno.exe / a POSIX binary): no shell, so Node
|
|
133
|
-
// escapes each argument for us.
|
|
134
62
|
child = spawn(spawnBin, denoArgs, {
|
|
135
63
|
stdio: "inherit",
|
|
136
64
|
shell: false,
|
|
137
65
|
windowsHide: false,
|
|
138
|
-
env: childEnv,
|
|
139
66
|
});
|
|
140
67
|
}
|
|
141
68
|
|
|
@@ -164,64 +91,10 @@ child.on("exit", (code, signal) => {
|
|
|
164
91
|
process.exit(code ?? 1);
|
|
165
92
|
});
|
|
166
93
|
|
|
167
|
-
/**
|
|
168
|
-
* Decide whether to inject a 1tube-managed lockfile, and where.
|
|
169
|
-
*
|
|
170
|
-
* Returns the absolute lock path to pass to `--lock`, or null when the
|
|
171
|
-
* caller already made an explicit locking choice (`--lock`, `--no-lock`,
|
|
172
|
-
* or `--frozen`) — in which case we stay out of the way entirely.
|
|
173
|
-
*
|
|
174
|
-
* The managed lock lives at `<cwd>/.1tube/deno.lock`: a scratch path that
|
|
175
|
-
* 1tube already owns and gitignores, so enabling it adds no git churn and
|
|
176
|
-
* never clashes with a project's own committed `deno.lock`.
|
|
177
|
-
*
|
|
178
|
-
* @param {string[]} passthrough Raw deno flags being forwarded to `deno run`.
|
|
179
|
-
* @returns {string | null}
|
|
180
|
-
*/
|
|
181
|
-
function resolveManagedLock(passthrough) {
|
|
182
|
-
const explicit = passthrough.some((a) =>
|
|
183
|
-
a === "--lock" || a.startsWith("--lock=") ||
|
|
184
|
-
a === "--no-lock" || a.startsWith("--no-lock=") ||
|
|
185
|
-
a === "--frozen" || a.startsWith("--frozen=")
|
|
186
|
-
);
|
|
187
|
-
if (explicit) return null;
|
|
188
|
-
|
|
189
|
-
const dir = join(process.cwd(), ".1tube");
|
|
190
|
-
const lockPath = join(dir, "deno.lock");
|
|
191
|
-
try {
|
|
192
|
-
mkdirSync(dir, { recursive: true });
|
|
193
|
-
} catch {
|
|
194
|
-
// If we can't create the scratch dir, fall back to no managed lock
|
|
195
|
-
// rather than crash the launcher.
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// `1TUBE_REFRESH_LOCK=1` forces a clean rebuild by dropping the managed
|
|
200
|
-
// lock; Deno then re-derives it from the current deps on this boot.
|
|
201
|
-
if (truthy(process.env.ONETUBE_REFRESH_LOCK ?? process.env["1TUBE_REFRESH_LOCK"])) {
|
|
202
|
-
try {
|
|
203
|
-
rmSync(lockPath, { force: true });
|
|
204
|
-
} catch {
|
|
205
|
-
// Best-effort.
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return lockPath;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Loose truthiness for env-var flags: "1", "true", "yes", "on" (any case).
|
|
213
|
-
*
|
|
214
|
-
* @param {string | undefined} v
|
|
215
|
-
* @returns {boolean}
|
|
216
|
-
*/
|
|
217
|
-
function truthy(v) {
|
|
218
|
-
return v !== undefined && /^(1|true|yes|on)$/i.test(v.trim());
|
|
219
|
-
}
|
|
220
|
-
|
|
221
94
|
/**
|
|
222
95
|
* Split a flag string into argv tokens, honoring single/double quotes so
|
|
223
|
-
* values with spaces (`--env-file="my env.env"`) survive. Good enough for
|
|
224
|
-
*
|
|
96
|
+
* values with spaces (`--env-file="my env.env"`) survive. Good enough for a
|
|
97
|
+
* dev-time convenience var — not a full shell parser.
|
|
225
98
|
*
|
|
226
99
|
* @param {string} s
|
|
227
100
|
* @returns {string[]}
|
|
@@ -238,14 +111,13 @@ function tokenizeArgs(s) {
|
|
|
238
111
|
|
|
239
112
|
/**
|
|
240
113
|
* Locate an executable on PATH (honoring PATHEXT on Windows). Returns an
|
|
241
|
-
* absolute path, or null when the name can't be resolved — in which case
|
|
242
|
-
*
|
|
114
|
+
* absolute path, or null when the name can't be resolved — in which case the
|
|
115
|
+
* caller hands the bare name to spawn and lets the OS/shell try.
|
|
243
116
|
*
|
|
244
117
|
* @param {string} bin
|
|
245
118
|
* @returns {string | null}
|
|
246
119
|
*/
|
|
247
120
|
function resolveExecutable(bin) {
|
|
248
|
-
// An explicit path (absolute or containing a separator) is used as-is.
|
|
249
121
|
if (isAbsolute(bin) || bin.includes("/") || bin.includes("\\")) {
|
|
250
122
|
return existsSync(bin) ? bin : null;
|
|
251
123
|
}
|
|
@@ -264,9 +136,9 @@ function resolveExecutable(bin) {
|
|
|
264
136
|
}
|
|
265
137
|
|
|
266
138
|
/**
|
|
267
|
-
* Quote a single argument for cmd.exe. Leaves simple tokens untouched to
|
|
268
|
-
*
|
|
269
|
-
*
|
|
139
|
+
* Quote a single argument for cmd.exe. Leaves simple tokens untouched to avoid
|
|
140
|
+
* over-quoting; wraps anything with whitespace or shell metacharacters in
|
|
141
|
+
* double quotes, doubling any embedded quotes.
|
|
270
142
|
*
|
|
271
143
|
* @param {string} arg
|
|
272
144
|
* @returns {string}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency pinning for the Deno edge runtime.
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves
|
|
5
|
+
* =======================
|
|
6
|
+
* 1tube's edge functions run under Deno with `--node-modules-dir=false`, so
|
|
7
|
+
* Deno resolves npm specifiers from its OWN cache + lockfile, NOT from the
|
|
8
|
+
* host project's `node_modules`. The import map a Supabase-style project ships
|
|
9
|
+
* commonly maps 1tube's surface with an *unconstrained* specifier:
|
|
10
|
+
*
|
|
11
|
+
* "1tube/edge": "npm:1tube/edge" // === npm:1tube@*
|
|
12
|
+
*
|
|
13
|
+
* `@*` is satisfied by ANY version. So once the lockfile pins, say,
|
|
14
|
+
* `1tube@0.1.43`, bumping the npm dependency (and `node_modules/1tube`) to
|
|
15
|
+
* `0.1.46` changes nothing for the edge runtime: the old pin still satisfies
|
|
16
|
+
* `*`, Deno never re-resolves, and the gateway (loaded from `node_modules`,
|
|
17
|
+
* now 0.1.46) ends up serving a *different* version of the edge surface than
|
|
18
|
+
* the functions import (still 0.1.43) — e.g. a missing export at boot.
|
|
19
|
+
*
|
|
20
|
+
* Deno's native lockfile auto-update is correct and fast, but it only triggers
|
|
21
|
+
* when a specifier's *version requirement* actually changes. Unconstrained
|
|
22
|
+
* specifiers never change, so they "stick" forever.
|
|
23
|
+
*
|
|
24
|
+
* The fix
|
|
25
|
+
* =======
|
|
26
|
+
* Before launching the gateway, rewrite every unconstrained `npm:` specifier
|
|
27
|
+
* in the import map to the EXACT version installed in the host's
|
|
28
|
+
* `node_modules` (the single source of truth the user controls via their
|
|
29
|
+
* package manager). `npm:1tube/edge` → `npm:1tube@0.1.46/edge`. Now the
|
|
30
|
+
* specifier version tracks the installed dependency, so bumping the dep makes
|
|
31
|
+
* Deno's native lock auto-update recompute that one entry — while every
|
|
32
|
+
* already-pinned dependency is left untouched, preserving lockfile
|
|
33
|
+
* reproducibility and warm-boot performance.
|
|
34
|
+
*
|
|
35
|
+
* This module is the pure, IO-free core (parsing + reconciliation) so it can
|
|
36
|
+
* be unit-tested; the file IO + process spawn live in `src/launch.ts`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** A parsed `npm:` import-map specifier. */
|
|
40
|
+
export interface NpmSpecifier {
|
|
41
|
+
/** Package name, including scope (e.g. `1tube`, `@supabase/server`). */
|
|
42
|
+
name: string;
|
|
43
|
+
/**
|
|
44
|
+
* Version requirement as written (e.g. `0.1.46`, `^1`, `4.4.3`), or
|
|
45
|
+
* `undefined` when the specifier is unconstrained (`npm:name` / `@*`).
|
|
46
|
+
*/
|
|
47
|
+
version?: string;
|
|
48
|
+
/** Subpath including the leading slash (e.g. `/edge`), or `""`. */
|
|
49
|
+
subpath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `npm:` body grammar: (@scope/)?name (@version)? (/subpath)?
|
|
53
|
+
// group 1 — name: scoped `@scope/name` or bare `name` (no `@` or `/` inside)
|
|
54
|
+
// group 2 — `@version` (everything up to the next `/`)
|
|
55
|
+
// group 3 — `/subpath`
|
|
56
|
+
const NPM_BODY_RE = /^(@[^/@]+\/[^/@]+|[^/@]+)(@[^/]+)?(\/.*)?$/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse an import-map value into its npm parts, or `null` when it is not an
|
|
60
|
+
* `npm:` specifier (e.g. `jsr:`, `https:`, `./local.ts`).
|
|
61
|
+
*/
|
|
62
|
+
export function parseNpmSpecifier(value: string): NpmSpecifier | null {
|
|
63
|
+
if (!value.startsWith("npm:")) return null;
|
|
64
|
+
const body = value.slice(4);
|
|
65
|
+
const m = NPM_BODY_RE.exec(body);
|
|
66
|
+
if (!m) return null;
|
|
67
|
+
return {
|
|
68
|
+
name: m[1],
|
|
69
|
+
version: m[2] ? m[2].slice(1) : undefined,
|
|
70
|
+
subpath: m[3] ?? "",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A single applied pin, for human-facing logging. */
|
|
75
|
+
export interface AppliedPin {
|
|
76
|
+
name: string;
|
|
77
|
+
version: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ReconcileResult {
|
|
81
|
+
/** The rewritten import map (a new object; input is not mutated). */
|
|
82
|
+
imports: Record<string, string>;
|
|
83
|
+
/** Distinct packages that were pinned, in first-seen order. */
|
|
84
|
+
pins: AppliedPin[];
|
|
85
|
+
/** True when at least one entry was rewritten. */
|
|
86
|
+
changed: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rewrite every UNCONSTRAINED `npm:` specifier to the installed version
|
|
91
|
+
* reported by `resolveVersion`. Constrained specifiers (those that already
|
|
92
|
+
* carry a version/range) are left exactly as-is — Deno's native lock handling
|
|
93
|
+
* already tracks those correctly. Non-npm specifiers pass through untouched.
|
|
94
|
+
*
|
|
95
|
+
* `resolveVersion` returns `undefined` when the package is not installed (or
|
|
96
|
+
* has no readable version), in which case the specifier is left unchanged
|
|
97
|
+
* rather than guessed.
|
|
98
|
+
*/
|
|
99
|
+
export function reconcileImports(
|
|
100
|
+
imports: Record<string, string>,
|
|
101
|
+
resolveVersion: (pkg: string) => string | undefined,
|
|
102
|
+
): ReconcileResult {
|
|
103
|
+
const out: Record<string, string> = {};
|
|
104
|
+
const pins: AppliedPin[] = [];
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
let changed = false;
|
|
107
|
+
|
|
108
|
+
for (const [key, value] of Object.entries(imports)) {
|
|
109
|
+
const spec = parseNpmSpecifier(value);
|
|
110
|
+
if (spec && spec.version === undefined) {
|
|
111
|
+
const version = resolveVersion(spec.name);
|
|
112
|
+
if (version) {
|
|
113
|
+
out[key] = `npm:${spec.name}@${version}${spec.subpath}`;
|
|
114
|
+
changed = true;
|
|
115
|
+
if (!seen.has(spec.name)) {
|
|
116
|
+
seen.add(spec.name);
|
|
117
|
+
pins.push({ name: spec.name, version });
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
out[key] = value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { imports: out, pins, changed };
|
|
126
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,51 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 1tube CLI entry point.
|
|
2
|
+
* 1tube CLI entry point (compatibility alias).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Subcommand modules are imported lazily so a CI box running
|
|
9
|
-
* `1tube build` never pays the gateway's static-import cost (Hono,
|
|
10
|
-
* registry, supervisor, every middleware, …) — and conversely, the
|
|
11
|
-
* serve hot path doesn't drag in esbuild's typings just because the
|
|
12
|
-
* build command exists.
|
|
13
|
-
*
|
|
14
|
-
* `Deno.args` is readonly so we cannot strip a leading `serve` token
|
|
15
|
-
* to forward only the tail. Instead, the gateway's `parseArgs` already
|
|
16
|
-
* silently ignores unknown positional tokens — so a literal `serve`
|
|
17
|
-
* passed in the args list just no-ops, which is exactly what we want.
|
|
4
|
+
* The real dispatcher + serve orchestrator lives in `./launch.ts` — this file
|
|
5
|
+
* exists only so existing references to `src/cli.ts` (downstream CI, the npm
|
|
6
|
+
* bin's historical entrypoint, docs) keep working. There is a single launcher
|
|
7
|
+
* implementation; this just forwards to it.
|
|
18
8
|
*/
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (arg0 === "build") {
|
|
23
|
-
const { runBuild } = await import("./cli/build.ts");
|
|
24
|
-
const code = await runBuild(Deno.args.slice(1));
|
|
25
|
-
Deno.exit(code);
|
|
26
|
-
} else if (arg0 === "package") {
|
|
27
|
-
const { runPackage } = await import("./cli/package.ts");
|
|
28
|
-
const code = await runPackage(Deno.args.slice(1));
|
|
29
|
-
Deno.exit(code);
|
|
30
|
-
} else if (arg0 === "--version" || arg0 === "-v") {
|
|
31
|
-
const { VERSION } = await import("./version.ts");
|
|
32
|
-
console.log(VERSION);
|
|
33
|
-
Deno.exit(0);
|
|
34
|
-
} else if (arg0 === "--help" || arg0 === "-h") {
|
|
35
|
-
console.log(`Usage: 1tube <command> [options]
|
|
36
|
-
|
|
37
|
-
Commands:
|
|
38
|
-
serve Run the gateway (default when no command given)
|
|
39
|
-
build Bundle functions for a target (--target workerd|vercel)
|
|
40
|
-
package Build and/or wrap dist/ into a signed .1tube firmware payload
|
|
41
|
-
--version Print version and exit
|
|
42
|
-
--help Print this help
|
|
10
|
+
import { run } from "./launch.ts";
|
|
43
11
|
|
|
44
|
-
|
|
45
|
-
`);
|
|
46
|
-
Deno.exit(0);
|
|
47
|
-
} else {
|
|
48
|
-
// Default path — load the gateway. Dynamic import so the heavy
|
|
49
|
-
// graph is only resolved after the dispatch decision above.
|
|
50
|
-
await import("./server.ts");
|
|
51
|
-
}
|
|
12
|
+
await run(Deno.args);
|