Repository: vercel/next.js
Pull request: feat(turbopack): externalType support script

This pr introduce a new external type: `Script`, which has the same behaviour as webpack's https://webpack.js.org/configuration/externals/#externalstypescript

And i also refer to the webpack's implement, this external type accept a request like: `root@script_url` which same as webpack:

![image](https://github.com/user-attachments/assets/f5b7489d-c4e5-4daa-aa5b-f92e10051c0d)

In the utoo, we use it as follows:

https://github.com/umijs/mako/blob/next/crates/pack-core/src/shared/resolve/externals_plugin.rs#L202-L208

It will generate runtime code to load the external package from the `script_url`

Changed files:
filename: turbopack/crates/turbopack-core/src/resolve/mod.rs
patch: @@ -475,6 +475,7 @@ pub enum ExternalType {
     CommonJs,
     EcmaScriptModule,
     Global,
+    Script,
 }
 
 impl Display for ExternalType {
@@ -484,6 +485,7 @@ impl Display for ExternalType {
             ExternalType::EcmaScriptModule => write!(f, "esm"),
             ExternalType::Url => write!(f, "url"),
             ExternalType::Global => write!(f, "global"),
+            ExternalType::Script => write!(f, "script"),
         }
     }
 }
@@ -2779,15 +2781,14 @@ async fn resolve_import_map_result(
                     **alias_lookup_path,
                     request,
                     match ty {
-                        ExternalType::Url => options,
                         // TODO is that root correct?
                         ExternalType::CommonJs => {
                             node_cjs_resolve_options(alias_lookup_path.root())
                         }
                         ExternalType::EcmaScriptModule => {
                             node_esm_resolve_options(alias_lookup_path.root())
                         }
-                        ExternalType::Global => options,
+                        ExternalType::Script | ExternalType::Url | ExternalType::Global => options,
                     },
                 )
                 .await?

filename: turbopack/crates/turbopack-ecmascript/src/references/external_module.rs
patch: @@ -23,6 +23,7 @@ use crate::{
     references::async_module::{AsyncModule, OptionAsyncModule},
     runtime_functions::{
         TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE,
+        TURBOPACK_LOAD_BY_URL,
     },
     utils::StringifyJs,
 };
@@ -45,6 +46,7 @@ pub enum CachedExternalType {
     EcmaScriptViaRequire,
     EcmaScriptViaImport,
     Global,
+    Script,
 }
 
 impl Display for CachedExternalType {
@@ -54,6 +56,7 @@ impl Display for CachedExternalType {
             CachedExternalType::EcmaScriptViaRequire => write!(f, "esm_require"),
             CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"),
             CachedExternalType::Global => write!(f, "global"),
+            CachedExternalType::Script => write!(f, "script"),
         }
     }
 }
@@ -103,6 +106,60 @@ impl CachedExternalModule {
                     )?;
                 }
             }
+            CachedExternalType::Script => {
+                // Parse the request format: "variableName@url"
+                // e.g., "foo@https://test.test.com"
+                if let Some(at_index) = self.request.find('@') {
+                    let variable_name = &self.request[..at_index];
+                    let url = &self.request[at_index + 1..];
+
+                    // Wrap the loading and variable access in a try-catch block
+                    writeln!(code, "let mod;")?;
+                    writeln!(code, "try {{")?;
+
+                    // First load the URL
+                    writeln!(
+                        code,
+                        "  await {TURBOPACK_LOAD_BY_URL}({});",
+                        StringifyJs(url)
+                    )?;
+
+                    // Then get the variable from global with existence check
+                    writeln!(
+                        code,
+                        "  if (typeof global[{}] === 'undefined') {{",
+                        StringifyJs(variable_name)
+                    )?;
+                    writeln!(
+                        code,
+                        "    throw new Error('Variable {} is not available on global object after \
+                         loading {}');",
+                        StringifyJs(variable_name),
+                        StringifyJs(url)
+                    )?;
+                    writeln!(code, "  }}")?;
+                    writeln!(code, "  mod = global[{}];", StringifyJs(variable_name))?;
+
+                    // Catch and re-throw errors with more context
+                    writeln!(code, "}} catch (error) {{")?;
+                    writeln!(
+                        code,
+                        "  throw new Error('Failed to load external URL module {}: ' + \
+                         (error.message || error));",
+                        StringifyJs(&self.request)
+                    )?;
+                    writeln!(code, "}}")?;
+                } else {
+                    // Invalid format - throw error
+                    writeln!(
+                        code,
+                        "throw new Error('Invalid URL external format. Expected \"variable@url\", \
+                         got: {}');",
+                        StringifyJs(&self.request)
+                    )?;
+                    writeln!(code, "const mod = undefined;")?;
+                }
+            }
             CachedExternalType::EcmaScriptViaRequire | CachedExternalType::CommonJs => {
                 writeln!(
                     code,

filename: turbopack/crates/turbopack/src/lib.rs
patch: @@ -668,8 +668,7 @@ async fn externals_tracing_module_context(ty: ExternalType) -> Result<Vc<ModuleA
         custom_conditions: match ty {
             ExternalType::CommonJs => vec!["require".into()],
             ExternalType::EcmaScriptModule => vec!["import".into()],
-            ExternalType::Url => vec![],
-            ExternalType::Global => vec![],
+            ExternalType::Url | ExternalType::Global | ExternalType::Script => vec![],
         },
         ..Default::default()
     };
@@ -1019,6 +1018,7 @@ pub async fn replace_external(
             }
         }
         ExternalType::Global => CachedExternalType::Global,
+        ExternalType::Script => CachedExternalType::Script,
         ExternalType::Url => {
             // we don't want to wrap url externals.
             return Ok(None);