diff --git a/.changeset/nasty-melons-camp.md b/.changeset/nasty-melons-camp.md
new file mode 100644
index 000000000000..2170634bb60e
--- /dev/null
+++ b/.changeset/nasty-melons-camp.md
@@ -0,0 +1,10 @@
+---
+"wrangler": minor
+---
+
+fix: allow `require`ing unenv aliased packages
+
+Before this PR `require`ing packages aliased in unenv would fail.
+That's because `require` would load the mjs file.
+
+This PR adds wraps the mjs file in a virtual ES module to allow `require`ing it.
diff --git a/fixtures/nodejs-hybrid-app/src/index.ts b/fixtures/nodejs-hybrid-app/src/index.ts
index 1006ec9c0b86..3f16b94fb2e1 100644
--- a/fixtures/nodejs-hybrid-app/src/index.ts
+++ b/fixtures/nodejs-hybrid-app/src/index.ts
@@ -26,18 +26,30 @@ export default {
return testPostgresLibrary(env, ctx);
case "/test-x509-certificate":
return testX509Certificate();
+ case "/test-require-alias":
+ return testRequireUenvAliasedPackages();
}
return new Response(
'Postgres query | ' +
'Test process global | ' +
'Test getRandomValues() | ' +
- 'Test X509Certificate',
+ 'Test X509Certificate' +
+ 'Test require unenv aliased packages',
{ headers: { "Content-Type": "text/html; charset=utf-8" } }
);
},
};
+function testRequireUenvAliasedPackages() {
+ const fetch = require("cross-fetch");
+ const supportsDefaultExports = typeof fetch === "function";
+ const supportsNamedExports = typeof fetch.Headers === "function";
+ return new Response(
+ supportsDefaultExports && supportsNamedExports ? `"OK!"` : `"KO!"`
+ );
+}
+
function testX509Certificate() {
try {
new nodeCrypto.X509Certificate(`-----BEGIN CERTIFICATE-----
diff --git a/fixtures/nodejs-hybrid-app/tests/index.test.ts b/fixtures/nodejs-hybrid-app/tests/index.test.ts
index 43479998939f..fb46992c03c9 100644
--- a/fixtures/nodejs-hybrid-app/tests/index.test.ts
+++ b/fixtures/nodejs-hybrid-app/tests/index.test.ts
@@ -63,4 +63,17 @@ describe("nodejs compat", () => {
await stop();
}
});
+
+ test("import unenv aliased packages", async ({ expect }) => {
+ const { ip, port, stop } = await runWranglerDev(
+ resolve(__dirname, "../src"),
+ ["--port=0", "--inspector-port=0"]
+ );
+ try {
+ const response = await fetch(`http://${ip}:${port}/test-require-alias`);
+ await expect(response.text()).resolves.toBe(`"OK!"`);
+ } finally {
+ await stop();
+ }
+ });
});
diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
index ed3c02b17fa7..726739b3d477 100644
--- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
+++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
@@ -6,6 +6,7 @@ import { getBasePath } from "../../paths";
import type { Plugin, PluginBuild } from "esbuild";
const REQUIRED_NODE_BUILT_IN_NAMESPACE = "node-built-in-modules";
+const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
export const nodejsHybridPlugin: () => Plugin = () => {
const { alias, inject, external } = env(nodeless, cloudflare);
@@ -14,7 +15,7 @@ export const nodejsHybridPlugin: () => Plugin = () => {
setup(build) {
errorOnServiceWorkerFormat(build);
handleRequireCallsToNodeJSBuiltins(build);
- handleAliasedNodeJSPackages(build, alias, external);
+ handleUnenvAliasedPackages(build, alias, external);
handleNodeJSGlobals(build, inject);
},
};
@@ -55,7 +56,7 @@ function errorOnServiceWorkerFormat(build: PluginBuild) {
}
/**
- * We must convert `require()` calls for Node.js to a virtual ES Module that can be imported avoiding the require calls.
+ * We must convert `require()` calls for Node.js modules to a virtual ES Module that can be imported avoiding the require calls.
* We do this by creating a special virtual ES module that re-exports the library in an onLoad handler.
* The onLoad handler is triggered by matching the "namespace" added to the resolve.
*/
@@ -81,38 +82,76 @@ function handleRequireCallsToNodeJSBuiltins(build: PluginBuild) {
);
}
-function handleAliasedNodeJSPackages(
+function handleUnenvAliasedPackages(
build: PluginBuild,
alias: Record,
external: string[]
) {
// esbuild expects alias paths to be absolute
- const aliasAbsolute = Object.fromEntries(
- Object.entries(alias)
- .map(([key, value]) => {
- let resolvedAliasPath;
- try {
- resolvedAliasPath = require.resolve(value);
- } catch (e) {
- // this is an alias for package that is not installed in the current app => ignore
- resolvedAliasPath = "";
- }
+ const aliasAbsolute: Record = {};
+ for (const [module, unresolvedAlias] of Object.entries(alias)) {
+ try {
+ aliasAbsolute[module] = require
+ .resolve(unresolvedAlias)
+ .replace(/\.cjs$/, ".mjs");
+ } catch (e) {
+ // this is an alias for package that is not installed in the current app => ignore
+ }
+ }
- return [key, resolvedAliasPath.replace(/\.cjs$/, ".mjs")];
- })
- .filter((entry) => entry[1] !== "")
- );
const UNENV_ALIAS_RE = new RegExp(
`^(${Object.keys(aliasAbsolute).join("|")})$`
);
build.onResolve({ filter: UNENV_ALIAS_RE }, (args) => {
+ const unresolvedAlias = alias[args.path];
+ // Convert `require()` calls for NPM packages to a virtual ES Module that can be imported avoiding the require calls.
+ // Note: Does not apply to Node.js packages that are handled in `handleRequireCallsToNodeJSBuiltins`
+ if (
+ args.kind === "require-call" &&
+ (unresolvedAlias.startsWith("unenv/runtime/npm/") ||
+ unresolvedAlias.startsWith("unenv/runtime/mock/"))
+ ) {
+ return {
+ path: args.path,
+ namespace: REQUIRED_UNENV_ALIAS_NAMESPACE,
+ };
+ }
// Resolve the alias to its absolute path and potentially mark it as external
return {
path: aliasAbsolute[args.path],
- external: external.includes(alias[args.path]),
+ external: external.includes(unresolvedAlias),
};
});
+
+ build.initialOptions.banner = { js: "", ...build.initialOptions.banner };
+ build.initialOptions.banner.js += dedent`
+ function __cf_cjs(esm) {
+ const cjs = 'default' in esm ? esm.default : {};
+ for (const [k, v] of Object.entries(esm)) {
+ if (k !== 'default') {
+ Object.defineProperty(cjs, k, {
+ enumerable: true,
+ value: v,
+ });
+ }
+ }
+ return cjs;
+ }
+ `;
+
+ build.onLoad(
+ { filter: /.*/, namespace: REQUIRED_UNENV_ALIAS_NAMESPACE },
+ ({ path }) => {
+ return {
+ contents: dedent`
+ import * as esm from '${path}';
+ module.exports = __cf_cjs(esm);
+ `,
+ loader: "js",
+ };
+ }
+ );
}
/**
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a52f5664d085..158e5a244a08 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,6 +6,12 @@ settings:
catalogs:
default:
+ '@vitest/runner':
+ specifier: ~2.1.1
+ version: 2.1.1
+ '@vitest/snapshot':
+ specifier: ~2.1.1
+ version: 2.1.1
vitest:
specifier: ~2.1.1
version: 2.1.1