import type { Plugin } from "esbuild"; import type { NodeJSCompatMode } from "miniflare"; import { relative } from "pathe"; import pc from "picocolors"; import { dedent } from "../../util/dedent.ts"; import { logger } from "../../util/logger.ts"; import { NODEJS_MODULES_RE } from "./nodejs-builtin-modules.ts"; /** * 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 esbuildPluginNodeCompatImports = ( mode: NodeJSCompatMode, ): Plugin => ({ name: "nodejs_compat-imports", setup(pluginBuild) { // Infinite loop detection const seen = new Set(); // Prevent multiple warnings per package const warnedPackages = new Map(); pluginBuild.onStart(() => { seen.clear(); warnedPackages.clear(); }); pluginBuild.onResolve( { filter: NODEJS_MODULES_RE }, async ({ path, kind, resolveDir, importer }) => { const specifier = `${path}:${kind}:${resolveDir}:${importer}`; if (seen.has(specifier)) { return; } seen.add(specifier); // Try to resolve this import as a normal package const result = await pluginBuild.resolve(path, { kind, resolveDir, importer, }); if (result.errors.length > 0) { // esbuild couldn't resolve the package // We should warn the user, but not fail the build const pathWarnedPackages = warnedPackages.get(path) ?? []; pathWarnedPackages.push(importer); warnedPackages.set(path, pathWarnedPackages); return { external: true }; } // This is a normal package—don't treat it specially return result; }, ); /** * If we are bundling a "Service Worker" formatted Worker, imports of external modules, * which won't be inlined/bundled by esbuild, are invalid. * * This `onEnd()` handler will error if it identifies node.js external imports. */ pluginBuild.onEnd(() => { if ( pluginBuild.initialOptions.format === "iife" && warnedPackages.size > 0 ) { const paths = new Intl.ListFormat("en-US").format( Array.from(warnedPackages.keys()) .map((p) => `"${p}"`) .sort(), ); return { errors: [ { text: dedent` Unexpected external import of ${paths}. Your worker has no default export, which means it is assumed to be a Service Worker format Worker. Did you mean to create a ES Module format Worker? If so, try adding \`export default { ... }\` in your entry-point. See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/. `, }, ], }; } }); // Wait until the build finishes to log warnings, so that all files which import a package // can be collated pluginBuild.onEnd(() => { 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. Your Worker may throw errors at runtime unless you enable the "nodejs_compat" compatibility flag. Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported from: ${toList(importers, pluginBuild.initialOptions.absWorkingDir)}`, ); }); } }); }, }); function toList(items: string[], absWorkingDir: string | undefined): string { return items .map((i) => ` - ${pc.blue(relative(absWorkingDir ?? "/", i))}`) .join("\n"); }