Repository: vercel/next.js
Pull request: Unify Node and Edge externals list

Unify the list of Node/Edge iexternals.
Turns out `readline/promises` and `stream/consumers` was missing in some places.

Closes https://github.com/vercel/next.js/issues/90765

Changed files:
filename: Cargo.lock
patch: @@ -4600,6 +4600,7 @@ dependencies = [
  "indexmap 2.13.0",
  "indoc",
  "modularize_imports",
+ "next-taskless",
  "once_cell",
  "pathdiff",
  "preset_env_base",

filename: crates/next-core/src/next_import_map.rs
patch: @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
 
 use anyhow::{Context, Result};
 use either::Either;
+use next_taskless::{EDGE_NODE_EXTERNALS, NODE_EXTERNALS};
 use rustc_hash::FxHashMap;
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::{FxIndexMap, ResolvedVc, Vc, fxindexmap};
@@ -37,61 +38,6 @@ use crate::{
     util::NextRuntime,
 };
 
-/// List of node.js internals that are not supported by edge runtime.
-/// If these imports are used & user does not provide alias for the polyfill,
-/// runtime error will be thrown.
-/// This is not identical to the list of entire node.js internals, refer
-/// https://vercel.com/docs/functions/runtimes/edge-runtime#compatible-node.js-modules
-/// for the allowed imports.
-static EDGE_UNSUPPORTED_NODE_INTERNALS: LazyLock<[RcStr; 44]> = LazyLock::new(|| {
-    [
-        rcstr!("child_process"),
-        rcstr!("cluster"),
-        rcstr!("console"),
-        rcstr!("constants"),
-        rcstr!("crypto"),
-        rcstr!("dgram"),
-        rcstr!("diagnostics_channel"),
-        rcstr!("dns"),
-        rcstr!("dns/promises"),
-        rcstr!("domain"),
-        rcstr!("fs"),
-        rcstr!("fs/promises"),
-        rcstr!("http"),
-        rcstr!("http2"),
-        rcstr!("https"),
-        rcstr!("inspector"),
-        rcstr!("module"),
-        rcstr!("net"),
-        rcstr!("os"),
-        rcstr!("path"),
-        rcstr!("path/posix"),
-        rcstr!("path/win32"),
-        rcstr!("perf_hooks"),
-        rcstr!("process"),
-        rcstr!("punycode"),
-        rcstr!("querystring"),
-        rcstr!("readline"),
-        rcstr!("repl"),
-        rcstr!("stream"),
-        rcstr!("stream/promises"),
-        rcstr!("stream/web"),
-        rcstr!("string_decoder"),
-        rcstr!("sys"),
-        rcstr!("timers"),
-        rcstr!("timers/promises"),
-        rcstr!("tls"),
-        rcstr!("trace_events"),
-        rcstr!("tty"),
-        rcstr!("v8"),
-        rcstr!("vm"),
-        rcstr!("wasi"),
-        rcstr!("worker_threads"),
-        rcstr!("zlib"),
-        rcstr!("pnpapi"),
-    ]
-});
-
 // Make sure to not add any external requests here.
 /// Computes the Next-specific client import map.
 #[turbo_tasks::function]
@@ -603,9 +549,13 @@ async fn insert_unsupported_node_internal_aliases(import_map: &mut ImportMap) ->
     ))
     .resolved_cell();
 
-    EDGE_UNSUPPORTED_NODE_INTERNALS.iter().for_each(|module| {
-        import_map.insert_alias(AliasPattern::exact(module.clone()), unsupported_replacer);
-    });
+    for module in NODE_EXTERNALS {
+        if EDGE_NODE_EXTERNALS.binary_search(&module).is_ok() {
+            continue;
+        }
+        import_map.insert_alias(AliasPattern::exact(module), unsupported_replacer);
+    }
+
     Ok(())
 }
 

filename: crates/next-custom-transforms/Cargo.toml
patch: @@ -66,6 +66,7 @@ swc_relay = { workspace = true }
 turbopack-ecmascript-plugins = { workspace = true, optional = true }
 turbo-rcstr = { workspace = true }
 urlencoding = { workspace = true }
+next-taskless = { workspace = true }
 
 react_remove_properties = { workspace = true }
 remove_console = { workspace = true }

filename: crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs
patch: @@ -1,5 +1,6 @@
 use std::sync::Arc;
 
+use next_taskless::{EDGE_NODE_EXTERNALS, NODE_EXTERNALS};
 use swc_core::{
     atoms::{Atom, Wtf8Atom},
     common::{SourceMap, Span, errors::HANDLER},
@@ -111,77 +112,6 @@ const EDGE_UNSUPPORTED_NODE_APIS: &[&str] = &[
     "WritableStreamDefaultController",
 ];
 
-/// https://vercel.com/docs/functions/runtimes/edge-runtime#compatible-node.js-modules
-const NODEJS_MODULE_NAMES: &[&str] = &[
-    "_http_agent",
-    "_http_client",
-    "_http_common",
-    "_http_incoming",
-    "_http_outgoing",
-    "_http_server",
-    "_stream_duplex",
-    "_stream_passthrough",
-    "_stream_readable",
-    "_stream_transform",
-    "_stream_wrap",
-    "_stream_writable",
-    "_tls_common",
-    "_tls_wrap",
-    // "assert",
-    // "assert/strict",
-    // "async_hooks",
-    // "buffer",
-    "child_process",
-    "cluster",
-    "console",
-    "constants",
-    "crypto",
-    "dgram",
-    "diagnostics_channel",
-    "dns",
-    "dns/promises",
-    "domain",
-    // "events",
-    "fs",
-    "fs/promises",
-    "http",
-    "http2",
-    "https",
-    "inspector",
-    "module",
-    "net",
-    "os",
-    "path",
-    "path/posix",
-    "path/win32",
-    "perf_hooks",
-    "process",
-    "punycode",
-    "querystring",
-    "readline",
-    "readline/promises",
-    "repl",
-    "stream",
-    "stream/consumers",
-    "stream/promises",
-    "stream/web",
-    "string_decoder",
-    "sys",
-    "timers",
-    "timers/promises",
-    "tls",
-    "trace_events",
-    "tty",
-    "url",
-    // "util",
-    // "util/types",
-    "v8",
-    "vm",
-    "wasi",
-    "worker_threads",
-    "zlib",
-];
-
 impl<EmitWarn, EmitError> WarnForEdgeRuntime<EmitWarn, EmitError>
 where
     EmitWarn: Fn(Span, String),
@@ -193,9 +123,12 @@ where
             return None;
         }
 
-        // Node.js modules can be loaded with `node:` prefix or directly
-        if module_specifier.starts_with("node:")
-            || NODEJS_MODULE_NAMES.contains(&module_specifier_str)
+        let module_name = module_specifier_str
+            .strip_prefix("node:")
+            .unwrap_or(module_specifier_str);
+
+        if NODE_EXTERNALS.binary_search(&module_name).is_ok()
+            && EDGE_NODE_EXTERNALS.binary_search(&module_name).is_err()
         {
             let loc = self.cm.lookup_line(span.lo).ok()?;
 

filename: crates/next-taskless/src/constants.rs
patch: @@ -1,4 +1,19 @@
-pub const NODE_EXTERNALS: [&str; 64] = [
+// SAFETY: This has to be sorted alphabetically since we are doing binary search on it
+pub const NODE_EXTERNALS: [&str; 68] = [
+    "_http_agent",
+    "_http_client",
+    "_http_common",
+    "_http_incoming",
+    "_http_outgoing",
+    "_http_server",
+    "_stream_duplex",
+    "_stream_passthrough",
+    "_stream_readable",
+    "_stream_transform",
+    "_stream_wrap",
+    "_stream_writable",
+    "_tls_common",
+    "_tls_wrap",
     "assert",
     "assert/strict",
     "async_hooks",
@@ -27,12 +42,15 @@ pub const NODE_EXTERNALS: [&str; 64] = [
     "path/posix",
     "path/win32",
     "perf_hooks",
+    "pnpapi",
     "process",
     "punycode",
     "querystring",
     "readline",
+    "readline/promises",
     "repl",
     "stream",
+    "stream/consumers",
     "stream/promises",
     "stream/web",
     "string_decoder",
@@ -50,28 +68,48 @@ pub const NODE_EXTERNALS: [&str; 64] = [
     "wasi",
     "worker_threads",
     "zlib",
-    "pnpapi",
-    "_http_agent",
-    "_http_client",
-    "_http_common",
-    "_http_incoming",
-    "_http_outgoing",
-    "_http_server",
-    "_stream_duplex",
-    "_stream_passthrough",
-    "_stream_readable",
-    "_stream_transform",
-    "_stream_wrap",
-    "_stream_writable",
 ];
 
-pub const EDGE_NODE_EXTERNALS: [&str; 5] = ["buffer", "events", "assert", "util", "async_hooks"];
+/// List of node.js internals that are supported by edge runtime.
+/// If anything Node.js builtin module apart from these these imports are
+/// used and user does not provide alias for the polyfill, a runtime error will be thrown.
+/// See https://vercel.com/docs/functions/runtimes/edge-runtime#compatible-node.js-modules
+// SAFETY: This has to be sorted alphabetically since we are doing binary search on it
+pub const EDGE_NODE_EXTERNALS: [&str; 7] = [
+    "assert",
+    "assert/strict",
+    "async_hooks",
+    "buffer",
+    "events",
+    "util",
+    "util/types",
+];
 
 pub const BUN_EXTERNALS: [&str; 6] = [
+    "bun",
     "bun:ffi",
     "bun:jsc",
     "bun:sqlite",
     "bun:test",
     "bun:wrap",
-    "bun",
 ];
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_node_externals_sorted() {
+        assert!(NODE_EXTERNALS.is_sorted())
+    }
+
+    #[test]
+    fn test_edge_node_externals_sorted() {
+        assert!(EDGE_NODE_EXTERNALS.is_sorted())
+    }
+
+    #[test]
+    fn test_bun_externals_sorted() {
+        assert!(BUN_EXTERNALS.is_sorted())
+    }
+}