Skip to content

Commit b88b67d

Browse files
committed
feat(turbopack): add loadScript method for script external
1 parent 46c7a81 commit b88b67d

File tree

4 files changed

+66
-20
lines changed

4 files changed

+66
-20
lines changed

turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/runtime-base.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,36 @@ function loadChunkByUrl(
252252
}
253253
browserContextPrototype.L = loadChunkByUrl
254254

255+
const loadedScripts = new Map<string, Promise<void>>()
256+
257+
/**
258+
* Load an external script by creating a <script> tag.
259+
* This is used for script externals that need to be loaded from CDN or other external sources.
260+
*/
261+
function loadScript(
262+
this: TurbopackBrowserBaseContext<Module>,
263+
scriptUrl: string
264+
): Promise<void> {
265+
// Return cached promise if script is already loading or loaded
266+
let promise = loadedScripts.get(scriptUrl)
267+
if (promise) {
268+
return promise
269+
}
270+
271+
promise = new Promise<void>((resolve, reject) => {
272+
const script = document.createElement('script')
273+
script.src = scriptUrl
274+
script.onload = () => resolve()
275+
script.onerror = () =>
276+
reject(new Error(`Failed to load script: ${scriptUrl}`))
277+
document.head.appendChild(script)
278+
})
279+
280+
loadedScripts.set(scriptUrl, promise)
281+
return promise
282+
}
283+
browserContextPrototype.S = loadScript
284+
255285
// Do not make this async. React relies on referential equality of the returned Promise.
256286
function loadChunkByUrlInternal(
257287
sourceType: SourceType,

turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type DynamicExport = (
6363

6464
type LoadChunk = (chunkPath: ChunkPath) => Promise<any> | undefined
6565
type LoadChunkByUrl = (chunkUrl: ChunkUrl) => Promise<any> | undefined
66+
type LoadScript = (scriptUrl: string) => Promise<void>
6667
type LoadWebAssembly = (
6768
wasmChunkPath: ChunkPath,
6869
edgeModule: () => WebAssembly.Module,
@@ -147,6 +148,7 @@ interface TurbopackBaseContext<M> {
147148
M: ModuleFactories
148149
l: LoadChunk
149150
L: LoadChunkByUrl
151+
S: LoadScript
150152
w: LoadWebAssembly
151153
u: LoadWebAssemblyModule
152154
P: ResolveAbsolutePath

turbopack/crates/turbopack-ecmascript/src/references/external_module.rs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use crate::{
3636
references::async_module::{AsyncModule, OptionAsyncModule},
3737
runtime_functions::{
3838
TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
39-
TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
39+
TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_SCRIPT,
4040
},
4141
utils::StringifyJs,
4242
};
@@ -210,42 +210,54 @@ impl CachedExternalModule {
210210
let variable_name = &self.request[..at_index];
211211
let url = &self.request[at_index + 1..];
212212

213-
// Wrap the loading and variable access in a try-catch block
214-
writeln!(code, "let mod;")?;
215-
writeln!(code, "try {{")?;
213+
// Similar to webpack's approach: wrap in a promise that checks variable before
214+
// and after loading
215+
writeln!(code, "const mod = await (async () => {{")?;
216216

217-
// First load the URL
217+
// First check if variable already exists (avoid redundant loading)
218218
writeln!(
219219
code,
220-
" await {TURBOPACK_LOAD_BY_URL}({});",
220+
" if (typeof globalThis[{}] !== 'undefined') {{",
221+
StringifyJs(variable_name)
222+
)?;
223+
writeln!(
224+
code,
225+
" return globalThis[{}];",
226+
StringifyJs(variable_name)
227+
)?;
228+
writeln!(code, " }}")?;
229+
230+
// Load the script if variable doesn't exist
231+
writeln!(
232+
code,
233+
" await {TURBOPACK_LOAD_SCRIPT}({});",
221234
StringifyJs(url)
222235
)?;
223236

224-
// Then get the variable from global with existence check
237+
// After loading, check again if the variable is available
225238
writeln!(
226239
code,
227-
" if (typeof global[{}] === 'undefined') {{",
240+
" if (typeof globalThis[{}] !== 'undefined') {{",
228241
StringifyJs(variable_name)
229242
)?;
230243
writeln!(
231244
code,
232-
" throw new Error('Variable {} is not available on global object after \
233-
loading {}');",
234-
StringifyJs(variable_name),
235-
StringifyJs(url)
245+
" return globalThis[{}];",
246+
StringifyJs(variable_name)
236247
)?;
237248
writeln!(code, " }}")?;
238-
writeln!(code, " mod = global[{}];", StringifyJs(variable_name))?;
239249

240-
// Catch and re-throw errors with more context
241-
writeln!(code, "}} catch (error) {{")?;
250+
// Variable not found after loading - throw error
242251
writeln!(
243252
code,
244-
" throw new Error('Failed to load external URL module {}: ' + \
245-
(error.message || error));",
246-
StringifyJs(&self.request)
253+
" const error = new Error('Loading script failed.\\n(missing: {})');",
254+
StringifyJs(url)
247255
)?;
248-
writeln!(code, "}}")?;
256+
writeln!(code, " error.name = 'ScriptExternalLoadError';")?;
257+
writeln!(code, " error.type = 'missing';")?;
258+
writeln!(code, " error.request = {};", StringifyJs(url))?;
259+
writeln!(code, " throw error;")?;
260+
writeln!(code, "}})();")?;
249261
} else {
250262
// Invalid format - throw error
251263
writeln!(

turbopack/crates/turbopack-ecmascript/src/runtime_functions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub const TURBOPACK_CACHE: &TurbopackRuntimeFunctionShortcut = make_shortcut!("c
7878
pub const TURBOPACK_MODULES: &TurbopackRuntimeFunctionShortcut = make_shortcut!("M");
7979
pub const TURBOPACK_LOAD: &TurbopackRuntimeFunctionShortcut = make_shortcut!("l");
8080
pub const TURBOPACK_LOAD_BY_URL: &TurbopackRuntimeFunctionShortcut = make_shortcut!("L");
81+
pub const TURBOPACK_LOAD_SCRIPT: &TurbopackRuntimeFunctionShortcut = make_shortcut!("S");
8182
pub const TURBOPACK_CLEAR_CHUNK_CACHE: &TurbopackRuntimeFunctionShortcut = make_shortcut!("C");
8283
pub const TURBOPACK_DYNAMIC: &TurbopackRuntimeFunctionShortcut = make_shortcut!("j");
8384
pub const TURBOPACK_RESOLVE_ABSOLUTE_PATH: &TurbopackRuntimeFunctionShortcut = make_shortcut!("P");
@@ -97,7 +98,7 @@ pub const TURBOPACK_PUBLIC_PATH: &TurbopackRuntimeFunctionShortcut = make_shortc
9798

9899
/// Adding an entry to this list will automatically ensure that `__turbopack_XXX__` can be called
99100
/// from user code (by inserting a replacement into free_var_references)
100-
pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctionShortcut); 24] = [
101+
pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctionShortcut); 25] = [
101102
("__turbopack_require__", TURBOPACK_REQUIRE),
102103
("__turbopack_module_context__", TURBOPACK_MODULE_CONTEXT),
103104
("__turbopack_import__", TURBOPACK_IMPORT),
@@ -108,6 +109,7 @@ pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctio
108109
("__turbopack_modules__", TURBOPACK_MODULES),
109110
("__turbopack_load__", TURBOPACK_LOAD),
110111
("__turbopack_load_by_url__", TURBOPACK_LOAD_BY_URL),
112+
("__turbopack_load_script__", TURBOPACK_LOAD_SCRIPT),
111113
("__turbopack_dynamic__", TURBOPACK_DYNAMIC),
112114
(
113115
"__turbopack_resolve_absolute_path__",

0 commit comments

Comments
 (0)