Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cli/commands/dev/port-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<member>` access in the npm build to
// `dntShim.Deno.<member>`, 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(/(?<![.\w$])Deno\.\w+/g) ?? [];

assertEquals(
rewrittenByDnt,
[],
`dnt would rewrite ${rewrittenByDnt.join(", ")} to the broken @deno/shim-deno namespace`,
);
});
});

describe("isPortInUseError", () => {
it("recognises the address-in-use error the runtime actually throws", () => {
const held = Deno.listen({ hostname: "127.0.0.1", port: 0 });
Expand Down
20 changes: 15 additions & 5 deletions cli/commands/dev/port-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<boolean> {
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;
Expand Down
Loading