Skip to content

Commit 7c339ae

Browse files
add support for native node:fs and node:fs/promises (#10224)
Co-authored-by: Peter Bacon Darwin <[email protected]>
1 parent c4e164c commit 7c339ae

File tree

4 files changed

+118
-12
lines changed

4 files changed

+118
-12
lines changed

.changeset/ripe-doodles-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/unenv-preset": minor
3+
---
4+
5+
add support for native `node:fs` and `node:fs/promises`

packages/unenv-preset/src/preset.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,28 +60,29 @@ export function getCloudflarePreset({
6060
compatibilityDate?: string;
6161
compatibilityFlags?: string[];
6262
}): Preset {
63-
const httpOverrides = getHttpOverrides({
63+
const compat = {
6464
compatibilityDate,
6565
compatibilityFlags,
66-
});
66+
};
6767

68-
const osOverrides = getOsOverrides({
69-
compatibilityDate,
70-
compatibilityFlags,
71-
});
68+
const httpOverrides = getHttpOverrides(compat);
69+
const osOverrides = getOsOverrides(compat);
70+
const fsOverrides = getFsOverrides(compat);
7271

7372
// "dynamic" as they depend on the compatibility date and flags
7473
const dynamicNativeModules = [
7574
...nativeModules,
7675
...httpOverrides.nativeModules,
7776
...osOverrides.nativeModules,
77+
...fsOverrides.nativeModules,
7878
];
7979

8080
// "dynamic" as they depend on the compatibility date and flags
8181
const dynamicHybridModules = [
8282
...hybridModules,
8383
...httpOverrides.hybridModules,
8484
...osOverrides.hybridModules,
85+
...fsOverrides.hybridModules,
8586
];
8687

8788
return {
@@ -229,3 +230,41 @@ function getOsOverrides({
229230
hybridModules: [],
230231
};
231232
}
233+
234+
/**
235+
* Returns the overrides for `node:fs` and `node:fs/promises` (unenv or workerd)
236+
*
237+
* The native fs implementation:
238+
* - can be enabled with the "enable_nodejs_fs_module" flag
239+
* - can be disabled with the "disable_nodejs_fs_module" flag
240+
*/
241+
function getFsOverrides({
242+
// eslint-disable-next-line unused-imports/no-unused-vars
243+
compatibilityDate,
244+
compatibilityFlags,
245+
}: {
246+
compatibilityDate: string;
247+
compatibilityFlags: string[];
248+
}): { nativeModules: string[]; hybridModules: string[] } {
249+
const disabledByFlag = compatibilityFlags.includes(
250+
"disable_nodejs_fs_module"
251+
);
252+
253+
const enabledByFlag =
254+
compatibilityFlags.includes("enable_nodejs_fs_module") &&
255+
compatibilityFlags.includes("experimental");
256+
257+
// TODO: add `enabledByDate` when a default date is set
258+
const enabled = enabledByFlag && !disabledByFlag;
259+
260+
// The native `fs` and `fs/promises` modules implement all the node APIs so we can use them directly
261+
return enabled
262+
? {
263+
nativeModules: ["fs/promises", "fs"],
264+
hybridModules: [],
265+
}
266+
: {
267+
nativeModules: [],
268+
hybridModules: [],
269+
};
270+
}

packages/wrangler/e2e/unenv-preset/preset.test.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ type TestConfig = {
1414
// "nodejs_compat" is included by default
1515
compatibilityFlags?: string[];
1616
// Assert runtime compatibility flag values
17-
expectRuntimeFlags?: {
18-
enable_nodejs_http_modules?: boolean;
19-
enable_nodejs_http_server_modules?: boolean;
20-
enable_nodejs_os_module?: boolean;
21-
};
17+
expectRuntimeFlags?: Record<string, boolean>;
2218
};
2319

2420
const testConfigs: TestConfig[] = [
@@ -127,7 +123,36 @@ const testConfigs: TestConfig[] = [
127123
},
128124
},
129125
],
130-
].flat();
126+
// node:fs and node:fs/promises
127+
[
128+
{
129+
name: "fs disabled by date",
130+
compatibilityDate: "2025-07-26",
131+
compatibilityFlags: ["experimental"],
132+
expectRuntimeFlags: {
133+
enable_nodejs_fs_module: false,
134+
},
135+
},
136+
// TODO: add a config when fs is enabled by default (date no set yet)
137+
{
138+
name: "fs enabled by flag",
139+
compatibilityDate: "2025-07-26",
140+
compatibilityFlags: ["enable_nodejs_fs_module", "experimental"],
141+
expectRuntimeFlags: {
142+
enable_nodejs_fs_module: true,
143+
},
144+
},
145+
// TODO: change the date pass the default enabled date (date not set yet)
146+
{
147+
name: "fs disabled by flag",
148+
compatibilityDate: "2025-07-26",
149+
compatibilityFlags: ["disable_nodejs_fs_module", "experimental"],
150+
expectRuntimeFlags: {
151+
enable_nodejs_fs_module: false,
152+
},
153+
},
154+
],
155+
].flat() as TestConfig[];
131156

132157
describe.each(testConfigs)(
133158
`Preset test: $name`,

packages/wrangler/e2e/unenv-preset/worker/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import assert from "node:assert";
23

34
export default {
@@ -350,4 +351,40 @@ export const WorkerdTests: Record<string, () => void> = {
350351
});
351352
assert.deepStrictEqual(result, { test: "require" });
352353
},
354+
355+
async testFs() {
356+
const fs = await import("node:fs");
357+
const fsp = await import("node:fs/promises");
358+
359+
const useNativeFs = getRuntimeFlagValue("enable_nodejs_fs_module");
360+
361+
if (useNativeFs) {
362+
fs.writeFileSync("/tmp/sync", "sync");
363+
assert.strictEqual(fs.readFileSync("/tmp/sync", "utf-8"), "sync");
364+
await fsp.writeFile("/tmp/async", "async");
365+
assert.strictEqual(await fsp.readFile("/tmp/async", "utf-8"), "async");
366+
367+
const blob = await fs.openAsBlob("/tmp/sync");
368+
assert.ok(blob instanceof Blob);
369+
370+
// Old names in fs namespace
371+
assert.strictEqual((fs as any).FileReadStream, fs.ReadStream);
372+
assert.strictEqual((fs as any).FileWriteStream, fs.WriteStream);
373+
assert.equal((fs as any).F_OK, 0);
374+
assert.equal((fs as any).R_OK, 4);
375+
assert.equal((fs as any).W_OK, 2);
376+
assert.equal((fs as any).X_OK, 1);
377+
} else {
378+
assert.throws(
379+
() => fs.readFileSync("/tmp/file", "utf-8"),
380+
/not implemented/
381+
);
382+
await assert.rejects(
383+
async () => await fsp.readFile("/tmp/file", "utf-8"),
384+
/not implemented/
385+
);
386+
387+
assert.throws(() => fs.openAsBlob("/tmp/sync"), /not implemented/);
388+
}
389+
},
353390
};

0 commit comments

Comments
 (0)