Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/ripe-doodles-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/unenv-preset": minor
---

add support for native `node:fs` and `node:fs/promises`
51 changes: 45 additions & 6 deletions packages/unenv-preset/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,29 @@ export function getCloudflarePreset({
compatibilityDate?: string;
compatibilityFlags?: string[];
}): Preset {
const httpOverrides = getHttpOverrides({
const compat = {
compatibilityDate,
compatibilityFlags,
});
};

const osOverrides = getOsOverrides({
compatibilityDate,
compatibilityFlags,
});
const httpOverrides = getHttpOverrides(compat);
const osOverrides = getOsOverrides(compat);
const fsOverrides = getFsOverrides(compat);

// "dynamic" as they depend on the compatibility date and flags
const dynamicNativeModules = [
...nativeModules,
...httpOverrides.nativeModules,
...osOverrides.nativeModules,
...fsOverrides.nativeModules,
];

// "dynamic" as they depend on the compatibility date and flags
const dynamicHybridModules = [
...hybridModules,
...httpOverrides.hybridModules,
...osOverrides.hybridModules,
...fsOverrides.hybridModules,
];

return {
Expand Down Expand Up @@ -229,3 +230,41 @@ function getOsOverrides({
hybridModules: [],
};
}

/**
* Returns the overrides for `node:fs` and `node:fs/promises` (unenv or workerd)
*
* The native fs implementation:
* - can be enabled with the "enable_nodejs_fs_module" flag
* - can be disabled with the "disable_nodejs_fs_module" flag
*/
function getFsOverrides({
// eslint-disable-next-line unused-imports/no-unused-vars
compatibilityDate,
compatibilityFlags,
}: {
compatibilityDate: string;
compatibilityFlags: string[];
}): { nativeModules: string[]; hybridModules: string[] } {
const disabledByFlag = compatibilityFlags.includes(
"disable_nodejs_fs_module"
);

const enabledByFlag =
compatibilityFlags.includes("enable_nodejs_fs_module") &&
compatibilityFlags.includes("experimental");

// TODO: add `enabledByDate` when a default date is set
const enabled = enabledByFlag && !disabledByFlag;

// The native `fs` and `fs/promises` modules implement all the node APIs so we can use them directly
return enabled
? {
nativeModules: ["fs/promises", "fs"],
hybridModules: [],
}
: {
nativeModules: [],
hybridModules: [],
};
}
37 changes: 31 additions & 6 deletions packages/wrangler/e2e/unenv-preset/preset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ type TestConfig = {
// "nodejs_compat" is included by default
compatibilityFlags?: string[];
// Assert runtime compatibility flag values
expectRuntimeFlags?: {
enable_nodejs_http_modules?: boolean;
enable_nodejs_http_server_modules?: boolean;
enable_nodejs_os_module?: boolean;
};
expectRuntimeFlags?: Record<string, boolean>;
};

const testConfigs: TestConfig[] = [
Expand Down Expand Up @@ -127,7 +123,36 @@ const testConfigs: TestConfig[] = [
},
},
],
].flat();
// node:fs and node:fs/promises
[
{
name: "fs disabled by date",
compatibilityDate: "2025-07-26",
compatibilityFlags: ["experimental"],
expectRuntimeFlags: {
enable_nodejs_fs_module: false,
},
},
// TODO: add a config when fs is enabled by default (date no set yet)
{
name: "fs enabled by flag",
compatibilityDate: "2025-07-26",
compatibilityFlags: ["enable_nodejs_fs_module", "experimental"],
expectRuntimeFlags: {
enable_nodejs_fs_module: true,
},
},
// TODO: change the date pass the default enabled date (date not set yet)
{
name: "fs disabled by flag",
compatibilityDate: "2025-07-26",
compatibilityFlags: ["disable_nodejs_fs_module", "experimental"],
expectRuntimeFlags: {
enable_nodejs_fs_module: false,
},
},
],
].flat() as TestConfig[];

describe.each(testConfigs)(
`Preset test: $name`,
Expand Down
37 changes: 37 additions & 0 deletions packages/wrangler/e2e/unenv-preset/worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import assert from "node:assert";

export default {
Expand Down Expand Up @@ -350,4 +351,40 @@ export const WorkerdTests: Record<string, () => void> = {
});
assert.deepStrictEqual(result, { test: "require" });
},

async testFs() {
const fs = await import("node:fs");
const fsp = await import("node:fs/promises");

const useNativeFs = getRuntimeFlagValue("enable_nodejs_fs_module");

if (useNativeFs) {
fs.writeFileSync("/tmp/sync", "sync");
assert.strictEqual(fs.readFileSync("/tmp/sync", "utf-8"), "sync");
await fsp.writeFile("/tmp/async", "async");
assert.strictEqual(await fsp.readFile("/tmp/async", "utf-8"), "async");

const blob = await fs.openAsBlob("/tmp/sync");
assert.ok(blob instanceof Blob);

// Old names in fs namespace
assert.strictEqual((fs as any).FileReadStream, fs.ReadStream);
assert.strictEqual((fs as any).FileWriteStream, fs.WriteStream);
assert.equal((fs as any).F_OK, 0);
assert.equal((fs as any).R_OK, 4);
assert.equal((fs as any).W_OK, 2);
assert.equal((fs as any).X_OK, 1);
} else {
assert.throws(
() => fs.readFileSync("/tmp/file", "utf-8"),
/not implemented/
);
await assert.rejects(
async () => await fsp.readFile("/tmp/file", "utf-8"),
/not implemented/
);

assert.throws(() => fs.openAsBlob("/tmp/sync"), /not implemented/);
}
},
};
Loading