@@ -179,25 +179,26 @@ pub async fn compile(
179179 "{} deno compile --bundle is experimental and may change." ,
180180 colors:: yellow( "Warning" )
181181 ) ;
182- // Write the bundle next to the working directory rather than the system
183- // temp dir so the embedded VFS path stays relative to the project and
184- // doesn't bake the build machine's temp path into the binary.
185- let initial_cwd = flags. initial_cwd . clone ( ) . unwrap_or_else ( || {
186- crate :: util:: env:: resolve_cwd ( None ) . unwrap ( ) . to_path_buf ( )
187- } ) ;
188- let bundle_path = initial_cwd. join ( format ! (
189- ".deno_compile_bundle_{:08x}.mjs" ,
190- rand:: thread_rng( ) . r#gen:: <u32 >( )
191- ) ) ;
192- // Register for cleanup before writing so a partially written file is
193- // removed even if bundling fails.
194- let guard = CleanupGuard ( vec ! [ bundle_path. clone( ) ] ) ;
195- let needs_npm_embed =
196- run_bundle_for_compile ( & flags, & compile_flags, & bundle_path) . await ?;
182+ let BundleForCompileResult {
183+ path : bundle_path,
184+ needs_npm_embed,
185+ extra_cleanup,
186+ } = run_bundle_for_compile ( & flags, & compile_flags)
187+ . boxed_local ( )
188+ . await ?;
197189 flags. internal . compile_bundle_embed_node_modules = needs_npm_embed;
198190 compile_flags. source_file = bundle_path. to_string_lossy ( ) . into_owned ( ) ;
191+ // Make sure any worker bundles travel along in the VFS so the runtime
192+ // `new Worker(new URL(..., import.meta.url))` lookup hits them.
193+ for worker_path in & extra_cleanup {
194+ compile_flags
195+ . include
196+ . push ( worker_path. display ( ) . to_string ( ) ) ;
197+ }
199198 flags. subcommand = DenoSubcommand :: Compile ( compile_flags. clone ( ) ) ;
200- Some ( guard)
199+ let mut cleanup = vec ! [ bundle_path] ;
200+ cleanup. extend ( extra_cleanup) ;
201+ Some ( CleanupGuard ( cleanup) )
201202 } else {
202203 None
203204 } ;
@@ -211,49 +212,163 @@ pub async fn compile(
211212 }
212213}
213214
214- /// Bundles the entrypoint and writes the result to `bundle_path`. Returns
215- /// `true` when the bundle needs the npm tree embedded in the binary, i.e.
216- /// esbuild's CJS-from-ESM wrapper appears in the output and runtime
217- /// require()s against npm package paths will happen.
215+ struct BundleForCompileResult {
216+ path : PathBuf ,
217+ /// True when esbuild's CJS-from-ESM wrapper appears in the bundle (in the
218+ /// main entry or any worker), which means runtime require()s against npm
219+ /// package paths will happen. The standalone binary writer reads this to
220+ /// decide whether to embed the npm tree.
221+ needs_npm_embed : bool ,
222+ /// Worker bundle files produced alongside the main one; they live next to
223+ /// the main bundle and must be cleaned up too.
224+ extra_cleanup : Vec < PathBuf > ,
225+ }
226+
218227async fn run_bundle_for_compile (
219228 flags : & Flags ,
220229 compile_flags : & CompileFlags ,
221- bundle_path : & Path ,
222- ) -> Result < bool , AnyError > {
230+ ) -> Result < BundleForCompileResult , AnyError > {
223231 let bundle_flags = Arc :: new ( flags. clone ( ) ) ;
232+ let initial_cwd = flags. initial_cwd . clone ( ) . unwrap_or_else ( || {
233+ crate :: util:: env:: resolve_cwd ( None ) . unwrap ( ) . to_path_buf ( )
234+ } ) ;
224235
236+ let main_bytes = bundle_one_for_compile (
237+ bundle_flags. clone ( ) ,
238+ compile_flags. source_file . clone ( ) ,
239+ )
240+ . await ?;
241+ let main_rewrite = rewrite_absolute_bundle_paths ( & main_bytes, & initial_cwd) ?;
242+ let mut needs_npm_embed = main_rewrite. rewrote_paths ;
243+
244+ // Scan the main bundle for `new Worker(new URL("X", import.meta.url))`
245+ // patterns. For each unique X, bundle the target as a separate entry,
246+ // write it next to the main bundle, and rewrite the URL string in the
247+ // main bundle to point at the worker bundle's file name. The worker
248+ // bundles are then reachable through `new URL(..., import.meta.url)`
249+ // at runtime, just like the main bundle.
250+ let main_src = std:: str:: from_utf8 ( & main_rewrite. bytes )
251+ . context ( "Bundle output is not valid UTF-8" ) ?;
252+ let worker_urls = discover_worker_urls ( main_src) ;
253+
254+ let mut url_replacements: Vec < ( String , String ) > = Vec :: new ( ) ;
255+ let mut extra_cleanup: Vec < PathBuf > = Vec :: new ( ) ;
256+ for worker_url in & worker_urls {
257+ let worker_abs = if Path :: new ( worker_url) . is_absolute ( ) {
258+ PathBuf :: from ( worker_url)
259+ } else {
260+ initial_cwd. join ( worker_url)
261+ } ;
262+ if !worker_abs. exists ( ) {
263+ log:: warn!(
264+ "{} Worker entrypoint '{}' was not found on disk; leaving it as-is. The compiled binary will fail to start this worker unless the file is provided via --include." ,
265+ colors:: yellow( "Warning" ) ,
266+ worker_url,
267+ ) ;
268+ continue ;
269+ }
270+ let worker_bytes = bundle_one_for_compile (
271+ bundle_flags. clone ( ) ,
272+ worker_abs. display ( ) . to_string ( ) ,
273+ )
274+ . await ?;
275+ let worker_rewrite =
276+ rewrite_absolute_bundle_paths ( & worker_bytes, & initial_cwd) ?;
277+ needs_npm_embed |= worker_rewrite. rewrote_paths ;
278+
279+ let worker_path = initial_cwd. join ( format ! (
280+ ".deno_compile_worker_{:08x}.mjs" ,
281+ rand:: thread_rng( ) . r#gen:: <u32 >( )
282+ ) ) ;
283+ std:: fs:: write ( & worker_path, & worker_rewrite. bytes ) . with_context ( || {
284+ format ! (
285+ "Writing bundled worker entrypoint to '{}'" ,
286+ worker_path. display( )
287+ )
288+ } ) ?;
289+ let worker_file_name = worker_path
290+ . file_name ( )
291+ . unwrap ( )
292+ . to_string_lossy ( )
293+ . into_owned ( ) ;
294+ url_replacements
295+ . push ( ( worker_url. clone ( ) , format ! ( "./{worker_file_name}" ) ) ) ;
296+ extra_cleanup. push ( worker_path) ;
297+ }
298+
299+ // Apply URL replacements to the main bundle source.
300+ let final_main_src = if url_replacements. is_empty ( ) {
301+ main_src. to_string ( )
302+ } else {
303+ rewrite_worker_urls ( main_src, & url_replacements)
304+ } ;
305+
306+ let bundle_path = initial_cwd. join ( format ! (
307+ ".deno_compile_bundle_{:08x}.mjs" ,
308+ rand:: thread_rng( ) . r#gen:: <u32 >( )
309+ ) ) ;
310+ std:: fs:: write ( & bundle_path, final_main_src. as_bytes ( ) ) . with_context (
311+ || format ! ( "Writing bundled entrypoint to '{}'" , bundle_path. display( ) ) ,
312+ ) ?;
313+
314+ Ok ( BundleForCompileResult {
315+ path : bundle_path,
316+ needs_npm_embed,
317+ extra_cleanup,
318+ } )
319+ }
320+
321+ async fn bundle_one_for_compile (
322+ flags : Arc < Flags > ,
323+ entrypoint : String ,
324+ ) -> Result < Vec < u8 > , AnyError > {
225325 // Always leave `.node` files external. esbuild has no loader for them
226326 // and would error if it tried to inline a native binary; with this
227327 // pattern the require() calls are emitted verbatim and resolved at
228328 // runtime against the embedded VFS by the native addon loader.
229329 let external = vec ! [ "*.node" . to_string( ) ] ;
330+ super :: bundle:: bundle_for_compile ( flags, entrypoint, external)
331+ . boxed_local ( )
332+ . await
333+ }
230334
231- let bytes = super :: bundle:: bundle_for_compile (
232- bundle_flags,
233- compile_flags. source_file . clone ( ) ,
234- external,
235- )
236- . boxed_local ( )
237- . await ?;
335+ /// Pull the relative path argument out of every
336+ /// `new Worker(new URL("X", import.meta.url), …)` in the bundle source.
337+ /// Returns unique paths in source order.
338+ fn discover_worker_urls ( bundle_src : & str ) -> Vec < String > {
339+ let pattern = lazy_regex:: regex!(
340+ r#"new\s+Worker\s*\(\s*new\s+URL\s*\(\s*"([^"]+)"\s*,\s*import\.meta\.url"#
341+ ) ;
342+ let mut seen = std:: collections:: HashSet :: new ( ) ;
343+ let mut out = Vec :: new ( ) ;
344+ for caps in pattern. captures_iter ( bundle_src) {
345+ let url = caps. get ( 1 ) . unwrap ( ) . as_str ( ) . to_string ( ) ;
346+ if seen. insert ( url. clone ( ) ) {
347+ out. push ( url) ;
348+ }
349+ }
350+ out
351+ }
238352
239- // The bundle is written next to the working directory, so rewrite paths
240- // relative to that directory.
241- let bundle_dir = bundle_path. parent ( ) . unwrap_or_else ( || Path :: new ( "." ) ) ;
242-
243- // esbuild's CJS-from-ESM wrapper hardcodes the absolute on-disk path of
244- // each CJS module it imports. At runtime the embedded VFS is mounted
245- // somewhere else (a temp dir under the user's $TMPDIR), so those paths
246- // miss. Rewrite each such literal to a runtime-relative resolution
247- // against `import.meta.url` so it survives the relocation.
248- let RewriteResult {
249- bytes : rewritten,
250- rewrote_paths,
251- } = rewrite_absolute_bundle_paths ( & bytes, bundle_dir) ?;
252-
253- std:: fs:: write ( bundle_path, & rewritten) . with_context ( || {
254- format ! ( "Writing bundled entrypoint to '{}'" , bundle_path. display( ) )
255- } ) ?;
256- Ok ( rewrote_paths)
353+ fn rewrite_worker_urls (
354+ bundle_src : & str ,
355+ replacements : & [ ( String , String ) ] ,
356+ ) -> String {
357+ let pattern = lazy_regex:: regex!(
358+ r#"(new\s+Worker\s*\(\s*new\s+URL\s*\(\s*")([^"]+)("\s*,\s*import\.meta\.url)"#
359+ ) ;
360+ pattern
361+ . replace_all ( bundle_src, |caps : & regex:: Captures < ' _ > | {
362+ let original = caps. get ( 2 ) . unwrap ( ) . as_str ( ) ;
363+ if let Some ( ( _, replacement) ) =
364+ replacements. iter ( ) . find ( |( orig, _) | orig == original)
365+ {
366+ format ! ( "{}{}{}" , & caps[ 1 ] , replacement, & caps[ 3 ] )
367+ } else {
368+ caps[ 0 ] . to_string ( )
369+ }
370+ } )
371+ . into_owned ( )
257372}
258373
259374struct RewriteResult {
0 commit comments