use rspack_core::{
ContextDependency, ContextMode, ContextNameSpaceObject, ContextOptions, Dependency,
DependencyCategory, JavascriptParserUrl, RuntimeGlobals, RuntimeRequirementsDependency,
};
use rspack_util::SpanExt;
use swc_core::{
common::Spanned,
ecma::{
ast::{Expr, ExprOrSpread, MemberExpr, MetaPropKind, NewExpr},
visit::{Visit, VisitWith},
},
};
use url::Url;
use super::{JavascriptParserPlugin, inner_graph::state::InnerGraphUsageOperation};
use crate::{
InnerGraphParserPlugin,
dependency::{URLContextDependency, URLDependency},
magic_comment::try_extract_magic_comment,
visitors::{ExprRef, JavascriptParser, context_reg_exp, create_context_dependency},
};
#[derive(Default)]
struct NestedNewUrlVisitor {
has_nested_new_url: bool,
}
impl Visit for NestedNewUrlVisitor {
fn visit_new_expr(&mut self, expr: &NewExpr) {
if expr
.callee
.as_ident()
.is_some_and(|ident| ident.sym.eq("URL"))
{
self.has_nested_new_url = true;
}
}
}
pub fn is_meta_url(parser: &mut JavascriptParser, expr: &MemberExpr) -> bool {
let chain = parser.extract_member_expression_chain(ExprRef::Member(expr));
if let ExprRef::MetaProp(meta) = chain.object {
return meta.kind == MetaPropKind::ImportMeta
&& chain.members.len() == 1
&& chain.members.first().is_some_and(|member| member == "url");
}
false
}
pub fn get_url_request(
parser: &mut JavascriptParser,
expr: &NewExpr,
) -> Option<(String, u32, u32)> {
let args = expr.args.as_ref()?;
let ExprOrSpread {
spread: None,
expr: arg1,
} = args.first()?
else {
return None;
};
let arg2 = args.get(1);
if let Some(arg2) = arg2 {
let ExprOrSpread {
spread: None,
expr: arg2,
} = arg2
else {
return None;
};
let Expr::Member(arg2) = &**arg2 else {
return None;
};
if is_meta_url(parser, arg2) {
return parser
.evaluate_expression(arg1)
.as_string()
.map(|req| (req, arg1.span().real_lo(), arg2.span().real_hi()));
}
} else {
let Expr::Member(arg1) = &**arg1 else {
return None;
};
if is_meta_url(parser, arg1) {
return Some((
Url::from_file_path(parser.resource_data.resource())
.expect("should be a path")
.to_string(),
arg1.span().real_lo(),
arg1.span().real_hi(),
));
}
}
None
}
pub struct URLPlugin {
pub mode: Option<JavascriptParserUrl>,
}
#[rspack_macros::implemented_javascript_parser_hooks]
impl JavascriptParserPlugin for URLPlugin {
fn can_rename(&self, _parser: &mut JavascriptParser, for_name: &str) -> Option<bool> {
(for_name == "URL").then_some(true)
}
fn new_expression(
&self,
parser: &mut JavascriptParser,
expr: &NewExpr,
for_name: &str,
) -> Option<bool> {
if for_name != "URL" {
return None;
}
let args = expr.args.as_ref()?;
let arg = args.first()?;
let magic_comment_options = try_extract_magic_comment(parser, expr.span, arg.span());
if magic_comment_options.get_ignore().unwrap_or_default() {
if args.len() != 2 {
return None;
}
let arg2 = args.get(1)?;
if let ExprOrSpread {
spread: None,
expr: arg2_expr,
} = arg2
&& let Expr::Member(arg2) = &**arg2_expr
&& !is_meta_url(parser, arg2)
{
return None;
}
parser.add_presentational_dependency(Box::new(RuntimeRequirementsDependency::new(
arg2.span().into(),
RuntimeGlobals::BASE_URI,
)));
return Some(true);
}
if expr.args.as_ref().is_some_and(|args| {
args.len() == 1
&& args[0]
.expr
.as_member()
.is_some_and(|member| is_meta_url(parser, member))
}) {
return None;
}
if let Some((request, start, end)) = get_url_request(parser, expr) {
let dep = URLDependency::new(
request.into(),
expr.span.into(),
(start, end).into(),
self.mode,
);
let dep_id = *dep.id();
parser.add_dependency(Box::new(dep));
InnerGraphParserPlugin::on_usage(parser, InnerGraphUsageOperation::URLDependency(dep_id));
return Some(true);
}
let mut nested_new_url_visitor = NestedNewUrlVisitor::default();
arg.expr.visit_with(&mut nested_new_url_visitor);
if nested_new_url_visitor.has_nested_new_url {
return None;
}
let arg2 = args.get(1)?;
if !arg2
.expr
.as_member()
.is_some_and(|member| is_meta_url(parser, member))
{
return None;
}
let param = parser.evaluate_expression(&arg.expr);
let result = create_context_dependency(¶m, parser);
let options = ContextOptions {
mode: ContextMode::Sync,
recursive: true,
reg_exp: context_reg_exp(&result.reg, "", None, parser),
include: magic_comment_options.get_include(),
exclude: magic_comment_options.get_exclude(),
category: DependencyCategory::Url,
request: format!("{}{}{}", result.context, result.query, result.fragment),
context: result.context,
namespace_object: ContextNameSpaceObject::Unset,
group_options: None,
replaces: result.replaces,
start: expr.span().real_lo(),
end: expr.span().real_hi(),
referenced_specifiers: None,
attributes: None,
phase: None,
};
let mut dep = URLContextDependency::new(
options,
expr.span().into(),
param.range().into(),
parser.in_try,
);
*dep.critical_mut() = result.critical;
parser.add_dependency(Box::new(dep));
Some(true)
}
fn is_pure(&self, parser: &mut JavascriptParser, expr: &Expr) -> Option<bool> {
let expr = expr.as_new()?;
let callee = expr.callee.as_ident()?;
if parser.get_free_info_from_variable(&callee.sym).is_none() || !callee.sym.eq("URL") {
return None;
}
get_url_request(parser, expr)?;
Some(true)
}
}