1tube 0.1.42 → 0.1.44

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.
Files changed (37) hide show
  1. package/README.md +57 -7
  2. package/drizzle/20260610161429_init_logs/snapshot.json +1 -1
  3. package/drizzle/20260610161430_logs_fts/snapshot.json +1 -1
  4. package/package.json +1 -1
  5. package/scripts/npm-cli.js +241 -19
  6. package/src/backends/deno/hot-reloader.ts +3 -1
  7. package/src/backends/deno/shared-runtime.ts +5 -1
  8. package/src/backends/deno/worker-entry-colocated.ts +386 -0
  9. package/src/backends/deno/worker-entry.ts +27 -8
  10. package/src/backends/deno/worker-host.ts +696 -3
  11. package/src/backends/vercel/wrapper.ts +2 -1
  12. package/src/backends/workerd/backend.ts +450 -339
  13. package/src/backends/workerd/capnp.ts +30 -11
  14. package/src/backends/workerd/hot-reloader.ts +28 -11
  15. package/src/backends/workerd/prebuilt.ts +66 -51
  16. package/src/backends/workerd/process.ts +26 -9
  17. package/src/backends/workerd/runtime-overrides.ts +199 -59
  18. package/src/backends/workerd/shared-runtime.ts +12 -5
  19. package/src/bundler/core.ts +3 -1
  20. package/src/cli/build.ts +17 -3
  21. package/src/cli/vercel-build.ts +12 -9
  22. package/src/discovery.ts +20 -9
  23. package/src/env-scope.ts +6 -2
  24. package/src/gateway/auth.ts +6 -2
  25. package/src/gateway/cors.ts +19 -8
  26. package/src/gateway/logging.ts +90 -30
  27. package/src/gateway/rate-limit.ts +9 -3
  28. package/src/gateway/route-prefix.ts +4 -4
  29. package/src/health.ts +3 -1
  30. package/src/logs/api.ts +1 -5
  31. package/src/logs/db.ts +3 -1
  32. package/src/logs/query.ts +3 -1
  33. package/src/logs/schema.ts +1 -6
  34. package/src/logs/writer.ts +3 -1
  35. package/src/manifest.ts +2 -1
  36. package/src/registry.ts +13 -8
  37. package/src/server.ts +67 -4
package/README.md CHANGED
@@ -41,7 +41,12 @@ in-process on filesystem changes (including newly created function folders).
41
41
  ## npm CLI
42
42
 
43
43
  The published npm package exposes a `1tube` binary. It is a tiny Node shim that
44
- launches this repo's Deno CLI, so machines using it need Deno on `PATH`.
44
+ launches this repo's Deno CLI, so machines using it need Deno on `PATH` (set
45
+ `DENO_BIN` to point at a specific `deno` if it isn't). **Prefer this launcher
46
+ over a hand-rolled `deno run … src/server.ts` line** — it applies 1tube's
47
+ recommended defaults (see [minimum dependency age](#minimum-dependency-age)
48
+ below) before Deno starts, which a flag added _inside_ the gateway can no longer
49
+ do.
45
50
 
46
51
  ```bash
47
52
  # Show CLI help
@@ -57,6 +62,44 @@ npx 1tube package --functions supabase/functions --in dist --out fw.1tube --sign
57
62
  This replaces local-source invocations such as
58
63
  `deno run -A ../1tube/src/cli.ts ...` in downstream CI pipelines.
59
64
 
65
+ ### Passing deno flags through the launcher
66
+
67
+ Arguments before a standalone `--` are 1tube CLI args; everything **after** `--`
68
+ is forwarded verbatim to `deno run` (inserted before the entrypoint). This lets
69
+ the launcher fully replace a project's bespoke `deno run … src/server.ts`
70
+ command — including project-level deno flags like `--config`, `--env-file`,
71
+ `--node-modules-dir` and `--no-lock`:
72
+
73
+ ```bash
74
+ npx 1tube serve --functions ./supabase/functions --dev --hmr \
75
+ -- --no-lock --env-file=.env --config ./supabase/functions/deno.json --node-modules-dir=false
76
+ ```
77
+
78
+ - If your passthrough includes its own `--config`, the launcher does **not** add
79
+ 1tube's bundled `deno.json` — your import map wins (the gateway's own sources
80
+ use fully-qualified `npm:`/`jsr:` specifiers, so they resolve regardless).
81
+ - Prefer keeping the command line clean? Set the same flags in
82
+ `1TUBE_DENO_ARGS` (space-separated) instead of after `--`. CLI passthrough
83
+ takes precedence on any duplicate flag.
84
+
85
+ ### Minimum dependency age
86
+
87
+ Deno 2.9 turns on a **24-hour minimum dependency age** by default: a function
88
+ that pins a just-published npm version fails to resolve locally with
89
+ _"… is newer than the specified minimum dependency date"_. Because Deno
90
+ snapshots npm config at process startup, this can only be changed by the
91
+ launcher, not from inside the running gateway.
92
+
93
+ The `1tube` launcher (and the .NET host) therefore export
94
+ `NPM_CONFIG_MIN_RELEASE_AGE=0` for the Deno child **unless you've already set
95
+ it**. That env var is the lowest explicit tier in Deno's precedence chain
96
+ (`--minimum-dependency-age` > `deno.json` `minimumDependencyAge` > `.npmrc`
97
+ `min-release-age` > this env var > the 24h built-in default), so it cancels
98
+ _only_ the new built-in default — any age you configure through `.npmrc`,
99
+ `deno.json`, or the CLI flag still wins. To re-enable a guard, set
100
+ `NPM_CONFIG_MIN_RELEASE_AGE` (e.g. `24h`) yourself or use any higher-precedence
101
+ mechanism.
102
+
60
103
  ### Deno npm lifecycle scripts
61
104
 
62
105
  Deno does not run npm `postinstall` / build scripts unless you allow them.
@@ -67,12 +110,12 @@ dependency install — **1tube’s runtime graph is just Hono + JSR std**.
67
110
 
68
111
  ## Endpoints
69
112
 
70
- | Path | Description |
71
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
72
- | `GET /` | Liveness probe (`{"status":"ok"}`) — intentionally minimal so unauthenticated callers don't see the function map. |
73
- | `POST /functions/v1/:name` | Invoke an edge function |
74
- | `GET /health` | Auth-gated health (`Authorization: Bearer $INTERNAL_KEY`); without auth returns the same minimal `{"status":"ok"}`. |
75
- | `GET /metrics` | Auth-gated Prometheus exposition (same scheme). |
113
+ | Path | Description |
114
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `GET /` | Liveness probe (`{"status":"ok"}`) — intentionally minimal so unauthenticated callers don't see the function map. |
116
+ | `POST /functions/v1/:name` | Invoke an edge function |
117
+ | `GET /health` | Auth-gated health (`Authorization: Bearer $INTERNAL_KEY`); without auth returns the same minimal `{"status":"ok"}`. |
118
+ | `GET /metrics` | Auth-gated Prometheus exposition (same scheme). |
76
119
  | `GET /1tube/warmup` | Boot progress (deferred boot): `{ready, total, loaded, loading, queued, failed}`. CORS-enabled so frontends can poll it to drive a warm-up overlay. |
77
120
 
78
121
  ## Configuration
@@ -94,6 +137,12 @@ All knobs default to safe-but-backwards-compatible values. The TS gateway
94
137
  | `1TUBE_BODY_LIMIT_MB` | `30` | Hono `bodyLimit`; matches Supabase. Returns 413 before the handler runs. |
95
138
  | `1TUBE_BODY_READ_MS` | `30000` | Slow-loris guard. Max idle gap (ms) between body chunks before the request is aborted with **408**. NOT a total body-read deadline — large but fast uploads pass through. Set `0` to disable. |
96
139
  | `1TUBE_CORS_ORIGIN` | `*` (dev only) | Comma-separated allowlist or `*`. In prod, leaving unset disables CORS. |
140
+ | `1TUBE_CORS_ALLOW_HEADERS` | Supabase defaults | Comma-separated `Access-Control-Allow-Headers`. Replaces the built-in default list (`authorization, x-client-info, apikey, content-type`, …) when set. |
141
+ | `1TUBE_CORS_ALLOW_METHODS` | Supabase defaults | Comma-separated `Access-Control-Allow-Methods` (default `GET, POST, PUT, PATCH, DELETE, OPTIONS`). |
142
+ | `1TUBE_CORS_EXPOSE_HEADERS` | 1tube internals | Comma-separated `Access-Control-Expose-Headers`. **Merged with** (not replacing) the headers 1tube must always expose (e.g. `X-1tube-Warming`). |
143
+ | `1TUBE_CORS_MAX_AGE` | unset | Seconds for `Access-Control-Max-Age` (preflight cache). Omitted when unset. |
144
+ | `1TUBE_CORS_ALLOW_CREDENTIALS` | off | `1`/`true` emits `Access-Control-Allow-Credentials: true`. Per the Fetch spec this is incompatible with `Origin: *`, so when credentials are on, a literal `*` allowlist is rejected — set an explicit origin. |
145
+ | `--route-prefix` / `1TUBE_ROUTE_PREFIX` | `/functions/v1` | URL prefix functions are mounted under. Change it to e.g. `/api` to serve at `/api/<name>`. Normalized to a leading-slash, no-trailing-slash form; logging and rate-limiting follow it automatically. |
97
146
  | `1TUBE_TRUSTED_PROXIES` | empty | Comma-separated list of remote IPs whose `X-Forwarded-For` is honored. Anything else uses the raw socket address — XFF spoofing no longer mints fresh rate-limit buckets. |
98
147
  | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | SIGINT/SIGTERM drain budget. |
99
148
  | `INTERNAL_KEY` | unset | Required to read detailed `/health` and `/metrics`. Header-only: `Authorization: Bearer $INTERNAL_KEY`. |
@@ -105,6 +154,7 @@ All knobs default to safe-but-backwards-compatible values. The TS gateway
105
154
  | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | Total wall-clock budget (ms) for shutdown, split between (1) draining gateway in-flight requests and (2) tearing down the workerd subprocess. See [Graceful shutdown](#graceful-shutdown) below. |
106
155
  | `1TUBE_DEFAULT_RPM` | `120` | Override the gateway-wide default rate limit (requests per minute, per IP, per function). Per-function `1tube.json#rpm` still wins. |
107
156
  | `1TUBE_DISABLE_RATE_LIMIT` | unset | Set to `1` to bypass rate limiting entirely. **Load-test / dev only** — production deployments must keep the limiter on. The gateway prints a clear warning at boot when this is enabled. |
157
+ | `NPM_CONFIG_MIN_RELEASE_AGE` | `0` (set by launcher) | Deno 2.9's minimum dependency age. The `1tube` launcher and .NET host export `0` for the Deno child unless you set it, cancelling only Deno's new 24h default. See [Minimum dependency age](#minimum-dependency-age). Must be set _before_ Deno starts — a gateway flag can't change it. |
108
158
 
109
159
  ````
110
160
  ## Deferred boot & warm-up overlay
@@ -302,4 +302,4 @@
302
302
  }
303
303
  ],
304
304
  "renames": []
305
- }
305
+ }
@@ -302,4 +302,4 @@
302
302
  }
303
303
  ],
304
304
  "renames": []
305
- }
305
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
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",
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { dirname, resolve } from "node:path";
4
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
5
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
5
6
  import process from "node:process";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
@@ -9,26 +10,134 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
10
  const denoConfig = resolve(packageRoot, "deno.json");
10
11
  const cliEntrypoint = resolve(packageRoot, "src", "cli.ts");
11
12
  const denoBin = process.env.DENO_BIN || "deno";
12
- const cliArgs = process.argv.slice(2);
13
- const forwardedArgs = cliArgs.length === 0 ? ["--help"] : cliArgs;
14
-
15
- const child = spawn(
16
- denoBin,
17
- [
18
- "run",
19
- "--quiet",
20
- "-A",
21
- "--config",
22
- denoConfig,
23
- cliEntrypoint,
24
- ...forwardedArgs,
25
- ],
26
- {
13
+
14
+ // Arg model
15
+ // ---------
16
+ // Everything BEFORE a standalone `--` is a 1tube CLI argument (forwarded
17
+ // after the entrypoint). Everything AFTER `--` is a raw `deno run` flag,
18
+ // inserted BEFORE the entrypoint — so this launcher can fully replace a
19
+ // hand-rolled `deno run … src/server.ts` line:
20
+ //
21
+ // 1tube serve --functions ./supabase/functions --dev --hmr \
22
+ // -- --no-lock --env-file=.env --config ./supabase/functions/deno.json --node-modules-dir=false
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 = [
34
+ ...tokenizeArgs(
35
+ process.env.ONETUBE_DENO_ARGS ?? process.env["1TUBE_DENO_ARGS"] ?? "",
36
+ ),
37
+ ...cliDenoArgs,
38
+ ];
39
+
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
+ // 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
+ // bare binary on POSIX) with `shell:false` lets Node escape arguments
107
+ // properly — important now that we forward arbitrary deno flags, some of
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).
111
+ const resolvedDeno = resolveExecutable(denoBin);
112
+ const needsShell = resolvedDeno
113
+ ? /\.(cmd|bat)$/i.test(resolvedDeno)
114
+ : process.platform === "win32";
115
+ const spawnBin = resolvedDeno ?? denoBin;
116
+
117
+ let child;
118
+ 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
+ const command = [spawnBin, ...denoArgs].map(quoteForCmd).join(" ");
125
+ child = spawn(command, {
27
126
  stdio: "inherit",
28
- shell: process.platform === "win32",
127
+ shell: true,
29
128
  windowsHide: false,
30
- },
31
- );
129
+ env: childEnv,
130
+ });
131
+ } else {
132
+ // Real executable (deno.exe / a POSIX binary): no shell, so Node
133
+ // escapes each argument for us.
134
+ child = spawn(spawnBin, denoArgs, {
135
+ stdio: "inherit",
136
+ shell: false,
137
+ windowsHide: false,
138
+ env: childEnv,
139
+ });
140
+ }
32
141
 
33
142
  for (const signal of ["SIGINT", "SIGTERM"]) {
34
143
  process.on(signal, () => {
@@ -54,3 +163,116 @@ child.on("exit", (code, signal) => {
54
163
  }
55
164
  process.exit(code ?? 1);
56
165
  });
166
+
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
+ /**
222
+ * 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
+ * a dev-time convenience var — not a full shell parser.
225
+ *
226
+ * @param {string} s
227
+ * @returns {string[]}
228
+ */
229
+ function tokenizeArgs(s) {
230
+ const out = [];
231
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
232
+ let m;
233
+ while ((m = re.exec(s)) !== null) {
234
+ out.push(m[1] ?? m[2] ?? m[3]);
235
+ }
236
+ return out;
237
+ }
238
+
239
+ /**
240
+ * 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
+ * the caller hands the bare name to spawn and lets the OS/shell try.
243
+ *
244
+ * @param {string} bin
245
+ * @returns {string | null}
246
+ */
247
+ function resolveExecutable(bin) {
248
+ // An explicit path (absolute or containing a separator) is used as-is.
249
+ if (isAbsolute(bin) || bin.includes("/") || bin.includes("\\")) {
250
+ return existsSync(bin) ? bin : null;
251
+ }
252
+ const isWin = process.platform === "win32";
253
+ const exts = isWin
254
+ ? (process.env.PATHEXT?.split(";").map((e) => e.trim()).filter(Boolean) ??
255
+ [".EXE", ".CMD", ".BAT"])
256
+ : [""];
257
+ for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
258
+ for (const ext of exts) {
259
+ const candidate = join(dir, bin + ext);
260
+ if (existsSync(candidate)) return candidate;
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /**
267
+ * Quote a single argument for cmd.exe. Leaves simple tokens untouched to
268
+ * avoid over-quoting; wraps anything with whitespace or shell
269
+ * metacharacters in double quotes, doubling any embedded quotes.
270
+ *
271
+ * @param {string} arg
272
+ * @returns {string}
273
+ */
274
+ function quoteForCmd(arg) {
275
+ if (arg === "") return '""';
276
+ if (!/[\s"&|<>^()%!]/.test(arg)) return arg;
277
+ return '"' + arg.replace(/"/g, '""') + '"';
278
+ }
@@ -398,7 +398,9 @@ export function createDenoHotReloader(
398
398
  }`;
399
399
  log(`[1tube] HMR ${reason}`);
400
400
  try {
401
- const summary = await opts.host.reload(affected, reason);
401
+ const summary = await opts.host.reload(affected, reason, {
402
+ changedPaths: paths,
403
+ });
402
404
  const parts: string[] = [];
403
405
  if (summary.reloaded.length > 0) {
404
406
  parts.push(`reloaded=${summary.reloaded.join(",")}`);
@@ -59,7 +59,11 @@ export interface DenoSharedRuntime {
59
59
  * Invoke a function exported by a shared module. Throws if the
60
60
  * module or export is unknown, or if the export itself throws.
61
61
  */
62
- call(moduleId: string, exportName: string, args: readonly unknown[]): Promise<unknown>;
62
+ call(
63
+ moduleId: string,
64
+ exportName: string,
65
+ args: readonly unknown[],
66
+ ): Promise<unknown>;
63
67
  /**
64
68
  * Re-import a shared module after its source file changed. Returns
65
69
  * the new record. If the export list changed, callers should