#![feature(box_patterns)]
mod collect_ts_info;
mod isolated_dts;
mod options;
mod plugin;
mod rsc_transforms;
mod transformer;
use std::{cell::RefCell, default::Default, path::Path, rc::Rc, sync::Arc};
use isolated_dts::{handle_isolated_dts_diagnostics, set_build_info};
use options::SwcCompilerOptionsWithAdditional;
pub use options::SwcLoaderJsOptions;
pub use plugin::SwcLoaderPlugin;
use rspack_cacheable::{cacheable, cacheable_dyn};
use rspack_core::{COLLECTED_TYPESCRIPT_INFO_PARSE_META_KEY, Mode, Module, RscMeta, RunnerContext};
use rspack_error::{Diagnostic, Error, Result, SerdeResultToRspackResultExt};
use rspack_javascript_compiler::{JavaScriptCompiler, TransformOutput};
use rspack_loader_runner::{Identifier, Loader, LoaderContext};
#[cfg(allocative)]
use rspack_util::allocative;
pub use rspack_workspace::rspack_swc_core_version;
use sugar_path::SugarPath;
use swc_config::{merge::Merge, types::MergingOption};
use swc_core::{
base::config::{InputSourceMap, TransformConfig},
common::{FileName, SyntaxContext, comments::SingleThreadedComments},
ecma::ast::{EsVersion, noop_pass},
};
use crate::{
collect_ts_info::collect_typescript_info,
rsc_transforms::{RscTransformOptions, rsc_transform, to_server_entry},
};
#[cacheable]
#[derive(Debug)]
#[cfg_attr(allocative, derive(allocative::Allocative))]
pub struct SwcLoader {
identifier: Identifier,
#[cfg_attr(allocative, allocative(skip))]
options_with_additional: SwcCompilerOptionsWithAdditional,
}
impl SwcLoader {
pub fn new(raw_options: &str) -> Result<Self, serde_json::Error> {
Ok(Self {
identifier: SWC_LOADER_IDENTIFIER.into(),
options_with_additional: raw_options.try_into()?,
})
}
pub fn with_identifier(mut self, identifier: Identifier) -> Self {
assert!(identifier.starts_with(SWC_LOADER_IDENTIFIER));
self.identifier = identifier;
self
}
fn loader_impl(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
let resource_path = loader_context
.resource_path()
.map(|p| p.to_path_buf())
.unwrap_or_default();
loader_context.context.module.build_info_mut().isolated_dts = None;
let Some(content) = loader_context.take_content() else {
return Ok(());
};
let swc_options = {
let mut swc_options = self.options_with_additional.swc_options.clone();
if let Some(resource_specific_jsc) = self
.options_with_additional
.parse_resource_specific_jsc(&resource_path)
.to_rspack_result_with_detail(
self.options_with_additional.raw_options(),
"failed to parse builtin:swc-loader options",
)?
{
swc_options.config.jsc = resource_specific_jsc;
}
if swc_options.config.jsc.transform.as_ref().is_some() {
let mut transform = TransformConfig::default();
transform.react.development =
Some(Mode::is_development(&loader_context.context.options.mode));
swc_options
.config
.jsc
.transform
.merge(MergingOption::from(Some(transform)));
}
if loader_context.context.source_map_kind.enabled() {
if let Some(pre_source_map) = loader_context.source_map() {
swc_options.config.input_source_map = Some(InputSourceMap::Str(pre_source_map.to_json()))
}
} else {
swc_options.config.input_source_map = Some(InputSourceMap::Bool(false));
}
swc_options.filename = resource_path.as_str().to_string();
swc_options.source_file_name = Some(resource_path.as_str().to_string());
if swc_options.config.jsc.target.is_some() && swc_options.config.env.is_some() {
loader_context.emit_diagnostic(Diagnostic::warn(
SWC_LOADER_IDENTIFIER.to_string(),
"`env` and `jsc.target` cannot be used together".to_string(),
));
}
#[cfg(feature = "plugin")]
{
swc_options.runtime_options =
swc_options
.runtime_options
.plugin_runtime(std::sync::Arc::new(
rspack_util::swc::runtime::WasmtimeRuntime,
));
}
swc_options
};
let javascript_compiler = JavaScriptCompiler::new();
let filename = Arc::new(FileName::Real(resource_path.clone().into_std_path_buf()));
let comments = Rc::new(SingleThreadedComments::default());
let source = content.into_string_lossy();
let is_typescript =
matches!(swc_options.config.jsc.syntax, Some(syntax) if syntax.typescript());
let isolated_dts_context = (is_typescript
&& swc_options
.config
.jsc
.experimental
.emit_isolated_dts
.into_bool())
.then(|| {
(
swc_options.config.jsc.target.unwrap_or(EsVersion::EsNext),
filename.clone(),
comments.clone(),
)
});
let mut isolated_dts = Ok(None);
let mut collected_ts_info = None;
let rsc_meta: RefCell<Option<RscMeta>> = Default::default();
let TransformOutput {
code,
mut map,
diagnostics,
} = javascript_compiler.transform(
source,
Some(filename.clone()),
comments.clone(),
swc_options,
Some(loader_context.context.source_map_kind),
|program, unresolved_mark| {
if !is_typescript {
return;
}
if let Some((target, dts_filename, dts_comments)) = &isolated_dts_context {
isolated_dts = javascript_compiler
.emit_isolated_dts(
program,
dts_filename.clone(),
unresolved_mark,
*target,
dts_comments,
)
.map(Some);
}
let Some(options) = &self.options_with_additional.collect_typescript_info else {
return;
};
collected_ts_info = Some(collect_typescript_info(
program,
SyntaxContext::empty().apply_mark(unresolved_mark),
options,
));
},
|_| {
(
if self
.options_with_additional
.rspack_experiments
.react_server_components
.enabled
{
let module = &loader_context.context.module;
let is_react_server_layer = module
.get_layer()
.is_some_and(|layer| layer == "react-server-components");
let server_entry_proxy = loader_context
.resource_query()
.is_some_and(|q| q.contains("rsc-server-entry-proxy=true"));
let module_resource = match loader_context.resource_query() {
Some(query) => format!("{resource_path}{query}"),
None => resource_path.to_string(),
};
let disable_client_api_checks = self
.options_with_additional
.rspack_experiments
.react_server_components
.disable_client_api_checks;
swc_core::common::pass::Either::Left(rsc_transform(
filename,
resource_path.to_string(),
module_resource,
comments,
&rsc_meta,
RscTransformOptions {
is_react_server_layer,
enable_server_entry: !server_entry_proxy,
disable_client_api_checks,
is_development: Mode::is_development(&loader_context.context.options.mode),
hash_salt: String::new(),
},
))
} else {
swc_core::common::pass::Either::Right(noop_pass())
},
transformer::transform(
&self.options_with_additional.transform_import,
&self.options_with_additional.rspack_experiments,
),
)
},
)?;
for diagnostic in diagnostics {
loader_context.emit_diagnostic(Error::warning(diagnostic).into());
}
if let Some(isolated_dts) = isolated_dts? {
handle_isolated_dts_diagnostics(isolated_dts.diagnostics)?;
set_build_info(
loader_context.context.module.build_info_mut(),
resource_path.as_path(),
loader_context.context.options.context.as_path(),
isolated_dts.code,
isolated_dts.references,
);
}
if let Some(rsc) = rsc_meta.borrow_mut().take() {
let module = &mut loader_context.context.module;
module.build_info_mut().rsc = Some(rsc);
if let Some(code) = to_server_entry(module)? {
loader_context.finish_with(code);
return Ok(());
}
}
if let Some(collected_ts_info) = collected_ts_info {
loader_context.parse_meta.insert(
COLLECTED_TYPESCRIPT_INFO_PARSE_META_KEY.to_string(),
Box::new(collected_ts_info),
);
}
if let (Some(map), Some(resource_dir)) = (map.as_mut(), resource_path.parent()) {
map.set_sources(
map
.sources()
.iter()
.map(|source| {
let source_path = Path::new(source.as_ref());
if source_path.is_relative() {
source_path
.absolutize_with(resource_dir.as_std_path())
.to_string_lossy()
.into_owned()
} else {
source.to_string()
}
})
.collect::<Vec<_>>(),
);
}
let additional_data = loader_context.take_additional_data();
loader_context.finish_with((code, map, additional_data));
Ok(())
}
}
pub const SWC_LOADER_IDENTIFIER: &str = "builtin:swc-loader";
#[cacheable_dyn]
#[async_trait::async_trait]
impl Loader<RunnerContext> for SwcLoader {
fn identifier(&self) -> Identifier {
self.identifier
}
#[tracing::instrument("loader:builtin-swc", skip_all, fields(
perfetto.track_name = "loader:builtin-swc",
perfetto.process_name = "Loader Analysis",
resource =loader_context.resource(),
))]
async fn run(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
#[allow(unused_mut)]
let mut inner = || self.loader_impl(loader_context);
#[cfg(all(debug_assertions, not(target_family = "wasm")))]
{
stacker::maybe_grow(
2 * 1024 * 1024,
4 * 1024 * 1024,
inner,
)
}
#[cfg(any(not(debug_assertions), target_family = "wasm"))]
inner()
}
}