diff --git a/src/platform/adapters/runtime/shared/node-filesystem-adapter.test.ts b/src/platform/adapters/runtime/shared/node-filesystem-adapter.test.ts index 80cb16b27c..6fa4c8b266 100644 --- a/src/platform/adapters/runtime/shared/node-filesystem-adapter.test.ts +++ b/src/platform/adapters/runtime/shared/node-filesystem-adapter.test.ts @@ -12,6 +12,7 @@ import { hasUsableWindowsSnapshotIdentity, NodeCompatibleFileSystemAdapter, readNodeFileSnapshotWithinLimit, + resolveNoFollowFlag, } from "./node-filesystem-adapter.ts"; import { setupNodeFsWatcher } from "./shared-watcher.ts"; @@ -39,7 +40,40 @@ function requireExclusiveCreator(adapter: NodeCompatibleFileSystemAdapter) { return adapter.createFileBytesExclusive; } +describe("resolveNoFollowFlag", () => { + it("does not throw when node:fs constants are unavailable (#3661)", () => { + // In a browser bundle `nodeFsConstants` is `undefined`. Reading `.O_NOFOLLOW` + // off it must degrade to "unavailable", not throw at construction. Passing + // `undefined` for `constants` reproduces the non-Node runtime directly. + assertEquals(resolveNoFollowFlag({}, undefined), undefined); + }); + + it("returns the runtime O_NOFOLLOW when the constants are present", () => { + assertEquals(resolveNoFollowFlag({}, { O_NOFOLLOW: 0x20000 }), 0x20000); + }); + + it("lets an own noFollow option win over the runtime constants (test seam)", () => { + assertEquals(resolveNoFollowFlag({ noFollow: 7 }, { O_NOFOLLOW: 0x20000 }), 7); + // An own `undefined` means "unavailable" even when constants exist. + assertEquals(resolveNoFollowFlag({ noFollow: undefined }, { O_NOFOLLOW: 0x20000 }), undefined); + }); +}); + describe("NodeCompatibleFileSystemAdapter", () => { + it("constructs without exact-snapshot support when node:fs constants are absent (#3661)", () => { + // Reproduce the browser path end to end: no runtime O_NOFOLLOW available, so + // the adapter must construct (not throw) and simply cannot bind an exact + // inode snapshot. `noFollow: undefined` is the documented "unavailable" seam. + const adapter = new NodeCompatibleFileSystemAdapter(undefined, { + noFollow: undefined, + platform: "posix", + }); + assertEquals( + (adapter as { readFileSnapshotWithinLimit?: unknown }).readFileSnapshotWithinLimit, + undefined, + ); + }); + it("reads empty and exact-limit snapshots and rejects invalid or oversized inputs", async () => { const root = await Deno.makeTempDir({ prefix: "veryfront-node-snapshot-" }); try { diff --git a/src/platform/adapters/runtime/shared/node-filesystem-adapter.ts b/src/platform/adapters/runtime/shared/node-filesystem-adapter.ts index 508e8fda51..cb57e2d6e6 100644 --- a/src/platform/adapters/runtime/shared/node-filesystem-adapter.ts +++ b/src/platform/adapters/runtime/shared/node-filesystem-adapter.ts @@ -396,6 +396,25 @@ async function createNodeFileBytesExclusive( ); } +/** + * Resolve the `O_NOFOLLOW` open flag that binds an exact-inode snapshot read. + * + * `constants` is `undefined` in a runtime without `node:fs` — e.g. a browser + * bundle that transitively imports this adapter. Dereferencing `.O_NOFOLLOW` on + * it unconditionally threw `Cannot read properties of undefined (reading + * 'O_NOFOLLOW')` during construction and aborted client hydration (#3661). + * Treat the missing constants as "unavailable" (`undefined`) — the same meaning + * {@link NodeFileSystemCapabilityOptions.noFollow} already documents for an own + * `undefined` — so `canOpenExactSnapshot` degrades to `false` instead of the + * constructor exploding. An own `noFollow` on `options` still wins (the test seam). + */ +export function resolveNoFollowFlag( + options: NodeFileSystemCapabilityOptions, + constants: { readonly O_NOFOLLOW?: number } | undefined, +): number | undefined { + return hasOwn(options, "noFollow") ? options.noFollow : constants?.O_NOFOLLOW; +} + /** * Filesystem implementation shared by runtimes that provide Node-compatible * `node:fs` APIs (currently Node.js and Bun). @@ -409,7 +428,7 @@ export class NodeCompatibleFileSystemAdapter implements FileSystemAdapter { ...nodeFileSystemOperations, ...options.operations, } as NodeFileSystemOperations; - const noFollow = hasOwn(options, "noFollow") ? options.noFollow : nodeFsConstants.O_NOFOLLOW; + const noFollow = resolveNoFollowFlag(options, nodeFsConstants); const platform = options.platform ?? (runtimeUsesWindowsPaths() ? "windows" : "posix"); const canOpenExactSnapshot = platform === "windows" ? hasUsableWindowsSnapshotIdentity(detectNodeCompatibleRuntime())