From 5bc177d41897b19e39107352e9e26f9aef76ac12 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 21:15:31 +0200 Subject: [PATCH] fix(cli): probe the dev port through the native Deno runtime Under a Deno-installed CLI, `veryfront dev` died before binding with an unclassified `Cannot read properties of null (reading 'fd')`, right after "Using local filesystem (no proxy mode)". `build`, `serve`, `routes` and `doctor` were fine; the npm-global install of the same version was fine. The port scan added in #3562 probes with a bare `Deno` listen. In the npm build dnt rewrites every bare `Deno` member access to `@deno/shim-deno`, whose TCP listen reads `server._handle.fd` immediately after `net.createServer().listen()` - and Deno's own `node:net` compat has not populated `_handle` by then. Node never reached it, because the shim is only in play when the published package runs under Deno. Resolve the namespace through `getDenoRuntime()`, which reads the global with `Reflect.get` and so survives the dnt rewrite, and guard the source against the bare access coming back. --- cli/commands/dev/port-fallback.test.ts | 24 ++++++++++++++++++++++++ cli/commands/dev/port-fallback.ts | 20 +++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/cli/commands/dev/port-fallback.test.ts b/cli/commands/dev/port-fallback.test.ts index 440882d3ab..33f1f36951 100644 --- a/cli/commands/dev/port-fallback.test.ts +++ b/cli/commands/dev/port-fallback.test.ts @@ -127,6 +127,30 @@ describe("cli/commands/dev/port-fallback", () => { }); }); + describe("npm build safety", () => { + it("never reaches the runtime through the binding dnt rewrites", async () => { + // dnt rewrites every bare `Deno.` access in the npm build to + // `dntShim.Deno.`, i.e. `@deno/shim-deno`. That shim implements + // its TCP listen as `net.createServer()` followed by an immediate read of + // `server._handle.fd`, and Deno's own `node:net` compat leaves `_handle` + // null at that point. So under a Deno-installed CLI the probe threw + // `TypeError: Cannot read properties of null (reading 'fd')` and + // `veryfront dev` died before the dev server could bind - while the very + // same package ran fine under Node, where the shim is not used. + // + // The runtime has to be reached through `getDenoRuntime()`, whose + // `Reflect.get(globalThis, "Deno")` dnt leaves alone. + const source = await Deno.readTextFile(new URL("./port-fallback.ts", import.meta.url)); + const rewrittenByDnt = source.match(/(? { it("recognises the address-in-use error the runtime actually throws", () => { const held = Deno.listen({ hostname: "127.0.0.1", port: 0 }); diff --git a/cli/commands/dev/port-fallback.ts b/cli/commands/dev/port-fallback.ts index 0d7894c1ff..3e9f16a1a6 100644 --- a/cli/commands/dev/port-fallback.ts +++ b/cli/commands/dev/port-fallback.ts @@ -14,7 +14,7 @@ import { LOCALHOST } from "veryfront/config"; import { PORT_IN_USE } from "veryfront/errors"; -import { isDeno } from "veryfront/platform"; +import { getDenoRuntime, isDeno } from "veryfront/platform"; /** How many consecutive ports to try before giving up. */ export const MAX_PORT_FALLBACK_ATTEMPTS = 10; @@ -33,12 +33,22 @@ export function isPortInUseError(error: unknown): boolean { message.includes("eaddrinuse") || message.includes("address already in use"); } -/** Binds `port` and releases it again, to see whether the dev server could have it. */ +/** + * Binds `port` and releases it again, to see whether the dev server could have it. + * + * The Deno namespace comes from `getDenoRuntime()` rather than the bare global: + * in the npm build dnt rewrites every bare `Deno` member access to + * `@deno/shim-deno`, whose TCP listen reads `server._handle.fd` straight after + * `net.createServer().listen()`. Deno's own `node:net` compat has not populated + * `_handle` by then, so a Deno-installed CLI crashed here with + * `Cannot read properties of null (reading 'fd')` before the dev server could + * bind. `getDenoRuntime()` reads the global reflectively, which dnt leaves alone. + */ export async function isPortAvailable(port: number): Promise { - if (isDeno) { + const denoRuntime = isDeno ? getDenoRuntime() : undefined; + if (denoRuntime) { try { - // @ts-ignore - Deno global - Deno.listen({ hostname: LOCALHOST.IPV4, port }).close(); + denoRuntime.listen({ hostname: LOCALHOST.IPV4, port }).close(); return true; } catch (error) { if (isPortInUseError(error)) return false;