rspack_plugin_wasm 0.101.5

rspack wasm plugin
Documentation
use std::borrow::Cow;

use rspack_cacheable::{cacheable, cacheable_dyn};
use rspack_collections::IdentifierIndexMap;
use rspack_core::{
  BoxDependency, BuildMetaExportsType, Dependency, DependencyId, DependencyType, ExportsArgument,
  GenerateContext, ImportPhase, Module, ModuleArgument, ModuleDependency, ModuleGraph,
  ModuleInitFragments, ParseContext, ParseResult, ParserAndGenerator, RuntimeGlobals, SourceType,
  StaticExportsDependency, StaticExportsSpec,
  rspack_sources::{BoxSource, RawStringSource, Source, SourceExt},
};
use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray};
use rspack_util::{itoa, json_stringify_str};
use swc_core::atoms::Atom;
use wasmparser::{Import, Parser, Payload};

use crate::dependency::WasmImportDependency;

#[cacheable]
#[derive(Debug)]
pub struct AsyncWasmParserAndGenerator;

pub(crate) static WASM_SOURCE_TYPE: &[SourceType; 2] = &[SourceType::Wasm, SourceType::JavaScript];
const WASM_MAGIC_HEADER: &[u8; 4] = b"\0asm";

#[derive(Debug)]
struct DepModule<'a> {
  request: &'a str,
  import_var: String,
  deps: Vec<(DependencyId, Atom)>,
}

#[cacheable_dyn]
#[async_trait::async_trait]
impl ParserAndGenerator for AsyncWasmParserAndGenerator {
  fn source_types(&self, _module: &dyn Module, _module_graph: &ModuleGraph) -> &[SourceType] {
    WASM_SOURCE_TYPE
  }

  async fn parse<'a>(
    &mut self,
    parse_context: ParseContext<'a>,
  ) -> Result<TWithDiagnosticArray<ParseResult>> {
    parse_context.build_info.strict = true;
    parse_context.build_meta.set_has_top_level_await(true);
    parse_context
      .build_meta
      .set_exports_type(BuildMetaExportsType::Namespace);

    let source = parse_context.source;

    let mut exports = Vec::with_capacity(1);
    let mut dependencies: Vec<BoxDependency> = Vec::with_capacity(1);
    let mut diagnostic = Vec::with_capacity(1);

    if parse_context.build_info.import_phase.is_source() {
      let bytes = source.buffer();
      if !bytes.starts_with(WASM_MAGIC_HEADER) {
        diagnostic.push(Diagnostic::error(
          "Wasm Parse Error".into(),
          "Source phase imports require valid WebAssembly modules. Invalid magic header (expected \\0asm).".into(),
        ));
      }
      dependencies.push(Box::new(StaticExportsDependency::new(
        StaticExportsSpec::Array(vec![Atom::from("default")]),
        false,
      )));

      return Ok(
        ParseResult {
          dependencies,
          blocks: vec![],
          presentational_dependencies: vec![],
          code_generation_dependencies: vec![],
          source,
          side_effects_bailout: None,
        }
        .with_diagnostic(diagnostic),
      );
    }

    for payload in Parser::new(0).parse_all(&source.buffer()) {
      match payload {
        Ok(payload) => match payload {
          Payload::ExportSection(s) => {
            for export in s {
              match export {
                Ok(export) => exports.push(export.name.to_string()),
                Err(err) => diagnostic.push(Diagnostic::error(
                  "Wasm Export Parse Error".into(),
                  err.to_string(),
                )),
              };
            }
          }
          Payload::ImportSection(s) => {
            for import in s {
              match import {
                Ok(Import { module, name, .. }) => {
                  dependencies.push(Box::new(WasmImportDependency::new(
                    module.into(),
                    name.into(),
                  )));
                }
                Err(err) => diagnostic.push(Diagnostic::error(
                  "Wasm Import Parse Error".into(),
                  err.to_string(),
                )),
              }
            }
          }
          _ => {}
        },
        Err(err) => {
          diagnostic.push(Diagnostic::error(
            "Wasm Parse Error".into(),
            err.to_string(),
          ));
        }
      }
    }

    dependencies.push(Box::new(StaticExportsDependency::new(
      StaticExportsSpec::Array(exports.iter().cloned().map(Atom::from).collect::<Vec<_>>()),
      false,
    )));

    Ok(
      ParseResult {
        dependencies,
        blocks: vec![],
        presentational_dependencies: vec![],
        code_generation_dependencies: vec![],
        source,
        side_effects_bailout: None,
      }
      .with_diagnostic(diagnostic),
    )
  }

  fn size(&self, module: &dyn Module, source_type: Option<&SourceType>) -> f64 {
    match source_type.unwrap_or(&SourceType::Wasm) {
      SourceType::JavaScript => {
        40.0
          + module
            .get_presentational_dependencies()
            .map_or(0.0, |i| i.len() as f64 * 10.0)
      }
      SourceType::Wasm => module.source().map_or(0, |source| source.size()) as f64,
      _ => 0.0,
    }
  }

  #[allow(clippy::unwrap_in_result)]
  async fn generate(
    &self,
    source: &BoxSource,
    module: &dyn Module,
    generate_context: &mut GenerateContext,
  ) -> Result<BoxSource> {
    let GenerateContext {
      compilation,
      runtime,
      runtime_template,
      ..
    } = generate_context;
    let hash = module
      .build_info()
      .hash
      .as_ref()
      .map(|hash| hash.rendered(16))
      .expect("should build info have hash");

    match generate_context.requested_source_type {
      SourceType::JavaScript => {
        if module.build_info().import_phase.is_source() {
          let module_argument = runtime_template.render_module_argument(ModuleArgument::Module);
          let exports_argument = runtime_template.render_exports_argument(ExportsArgument::Exports);
          let compile_call = format!(
            r#"{}({}, "{}")"#,
            runtime_template.render_runtime_globals(&RuntimeGlobals::COMPILE_WASM),
            runtime_template.render_runtime_globals(&RuntimeGlobals::MODULE_ID),
            &hash,
          );
          let source = RawStringSource::from(format!(
            r#"{}({module_argument}, async function (__rspack_handle_async_dependencies__, __rspack_async_done) {{
  try {{
    var __rspack_wasm_module__ = await {compile_call};
    {}({exports_argument}, {{}}, {{ default: __rspack_wasm_module__ }});
    __rspack_async_done();
  }} catch(e) {{ __rspack_async_done(e); }}
}}, 1);"#,
            runtime_template.render_runtime_globals(&RuntimeGlobals::ASYNC_MODULE),
            runtime_template.render_runtime_globals(&RuntimeGlobals::DEFINE_PROPERTY_GETTERS),
          ));
          return Ok(source.boxed());
        }

        let mut dep_modules = IdentifierIndexMap::<DepModule>::default();
        let mut promises: Vec<String> = vec![];

        let module_graph = &compilation.get_module_graph();

        module
          .get_dependencies()
          .iter()
          .map(|id| module_graph.dependency_by_id(id))
          .filter(|dep| dep.dependency_type() == &DependencyType::WasmImport)
          .map(|dep| {
            (
              dep,
              module_graph.module_graph_module_by_dependency_id(dep.id()),
            )
          })
          .for_each(|(dep, mgm)| {
            if let Some(mgm) = mgm {
              let dep = dep
                .as_any()
                .downcast_ref::<WasmImportDependency>()
                .expect("should be wasm import dependency");
              let request = dep.request();
              let len = dep_modules.len();
              let dep_module = dep_modules.entry(mgm.module_identifier).or_insert_with(|| {
                let mut len_buffer = itoa::Buffer::new();
                let len_str = len_buffer.format(len);
                let import_var = format!("rspack_import_{len_str}");
                if ModuleGraph::is_async(
                  &compilation.async_modules_artifact,
                  &mgm.module_identifier,
                ) {
                  promises.push(import_var.clone());
                }
                DepModule {
                  request,
                  import_var,
                  deps: vec![],
                }
              });
              dep_module.deps.push((*dep.id(), dep.name().clone()));
            }
          });

        let (imports_code, imports_compat_code): (Vec<String>, Vec<String>) = dep_modules
          .iter()
          .map(|(_, dep_module)| {
            runtime_template.import_statement(
              module,
              compilation,
              &dep_module.deps[0].0,
              &dep_module.import_var,
              dep_module.request,
              ImportPhase::Evaluation,
              false,
            )
          })
          .unzip();
        let imports_code = imports_code.join("");
        let imports_compat_code = imports_compat_code.join("");

        let import_obj_request_items = dep_modules
          .into_values()
          .map(|dep_module| {
            let deps = dep_module
              .deps
              .into_iter()
              .map(|(dep_id, export_name)| {
                let export = runtime_template.export_from_import(
                  compilation,
                  &mut ModuleInitFragments::default(),
                  module.identifier(),
                  *runtime,
                  true,
                  dep_module.request,
                  &dep_module.import_var,
                  std::slice::from_ref(&export_name),
                  &dep_id,
                  false,
                  false,
                  Some(true),
                  ImportPhase::Evaluation,
                );
                let name = json_stringify_str(&export_name);
                format!("{name}: {export}")
              })
              .collect::<Vec<_>>()
              .join(",\n");

            format!("{}: {{\n{deps}\n}}", json_stringify_str(dep_module.request))
          })
          .collect::<Vec<_>>();

        let imports_obj = if !import_obj_request_items.is_empty() {
          Some(format!(
            ", {{\n{}\n}}",
            &import_obj_request_items.join(",\n")
          ))
        } else {
          None
        };

        let module_argument = runtime_template.render_module_argument(ModuleArgument::Module);
        let exports_argument = runtime_template.render_exports_argument(ExportsArgument::Exports);
        let instantiate_call = format!(
          r#"{}({exports_argument}, {}, "{}" {})"#,
          runtime_template.render_runtime_globals(&RuntimeGlobals::INSTANTIATE_WASM),
          runtime_template.render_runtime_globals(&RuntimeGlobals::MODULE_ID),
          &hash,
          imports_obj.unwrap_or_default()
        );

        let source = if !promises.is_empty() {
          let promises = promises.join(", ");
          let decl = format!(
            "var __rspack_instantiate__ = function ([{promises}]) {{\n{imports_compat_code}return {instantiate_call};\n}}\n",
          );
          let async_dependencies = format!(
"{}({module_argument}, async function (__rspack_load_async_deps, __rspack_async_done) {{
  try {{
{imports_code}
    var __rspack_async_deps = __rspack_load_async_deps([{promises}]);
    var [{promises}] = __rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps;
    {imports_compat_code}await {instantiate_call};

  __rspack_async_done();

  }} catch(e) {{ __rspack_async_done(e); }}
}}, 1);
",
            runtime_template.render_runtime_globals(&RuntimeGlobals::ASYNC_MODULE),
          );

          RawStringSource::from(format!("{decl}{async_dependencies}"))
        } else {
          RawStringSource::from(format!(
            "{imports_code}{imports_compat_code}{module_argument}.exports = {instantiate_call};"
          ))
        };

        Ok(source.boxed())
      }
      _ => Ok(source.clone()),
    }
  }

  fn get_concatenation_bailout_reason(
    &self,
    _module: &dyn Module,
    _mg: &rspack_core::ModuleGraph,
    _cg: &rspack_core::ChunkGraph,
  ) -> Option<Cow<'static, str>> {
    Some("Module Concatenation is not implemented for AsyncWasmParserAndGenerator".into())
  }
}