rspack_loader_swc 0.102.2

rspack builtin swc loader
#![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()?,
    })
  }

  /// Panics:
  /// Panics if `identifier` passed in is not starting with `builtin:swc-loader`.
  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");

            // Avoid transforming the redirected server entry module to prevent duplicate RSC metadata generation.
            let server_entry_proxy = loader_context
              .resource_query()
              .is_some_and(|q| q.contains("rsc-server-entry-proxy=true"));

            // Match the RSC manifest resource key from get_module_resource: path + query.
            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),
      );
    }

    // When compiling target modules, SWC retrieves the source map via sourceMapUrl.
    // The sources paths in the source map are relative to the target module. We need to resolve these paths
    // to absolute paths using the resource path to avoid incorrect project path references.
    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")))]
    {
      // Adjust stack to avoid stack overflow.
      stacker::maybe_grow(
        2 * 1024 * 1024, /* 2mb */
        4 * 1024 * 1024, /* 4mb */
        inner,
      )
    }
    #[cfg(any(not(debug_assertions), target_family = "wasm"))]
    inner()
  }
}