From 4bdc448b8f53c515494b15f6cdc7fffdd4ba4eae Mon Sep 17 00:00:00 2001 From: 0x5457 <0x5457@protonmail.com> Date: Wed, 22 Oct 2025 19:44:25 +0800 Subject: [PATCH 1/5] feat(import-map): add support for import maps in user workers --- crates/base/src/runtime/mod.rs | 10 ++-- .../user-worker-with-import-map/helper.ts | 3 ++ .../import_map.json | 5 ++ .../user-worker-with-import-map/index.ts | 38 ++++++++++++++ .../user_worker/index.ts | 16 ++++++ crates/base/tests/integration_tests.rs | 49 ++++++++++++++++++- crates/deno_facade/emitter.rs | 12 +++++ crates/deno_facade/import_map.rs | 27 +++++----- deno/lib.rs | 4 +- 9 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 crates/base/test_cases/user-worker-with-import-map/helper.ts create mode 100644 crates/base/test_cases/user-worker-with-import-map/import_map.json create mode 100644 crates/base/test_cases/user-worker-with-import-map/index.ts create mode 100644 crates/base/test_cases/user-worker-with-import-map/user_worker/index.ts diff --git a/crates/base/src/runtime/mod.rs b/crates/base/src/runtime/mod.rs index e65320ee9..6c20a8ac4 100644 --- a/crates/base/src/runtime/mod.rs +++ b/crates/base/src/runtime/mod.rs @@ -517,6 +517,11 @@ where let base_dir_path = std::env::current_dir().map(|p| p.join(&service_path))?; + let maybe_import_map_path = context + .get("importMapPath") + .and_then(|it| it.as_str()) + .map(str::to_string); + let eszip = if let Some(eszip_payload) = maybe_eszip { eszip_payload } else { @@ -564,6 +569,7 @@ where CacheSetting::Use }; + emitter_factory.set_import_map_path(maybe_import_map_path.clone()); emitter_factory .set_permissions_options(Some(permissions_options.clone())); @@ -634,10 +640,6 @@ where .get("sourceMap") .and_then(serde_json::Value::as_bool) .unwrap_or_default(); - let maybe_import_map_path = context - .get("importMapPath") - .and_then(|it| it.as_str()) - .map(str::to_string); let rt_provider = create_module_loader_for_standalone_from_eszip_kind( eszip, diff --git a/crates/base/test_cases/user-worker-with-import-map/helper.ts b/crates/base/test_cases/user-worker-with-import-map/helper.ts new file mode 100644 index 000000000..78d5d4fb3 --- /dev/null +++ b/crates/base/test_cases/user-worker-with-import-map/helper.ts @@ -0,0 +1,3 @@ +export function getHelperMessage(): string { + return "import map works!"; +} diff --git a/crates/base/test_cases/user-worker-with-import-map/import_map.json b/crates/base/test_cases/user-worker-with-import-map/import_map.json new file mode 100644 index 000000000..896da0b00 --- /dev/null +++ b/crates/base/test_cases/user-worker-with-import-map/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "helper-from-import-map": "./helper.ts" + } +} diff --git a/crates/base/test_cases/user-worker-with-import-map/index.ts b/crates/base/test_cases/user-worker-with-import-map/index.ts new file mode 100644 index 000000000..5a008255f --- /dev/null +++ b/crates/base/test_cases/user-worker-with-import-map/index.ts @@ -0,0 +1,38 @@ +import * as path from "jsr:@std/path"; + +Deno.serve(async (req: Request) => { + const basePath = "test_cases/user-worker-with-import-map"; + const url = new URL(req.url); + const { pathname } = url; + + const userWorkerPath = path.join(basePath, "user_worker"); + if (pathname === "/import_map") { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath: userWorkerPath, + forceCreate: true, + context: { + importMapPath: path.join(basePath, "import_map.json"), + }, + }); + return worker.fetch(req); + } + if (pathname === "/inline_import_map") { + const inlineImportMap = { + imports: { + "helper-from-import-map": `./${path.join(basePath, "helper.ts")}`, + }, + }; + const importMapPath = `data:${encodeURIComponent(JSON.stringify(inlineImportMap))}`; + + const worker = await EdgeRuntime.userWorkers.create({ + servicePath: userWorkerPath, + forceCreate: true, + context: { + importMapPath: importMapPath, + }, + }); + return worker.fetch(req); + } + + return new Response("Not Found", { status: 404 }); +}); diff --git a/crates/base/test_cases/user-worker-with-import-map/user_worker/index.ts b/crates/base/test_cases/user-worker-with-import-map/user_worker/index.ts new file mode 100644 index 000000000..5dadf827a --- /dev/null +++ b/crates/base/test_cases/user-worker-with-import-map/user_worker/index.ts @@ -0,0 +1,16 @@ +import { getHelperMessage } from "helper-from-import-map"; + +Deno.serve((_req: Request) => { + return new Response( + JSON.stringify({ + message: getHelperMessage(), + success: true, + }), + { + headers: { + "Content-Type": "application/json", + Connection: "keep-alive", + }, + }, + ); +}); diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 570a0a46f..6bca6fa35 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -31,7 +31,6 @@ use base::utils::test_utils::test_user_runtime_opts; use base::utils::test_utils::test_user_worker_pool_policy; use base::utils::test_utils::TestBed; use base::utils::test_utils::TestBedBuilder; -use base::utils::test_utils::{self}; use base::worker; use base::worker::TerminationToken; use base::WorkerKind; @@ -4176,6 +4175,54 @@ directory." } } +#[tokio::test] +#[serial] +async fn test_user_worker_with_import_map() { + let assert_fn = |resp: Result| async { + let res = resp.unwrap(); + let status = res.status().as_u16(); + + let body_bytes = res.bytes().await.unwrap(); + let body_str = String::from_utf8_lossy(&body_bytes); + + assert_eq!( + status, 200, + "Expected 200, got {} with body: {}", + status, body_str + ); + + assert!( + body_str.contains("import map works!"), + "Expected import map works!, got: {}", + body_str + ); + }; + { + integration_test!( + "./test_cases/user-worker-with-import-map", + NON_SECURE_PORT, + "import_map", + None, + None, + None, + (assert_fn), + TerminationToken::new() + ); + } + { + integration_test!( + "./test_cases/user-worker-with-import-map", + NON_SECURE_PORT, + "inline_import_map", + None, + None, + None, + (assert_fn), + TerminationToken::new() + ); + } +} + #[derive(Deserialize)] struct ErrorResponsePayload { msg: String, diff --git a/crates/deno_facade/emitter.rs b/crates/deno_facade/emitter.rs index 8990de566..7603385ab 100644 --- a/crates/deno_facade/emitter.rs +++ b/crates/deno_facade/emitter.rs @@ -58,6 +58,7 @@ use ext_node::DenoFsNodeResolverEnv; use ext_node::NodeResolver; use ext_node::PackageJsonResolver; +use crate::import_map::load_import_map; use crate::permissions::RuntimePermissionDescriptorParser; struct Deferred(once_cell::unsync::OnceCell); @@ -120,6 +121,7 @@ pub struct EmitterFactory { sloppy_imports_resolver: Deferred>>, workspace_resolver: Deferred>, + import_map_path: Option, cache_strategy: Option, deno_dir: DenoDir, deno_options: Option>, @@ -160,6 +162,7 @@ impl EmitterFactory { sloppy_imports_resolver: Default::default(), workspace_resolver: Default::default(), + import_map_path: Default::default(), cache_strategy: None, deno_dir, deno_options: None, @@ -180,6 +183,11 @@ impl EmitterFactory { self } + pub fn set_import_map_path(&mut self, value: Option) -> &mut Self { + self.import_map_path = value; + self + } + pub fn set_cache_strategy( &mut self, value: Option, @@ -528,8 +536,12 @@ impl EmitterFactory { ) -> Result<&Arc, anyhow::Error> { self.workspace_resolver.get_or_try_init(|| { let options = self.deno_options()?; + let specified_import_map = + load_import_map(self.import_map_path.as_deref())?; + let resolver = options.create_workspace_resolver( self.file_fetcher()?, + specified_import_map, if options.use_byonm() { PackageJsonDepResolution::Disabled } else { diff --git a/crates/deno_facade/import_map.rs b/crates/deno_facade/import_map.rs index d7e0746de..540ee1b02 100644 --- a/crates/deno_facade/import_map.rs +++ b/crates/deno_facade/import_map.rs @@ -1,16 +1,15 @@ use anyhow::anyhow; use anyhow::Error; +use deno_config::workspace::SpecifiedImportMap; +use deno_core::serde_json; use deno_core::url::Url; -use import_map::parse_from_json; -use import_map::ImportMap; -use urlencoding::decode; - use std::fs; use std::path::Path; +use urlencoding::decode; pub fn load_import_map( - maybe_path: Option, -) -> Result, Error> { + maybe_path: Option<&str>, +) -> Result, Error> { if let Some(path_str) = maybe_path { let json_str; let base_url; @@ -21,10 +20,13 @@ pub fn load_import_map( if path_str.starts_with("data:") { let data_uri = Url::parse(&path_str)?; json_str = decode(data_uri.path())?.into_owned(); - base_url = Url::from_directory_path( - decode(data_uri.query().unwrap_or(""))?.into_owned(), - ) - .map_err(|_| anyhow!("invalid import map base url"))?; + if let Some(query) = data_uri.query() { + base_url = Url::from_directory_path(decode(query)?.into_owned()) + .map_err(|_| anyhow!("invalid import map base url"))?; + } else { + base_url = Url::from_directory_path(std::env::current_dir().unwrap()) + .map_err(|_| anyhow!("invalid import map base url"))?; + } } else { let path = Path::new(&path_str); let abs_path = std::env::current_dir().map(|p| p.join(path))?; @@ -33,8 +35,9 @@ pub fn load_import_map( .map_err(|_| anyhow!("invalid import map base url"))?; } - let result = parse_from_json(base_url, json_str.as_str())?; - Ok(Some(result.import_map)) + let value = serde_json::from_str(&json_str)?; + + Ok(Some(SpecifiedImportMap { base_url, value })) } else { Ok(None) } diff --git a/deno/lib.rs b/deno/lib.rs index d1ef3baee..1259d74a0 100644 --- a/deno/lib.rs +++ b/deno/lib.rs @@ -16,6 +16,7 @@ use deno_config::deno_json::TsConfigForEmit; use deno_config::deno_json::TsConfigType; use deno_config::workspace::CreateResolverOptions; use deno_config::workspace::PackageJsonDepResolution; +use deno_config::workspace::SpecifiedImportMap; use deno_config::workspace::VendorEnablement; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDirectoryEmptyOptions; @@ -199,12 +200,13 @@ impl DenoOptions { pub fn create_workspace_resolver( &self, _file_fetcher: &FileFetcher, + specified_import_map: Option, pkg_json_dep_resolution: PackageJsonDepResolution, ) -> Result { Ok(self.workspace().create_resolver( CreateResolverOptions { pkg_json_dep_resolution, - specified_import_map: None, + specified_import_map, }, |path| Ok(std::fs::read_to_string(path)?), )?) From 9481aedaf45d1b9fa2349f56ce9bdd91b757c04e Mon Sep 17 00:00:00 2001 From: 0x5457 <0x5457@protonmail.com> Date: Wed, 22 Oct 2025 20:01:18 +0800 Subject: [PATCH 2/5] chore: deno fmt --- crates/base/test_cases/user-worker-with-import-map/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/base/test_cases/user-worker-with-import-map/index.ts b/crates/base/test_cases/user-worker-with-import-map/index.ts index 5a008255f..17c568362 100644 --- a/crates/base/test_cases/user-worker-with-import-map/index.ts +++ b/crates/base/test_cases/user-worker-with-import-map/index.ts @@ -22,7 +22,9 @@ Deno.serve(async (req: Request) => { "helper-from-import-map": `./${path.join(basePath, "helper.ts")}`, }, }; - const importMapPath = `data:${encodeURIComponent(JSON.stringify(inlineImportMap))}`; + const importMapPath = `data:${ + encodeURIComponent(JSON.stringify(inlineImportMap)) + }`; const worker = await EdgeRuntime.userWorkers.create({ servicePath: userWorkerPath, From 76eb42fee3415ac606e20a40fe96974f0bc79fa9 Mon Sep 17 00:00:00 2001 From: 0x5457 <0x5457@protonmail.com> Date: Wed, 22 Oct 2025 20:22:22 +0800 Subject: [PATCH 3/5] chore: cargo clippy --- crates/base/tests/integration_tests.rs | 1 + crates/deno_facade/import_map.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 6bca6fa35..0d3b989c1 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -25,6 +25,7 @@ use base::server::ServerEvent; use base::server::ServerFlags; use base::server::ServerHealth; use base::server::Tls; +use base::utils::test_utils; use base::utils::test_utils::create_test_user_worker; use base::utils::test_utils::ensure_npm_package_installed; use base::utils::test_utils::test_user_runtime_opts; diff --git a/crates/deno_facade/import_map.rs b/crates/deno_facade/import_map.rs index 540ee1b02..f6427ce54 100644 --- a/crates/deno_facade/import_map.rs +++ b/crates/deno_facade/import_map.rs @@ -18,7 +18,7 @@ pub fn load_import_map( // the data URI takes the following format // data:{encodeURIComponent(mport_map.json)?{encodeURIComponent(base_path)} if path_str.starts_with("data:") { - let data_uri = Url::parse(&path_str)?; + let data_uri = Url::parse(path_str)?; json_str = decode(data_uri.path())?.into_owned(); if let Some(query) = data_uri.query() { base_url = Url::from_directory_path(decode(query)?.into_owned()) From 9a8f83086cd0cf95c209fe074a7242b854732ac9 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Fri, 31 Oct 2025 02:05:41 +0000 Subject: [PATCH 4/5] chore: update dependency --- Cargo.lock | 2 +- crates/deno_facade/Cargo.toml | 1 - deno/Cargo.toml | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d794d7c3..3db2c77e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1892,6 +1892,7 @@ dependencies = [ "tokio", "tokio-util", "twox-hash", + "urlencoding", "winapi", ] @@ -2172,7 +2173,6 @@ dependencies = [ "tokio", "tracing", "url", - "urlencoding", ] [[package]] diff --git a/crates/deno_facade/Cargo.toml b/crates/deno_facade/Cargo.toml index abbf77931..d0435df38 100644 --- a/crates/deno_facade/Cargo.toml +++ b/crates/deno_facade/Cargo.toml @@ -39,7 +39,6 @@ thiserror.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true -urlencoding.workspace = true dashmap = "5.5.3" hashlink = "0.8" diff --git a/deno/Cargo.toml b/deno/Cargo.toml index 3f0009e15..86a24a05f 100644 --- a/deno/Cargo.toml +++ b/deno/Cargo.toml @@ -77,6 +77,7 @@ thiserror.workspace = true tokio.workspace = true tokio-util.workspace = true twox-hash.workspace = true +urlencoding.workspace = true bincode = "=1.3.3" dissimilar = "=1.0.4" From bb64214fde8b7af31a77e00964ec6d574fcb3f17 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Fri, 31 Oct 2025 02:06:10 +0000 Subject: [PATCH 5/5] stamp: polishing --- crates/base/src/runtime/mod.rs | 5 +++-- crates/deno_facade/emitter.rs | 12 ------------ crates/deno_facade/lib.rs | 1 - {crates/deno_facade => deno}/import_map.rs | 0 deno/lib.rs | 20 +++++++++++++++++--- 5 files changed, 20 insertions(+), 18 deletions(-) rename {crates/deno_facade => deno}/import_map.rs (100%) diff --git a/crates/base/src/runtime/mod.rs b/crates/base/src/runtime/mod.rs index 6c20a8ac4..8567eb439 100644 --- a/crates/base/src/runtime/mod.rs +++ b/crates/base/src/runtime/mod.rs @@ -569,7 +569,6 @@ where CacheSetting::Use }; - emitter_factory.set_import_map_path(maybe_import_map_path.clone()); emitter_factory .set_permissions_options(Some(permissions_options.clone())); @@ -591,7 +590,9 @@ where if let Some(module_url) = main_module_url.as_ref() { builder.set_entrypoint(Some(module_url.to_file_path().unwrap())); } - builder.set_no_npm(no_npm); + builder + .set_no_npm(no_npm) + .set_import_map_path(maybe_import_map_path.clone()); emitter_factory.set_deno_options(builder.build()?); diff --git a/crates/deno_facade/emitter.rs b/crates/deno_facade/emitter.rs index 7603385ab..8990de566 100644 --- a/crates/deno_facade/emitter.rs +++ b/crates/deno_facade/emitter.rs @@ -58,7 +58,6 @@ use ext_node::DenoFsNodeResolverEnv; use ext_node::NodeResolver; use ext_node::PackageJsonResolver; -use crate::import_map::load_import_map; use crate::permissions::RuntimePermissionDescriptorParser; struct Deferred(once_cell::unsync::OnceCell); @@ -121,7 +120,6 @@ pub struct EmitterFactory { sloppy_imports_resolver: Deferred>>, workspace_resolver: Deferred>, - import_map_path: Option, cache_strategy: Option, deno_dir: DenoDir, deno_options: Option>, @@ -162,7 +160,6 @@ impl EmitterFactory { sloppy_imports_resolver: Default::default(), workspace_resolver: Default::default(), - import_map_path: Default::default(), cache_strategy: None, deno_dir, deno_options: None, @@ -183,11 +180,6 @@ impl EmitterFactory { self } - pub fn set_import_map_path(&mut self, value: Option) -> &mut Self { - self.import_map_path = value; - self - } - pub fn set_cache_strategy( &mut self, value: Option, @@ -536,12 +528,8 @@ impl EmitterFactory { ) -> Result<&Arc, anyhow::Error> { self.workspace_resolver.get_or_try_init(|| { let options = self.deno_options()?; - let specified_import_map = - load_import_map(self.import_map_path.as_deref())?; - let resolver = options.create_workspace_resolver( self.file_fetcher()?, - specified_import_map, if options.use_byonm() { PackageJsonDepResolution::Disabled } else { diff --git a/crates/deno_facade/lib.rs b/crates/deno_facade/lib.rs index b7e6a540e..b26056abf 100644 --- a/crates/deno_facade/lib.rs +++ b/crates/deno_facade/lib.rs @@ -15,7 +15,6 @@ mod eszip; pub mod errors; pub mod graph; -pub mod import_map; pub mod jsr; pub mod metadata; pub mod module_loader; diff --git a/crates/deno_facade/import_map.rs b/deno/import_map.rs similarity index 100% rename from crates/deno_facade/import_map.rs rename to deno/import_map.rs diff --git a/deno/lib.rs b/deno/lib.rs index 1259d74a0..58257e8d5 100644 --- a/deno/lib.rs +++ b/deno/lib.rs @@ -16,7 +16,6 @@ use deno_config::deno_json::TsConfigForEmit; use deno_config::deno_json::TsConfigType; use deno_config::workspace::CreateResolverOptions; use deno_config::workspace::PackageJsonDepResolution; -use deno_config::workspace::SpecifiedImportMap; use deno_config::workspace::VendorEnablement; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDirectoryEmptyOptions; @@ -29,6 +28,7 @@ use deno_npm::npm_rc::ResolvedNpmRc; use deno_path_util::normalize_path; use dotenvy::from_filename; use file_fetcher::FileFetcher; +use import_map::load_import_map; pub mod args; pub mod auth_tokens; @@ -38,6 +38,7 @@ pub mod errors; pub mod file_fetcher; pub mod graph_util; pub mod http_util; +pub mod import_map; pub mod node; pub mod npm; pub mod npmrc; @@ -200,13 +201,14 @@ impl DenoOptions { pub fn create_workspace_resolver( &self, _file_fetcher: &FileFetcher, - specified_import_map: Option, pkg_json_dep_resolution: PackageJsonDepResolution, ) -> Result { Ok(self.workspace().create_resolver( CreateResolverOptions { pkg_json_dep_resolution, - specified_import_map, + specified_import_map: load_import_map( + self.builder.import_map_path.as_deref(), + )?, }, |path| Ok(std::fs::read_to_string(path)?), )?) @@ -375,6 +377,7 @@ pub struct DenoOptionsBuilder { env_file: Option>, frozen_lockfile: Option, force_global_cache: Option, + import_map_path: Option, } impl Default for DenoOptionsBuilder { @@ -399,6 +402,7 @@ impl DenoOptionsBuilder { env_file: None, frozen_lockfile: None, force_global_cache: None, + import_map_path: None, } } @@ -538,6 +542,16 @@ impl DenoOptionsBuilder { self } + pub fn import_map_path(mut self, value: String) -> Self { + self.import_map_path = Some(value); + self + } + + pub fn set_import_map_path(&mut self, value: Option) -> &mut Self { + self.import_map_path = value; + self + } + fn lockfile_skip_write(&self) -> bool { self.frozen_lockfile.is_none() }