Skip to content

Commit 94b0253

Browse files
committed
feat: support bundle new url
1 parent 42c39a2 commit 94b0253

File tree

10 files changed

+404
-5
lines changed

10 files changed

+404
-5
lines changed

crates/rspack_binding_api/src/raw_options/raw_module/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ pub struct RawJavascriptParserOptions {
276276
pub dynamic_import_prefetch: Option<String>,
277277
pub dynamic_import_fetch_priority: Option<String>,
278278
pub url: Option<String>,
279+
/// Bundle new URL() targets as separate entry chunks (single-file output)
280+
#[napi(js_name = "bundleNewUrl")]
281+
pub bundle_new_url: Option<bool>,
279282
pub expr_context_critical: Option<bool>,
280283
pub unknown_context_critical: Option<bool>,
281284
pub wrapped_context_critical: Option<bool>,
@@ -342,6 +345,7 @@ impl From<RawJavascriptParserOptions> for JavascriptParserOptions {
342345
.dynamic_import_fetch_priority
343346
.map(|x| DynamicImportFetchPriority::from(x.as_str())),
344347
url: value.url.map(|v| JavascriptParserUrl::from(v.as_str())),
348+
bundle_new_url: value.bundle_new_url,
345349
expr_context_critical: value.expr_context_critical,
346350
unknown_context_critical: value.unknown_context_critical,
347351
wrapped_context_reg_exp: value.wrapped_context_reg_exp,

crates/rspack_core/src/dependency/dependency_type.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub enum DependencyType {
4747
NewUrlContext,
4848
// new Worker()
4949
NewWorker,
50+
// new URL() bundle mode (single-file entry)
51+
NewUrlBundle,
5052
// create script url
5153
CreateScriptUrl,
5254
// import.meta.webpackHot.accept
@@ -151,6 +153,7 @@ impl DependencyType {
151153
DependencyType::NewUrl => "new URL()",
152154
DependencyType::NewUrlContext => "new URL() context",
153155
DependencyType::NewWorker => "new Worker()",
156+
DependencyType::NewUrlBundle => "new URL() bundle",
154157
DependencyType::CreateScriptUrl => "create script url",
155158
DependencyType::ImportMetaHotAccept => "import.meta.webpackHot.accept",
156159
DependencyType::ImportMetaHotDecline => "import.meta.webpackHot.decline",

crates/rspack_core/src/options/module.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ pub struct JavascriptParserOptions {
289289
pub dynamic_import_prefetch: Option<JavascriptParserOrder>,
290290
pub dynamic_import_fetch_priority: Option<DynamicImportFetchPriority>,
291291
pub url: Option<JavascriptParserUrl>,
292+
/// Bundle new URL() targets as separate entry chunks (single-file output)
293+
pub bundle_new_url: Option<bool>,
292294
pub unknown_context_critical: Option<bool>,
293295
pub expr_context_critical: Option<bool>,
294296
pub wrapped_context_critical: Option<bool>,

crates/rspack_plugin_javascript/src/dependency/url/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod url_bundle;
12
use std::sync::LazyLock;
23

34
use regex::Regex;
@@ -11,6 +12,7 @@ use rspack_core::{
1112
URLStaticMode, UsedByExports,
1213
};
1314
use swc_core::ecma::atoms::Atom;
15+
pub use url_bundle::{URLBundleDependency, URLBundleDependencyTemplate};
1416

1517
use crate::{connection_active_used_by_exports, runtime::AUTO_PUBLIC_PATH_PLACEHOLDER};
1618

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use rspack_cacheable::{cacheable, cacheable_dyn};
2+
use rspack_core::{
3+
AsContextDependency, Compilation, Dependency, DependencyCategory, DependencyCodeGeneration,
4+
DependencyId, DependencyRange, DependencyTemplate, DependencyTemplateType, DependencyType,
5+
ExtendedReferencedExport, FactorizeInfo, ModuleDependency, ModuleGraph, ModuleGraphCacheArtifact,
6+
RuntimeGlobals, RuntimeSpec, TemplateContext, TemplateReplaceSource,
7+
};
8+
use rspack_util::ext::DynHash;
9+
10+
/// Dependency for `new URL()` when in bundle mode.
11+
/// This creates an async block that bundles the target module as a single-file entry.
12+
#[cacheable]
13+
#[derive(Debug, Clone)]
14+
pub struct URLBundleDependency {
15+
id: DependencyId,
16+
request: String,
17+
public_path: String,
18+
range: DependencyRange,
19+
range_url: DependencyRange,
20+
factorize_info: FactorizeInfo,
21+
/// Whether to render using relative URL (when url mode is NewUrlRelative)
22+
use_relative: bool,
23+
}
24+
25+
impl URLBundleDependency {
26+
pub fn new(
27+
request: String,
28+
public_path: String,
29+
range: DependencyRange,
30+
range_url: DependencyRange,
31+
use_relative: bool,
32+
) -> Self {
33+
Self {
34+
id: DependencyId::new(),
35+
request,
36+
public_path,
37+
range,
38+
range_url,
39+
factorize_info: Default::default(),
40+
use_relative,
41+
}
42+
}
43+
}
44+
45+
#[cacheable_dyn]
46+
impl Dependency for URLBundleDependency {
47+
fn id(&self) -> &DependencyId {
48+
&self.id
49+
}
50+
51+
fn category(&self) -> &DependencyCategory {
52+
&DependencyCategory::Url
53+
}
54+
55+
fn dependency_type(&self) -> &DependencyType {
56+
&DependencyType::NewUrlBundle
57+
}
58+
59+
fn range(&self) -> Option<DependencyRange> {
60+
Some(self.range)
61+
}
62+
63+
fn get_referenced_exports(
64+
&self,
65+
_module_graph: &ModuleGraph,
66+
_module_graph_cache: &ModuleGraphCacheArtifact,
67+
_runtime: Option<&RuntimeSpec>,
68+
) -> Vec<ExtendedReferencedExport> {
69+
vec![]
70+
}
71+
72+
fn could_affect_referencing_module(&self) -> rspack_core::AffectType {
73+
rspack_core::AffectType::True
74+
}
75+
}
76+
77+
#[cacheable_dyn]
78+
impl ModuleDependency for URLBundleDependency {
79+
fn request(&self) -> &str {
80+
&self.request
81+
}
82+
83+
fn user_request(&self) -> &str {
84+
&self.request
85+
}
86+
87+
fn factorize_info(&self) -> &FactorizeInfo {
88+
&self.factorize_info
89+
}
90+
91+
fn factorize_info_mut(&mut self) -> &mut FactorizeInfo {
92+
&mut self.factorize_info
93+
}
94+
}
95+
96+
#[cacheable_dyn]
97+
impl DependencyCodeGeneration for URLBundleDependency {
98+
fn dependency_template(&self) -> Option<DependencyTemplateType> {
99+
Some(URLBundleDependencyTemplate::template_type())
100+
}
101+
102+
fn update_hash(
103+
&self,
104+
hasher: &mut dyn std::hash::Hasher,
105+
_compilation: &Compilation,
106+
_runtime: Option<&RuntimeSpec>,
107+
) {
108+
self.public_path.dyn_hash(hasher);
109+
}
110+
}
111+
112+
impl AsContextDependency for URLBundleDependency {}
113+
114+
#[cacheable]
115+
#[derive(Debug, Clone, Default)]
116+
pub struct URLBundleDependencyTemplate;
117+
118+
impl URLBundleDependencyTemplate {
119+
pub fn template_type() -> DependencyTemplateType {
120+
DependencyTemplateType::Dependency(DependencyType::NewUrlBundle)
121+
}
122+
}
123+
124+
impl DependencyTemplate for URLBundleDependencyTemplate {
125+
fn render(
126+
&self,
127+
dep: &dyn DependencyCodeGeneration,
128+
source: &mut TemplateReplaceSource,
129+
code_generatable_context: &mut TemplateContext,
130+
) {
131+
let dep = dep
132+
.as_any()
133+
.downcast_ref::<URLBundleDependency>()
134+
.expect("URLBundleDependencyTemplate should be used for URLBundleDependency");
135+
let TemplateContext {
136+
compilation,
137+
runtime_requirements,
138+
..
139+
} = code_generatable_context;
140+
141+
// Get the chunk ID from the async block
142+
let chunk_id = compilation
143+
.get_module_graph()
144+
.get_parent_block(&dep.id)
145+
.and_then(|block| {
146+
compilation
147+
.chunk_graph
148+
.get_block_chunk_group(block, &compilation.chunk_group_by_ukey)
149+
})
150+
.map(|entrypoint| entrypoint.get_entrypoint_chunk())
151+
.and_then(|ukey| compilation.chunk_by_ukey.get(&ukey))
152+
.and_then(|chunk| chunk.id())
153+
.and_then(|chunk_id| serde_json::to_string(chunk_id).ok())
154+
.expect("failed to get json stringified chunk id");
155+
156+
runtime_requirements.insert(RuntimeGlobals::GET_CHUNK_SCRIPT_FILENAME);
157+
158+
let url_import_str = if dep.use_relative {
159+
// Render with relative path using new URL(filename, import.meta.url) pattern
160+
format!(
161+
"/* url bundle import */ new URL({}({}), import.meta.url)",
162+
compilation
163+
.runtime_template
164+
.render_runtime_globals(&RuntimeGlobals::GET_CHUNK_SCRIPT_FILENAME),
165+
chunk_id
166+
)
167+
} else {
168+
// Standard rendering with PUBLIC_PATH and BASE_URI
169+
let url_base = if !dep.public_path.is_empty() {
170+
format!("\"{}\"", dep.public_path)
171+
} else {
172+
compilation
173+
.runtime_template
174+
.render_runtime_globals(&RuntimeGlobals::PUBLIC_PATH)
175+
};
176+
177+
runtime_requirements.insert(RuntimeGlobals::PUBLIC_PATH);
178+
runtime_requirements.insert(RuntimeGlobals::BASE_URI);
179+
180+
// Generate: new URL(publicPath + __webpack_require__.u(chunkId), __webpack_require__.b)
181+
format!(
182+
"/* url bundle import */ new URL({} + {}({}), {})",
183+
url_base,
184+
compilation
185+
.runtime_template
186+
.render_runtime_globals(&RuntimeGlobals::GET_CHUNK_SCRIPT_FILENAME),
187+
chunk_id,
188+
compilation
189+
.runtime_template
190+
.render_runtime_globals(&RuntimeGlobals::BASE_URI)
191+
)
192+
};
193+
194+
source.replace(
195+
dep.range.start,
196+
dep.range.end,
197+
url_import_str.as_str(),
198+
None,
199+
);
200+
}
201+
}

crates/rspack_plugin_javascript/src/parser_plugin/url_plugin.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use std::hash::Hash;
2+
13
use rspack_core::{
2-
ConstDependency, ContextDependency, ContextMode, ContextNameSpaceObject, ContextOptions,
3-
DependencyCategory, JavascriptParserUrl, RuntimeGlobals,
4+
AsyncDependenciesBlock, ChunkLoading, ConstDependency, ContextDependency, ContextMode,
5+
ContextNameSpaceObject, ContextOptions, DependencyCategory, DependencyRange, EntryOptions,
6+
GroupOptions, JavascriptParserUrl, RuntimeGlobals, SharedSourceMap,
47
};
8+
use rspack_hash::RspackHash;
59
use rspack_util::SpanExt;
610
use swc_core::{
711
common::Spanned,
@@ -15,7 +19,7 @@ use url::Url;
1519
use super::JavascriptParserPlugin;
1620
use crate::{
1721
InnerGraphPlugin,
18-
dependency::{URLContextDependency, URLDependency},
22+
dependency::{URLBundleDependency, URLContextDependency, URLDependency},
1923
magic_comment::try_extract_magic_comment,
2024
parser_plugin::inner_graph::state::InnerGraphUsageOperation,
2125
visitors::{ExprRef, JavascriptParser, context_reg_exp, create_context_dependency},
@@ -101,6 +105,8 @@ pub fn get_url_request(
101105

102106
pub struct URLPlugin {
103107
pub mode: Option<JavascriptParserUrl>,
108+
/// Whether to bundle new URL() targets as separate entry chunks
109+
pub bundle_new_url: Option<bool>,
104110
}
105111

106112
impl JavascriptParserPlugin for URLPlugin {
@@ -159,6 +165,11 @@ impl JavascriptParserPlugin for URLPlugin {
159165
}
160166

161167
if let Some((request, start, end)) = get_url_request(parser, expr) {
168+
// Handle Bundle mode - create AsyncDependenciesBlock with special EntryOptions
169+
if self.bundle_new_url == Some(true) {
170+
return self.handle_bundle_mode(parser, expr, request, start, end);
171+
}
172+
162173
let dep = URLDependency::new(
163174
request.into(),
164175
expr.span.into(),
@@ -228,3 +239,71 @@ impl JavascriptParserPlugin for URLPlugin {
228239
Some(true)
229240
}
230241
}
242+
243+
impl URLPlugin {
244+
/// Handle Bundle mode: creates an AsyncDependenciesBlock with EntryOptions
245+
/// that force single-file output (no chunk loading, no async chunks)
246+
fn handle_bundle_mode(
247+
&self,
248+
parser: &mut JavascriptParser,
249+
expr: &NewExpr,
250+
request: String,
251+
start: u32,
252+
end: u32,
253+
) -> Option<bool> {
254+
let output_options = &parser.compiler_options.output;
255+
let mut hasher = RspackHash::from(output_options);
256+
parser.module_identifier.hash(&mut hasher);
257+
parser.worker_index.hash(&mut hasher);
258+
parser.worker_index += 1;
259+
let digest = hasher.digest(&output_options.hash_digest);
260+
let runtime = digest
261+
.rendered(output_options.hash_digest_length)
262+
.to_owned();
263+
264+
// Use empty public_path - it will be resolved at runtime via RuntimeGlobals::PUBLIC_PATH
265+
let public_path = String::new();
266+
267+
// Use relative URL when mode is NewUrlRelative
268+
let use_relative = matches!(self.mode, Some(JavascriptParserUrl::NewUrlRelative));
269+
270+
let dep = Box::new(URLBundleDependency::new(
271+
request.clone(),
272+
public_path,
273+
expr.span.into(),
274+
(start, end).into(),
275+
use_relative,
276+
));
277+
278+
let source_map: SharedSourceMap = parser.source_rope().clone();
279+
let mut block = AsyncDependenciesBlock::new(
280+
*parser.module_identifier,
281+
Into::<DependencyRange>::into(expr.span).to_loc(Some(&source_map)),
282+
None,
283+
vec![dep],
284+
Some(request),
285+
);
286+
287+
// Set EntryOptions with:
288+
// - chunk_loading: Disable (no chunk loading runtime needed)
289+
// - async_chunks: false (prevent further async chunk splitting)
290+
// This ensures the bundle is a single self-contained file
291+
block.set_group_options(GroupOptions::Entrypoint(Box::new(EntryOptions {
292+
name: None, // Anonymous entry - will get auto-generated chunk name
293+
runtime: Some(runtime.into()),
294+
chunk_loading: Some(ChunkLoading::Disable),
295+
wasm_loading: None,
296+
async_chunks: Some(false),
297+
public_path: None,
298+
base_uri: None,
299+
filename: None,
300+
library: None,
301+
depend_on: None,
302+
layer: None,
303+
})));
304+
305+
parser.add_block(Box::new(block));
306+
307+
Some(true)
308+
}
309+
}

crates/rspack_plugin_javascript/src/plugin/url_plugin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async fn normal_module_factory_parser(
4848
if !matches!(options.url, Some(JavascriptParserUrl::Disable)) {
4949
parser.add_parser_plugin(Box::new(crate::parser_plugin::URLPlugin {
5050
mode: options.url,
51+
bundle_new_url: options.bundle_new_url,
5152
}));
5253
}
5354
}

0 commit comments

Comments
 (0)