diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 3767538fce81..c5f920d03d0a 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -1,7 +1,5 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill"; -import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill"; import chalk from "chalk"; import * as esbuild from "esbuild"; import { @@ -18,12 +16,9 @@ import { } from "./build-failures"; import { dedupeModulesByName } from "./dedupe-modules"; import { getEntryPointFromMetafile } from "./entry-point-from-metafile"; -import { asyncLocalStoragePlugin } from "./esbuild-plugins/als-external"; import { cloudflareInternalPlugin } from "./esbuild-plugins/cloudflare-internal"; import { configProviderPlugin } from "./esbuild-plugins/config-provider"; -import { nodejsHybridPlugin } from "./esbuild-plugins/hybrid-nodejs-compat"; -import { nodejsCompatPlugin } from "./esbuild-plugins/nodejs-compat"; -import { standardURLPlugin } from "./esbuild-plugins/standard-url"; +import { getNodeJSCompatPlugins } from "./esbuild-plugins/nodejs-plugins"; import { writeAdditionalModules } from "./find-additional-modules"; import { noopModuleCollector } from "./module-collection"; import type { Config } from "../config"; @@ -440,20 +435,7 @@ export async function bundleWorker( plugins: [ aliasPlugin, moduleCollector.plugin, - ...(nodejsCompatMode === "als" ? [asyncLocalStoragePlugin] : []), - ...(nodejsCompatMode === "legacy" - ? [ - NodeGlobalsPolyfills({ buffer: true }), - standardURLPlugin(), - NodeModulesPolyfills(), - ] - : []), - // Runtime Node.js compatibility (will warn if not using nodejs compat flag and are trying to import from a Node.js builtin). - ...(nodejsCompatMode === "v1" || nodejsCompatMode !== "v2" - ? [nodejsCompatPlugin(nodejsCompatMode === "v1")] - : []), - // Hybrid Node.js compatibility - ...(nodejsCompatMode === "v2" ? [nodejsHybridPlugin()] : []), + ...getNodeJSCompatPlugins(nodejsCompatMode ?? null), cloudflareInternalPlugin, buildResultPlugin, ...(plugins || []), 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 a6e46864c59b..e5a6ff9960f8 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 @@ -79,8 +79,8 @@ function handleRequireCallsToNodeJSBuiltins(build: PluginBuild) { ({ path }) => { return { contents: dedent` - import libDefault from '${path}'; - module.exports = libDefault;`, + import libDefault from '${path}'; + module.exports = libDefault;`, loader: "js", }; } @@ -150,8 +150,8 @@ function handleUnenvAliasedPackages( ({ path }) => { return { contents: dedent` - import * as esm from '${path}'; - module.exports = __cf_cjs(esm); + import * as esm from '${path}'; + module.exports = __cf_cjs(esm); `, loader: "js", }; @@ -188,7 +188,10 @@ function handleNodeJSGlobals( const { importStatement, exportName } = getGlobalInject(inject[globalName]); return { - contents: `${importStatement}\nglobalThis.${globalName} = ${exportName};`, + contents: dedent` + ${importStatement} + globalThis.${globalName} = ${exportName}; + `, }; }); } @@ -213,38 +216,21 @@ function getGlobalInject(globalInject: string | string[]) { } /** - * Encodes a case sensitive string to lowercase string by prefixing all uppercase letters - * with $ and turning them into lowercase letters. + * Encodes a case sensitive string to lowercase string. + * + * - Escape $ with another $ ("$" -> "$$") + * - Escape uppercase letters with $ and turn them into lowercase letters ("L" -> "$L") * * This function exists because ESBuild requires that all resolved paths are case insensitive. * Without this transformation, ESBuild will clobber /foo/bar.js with /foo/Bar.js - * - * This is important to support `inject` config for `performance` and `Performance` introduced - * in https://github.com/unjs/unenv/pull/257 */ export function encodeToLowerCase(str: string): string { - return str - .replaceAll(/\$/g, () => "$$") - .replaceAll(/[A-Z]/g, (letter) => `$${letter.toLowerCase()}`); + return str.replace(/[A-Z$]/g, (escape) => `$${escape.toLowerCase()}`); } /** * Decodes a string lowercased using `encodeToLowerCase` to the original strings */ export function decodeFromLowerCase(str: string): string { - let out = ""; - let i = 0; - while (i < str.length - 1) { - if (str[i] === "$") { - i++; - out += str[i].toUpperCase(); - } else { - out += str[i]; - } - i++; - } - if (i < str.length) { - out += str[i]; - } - return out; + return str.replace(/\$[a-z$]/g, (escaped) => escaped[1].toUpperCase()); } diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts index bc711ddec6a9..485465f8a6b5 100644 --- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts @@ -3,29 +3,32 @@ import chalk from "chalk"; import { logger } from "../../logger"; import { dedent } from "../../utils/dedent"; import type { Plugin } from "esbuild"; +import type { NodeJSCompatMode } from "miniflare"; /** - * An esbuild plugin that will mark any `node:...` imports as external. + * An esbuild plugin that will: + * - mark any `node:...` imports as external + * - warn if there are node imports (if not in v1 mode) + * + * Applies to: null, als, legacy and v1 modes. */ -export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( - silenceWarnings -) => ({ +export const nodejsCompatPlugin = (mode: NodeJSCompatMode): Plugin => ({ name: "nodejs_compat-imports", setup(pluginBuild) { // Infinite loop detection const seen = new Set(); // Prevent multiple warnings per package - const warnedPackaged = new Map(); + const warnedPackages = new Map(); pluginBuild.onStart(() => { seen.clear(); - warnedPackaged.clear(); + warnedPackages.clear(); }); pluginBuild.onResolve( { filter: /node:.*/ }, - async ({ path, kind, resolveDir, ...opts }) => { - const specifier = `${path}:${kind}:${resolveDir}:${opts.importer}`; + async ({ path, kind, resolveDir, importer }) => { + const specifier = `${path}:${kind}:${resolveDir}:${importer}`; if (seen.has(specifier)) { return; } @@ -35,18 +38,15 @@ export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( const result = await pluginBuild.resolve(path, { kind, resolveDir, - importer: opts.importer, + importer, }); if (result.errors.length > 0) { // esbuild couldn't resolve the package // We should warn the user, but not fail the build - - let pathWarnedPackaged = warnedPackaged.get(path); - if (pathWarnedPackaged === undefined) { - warnedPackaged.set(path, (pathWarnedPackaged = [])); - } - pathWarnedPackaged.push(opts.importer); + const pathWarnedPackages = warnedPackages.get(path) ?? []; + pathWarnedPackages.push(importer); + warnedPackages.set(path, pathWarnedPackages); return { external: true }; } @@ -64,10 +64,10 @@ export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( pluginBuild.onEnd(() => { if ( pluginBuild.initialOptions.format === "iife" && - warnedPackaged.size > 0 + warnedPackages.size > 0 ) { const paths = new Intl.ListFormat("en-US").format( - Array.from(warnedPackaged.keys()) + Array.from(warnedPackages.keys()) .map((p) => `"${p}"`) .sort() ); @@ -90,8 +90,8 @@ export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( // Wait until the build finishes to log warnings, so that all files which import a package // can be collated pluginBuild.onEnd(() => { - if (!silenceWarnings) { - warnedPackaged.forEach((importers: string[], path: string) => { + if (mode !== "v1") { + warnedPackages.forEach((importers: string[], path: string) => { logger.warn( dedent` The package "${path}" wasn't found on the file system but is built into node. diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts new file mode 100644 index 000000000000..b59d8fa190d7 --- /dev/null +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts @@ -0,0 +1,31 @@ +import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill"; +import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill"; +import { asyncLocalStoragePlugin } from "./als-external"; +import { nodejsHybridPlugin } from "./hybrid-nodejs-compat"; +import { nodejsCompatPlugin } from "./nodejs-compat"; +import { standardURLPlugin } from "./standard-url"; +import type { Plugin } from "esbuild"; +import type { NodeJSCompatMode } from "miniflare"; + +/** + * Returns the list of ESBuild plugins to use for a given compat mode. + */ +export function getNodeJSCompatPlugins(mode: NodeJSCompatMode): Plugin[] { + switch (mode) { + case "als": + return [asyncLocalStoragePlugin, nodejsCompatPlugin(mode)]; + case "legacy": + return [ + NodeGlobalsPolyfills({ buffer: true }), + standardURLPlugin(), + NodeModulesPolyfills(), + nodejsCompatPlugin(mode), + ]; + case "v1": + return [nodejsCompatPlugin(mode)]; + case "v2": + return [nodejsHybridPlugin()]; + case null: + return [nodejsCompatPlugin(mode)]; + } +}