diff --git a/.changeset/ninety-squids-ring.md b/.changeset/ninety-squids-ring.md new file mode 100644 index 000000000000..b25b5f8b500c --- /dev/null +++ b/.changeset/ninety-squids-ring.md @@ -0,0 +1,7 @@ +--- +"@rspack/binding": patch +"@rspack/core": patch +"@rspack/cli": patch +--- + +add async-wasm & js-async-module support diff --git a/Cargo.lock b/Cargo.lock index ee64867dc5c4..4a5ad634fe52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2427,6 +2427,7 @@ dependencies = [ "rspack_plugin_remove_empty_chunks", "rspack_plugin_runtime", "rspack_plugin_split_chunks", + "rspack_plugin_wasm", "rspack_regex", "serde", "serde_json", @@ -2884,6 +2885,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "rspack_plugin_wasm" +version = "0.1.0" +dependencies = [ + "async-trait", + "dashmap", + "rayon", + "rspack_core", + "rspack_error", + "rspack_identifier", + "rspack_plugin_asset", + "rspack_plugin_runtime", + "rspack_testing", + "rspack_util", + "rustc-hash", + "serde_json", + "sugar_path", + "tracing", + "wasmparser", +] + [[package]] name = "rspack_regex" version = "0.1.0" @@ -2948,6 +2970,7 @@ dependencies = [ "rspack_plugin_progress", "rspack_plugin_remove_empty_chunks", "rspack_plugin_runtime", + "rspack_plugin_wasm", "rspack_regex", "rspack_tracing", "schemars", @@ -5095,6 +5118,16 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasmparser" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" +dependencies = [ + "indexmap", + "url", +] + [[package]] name = "web-sys" version = "0.3.60" diff --git a/crates/node_binding/Cargo.lock b/crates/node_binding/Cargo.lock index 782b0136d071..7fb88b54423d 100644 --- a/crates/node_binding/Cargo.lock +++ b/crates/node_binding/Cargo.lock @@ -2021,6 +2021,7 @@ dependencies = [ "rspack_plugin_remove_empty_chunks", "rspack_plugin_runtime", "rspack_plugin_split_chunks", + "rspack_plugin_wasm", "rspack_regex", "serde", "serde_json", @@ -2479,6 +2480,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "rspack_plugin_wasm" +version = "0.1.0" +dependencies = [ + "async-trait", + "dashmap", + "rayon", + "rspack_core", + "rspack_error", + "rspack_identifier", + "rspack_plugin_asset", + "rspack_plugin_runtime", + "rspack_util", + "rustc-hash", + "serde_json", + "sugar_path", + "tracing", + "wasmparser", +] + [[package]] name = "rspack_regex" version = "0.1.0" @@ -4525,6 +4546,16 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasmparser" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" +dependencies = [ + "indexmap", + "url", +] + [[package]] name = "which" version = "4.3.0" diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 6c91cd5d0467..879a171c12bb 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -160,6 +160,7 @@ export interface RawEntryItem { export interface RawExperiments { lazyCompilation: boolean incrementalRebuild: boolean + asyncWebAssembly: boolean } export interface RawExternalItem { type: "string" | "regexp" | "object" @@ -303,6 +304,7 @@ export interface RawOutputOptions { path: string publicPath: string assetModuleFilename: string + webassemblyModuleFilename: string filename: string chunkFilename: string cssFilename: string diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index 9a3a3bc90b70..23f0892b774f 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -43,6 +43,7 @@ rspack_plugin_progress = { path = "../rspack_plugin_progress" } rspack_plugin_remove_empty_chunks = { path = "../rspack_plugin_remove_empty_chunks" } rspack_plugin_runtime = { path = "../rspack_plugin_runtime" } rspack_plugin_split_chunks = { path = "../rspack_plugin_split_chunks" } +rspack_plugin_wasm = { path = "../rspack_plugin_wasm" } rspack_regex = { path = "../rspack_regex" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/rspack_binding_options/src/options/mod.rs b/crates/rspack_binding_options/src/options/mod.rs index 205245195e26..aaf0cf73c90b 100644 --- a/crates/rspack_binding_options/src/options/mod.rs +++ b/crates/rspack_binding_options/src/options/mod.rs @@ -146,7 +146,12 @@ impl RawOptionsApply for RawOptions { if self.externals_presets.node { plugins.push(rspack_plugin_externals::node_target_plugin()); } + if experiments.async_web_assembly { + plugins.push(rspack_plugin_wasm::AsyncWasmPlugin::new().boxed()); + plugins.push(rspack_plugin_wasm::FetchCompileAsyncWasmPlugin {}.boxed()); + } plugins.push(rspack_plugin_javascript::JsPlugin::new().boxed()); + plugins.push(rspack_plugin_javascript::InferAsyncModulesPlugin {}.boxed()); plugins.push( rspack_plugin_devtool::DevtoolPlugin::new(rspack_plugin_devtool::DevtoolPluginOptions { inline: devtool.inline(), diff --git a/crates/rspack_binding_options/src/options/raw_experiments.rs b/crates/rspack_binding_options/src/options/raw_experiments.rs index f22444737eac..59533acc34cb 100644 --- a/crates/rspack_binding_options/src/options/raw_experiments.rs +++ b/crates/rspack_binding_options/src/options/raw_experiments.rs @@ -8,6 +8,7 @@ use serde::Deserialize; pub struct RawExperiments { pub lazy_compilation: bool, pub incremental_rebuild: bool, + pub async_web_assembly: bool, } impl From for Experiments { @@ -15,6 +16,7 @@ impl From for Experiments { Self { lazy_compilation: value.lazy_compilation, incremental_rebuild: value.incremental_rebuild, + async_web_assembly: value.async_web_assembly, } } } diff --git a/crates/rspack_binding_options/src/options/raw_output.rs b/crates/rspack_binding_options/src/options/raw_output.rs index bb9985352878..e1e96964083f 100644 --- a/crates/rspack_binding_options/src/options/raw_output.rs +++ b/crates/rspack_binding_options/src/options/raw_output.rs @@ -77,6 +77,7 @@ pub struct RawOutputOptions { pub path: String, pub public_path: String, pub asset_module_filename: String, + pub webassembly_module_filename: String, pub filename: String, pub chunk_filename: String, pub css_filename: String, @@ -101,6 +102,7 @@ impl RawOptionsApply for RawOutputOptions { path: self.path.into(), public_path: self.public_path.into(), asset_module_filename: self.asset_module_filename.into(), + webassembly_module_filename: self.webassembly_module_filename.into(), unique_name: self.unique_name, filename: self.filename.into(), chunk_filename: self.chunk_filename.into(), diff --git a/crates/rspack_core/src/dependency/mod.rs b/crates/rspack_core/src/dependency/mod.rs index 0136433ef970..6670fb1e29de 100644 --- a/crates/rspack_core/src/dependency/mod.rs +++ b/crates/rspack_core/src/dependency/mod.rs @@ -15,6 +15,8 @@ use std::{any::Any, fmt::Debug, hash::Hash}; use dyn_clone::{clone_trait_object, DynClone}; pub use require_context_dependency::RequireContextDependency; +mod static_exports_dependency; +pub use static_exports_dependency::*; use crate::{ AsAny, ContextMode, ContextOptions, DynEq, DynHash, ErrorSpan, ModuleGraph, ModuleIdentifier, @@ -59,6 +61,12 @@ pub enum DependencyType { CommonJSRequireContext, // require.context RequireContext, + /// wasm import + WasmImport, + /// wasm export import + WasmExportImported, + /// static exports + StaticExports, } #[derive(Default, Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -70,6 +78,7 @@ pub enum DependencyCategory { Url, CssImport, CssCompose, + Wasm, } pub trait Dependency: diff --git a/crates/rspack_core/src/dependency/static_exports_dependency.rs b/crates/rspack_core/src/dependency/static_exports_dependency.rs new file mode 100644 index 000000000000..faed858ee43b --- /dev/null +++ b/crates/rspack_core/src/dependency/static_exports_dependency.rs @@ -0,0 +1,65 @@ +use crate::{ + CodeGeneratable, CodeGeneratableContext, CodeGeneratableResult, Dependency, DependencyCategory, + DependencyId, DependencyType, ErrorSpan, ModuleDependency, ModuleIdentifier, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StaticExportsDependency { + pub id: Option, + exports: Vec, + can_mangle: bool, + + request: String, +} +impl StaticExportsDependency { + pub fn new(exports: Vec, can_mangle: bool) -> Self { + Self { + id: None, + request: "".to_string(), + exports, + can_mangle, + } + } +} + +impl Dependency for StaticExportsDependency { + fn id(&self) -> Option { + self.id + } + fn set_id(&mut self, id: Option) { + self.id = id; + } + fn parent_module_identifier(&self) -> Option<&ModuleIdentifier> { + None + } + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Unknown + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::StaticExports + } +} + +impl ModuleDependency for StaticExportsDependency { + fn request(&self) -> &str { + &self.request + } + + fn user_request(&self) -> &str { + &self.request + } + + fn span(&self) -> Option<&ErrorSpan> { + None + } +} + +impl CodeGeneratable for StaticExportsDependency { + fn generate( + &self, + _code_generatable_context: &mut CodeGeneratableContext, + ) -> rspack_error::Result { + todo!() + } +} diff --git a/crates/rspack_core/src/lib.rs b/crates/rspack_core/src/lib.rs index ceee70375c7c..658b6c43c6f3 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -76,6 +76,7 @@ pub use rspack_sources; pub enum SourceType { JavaScript, Css, + Wasm, Asset, #[default] Unknown, @@ -94,6 +95,8 @@ pub enum ModuleType { JsxEsm, Tsx, Ts, + WasmSync, + WasmAsync, AssetInline, AssetResource, AssetSource, @@ -124,6 +127,10 @@ impl ModuleType { ModuleType::Tsx | ModuleType::Jsx | ModuleType::JsxEsm | ModuleType::JsxDynamic ) } + + pub fn is_wasm_like(&self) -> bool { + matches!(self, ModuleType::WasmSync | ModuleType::WasmAsync) + } } impl fmt::Display for ModuleType { @@ -148,6 +155,9 @@ impl fmt::Display for ModuleType { ModuleType::Json => "json", + ModuleType::WasmSync => "webassembly/sync", + ModuleType::WasmAsync => "webassembly/async", + ModuleType::Asset => "asset", ModuleType::AssetSource => "asset/source", ModuleType::AssetResource => "asset/resource", @@ -180,6 +190,9 @@ impl TryFrom<&str> for ModuleType { "json" => Ok(Self::Json), + "webassembly/sync" => Ok(Self::WasmSync), + "webassembly/async" => Ok(Self::WasmAsync), + "asset" => Ok(Self::Asset), "asset/resource" => Ok(Self::AssetResource), "asset/source" => Ok(Self::AssetSource), diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index e97b436bbb15..fe04c253f5b2 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -34,6 +34,7 @@ pub struct BuildInfo { #[derive(Debug, Default, Clone)] pub struct BuildMeta { pub strict_harmony_module: bool, + pub is_async: bool, // TODO webpack exportsType pub esm: bool, } diff --git a/crates/rspack_core/src/module_graph/mod.rs b/crates/rspack_core/src/module_graph/mod.rs index d1326917ead6..59276b6ca29a 100644 --- a/crates/rspack_core/src/module_graph/mod.rs +++ b/crates/rspack_core/src/module_graph/mod.rs @@ -304,6 +304,29 @@ impl ModuleGraph { .and_then(|mgm| mgm.get_issuer().get_module(self)) } + pub fn is_async(&self, module: &ModuleIdentifier) -> bool { + self + .module_graph_module_by_identifier(module) + .map(|mgm| { + mgm + .build_meta + .as_ref() + .expect("build_meta should be initialized") + .is_async + }) + .unwrap_or_default() + } + + pub fn set_async(&mut self, module: &ModuleIdentifier) { + if let Some(mgm) = self.module_graph_module_by_identifier_mut(module) { + mgm + .build_meta + .as_mut() + .expect("build_meta should be initialized") + .is_async = true; + } + } + pub fn get_outgoing_connections(&self, module: &BoxModule) -> HashSet<&ModuleGraphConnection> { self .module_graph_module_by_identifier(&module.identifier()) diff --git a/crates/rspack_core/src/options/experiments.rs b/crates/rspack_core/src/options/experiments.rs index 698683da7cbd..bdd14f6f9012 100644 --- a/crates/rspack_core/src/options/experiments.rs +++ b/crates/rspack_core/src/options/experiments.rs @@ -2,4 +2,5 @@ pub struct Experiments { pub lazy_compilation: bool, pub incremental_rebuild: bool, + pub async_web_assembly: bool, } diff --git a/crates/rspack_core/src/options/output.rs b/crates/rspack_core/src/options/output.rs index 3a53d8d1f6ea..ba85020355ac 100644 --- a/crates/rspack_core/src/options/output.rs +++ b/crates/rspack_core/src/options/output.rs @@ -15,6 +15,7 @@ pub struct OutputOptions { pub path: PathBuf, pub public_path: PublicPath, pub asset_module_filename: Filename, + pub webassembly_module_filename: Filename, pub unique_name: String, //todo we are not going to support file_name & chunk_file_name as function in the near feature pub filename: Filename, diff --git a/crates/rspack_core/src/runtime_globals.rs b/crates/rspack_core/src/runtime_globals.rs index e9c19c9b6345..6b59965d8daf 100644 --- a/crates/rspack_core/src/runtime_globals.rs +++ b/crates/rspack_core/src/runtime_globals.rs @@ -152,3 +152,22 @@ pub const GLOBAL: &str = "__webpack_require__.g"; * runtime need to return the exports of the last entry module */ pub const RETURN_EXPORTS_FROM_RUNTIME: &str = "return-exports-from-runtime"; + +/** + * instantiate a wasm instance from module exports object, id, hash and importsObject + */ +pub const INSTANTIATE_WASM: &str = "__webpack_require__.v"; + +/** + * Creates an async module. The body function must be a async function. + * "module.exports" will be decorated with an AsyncModulePromise. + * The body function will be called. + * To handle async dependencies correctly do this: "([a, b, c] = await handleDependencies([a, b, c]));". + * If "hasAwaitAfterDependencies" is truthy, "handleDependencies()" must be called at the end of the body function. + * Signature: function( + * module: Module, + * body: (handleDependencies: (deps: AsyncModulePromise[]) => Promise & () => void, + * hasAwaitAfterDependencies?: boolean + * ) => void + */ +pub const ASYNC_MODULE: &str = "__webpack_require__.a"; diff --git a/crates/rspack_loader_sass/tests/fixtures.rs b/crates/rspack_loader_sass/tests/fixtures.rs index 0b43682b42a1..71986b789afd 100644 --- a/crates/rspack_loader_sass/tests/fixtures.rs +++ b/crates/rspack_loader_sass/tests/fixtures.rs @@ -45,6 +45,7 @@ async fn loader_test(actual: impl AsRef, expected: impl AsRef) { public_path: Default::default(), filename: rspack_core::Filename::from_str("").expect("TODO:"), asset_module_filename: rspack_core::Filename::from_str("").expect("TODO:"), + webassembly_module_filename: rspack_core::Filename::from_str("").expect("TODO:"), chunk_filename: rspack_core::Filename::from_str("").expect("TODO:"), unique_name: Default::default(), css_chunk_filename: rspack_core::Filename::from_str("").expect("TODO:"), diff --git a/crates/rspack_plugin_javascript/src/plugin.rs b/crates/rspack_plugin_javascript/src/plugin.rs index e9f91bda4bab..5c1321aee373 100644 --- a/crates/rspack_plugin_javascript/src/plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin.rs @@ -1,22 +1,25 @@ +use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::sync::mpsc; use async_trait::async_trait; +use linked_hash_set::LinkedHashSet; use rayon::prelude::*; use rspack_core::rspack_sources::{ BoxSource, ConcatSource, MapOptions, RawSource, Source, SourceExt, SourceMap, SourceMapSource, SourceMapSourceOptions, }; use rspack_core::{ - get_js_chunk_filename_template, runtime_globals, AstOrSource, ChunkKind, GenerateContext, - GenerationResult, Module, ModuleAst, ModuleType, ParseContext, ParseResult, ParserAndGenerator, - PathData, Plugin, PluginContext, PluginProcessAssetsOutput, PluginRenderManifestHookOutput, - ProcessAssetsArgs, RenderArgs, RenderChunkArgs, RenderManifestEntry, RenderStartupArgs, - SourceType, + get_js_chunk_filename_template, runtime_globals, AstOrSource, ChunkKind, Compilation, + DependencyType, GenerateContext, GenerationResult, Module, ModuleAst, ModuleType, ParseContext, + ParseResult, ParserAndGenerator, PathData, Plugin, PluginContext, PluginProcessAssetsOutput, + PluginRenderManifestHookOutput, ProcessAssetsArgs, RenderArgs, RenderChunkArgs, + RenderManifestEntry, RenderStartupArgs, SourceType, }; use rspack_error::{ internal_error, Diagnostic, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray, }; +use rspack_identifier::Identifier; use swc_core::base::{config::JsMinifyOptions, BoolOrDataConfig}; use swc_core::common::util::take::Take; use swc_core::ecma::ast; @@ -584,3 +587,60 @@ impl Plugin for JsPlugin { Ok(()) } } + +#[derive(Debug)] +pub struct InferAsyncModulesPlugin; + +#[async_trait::async_trait] +impl Plugin for InferAsyncModulesPlugin { + fn name(&self) -> &'static str { + "InferAsyncModulesPlugin" + } + + async fn finish_modules(&mut self, compilation: &mut Compilation) -> Result<()> { + // fix: mut for-in + let mut queue = LinkedHashSet::new(); + let mut uniques = HashSet::new(); + + let mut modules: Vec = compilation + .module_graph + .module_graph_modules() + .values() + .filter(|m| { + if let Some(meta) = &m.build_meta { + meta.is_async + } else { + false + } + }) + .map(|m| m.module_identifier) + .collect(); + + modules.retain(|m| queue.insert(*m)); + + let module_graph = &mut compilation.module_graph; + + while let Some(module) = queue.pop_front() { + module_graph.set_async(&module); + if let Some(mgm) = module_graph.module_graph_module_by_identifier(&module) { + mgm + .incoming_connections_unordered(module_graph)? + .filter(|con| { + if let Some(dep) = module_graph.dependency_by_id(&con.dependency_id) { + *dep.dependency_type() == DependencyType::EsmImport + } else { + false + } + }) + .for_each(|con| { + if let Some(id) = &con.original_module_identifier { + if uniques.insert(*id) { + queue.insert(*id); + } + } + }); + } + } + Ok(()) + } +} diff --git a/crates/rspack_plugin_javascript/src/runtime.rs b/crates/rspack_plugin_javascript/src/runtime.rs index 016fa1d6f2c8..8c43e1e3e82d 100644 --- a/crates/rspack_plugin_javascript/src/runtime.rs +++ b/crates/rspack_plugin_javascript/src/runtime.rs @@ -61,9 +61,19 @@ pub fn render_chunk_modules( .as_ref() .map(|m| m.strict) .unwrap_or_default(); + let is_async = mgm + .build_meta + .as_ref() + .map(|m| m.is_async) + .unwrap_or_default(); ( mgm.module_identifier, - render_module(module_source, strict, mgm.id(&compilation.chunk_graph)), + render_module( + module_source, + strict, + is_async, + mgm.id(&compilation.chunk_graph), + ), ) }) .collect::>(); @@ -86,7 +96,12 @@ pub fn render_chunk_modules( Ok(sources.boxed()) } -pub fn render_module(source: BoxSource, strict: bool, module_id: &str) -> BoxSource { +pub fn render_module( + source: BoxSource, + strict: bool, + is_async: bool, + module_id: &str, +) -> BoxSource { let mut sources = ConcatSource::new([ RawSource::from("\""), RawSource::from(module_id.to_string()), @@ -99,7 +114,20 @@ pub fn render_module(source: BoxSource, strict: bool, module_id: &str) -> BoxSou if strict { sources.add(RawSource::from("\"use strict\";\n")); } + if is_async { + sources.add(RawSource::from( + format!("{}(module, async function (__webpack_handle_async_dependencies__, __webpack_async_result__) {{ try {{\n" + ,runtime_globals::ASYNC_MODULE) )); + } + sources.add(source); + + if is_async { + sources.add(RawSource::from( + "\n__webpack_async_result__();\n} catch(e) { __webpack_async_result__(e); } });", + )); + } + sources.add(RawSource::from("},\n")); sources.boxed() diff --git a/crates/rspack_plugin_javascript/src/visitors/async_module.rs b/crates/rspack_plugin_javascript/src/visitors/async_module.rs new file mode 100644 index 000000000000..36b3d9673346 --- /dev/null +++ b/crates/rspack_plugin_javascript/src/visitors/async_module.rs @@ -0,0 +1,171 @@ +use std::collections::LinkedList; + +use swc_core::common::DUMMY_SP; +use swc_core::ecma::ast::{ + ArrayLit, ArrayPat, AssignExpr, AssignOp, AwaitExpr, BindingIdent, CallExpr, Callee, CondExpr, + Decl, Expr, ExprOrSpread, ExprStmt, Ident, MemberExpr, MemberProp, ParenExpr, Pat, PatOrExpr, + Stmt, VarDecl, +}; +use swc_core::ecma::ast::{VarDeclKind, VarDeclarator}; +use swc_core::ecma::atoms::JsWord; +use swc_core::ecma::{ + ast::ModuleItem, + visit::{as_folder, noop_visit_mut_type, Fold, VisitMut}, +}; + +pub fn build_async_module<'a>(promises: LinkedList) -> impl Fold + 'a { + as_folder(AsyncModuleVisitor { promises }) +} + +struct AsyncModuleVisitor { + promises: LinkedList, +} + +impl VisitMut for AsyncModuleVisitor { + noop_visit_mut_type!(); + + fn visit_mut_module_items(&mut self, items: &mut Vec) { + let mut args = vec![]; + let mut elems = vec![]; + + let last_import = items + .iter() + .enumerate() + .skip_while(|(_, item)| !matches!(item, ModuleItem::Stmt(Stmt::Decl(Decl::Var(_))))) + .take_while(|(_, item)| matches!(item, ModuleItem::Stmt(Stmt::Decl(Decl::Var(_))))) + .map(|(i, item)| { + if let Some(is_async) = self.promises.pop_front() && is_async { + if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var))) = item { + // Safety, after swc(variable hoisting) & treeshaking(remove unused import) + let decl = unsafe { var.decls.get_unchecked(0) }; + let var_name = decl.name.as_ident().expect("should be ok").sym.to_string(); + args.push(make_arg(&var_name)); + elems.push(make_elem(&var_name)); + } + } + i + }) + .last() + .map(|i| i + 1); + if let Some(index) = last_import { + let item = vec![make_var(args), make_stmt(elems)]; + items.splice(index..index, item); + } + } +} + +fn make_arg(arg: &str) -> Option { + Some(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from(arg), + optional: false, + })), + }) +} + +fn make_elem(elem: &str) -> Option { + Some(Pat::Ident(BindingIdent { + id: Ident { + span: DUMMY_SP, + sym: JsWord::from(elem), + optional: false, + }, + type_ann: None, + })) +} + +fn make_var(elems: Vec>) -> ModuleItem { + let call_expr = CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from("__webpack_handle_async_dependencies__"), + optional: false, + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::from(Expr::Array(ArrayLit { + span: DUMMY_SP, + elems, + })), + }], + type_args: None, + }; + + let decl = VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: Ident { + span: DUMMY_SP, + sym: JsWord::from("__webpack_async_dependencies__"), + optional: false, + }, + type_ann: None, + }), + init: Some(Box::from(call_expr)), + definite: false, + }; + + ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::from(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: vec![decl], + })))) +} +fn make_stmt(elems: Vec>) -> ModuleItem { + let assign = AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Pat(Box::from(ArrayPat { + span: DUMMY_SP, + elems, + optional: false, + type_ann: None, + })), + right: Box::new(Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from("__webpack_async_dependencies__"), + optional: false, + })), + prop: MemberProp::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from("then"), + optional: false, + }), + })), + cons: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::from(Expr::Await(AwaitExpr { + span: DUMMY_SP, + arg: Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from("__webpack_async_dependencies__"), + optional: false, + })), + }))), + args: vec![], + type_args: None, + })), + alt: Box::new(Expr::Ident(Ident { + span: DUMMY_SP, + sym: JsWord::from("__webpack_async_dependencies__"), + optional: false, + })), + })), + }; + + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: Box::from(Expr::Assign(assign)), + })), + })) +} diff --git a/crates/rspack_plugin_javascript/src/visitors/mod.rs b/crates/rspack_plugin_javascript/src/visitors/mod.rs index d12b449b6765..73ce1e0629f3 100644 --- a/crates/rspack_plugin_javascript/src/visitors/mod.rs +++ b/crates/rspack_plugin_javascript/src/visitors/mod.rs @@ -1,4 +1,6 @@ mod dependency; +use std::collections::LinkedList; + pub use dependency::*; mod finalize; use either::Either; @@ -13,7 +15,7 @@ mod strict; use strict::strict_mode; mod format; use format::*; -use rspack_core::{BuildInfo, EsVersion, Module, ModuleType}; +use rspack_core::{runtime_globals, BuildInfo, EsVersion, Module, ModuleType}; use swc_core::common::pass::Repeat; use swc_core::ecma::transforms::base::Assumptions; use swc_core::ecma::transforms::module::util::ImportInterop; @@ -32,7 +34,9 @@ use swc_core::ecma::transforms::base::pass::{noop, Optional}; use swc_core::ecma::transforms::module::common_js::Config as CommonjsConfig; use swc_emotion::EmotionOptions; use tree_shaking::tree_shaking_visitor; +mod async_module; +use crate::visitors::async_module::build_async_module; use crate::visitors::plugin_import::plugin_import; use crate::visitors::relay::relay; @@ -169,13 +173,13 @@ pub fn run_after_pass( .transform_with_handler(cm.clone(), |_, program, context| { let unresolved_mark = context.unresolved_mark; let top_level_mark = context.top_level_mark; - let builtin_tree_shaking = generate_context.compilation.options.builtins.tree_shaking; - let minify_options = &generate_context.compilation.options.builtins.minify_options; + let compilation = generate_context.compilation; + let builtin_tree_shaking = compilation.options.builtins.tree_shaking; + let minify_options = &compilation.options.builtins.minify_options; let comments = None; let dependency_visitors = collect_dependency_code_generation_visitors(module, generate_context)?; - let mgm = generate_context - .compilation + let mgm = compilation .module_graph .module_graph_module_by_identifier(&module.identifier()) .expect("should have module graph module"); @@ -205,16 +209,26 @@ pub fn run_after_pass( } } + let mut promises = LinkedList::new(); + if build_meta.is_async { + let runtime_requirements = &mut generate_context.runtime_requirements; + runtime_requirements.insert(runtime_globals::MODULE); + runtime_requirements.insert(runtime_globals::ASYNC_MODULE); + decl_mappings.iter().for_each(|(_, referenced)| { + promises.push_back(compilation.module_graph.is_async(referenced)) + }); + } + let mut pass = chain!( Optional::new( tree_shaking_visitor( &decl_mappings, - &generate_context.compilation.module_graph, + &compilation.module_graph, module.identifier(), - &generate_context.compilation.used_symbol_ref, + &compilation.used_symbol_ref, top_level_mark, - &generate_context.compilation.side_effects_free_modules, - &generate_context.compilation.module_item_map, + &compilation.side_effects_free_modules, + &compilation.module_item_map, context.helpers.mark() ), builtin_tree_shaking && need_tree_shaking @@ -247,8 +261,9 @@ pub fn run_after_pass( comments, Some(EsVersion::Es5) ), + Optional::new(build_async_module(promises), build_meta.is_async), inject_runtime_helper(unresolved_mark, generate_context.runtime_requirements), - finalize(module, generate_context.compilation, unresolved_mark), + finalize(module, compilation, unresolved_mark), swc_visitor::hygiene(false, top_level_mark), swc_visitor::fixer(comments.map(|v| v as &dyn Comments)), ); diff --git a/crates/rspack_plugin_runtime/src/lib.rs b/crates/rspack_plugin_runtime/src/lib.rs index d7f93cd8380d..d48580cb9492 100644 --- a/crates/rspack_plugin_runtime/src/lib.rs +++ b/crates/rspack_plugin_runtime/src/lib.rs @@ -6,6 +6,7 @@ use rspack_core::{ PluginAdditionalChunkRuntimeRequirementsOutput, PluginContext, RuntimeModuleExt, }; use rspack_error::Result; +use runtime_module::AsyncRuntimeModule; use crate::runtime_module::{EnsureChunkRuntimeModule, OnChunkLoadedRuntimeModule}; @@ -69,6 +70,10 @@ impl Plugin for RuntimePlugin { compilation.add_runtime_module(chunk, EnsureChunkRuntimeModule::new(true).boxed()); } + if runtime_requirements.contains(runtime_globals::ASYNC_MODULE) { + compilation.add_runtime_module(chunk, AsyncRuntimeModule::default().boxed()); + } + if runtime_requirements.contains(runtime_globals::ON_CHUNKS_LOADED) { compilation.add_runtime_module(chunk, OnChunkLoadedRuntimeModule::default().boxed()); } diff --git a/crates/rspack_plugin_runtime/src/runtime_module/async_module.rs b/crates/rspack_plugin_runtime/src/runtime_module/async_module.rs new file mode 100644 index 000000000000..8cb07014b943 --- /dev/null +++ b/crates/rspack_plugin_runtime/src/runtime_module/async_module.rs @@ -0,0 +1,30 @@ +use rspack_core::{ + rspack_sources::{BoxSource, RawSource, SourceExt}, + Compilation, RuntimeModule, +}; +use rspack_identifier::Identifier; + +use crate::impl_runtime_module; + +#[derive(Debug, Eq)] +pub struct AsyncRuntimeModule { + id: Identifier, +} +impl Default for AsyncRuntimeModule { + fn default() -> Self { + AsyncRuntimeModule { + id: Identifier::from("webpack/runtime/async_module"), + } + } +} + +impl RuntimeModule for AsyncRuntimeModule { + fn generate(&self, _compilation: &Compilation) -> BoxSource { + RawSource::from(include_str!("runtime/async_module.js")).boxed() + } + + fn name(&self) -> Identifier { + self.id + } +} +impl_runtime_module!(AsyncRuntimeModule); diff --git a/crates/rspack_plugin_runtime/src/runtime_module/mod.rs b/crates/rspack_plugin_runtime/src/runtime_module/mod.rs index 6b0436b5aa32..abbdb69cfb2c 100644 --- a/crates/rspack_plugin_runtime/src/runtime_module/mod.rs +++ b/crates/rspack_plugin_runtime/src/runtime_module/mod.rs @@ -1,3 +1,4 @@ +mod async_module; mod css_loading; mod ensure_chunk; mod get_chunk_filename; @@ -14,6 +15,7 @@ mod on_chunk_loaded; mod public_path; mod require_js_chunk_loading; mod utils; +pub use async_module::AsyncRuntimeModule; pub use css_loading::CssLoadingRuntimeModule; pub use ensure_chunk::EnsureChunkRuntimeModule; pub use get_chunk_filename::GetChunkFilenameRuntimeModule; diff --git a/crates/rspack_plugin_runtime/src/runtime_module/runtime/async_module.js b/crates/rspack_plugin_runtime/src/runtime_module/runtime/async_module.js new file mode 100644 index 000000000000..67712cb69678 --- /dev/null +++ b/crates/rspack_plugin_runtime/src/runtime_module/runtime/async_module.js @@ -0,0 +1,88 @@ +var webpackQueues = + typeof Symbol === "function" + ? Symbol("webpack queues") + : "__webpack_queues__"; +var webpackExports = + typeof Symbol === "function" + ? Symbol("webpack exports") + : "__webpack_exports__"; +var webpackError = + typeof Symbol === "function" ? Symbol("webpack error") : "__webpack_error__"; +var resolveQueue = queue => { + if (queue && !queue.d) { + queue.d = 1; + queue.forEach(fn => fn.r--); + queue.forEach(fn => (fn.r-- ? fn.r++ : fn())); + } +}; +var wrapDeps = deps => + depsdeps.map(dep=>dep["default"]?dep["default"]:dep).map(dep => { + if (dep !== null && typeof dep === "object") { + if (dep[webpackQueues]) return dep; + if (dep.then) { + var queue = []; + queue.d = 0; + dep.then( + r => { + obj[webpackExports] = r; + resolveQueue(queue); + }, + e => { + obj[webpackError] = e; + resolveQueue(queue); + } + ); + var obj = {}; + obj[webpackQueues] = fn => fn(queue); + return obj; + } + } + var ret = {}; + ret[webpackQueues] = x => {}; + ret[webpackExports] = dep; + return ret; + }); +__webpack_require__.a = (module, body, hasAwait) => { + var queue; + hasAwait && ((queue = []).d = 1); + var depQueues = new Set(); + var exports = module.exports; + var currentDeps; + var outerResolve; + var reject; + var promise = new Promise((resolve, rej) => { + reject = rej; + outerResolve = resolve; + }); + promise[webpackExports] = exports; + promise[webpackQueues] = fn => ( + queue && fn(queue), depQueues.forEach(fn), promise["catch"](x => {}) + ); + module.exports = promise; + body( + deps => { + currentDeps = wrapDeps(deps); + var fn; + var getResult = () => + currentDeps.map(d => { + if (d[webpackError]) throw d[webpackError]; + return d[webpackExports]; + }); + var promise = new Promise(resolve => { + fn = () => resolve(getResult); + fn.r = 0; + var fnQueue = q => + q !== queue && + !depQueues.has(q) && + (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn))); + currentDeps.map(dep => dep[webpackQueues](fnQueue)); + }); + return fn.r ? promise : getResult(); + }, + err => ( + err ? reject((promise[webpackError] = err)) : outerResolve(exports), + resolveQueue(queue) + ) + ); + queue && (queue.d = 0); +}; diff --git a/crates/rspack_plugin_wasm/Cargo.toml b/crates/rspack_plugin_wasm/Cargo.toml new file mode 100644 index 000000000000..29299222bcfb --- /dev/null +++ b/crates/rspack_plugin_wasm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_plugin_wasm" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +dashmap = { workspace = true } +rayon = { workspace = true } +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_identifier = { path = "../rspack_identifier" } +rspack_plugin_asset = { path = "../rspack_plugin_asset" } +rspack_plugin_runtime = { path = "../rspack_plugin_runtime" } +rspack_util = { path = "../rspack_util" } +rustc-hash = { workspace = true } +serde_json = { workspace = true } +sugar_path = { workspace = true } +tracing = { workspace = true } +wasmparser = "0.102.0" + + +[dev-dependencies] +rspack_testing = { path = "../rspack_testing" } diff --git a/crates/rspack_plugin_wasm/README.md b/crates/rspack_plugin_wasm/README.md new file mode 100644 index 000000000000..05901e9a72ae --- /dev/null +++ b/crates/rspack_plugin_wasm/README.md @@ -0,0 +1,7 @@ +Async wasm + + +1. wasm binary +2. wasm loading plugin +3. sync wasm plugin +4. wast text loader \ No newline at end of file diff --git a/crates/rspack_plugin_wasm/src/ast/mod.rs b/crates/rspack_plugin_wasm/src/ast/mod.rs new file mode 100644 index 000000000000..8bec73283a52 --- /dev/null +++ b/crates/rspack_plugin_wasm/src/ast/mod.rs @@ -0,0 +1,3 @@ +use wasmparser::TypeRef; + +pub type WasmNode = TypeRef; diff --git a/crates/rspack_plugin_wasm/src/dependency.rs b/crates/rspack_plugin_wasm/src/dependency.rs new file mode 100644 index 000000000000..b803eb572315 --- /dev/null +++ b/crates/rspack_plugin_wasm/src/dependency.rs @@ -0,0 +1,99 @@ +use core::hash::Hash; +use std::hash::Hasher; + +use rspack_core::{ + CodeGeneratable, CodeGeneratableContext, CodeGeneratableResult, Dependency, DependencyCategory, + DependencyId, DependencyType, ErrorSpan, ModuleDependency, ModuleIdentifier, +}; + +use crate::WasmNode; + +#[derive(Debug, Clone)] +pub struct WasmImportDependency { + id: Option, + parent_module_identifier: Option, + name: String, + request: String, + // only_direct_import: bool, + /// the WASM AST node + pub desc: WasmNode, + + span: Option, +} + +impl WasmImportDependency { + pub fn new(request: String, name: String, desc: WasmNode) -> Self { + Self { + id: None, + parent_module_identifier: None, + name, + request, + desc, + // only_direct_import, + span: None, + } + } + pub fn name(&self) -> &str { + &self.name + } +} + +impl Dependency for WasmImportDependency { + fn id(&self) -> Option { + self.id + } + fn set_id(&mut self, id: Option) { + self.id = id; + } + fn parent_module_identifier(&self) -> Option<&ModuleIdentifier> { + self.parent_module_identifier.as_ref() + } + + fn set_parent_module_identifier(&mut self, identifier: Option) { + self.parent_module_identifier = identifier; + } + + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Wasm + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::WasmImport + } +} + +impl ModuleDependency for WasmImportDependency { + fn request(&self) -> &str { + &self.request + } + + fn user_request(&self) -> &str { + &self.request + } + + fn span(&self) -> Option<&ErrorSpan> { + self.span.as_ref() + } +} + +impl CodeGeneratable for WasmImportDependency { + fn generate( + &self, + _code_generatable_context: &mut CodeGeneratableContext, + ) -> rspack_error::Result { + todo!() + } +} + +impl PartialEq for WasmImportDependency { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.parent_module_identifier == other.parent_module_identifier + && self.name == other.name + && self.request == other.request + } +} +impl Eq for WasmImportDependency {} +impl Hash for WasmImportDependency { + fn hash(&self, _state: &mut H) {} +} diff --git a/crates/rspack_plugin_wasm/src/lib.rs b/crates/rspack_plugin_wasm/src/lib.rs new file mode 100644 index 000000000000..b2636858f5de --- /dev/null +++ b/crates/rspack_plugin_wasm/src/lib.rs @@ -0,0 +1,15 @@ +#![feature(box_syntax)] +#![feature(let_chains)] + +mod ast; +mod dependency; +mod loading_plugin; +mod parser_and_generator; +mod runtime; +mod wasm_plugin; + +pub use ast::*; +pub use loading_plugin::*; +pub use parser_and_generator::*; +pub use runtime::*; +pub use wasm_plugin::*; diff --git a/crates/rspack_plugin_wasm/src/loading_plugin.rs b/crates/rspack_plugin_wasm/src/loading_plugin.rs new file mode 100644 index 000000000000..cbeaf573389c --- /dev/null +++ b/crates/rspack_plugin_wasm/src/loading_plugin.rs @@ -0,0 +1,37 @@ +use rspack_core::{ + runtime_globals, AdditionalChunkRuntimeRequirementsArgs, Plugin, + PluginAdditionalChunkRuntimeRequirementsOutput, PluginContext, RuntimeModuleExt, +}; + +use crate::AsyncWasmRuntimeModule; + +// TODO: for ChunkLoading +// #[derive(Debug)] +// pub struct FetchCompileWasmPlugin; + +#[derive(Debug)] +pub struct FetchCompileAsyncWasmPlugin; + +#[async_trait::async_trait] +impl Plugin for FetchCompileAsyncWasmPlugin { + fn name(&self) -> &'static str { + "FetchCompileWasmPlugin" + } + + fn runtime_requirements_in_tree( + &self, + _ctx: PluginContext, + args: &mut AdditionalChunkRuntimeRequirementsArgs, + ) -> PluginAdditionalChunkRuntimeRequirementsOutput { + let runtime_requirements = &mut args.runtime_requirements; + + if runtime_requirements.contains(runtime_globals::INSTANTIATE_WASM) { + runtime_requirements.insert(runtime_globals::PUBLIC_PATH); + args + .compilation + .add_runtime_module(args.chunk, AsyncWasmRuntimeModule::default().boxed()); + } + + Ok(()) + } +} diff --git a/crates/rspack_plugin_wasm/src/parser_and_generator.rs b/crates/rspack_plugin_wasm/src/parser_and_generator.rs new file mode 100644 index 000000000000..b53e398003ec --- /dev/null +++ b/crates/rspack_plugin_wasm/src/parser_and_generator.rs @@ -0,0 +1,310 @@ +use std::collections::hash_map::DefaultHasher; +use std::ffi::OsStr; +use std::hash::{Hash, Hasher}; +use std::path::Path; + +use dashmap::DashMap; +use rspack_core::rspack_sources::{RawSource, Source, SourceExt}; +use rspack_core::DependencyType::WasmImport; +use rspack_core::{ + runtime_globals, AstOrSource, Context, Dependency, Filename, FilenameRenderOptions, + GenerateContext, GenerationResult, Module, ModuleDependency, ModuleIdentifier, NormalModule, + ParseContext, ParseResult, ParserAndGenerator, SourceType, StaticExportsDependency, +}; +use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; +use rspack_identifier::Identifier; +use rspack_plugin_asset::ModuleIdToFileName; +use sugar_path::SugarPath; +use wasmparser::{Import, Parser, Payload}; + +use crate::dependency::WasmImportDependency; + +#[derive(Debug)] +pub struct AsyncWasmParserAndGenerator { + pub(crate) module_id_to_filename: ModuleIdToFileName, +} + +pub(crate) static WASM_SOURCE_TYPE: &[SourceType; 2] = &[SourceType::Wasm, SourceType::JavaScript]; + +impl ParserAndGenerator for AsyncWasmParserAndGenerator { + fn source_types(&self) -> &[SourceType] { + WASM_SOURCE_TYPE + } + + fn parse(&mut self, parse_context: ParseContext) -> Result> { + parse_context.build_info.strict = true; + parse_context.build_meta.is_async = true; + + let source = parse_context.source; + + let mut exports = Vec::with_capacity(1); + let mut dependencies = Vec::with_capacity(1); + let mut diagnostic = Vec::with_capacity(1); + + 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(), + 0, + 0, + )), + }; + } + } + Payload::ImportSection(s) => { + for import in s { + match import { + Ok(Import { module, name, ty }) => { + let dep = box WasmImportDependency::new(module.into(), name.into(), ty) + as Box; + + dependencies.push(dep); + } + Err(err) => diagnostic.push(Diagnostic::error( + "Wasm Import Parse Error".into(), + err.to_string(), + 0, + 0, + )), + } + } + } + _ => {} + }, + Err(err) => { + diagnostic.push(Diagnostic::error( + "Wasm Parse Error".into(), + err.to_string(), + 0, + 0, + )); + } + } + } + + dependencies + .push(box StaticExportsDependency::new(exports, false) as Box); + + Ok( + ParseResult { + dependencies, + presentational_dependencies: vec![], + ast_or_source: source.into(), + } + .with_diagnostic(diagnostic), + ) + } + + fn size(&self, module: &dyn Module, source_type: &SourceType) -> f64 { + let base = module.size(source_type); + match source_type { + SourceType::JavaScript => 40.0 + base, + SourceType::Wasm => base, + _ => 0.0, + } + } + + #[allow(clippy::unwrap_in_result)] + fn generate( + &self, + ast_or_source: &AstOrSource, + module: &dyn Module, + generate_context: &mut GenerateContext, + ) -> Result { + let compilation = generate_context.compilation; + let wasm_filename_template = &compilation.options.output.webassembly_module_filename; + let hash = hash_for_ast_or_source(ast_or_source); + let normal_module = module.as_normal_module(); + let wasm_filename = render_wasm_name( + &compilation.options.context, + normal_module, + wasm_filename_template, + hash, + ); + + self + .module_id_to_filename + .insert(module.identifier(), wasm_filename.clone()); + + match generate_context.requested_source_type { + SourceType::JavaScript => { + let runtime_requirements = &mut generate_context.runtime_requirements; + runtime_requirements.insert(runtime_globals::MODULE); + runtime_requirements.insert(runtime_globals::MODULE_ID); + runtime_requirements.insert(runtime_globals::INSTANTIATE_WASM); + + let dep_modules = DashMap::::new(); + let wasm_deps_by_request = DashMap::<&str, Vec<(Identifier, String)>>::new(); + let mut promises: Vec = vec![]; + + let module_graph = &compilation.module_graph; + let chunk_graph = &compilation.chunk_graph; + + if let Some(dependencies) = module_graph + .module_graph_module_by_identifier(&module.identifier()) + .map(|mgm| &mgm.dependencies) + { + dependencies + .iter() + .map(|id| module_graph.dependency_by_id(id).expect("should be ok")) + .filter(|dep| dep.dependency_type() == &WasmImport) + .map(|dep| { + ( + dep, + module_graph.module_graph_module_by_dependency_id(&dep.id().expect("should be ok")), + ) + }) + .for_each(|(dep, mgm)| { + if let Some(mgm) = mgm { + if !dep_modules.contains_key(&mgm.module_identifier) { + let import_var = format!("WEBPACK_IMPORTED_MODULE_{}", dep_modules.len()); + let val = (import_var.clone(), mgm.id(chunk_graph)); + + if let Some(meta)=&mgm.build_meta&&meta.is_async{ + promises.push(import_var); + } + dep_modules.insert(mgm.module_identifier, val); + } + + let dep = dep + .as_any() + .downcast_ref::() + .expect("should be wasm import dependency"); + + let dep_name = serde_json::to_string(dep.name()).expect("should be ok."); + let request = dep.request(); + let val = (mgm.module_identifier, dep_name); + if let Some(deps) = &mut wasm_deps_by_request.get_mut(&request) { + deps.value_mut().push(val); + } else { + wasm_deps_by_request.insert(request, vec![val]); + } + } + }) + } + + let imports_code = dep_modules + .iter() + .map(|val| render_import_stmt(&val.value().0, val.value().1)) + .collect::>() + .join(""); + + let import_obj_request_items = wasm_deps_by_request + .into_iter() + .map(|(request, deps)| { + let deps = deps + .into_iter() + .map(|(id, name)| { + let import_var = dep_modules.get(&id).expect("should be ok"); + let import_var = &import_var.value().0; + format!("{name}: {import_var}[{name}]") + }) + .collect::>() + .join(",\n"); + + format!( + "{}: {{{deps}}}", + serde_json::to_string(request).expect("should be ok") + ) + }) + .collect::>(); + + let imports_obj = if !import_obj_request_items.is_empty() { + Some(format!(", {{{}}}", &import_obj_request_items.join(",\n"))) + } else { + None + }; + + let instantiate_call = format!( + "{}(exports, module.id, {} {})", + runtime_globals::INSTANTIATE_WASM, + serde_json::to_string(&wasm_filename).expect("should be ok"), + imports_obj.unwrap_or_default() + ); + + let source = if !promises.is_empty() { + generate_context + .runtime_requirements + .insert(runtime_globals::ASYNC_MODULE); + let promises = promises.join(", "); + let decl = format!( + "var __webpack_instantiate__=function([{promises}]){{ return {instantiate_call}}}\n", + ); + let async_dependencies = format!( + "{}(module, async function (__webpack_handle_async_dependencies__, __webpack_async_result__){{ + try {{ + {imports_code} + var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([{promises}]); + var [{promises}] = __webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__; + await {instantiate_call}; + + __webpack_async_result__(); + + }} catch(e) {{ __webpack_async_result__(e); }} + }}, 1); + ", + runtime_globals::ASYNC_MODULE, + ); + + RawSource::from(format!("{decl}{async_dependencies}")) + } else { + RawSource::from(format!( + "{imports_code} module.exports = {instantiate_call};" + )) + }; + + Ok(GenerationResult { + ast_or_source: source.boxed().into(), + }) + } + _ => Ok(ast_or_source.clone().into()), + } + } +} + +fn render_wasm_name( + ctx: &Context, + normal_module: Option<&NormalModule>, + wasm_filename_template: &Filename, + hash: String, +) -> String { + wasm_filename_template.render(FilenameRenderOptions { + name: normal_module.and_then(|m| { + let p = Path::new(&m.resource_resolved_data().resource_path); + p.file_stem().map(|s| s.to_string_lossy().to_string()) + }), + path: normal_module.map(|m| { + Path::new(&m.resource_resolved_data().resource_path) + .relative(ctx) + .to_string_lossy() + .to_string() + }), + extension: normal_module.and_then(|m| { + Path::new(&m.resource_resolved_data().resource_path) + .extension() + .and_then(OsStr::to_str) + .map(|str| format!("{}{}", ".", str)) + }), + contenthash: Some(hash.clone()), + chunkhash: Some(hash.clone()), + hash: Some(hash), + ..Default::default() + }) +} + +fn render_import_stmt(import_var: &str, module_id: &str) -> String { + let module_id = serde_json::to_string(&module_id).expect("TODO"); + format!("var {import_var} = __webpack_require__({module_id});\n",) +} + +fn hash_for_ast_or_source(ast_or_source: &AstOrSource) -> String { + let mut hasher = DefaultHasher::new(); + ast_or_source.hash(&mut hasher); + format!("{:x}", hasher.finish()) +} diff --git a/crates/rspack_plugin_wasm/src/runtime.rs b/crates/rspack_plugin_wasm/src/runtime.rs new file mode 100644 index 000000000000..445274da2036 --- /dev/null +++ b/crates/rspack_plugin_wasm/src/runtime.rs @@ -0,0 +1,36 @@ +use rspack_core::rspack_sources::{BoxSource, RawSource, SourceExt}; +use rspack_core::{runtime_globals, Compilation, RuntimeModule, RUNTIME_MODULE_STAGE_ATTACH}; +use rspack_identifier::Identifier; +use rspack_plugin_runtime::impl_runtime_module; + +#[derive(Debug, Eq)] +pub struct AsyncWasmRuntimeModule { + id: Identifier, +} + +impl Default for AsyncWasmRuntimeModule { + fn default() -> Self { + Self { + id: Identifier::from("rspack/runtime/wasm loading"), + } + } +} + +impl RuntimeModule for AsyncWasmRuntimeModule { + fn name(&self) -> Identifier { + self.id + } + fn generate(&self, _compilation: &Compilation) -> BoxSource { + RawSource::from(include_str!("runtime/async-wasm-loading.js").replace( + "$REQ$", + &format!("fetch({}+wasmModuleHash)", runtime_globals::PUBLIC_PATH), + )) + .boxed() + } + + fn stage(&self) -> u8 { + RUNTIME_MODULE_STAGE_ATTACH + } +} + +impl_runtime_module!(AsyncWasmRuntimeModule); diff --git a/crates/rspack_plugin_wasm/src/runtime/async-wasm-loading.js b/crates/rspack_plugin_wasm/src/runtime/async-wasm-loading.js new file mode 100644 index 000000000000..bc881723b622 --- /dev/null +++ b/crates/rspack_plugin_wasm/src/runtime/async-wasm-loading.js @@ -0,0 +1,12 @@ +__webpack_require__.v = (exports, wasmModuleId, wasmModuleHash, importsObj) => { + var req = $REQ$; + if (typeof WebAssembly.instantiateStreaming === "function") { + return WebAssembly.instantiateStreaming(req, importsObj).then(res => + Object.assign(exports, res.instance.exports) + ); + } + return req + .then(x => x.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, importsObj)) + .then(res => Object.assign(exports, res.instance.exports)); +}; diff --git a/crates/rspack_plugin_wasm/src/wasm_plugin.rs b/crates/rspack_plugin_wasm/src/wasm_plugin.rs new file mode 100644 index 000000000000..3281e637f008 --- /dev/null +++ b/crates/rspack_plugin_wasm/src/wasm_plugin.rs @@ -0,0 +1,111 @@ +use std::fmt::Debug; + +use rayon::prelude::*; +use rspack_core::{ + ApplyContext, Module, ModuleType, ParserAndGenerator, PathData, Plugin, PluginContext, + PluginRenderManifestHookOutput, RenderManifestArgs, RenderManifestEntry, SourceType, +}; +use rspack_error::Result; +use rspack_plugin_asset::ModuleIdToFileName; + +use crate::AsyncWasmParserAndGenerator; + +pub struct EnableWasmLoadingPlugin {} + +#[derive(Debug, Default)] +pub struct AsyncWasmPlugin { + pub module_id_to_filename_without_ext: ModuleIdToFileName, +} + +impl AsyncWasmPlugin { + pub fn new() -> AsyncWasmPlugin { + Self { + module_id_to_filename_without_ext: Default::default(), + } + } +} + +#[async_trait::async_trait] +impl Plugin for AsyncWasmPlugin { + fn name(&self) -> &'static str { + "AsyncWebAssemblyModulesPlugin" + } + + fn apply(&mut self, ctx: PluginContext<&mut ApplyContext>) -> Result<()> { + let module_id_to_filename_without_ext = self.module_id_to_filename_without_ext.clone(); + + let builder = move || { + Box::new({ + AsyncWasmParserAndGenerator { + module_id_to_filename: module_id_to_filename_without_ext.clone(), + } + }) as Box + }; + + ctx + .context + .register_parser_and_generator_builder(ModuleType::WasmAsync, Box::new(builder)); + + Ok(()) + } + + // fn render(&self, _ctx: PluginContext, _args: &RenderArgs) -> PluginRenderStartupHookOutput { + // + // } + + async fn render_manifest( + &self, + _ctx: PluginContext, + args: RenderManifestArgs<'_>, + ) -> PluginRenderManifestHookOutput { + let compilation = args.compilation; + let chunk = args.chunk(); + let module_graph = &compilation.module_graph; + + let ordered_modules = compilation + .chunk_graph + .get_chunk_modules(&args.chunk_ukey, module_graph); + + let files = ordered_modules + .par_iter() + .filter(|m| *m.module_type() == ModuleType::WasmAsync) + .map(|m| { + let code_gen_result = compilation + .code_generation_results + .get(&m.identifier(), Some(&chunk.runtime))?; + + let result = code_gen_result + .get(&SourceType::Wasm) + .map(|result| result.ast_or_source.clone().try_into_source()) + .transpose()? + .map(|source| { + let options = &compilation.options; + let wasm_filename = self + .module_id_to_filename_without_ext + .get(&m.identifier()) + .map(|s| s.clone()); + let path_options = PathData { + chunk_ukey: args.chunk_ukey, + }; + RenderManifestEntry::new( + source, + wasm_filename.unwrap_or_else(|| { + options + .output + .webassembly_module_filename + .render_with_chunk(chunk, ".wasm", &SourceType::Wasm) + }), + path_options, + ) + }); + + Ok(result) + }) + .collect::>>>()? + .into_iter() + .flatten() + .collect::>(); + + Ok(files) + } +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures.rs b/crates/rspack_plugin_wasm/tests/fixtures.rs new file mode 100644 index 000000000000..a0728eccf2cf --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +use rspack_testing::{fixture, test_fixture}; + +#[fixture("tests/fixtures/**/*.config.js*")] +fn wasm(config_path: PathBuf) { + let fixture_path = config_path.parent().expect("TODO:"); + test_fixture(fixture_path); +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/expected/main.js b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/expected/main.js new file mode 100644 index 000000000000..d02d26416368 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/expected/main.js @@ -0,0 +1,14 @@ +(self['webpackChunkwebpack'] = self['webpackChunkwebpack'] || []).push([["main"], { +"./index.js": function (module, exports, __webpack_require__) { +it("should allow to run a WebAssembly module importing from multiple modules", function() { + return __webpack_require__.el("./module.js").then(__webpack_require__.bind(__webpack_require__, "./module.js")).then(__webpack_require__.ir).then(function(mod) { + expect(mod.result).toBe(42); + }); +}); +}, + +},function(__webpack_require__) { +var __webpack_exports__ = __webpack_require__('./index.js'); + +} +]); \ No newline at end of file diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/index.js b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/index.js new file mode 100644 index 000000000000..5667add55b04 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/index.js @@ -0,0 +1,5 @@ +it("should allow to run a WebAssembly module importing from multiple modules", function () { + return import("./module").then(function (mod) { + expect(mod.result).toBe(42); + }); +}); diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module.js b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module.js new file mode 100644 index 000000000000..deccad21fe43 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module.js @@ -0,0 +1,7 @@ +import { getResult } from "./wasm.wasm"; + +export var result = getResult(1); + +export function getNumber() { + return 20; +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module2.js b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module2.js new file mode 100644 index 000000000000..60b7eac0eaba --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/module2.js @@ -0,0 +1,5 @@ +import { getNumber as getN } from "./wasm.wasm"; + +export function getNumber() { + return getN(); +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/test.config.json b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/test.config.json new file mode 100644 index 000000000000..55d2ddd80816 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/test.config.json @@ -0,0 +1,5 @@ +{ + "experiments": { + "asyncWebAssembly": true + } +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/wasm.wasm b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/wasm.wasm new file mode 100644 index 000000000000..19fef4041f6c Binary files /dev/null and b/crates/rspack_plugin_wasm/tests/fixtures/imports-multiple/wasm.wasm differ diff --git a/crates/rspack_plugin_wasm/tests/fixtures/v128/expected/main.js b/crates/rspack_plugin_wasm/tests/fixtures/v128/expected/main.js new file mode 100644 index 000000000000..0b9c94cdc154 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/v128/expected/main.js @@ -0,0 +1,28 @@ +(self['webpackChunkwebpack'] = self['webpackChunkwebpack'] || []).push([["main"], { +"./index.js": function (module, exports, __webpack_require__) { +"use strict"; +__webpack_require__.a(module, async function (__webpack_handle_async_dependencies__, __webpack_async_result__) { try { +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _v128Wasm = __webpack_require__.ir(__webpack_require__("./v128.wasm")); +var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([ + _v128Wasm +]); +[_v128Wasm] = __webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__; +console.log(_v128Wasm.default.x); + +__webpack_async_result__(); +} catch(e) { __webpack_async_result__(e); } });}, +"./v128.wasm": function (module, exports, __webpack_require__) { +"use strict"; +__webpack_require__.a(module, async function (__webpack_handle_async_dependencies__, __webpack_async_result__) { try { + module.exports = __webpack_require__.v(exports, module.id, "c61e7cc882ba31f8.module.wasm" ); +__webpack_async_result__(); +} catch(e) { __webpack_async_result__(e); } });}, + +},function(__webpack_require__) { +var __webpack_exports__ = __webpack_require__('./index.js'); + +} +]); \ No newline at end of file diff --git a/crates/rspack_plugin_wasm/tests/fixtures/v128/index.js b/crates/rspack_plugin_wasm/tests/fixtures/v128/index.js new file mode 100644 index 000000000000..e7067e6939bc --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/v128/index.js @@ -0,0 +1,3 @@ +import v128 from "./v128.wasm"; + +console.log(v128.x); diff --git a/crates/rspack_plugin_wasm/tests/fixtures/v128/test.config.json b/crates/rspack_plugin_wasm/tests/fixtures/v128/test.config.json new file mode 100644 index 000000000000..55d2ddd80816 --- /dev/null +++ b/crates/rspack_plugin_wasm/tests/fixtures/v128/test.config.json @@ -0,0 +1,5 @@ +{ + "experiments": { + "asyncWebAssembly": true + } +} diff --git a/crates/rspack_plugin_wasm/tests/fixtures/v128/v128.wasm b/crates/rspack_plugin_wasm/tests/fixtures/v128/v128.wasm new file mode 100644 index 000000000000..e791af8ef934 Binary files /dev/null and b/crates/rspack_plugin_wasm/tests/fixtures/v128/v128.wasm differ diff --git a/crates/rspack_testing/Cargo.toml b/crates/rspack_testing/Cargo.toml index 6f4d9885d271..12f8d85d5c7e 100644 --- a/crates/rspack_testing/Cargo.toml +++ b/crates/rspack_testing/Cargo.toml @@ -30,6 +30,7 @@ rspack_plugin_json = { path = "../rspack_plugin_json" } rspack_plugin_progress = { path = "../rspack_plugin_progress" } rspack_plugin_remove_empty_chunks = { path = "../rspack_plugin_remove_empty_chunks" } rspack_plugin_runtime = { path = "../rspack_plugin_runtime" } +rspack_plugin_wasm = { path = "../rspack_plugin_wasm" } rspack_regex = { path = "../rspack_regex" } rspack_tracing = { path = "../rspack_tracing" } diff --git a/crates/rspack_testing/src/test_config.rs b/crates/rspack_testing/src/test_config.rs index b15bc836fbc1..382e3d7cc122 100644 --- a/crates/rspack_testing/src/test_config.rs +++ b/crates/rspack_testing/src/test_config.rs @@ -83,6 +83,16 @@ pub struct TestConfig { pub optimization: Optimization, #[serde(default)] pub devtool: String, + #[serde(default)] + pub experiments: Experiments, +} + +#[derive(Debug, Default, JsonSchema, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Experiments { + // True by default to reduce code in snapshots. + #[serde(default = "true_by_default")] + pub async_web_assembly: bool, } #[derive(Debug, JsonSchema, Deserialize)] @@ -293,6 +303,7 @@ impl TestConfig { rule!("\\.ts$", "ts"), rule!("\\.tsx$", "tsx"), rule!("\\.css$", "css"), + rule!("\\.wasm$", "webassembly/async"), ]; rules.extend(self.module.rules.into_iter().map(|rule| { c::ModuleRule { @@ -360,6 +371,8 @@ impl TestConfig { css_chunk_filename: c::Filename::from_str(&self.output.css_chunk_filename) .expect("Should exist"), asset_module_filename: c::Filename::from_str("[hash][ext][query]").expect("Should exist"), + webassembly_module_filename: c::Filename::from_str("[hash].module.wasm") + .expect("Should exist"), public_path: c::PublicPath::String("/".to_string()), unique_name: "__rspack_test__".to_string(), path: context.join("dist"), @@ -375,10 +388,12 @@ impl TestConfig { target: c::Target::new(&self.target).expect("Can't construct target"), resolve: c::Resolve { extensions: Some( - [".js", ".jsx", ".ts", ".tsx", ".json", ".d.ts", ".css"] - .into_iter() - .map(|i| i.to_string()) - .collect(), + [ + ".js", ".jsx", ".ts", ".tsx", ".json", ".d.ts", ".css", ".wasm", + ] + .into_iter() + .map(|i| i.to_string()) + .collect(), ), ..Default::default() }, @@ -492,6 +507,12 @@ impl TestConfig { // Notice the plugin need to be placed after SplitChunksPlugin plugins.push(rspack_plugin_remove_empty_chunks::RemoveEmptyChunksPlugin.boxed()); + plugins.push(rspack_plugin_javascript::InferAsyncModulesPlugin {}.boxed()); + if self.experiments.async_web_assembly { + plugins.push(rspack_plugin_wasm::FetchCompileAsyncWasmPlugin {}.boxed()); + plugins.push(rspack_plugin_wasm::AsyncWasmPlugin::new().boxed()); + } + (options, plugins) } diff --git a/crates/rspack_testing/test.config.scheme.json b/crates/rspack_testing/test.config.scheme.json index 37ad5dbc5f60..ca52f375de8f 100644 --- a/crates/rspack_testing/test.config.scheme.json +++ b/crates/rspack_testing/test.config.scheme.json @@ -17,6 +17,13 @@ "$ref": "#/definitions/EntryItem" } }, + "experiments": { + "$ref": "#/definitions/Experiments" + }, + "mode": { + "default": "", + "type": "string" + }, "module": { "$ref": "#/definitions/Module" }, @@ -52,6 +59,10 @@ "type": "string" } }, + "devFriendlySplitChunks": { + "default": false, + "type": "boolean" + }, "html": { "type": "array", "items": { @@ -72,19 +83,14 @@ "$ref": "#/definitions/Postcss" }, "presetEnv": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["usage", "entry"] + "anyOf": [ + { + "$ref": "#/definitions/PresetEnv" }, - "targets": { - "type": "array", - "items": { - "type": "string" - } + { + "type": "null" } - } + ] }, "treeShaking": { "default": false, @@ -124,6 +130,16 @@ }, "additionalProperties": false }, + "Experiments": { + "type": "object", + "properties": { + "asyncWebAssembly": { + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, "HtmlPluginConfig": { "type": "object", "properties": { @@ -412,6 +428,33 @@ }, "additionalProperties": false }, + "PresetEnv": { + "type": "object", + "required": [ + "targets" + ], + "properties": { + "coreJs": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "type": [ + "string", + "null" + ] + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "PxToRem": { "type": "object", "properties": { diff --git a/examples/wasm-simple/add.wasm b/examples/wasm-simple/add.wasm new file mode 100644 index 000000000000..357f72da7a0d Binary files /dev/null and b/examples/wasm-simple/add.wasm differ diff --git a/examples/wasm-simple/example.js b/examples/wasm-simple/example.js new file mode 100644 index 000000000000..d42a023d1120 --- /dev/null +++ b/examples/wasm-simple/example.js @@ -0,0 +1,28 @@ +import { add } from "./add.wasm"; +import { + add as mathAdd, + factorial, + factorialJavascript, + fibonacci, + fibonacciJavascript +} from "./math"; + +console.log(add(22, 2200)); +console.log(mathAdd(10, 101)); +console.log(factorial(15)); +console.log(factorialJavascript(15)); +console.log(fibonacci(15)); +console.log(fibonacciJavascript(15)); +timed("wasm factorial", () => factorial(1500)); +timed("js factorial", () => factorialJavascript(1500)); +timed("wasm fibonacci", () => fibonacci(22)); +timed("js fibonacci", () => fibonacciJavascript(22)); + +function timed(name, fn) { + if (!console.time || !console.timeEnd) return fn(); + // warmup + for (var i = 0; i < 10; i++) fn(); + console.time(name); + for (var i = 0; i < 5000; i++) fn(); + console.timeEnd(name); +} diff --git a/examples/wasm-simple/factorial.wasm b/examples/wasm-simple/factorial.wasm new file mode 100644 index 000000000000..0e0d759df538 Binary files /dev/null and b/examples/wasm-simple/factorial.wasm differ diff --git a/examples/wasm-simple/fibonacci.wasm b/examples/wasm-simple/fibonacci.wasm new file mode 100644 index 000000000000..cffb563bd92d Binary files /dev/null and b/examples/wasm-simple/fibonacci.wasm differ diff --git a/examples/wasm-simple/index.html b/examples/wasm-simple/index.html new file mode 100644 index 000000000000..40df6870b010 --- /dev/null +++ b/examples/wasm-simple/index.html @@ -0,0 +1,15 @@ + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/examples/wasm-simple/math.js b/examples/wasm-simple/math.js new file mode 100644 index 000000000000..9afa2680aad4 --- /dev/null +++ b/examples/wasm-simple/math.js @@ -0,0 +1,15 @@ +import { add } from "./add.wasm"; +import { factorial } from "./factorial.wasm"; +import { fibonacci } from "./fibonacci.wasm"; + +export { add, factorial, fibonacci }; + +export function factorialJavascript(i) { + if (i < 1) return 1; + return i * factorialJavascript(i - 1); +} + +export function fibonacciJavascript(i) { + if (i < 2) return 1; + return fibonacciJavascript(i - 1) + fibonacciJavascript(i - 2); +} diff --git a/examples/wasm-simple/package.json b/examples/wasm-simple/package.json new file mode 100644 index 000000000000..ad4934e4ed23 --- /dev/null +++ b/examples/wasm-simple/package.json @@ -0,0 +1,14 @@ +{ + "name": "example-wasm-complex", + "version": "1.0.0", + "private": true, + "main": "example.js", + "scripts": { + "dev": "rspack serve", + "build": "rspack build" + }, + "license": "MIT", + "dependencies": { + "@rspack/cli": "workspace:*" + } +} diff --git a/examples/wasm-simple/rspack.config.js b/examples/wasm-simple/rspack.config.js new file mode 100644 index 000000000000..11852cf29edc --- /dev/null +++ b/examples/wasm-simple/rspack.config.js @@ -0,0 +1,22 @@ +/** + * @type {import('@rspack/cli').Configuration} + */ +module.exports = { + entry: { + main: './example.js' + }, + output: { + webassemblyModuleFilename: "[hash].wasm", + publicPath: 'dist/' + }, + experiments: { + asyncWebAssembly: true + }, + builtins: { + html: [ + { + template: './index.html' + } + ] + } +}; diff --git a/packages/rspack/src/config/adapter.ts b/packages/rspack/src/config/adapter.ts index 1ffd75c2693a..4ea4e862624a 100644 --- a/packages/rspack/src/config/adapter.ts +++ b/packages/rspack/src/config/adapter.ts @@ -149,7 +149,9 @@ function getRawOutput(output: OutputNormalized): RawOptions["output"] { !isNil(output.globalObject) && !isNil(output.importFunctionName) && !isNil(output.module) && - !isNil(output.iife), + !isNil(output.iife) && + !isNil(output.importFunctionName) && + !isNil(output.webassemblyModuleFilename), "fields should not be nil after defaults" ); return { @@ -167,7 +169,8 @@ function getRawOutput(output: OutputNormalized): RawOptions["output"] { globalObject: output.globalObject, importFunctionName: output.importFunctionName, iife: output.iife, - module: output.module + module: output.module, + webassemblyModuleFilename: output.webassemblyModuleFilename }; } @@ -424,11 +427,16 @@ function getRawSnapshotOptions( function getRawExperiments( experiments: Experiments ): RawOptions["experiments"] { - const { lazyCompilation, incrementalRebuild } = experiments; - assert(!isNil(lazyCompilation) && !isNil(incrementalRebuild)); + const { lazyCompilation, incrementalRebuild, asyncWebAssembly } = experiments; + assert( + !isNil(lazyCompilation) && + !isNil(incrementalRebuild) && + !isNil(asyncWebAssembly) + ); return { lazyCompilation, - incrementalRebuild + incrementalRebuild, + asyncWebAssembly }; } diff --git a/packages/rspack/src/config/defaults.ts b/packages/rspack/src/config/defaults.ts index 3d89ac3ca7c9..0f9a01cd4701 100644 --- a/packages/rspack/src/config/defaults.ts +++ b/packages/rspack/src/config/defaults.ts @@ -71,7 +71,10 @@ export const applyRspackOptionsDefaults = ( applySnapshotDefaults(options.snapshot, { production }); - applyModuleDefaults(options.module); + applyModuleDefaults(options.module, { + // syncWebAssembly: options.experiments.syncWebAssembly, + asyncWebAssembly: options.experiments.asyncWebAssembly! + }); applyOutputDefaults(options.output, { context: options.context!, @@ -133,6 +136,7 @@ const applyInfrastructureLoggingDefaults = ( const applyExperimentsDefaults = (experiments: Experiments) => { D(experiments, "incrementalRebuild", true); D(experiments, "lazyCompilation", false); + D(experiments, "asyncWebAssembly", false); }; const applySnapshotDefaults = ( @@ -151,7 +155,10 @@ const applySnapshotDefaults = ( ); }; -const applyModuleDefaults = (module: ModuleOptions) => { +const applyModuleDefaults = ( + module: ModuleOptions, + { asyncWebAssembly }: { asyncWebAssembly: boolean } +) => { F(module.parser!, "asset", () => ({})); F(module.parser!.asset!, "dataUrlCondition", () => ({})); if (typeof module.parser!.asset!.dataUrlCondition === "object") { @@ -228,6 +235,27 @@ const applyModuleDefaults = (module: ModuleOptions) => { } ] }); + + if (asyncWebAssembly) { + const wasm = { + type: "webassembly/async", + rules: [ + { + descriptionData: { + type: "module" + }, + resolve: { + fullySpecified: true + } + } + ] + }; + rules.push({ + test: /\.wasm$/i, + ...wasm + }); + } + return rules; }); }; @@ -280,6 +308,7 @@ const applyOutputDefaults = ( return "[id].css"; }); D(output, "assetModuleFilename", "[hash][ext][query]"); + D(output, "webassemblyModuleFilename", "[hash].module.wasm"); F(output, "path", () => path.join(process.cwd(), "dist")); D( output, @@ -290,6 +319,17 @@ const applyOutputDefaults = ( if (output.library) { F(output.library, "type", () => (output.module ? "module" : "var")); } + // F(output, "wasmLoading", () => { + // if (tp) { + // if (tp.fetchWasm) return "fetch"; + // if (tp.nodeBuiltins) + // return output.module ? "async-node-module" : "async-node"; + // if (tp.nodeBuiltins === null || tp.fetchWasm === null) { + // return "universal"; + // } + // } + // return false; + // }); A(output, "enabledLibraryTypes", () => { const enabledLibraryTypes = []; if (output.library) { @@ -298,6 +338,17 @@ const applyOutputDefaults = ( // TODO respect entryOptions.library return enabledLibraryTypes; }); + // A(output, "enabledWasmLoadingTypes", () => { + // const enabledWasmLoadingTypes = []; + // if (output.wasmLoading) { + // enabledWasmLoadingTypes.push(output.wasmLoading); + // } + // // if (output.workerWasmLoading) { + // // enabledWasmLoadingTypes.push(output.workerWasmLoading); + // // } + // // TODO respect entryOptions.wasmLoading + // return enabledWasmLoadingTypes; + // }); F(output, "globalObject", () => { if (tp) { if (tp.global) return "global"; @@ -400,7 +451,15 @@ const getResolveDefaults = ({ if (targetProperties.nwjs) conditions.push("nwjs"); } - const jsExtensions = [".tsx", ".jsx", ".ts", ".js", ".json", ".d.ts"]; + const jsExtensions = [ + ".tsx", + ".jsx", + ".ts", + ".js", + ".json", + ".d.ts", + ".wasm" + ]; const tp = targetProperties; const browserField = diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index 3d6b61d673fb..4905b1ac529e 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -52,6 +52,7 @@ export const getNormalizedRspackOptions = ( cssFilename: output.cssFilename, cssChunkFilename: output.cssChunkFilename, assetModuleFilename: output.assetModuleFilename, + webassemblyModuleFilename: output.webassemblyModuleFilename, uniqueName: output.uniqueName, enabledLibraryTypes: output.enabledLibraryTypes ? [...output.enabledLibraryTypes] diff --git a/packages/rspack/src/config/schema.js b/packages/rspack/src/config/schema.js index 21c5cdd43163..fa58231ff549 100644 --- a/packages/rspack/src/config/schema.js +++ b/packages/rspack/src/config/schema.js @@ -85,6 +85,43 @@ module.exports = { } ] }, + WebassemblyModuleFilename: { + description: + "The filename of WebAssembly modules as relative path inside the 'output.path' directory.", + type: "string" + }, + EnabledWasmLoadingTypes: { + description: + "List of wasm loading types enabled for use by entry points.", + type: "array", + items: { + $ref: "#/definitions/WasmLoadingType" + } + }, + WasmLoading: { + description: + "The method of loading WebAssembly Modules (methods included by default are 'fetch' (web/WebWorker), 'async-node' (node.js), but others might be added by plugins).", + anyOf: [ + { + enum: [false] + }, + { + $ref: "#/definitions/WasmLoadingType" + } + ] + }, + WasmLoadingType: { + description: + "The method of loading WebAssembly Modules (methods included by default are 'fetch' (web/WebWorker), 'async-node' (node.js), but others might be added by plugins).", + anyOf: [ + { + enum: ["fetch-streaming", "fetch", "async-node"] + }, + { + type: "string" + } + ] + }, Dependencies: { description: "References to other configurations to depend on.", type: "array", @@ -136,6 +173,9 @@ module.exports = { }, runtime: { $ref: "#/definitions/EntryRuntime" + }, + wasmLoading: { + $ref: "#/definitions/WasmLoading" } }, required: ["import"] @@ -227,6 +267,10 @@ module.exports = { type: "object", additionalProperties: false, properties: { + asyncWebAssembly: { + description: "Support WebAssembly as asynchronous EcmaScript Module.", + type: "boolean" + }, incrementalRebuild: { description: "Rebuild incrementally", type: "boolean" @@ -949,6 +993,15 @@ module.exports = { cssFilename: { $ref: "#/definitions/CssFilename" }, + enabledWasmLoadingTypes: { + $ref: "#/definitions/EnabledWasmLoadingTypes" + }, + wasmLoading: { + $ref: "#/definitions/WasmLoading" + }, + webassemblyModuleFilename: { + $ref: "#/definitions/WebassemblyModuleFilename" + }, enabledLibraryTypes: { $ref: "#/definitions/EnabledLibraryTypes" }, diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 1a0e032f7305..d5eb110fae53 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -129,11 +129,15 @@ export interface Output { globalObject?: GlobalObject; importFunctionName?: ImportFunctionName; iife?: Iife; + // enabledWasmLoadingTypes?: EnabledWasmLoadingTypes; + // wasmLoading?: WasmLoading; + webassemblyModuleFilename?: WebassemblyModuleFilename; } export type Path = string; export type PublicPath = "auto" | RawPublicPath; export type RawPublicPath = string; export type AssetModuleFilename = string; +export type WebassemblyModuleFilename = string; export type Filename = FilenameTemplate; export type ChunkFilename = FilenameTemplate; export type CssFilename = FilenameTemplate; @@ -163,6 +167,12 @@ export interface LibraryCustomUmdObject { commonjs?: string; root?: string | string[]; } + +export type WasmLoading = false | WasmLoadingType; +export type WasmLoadingType = + | ("fetch-streaming" | "fetch" | "async-node") + | string; + export type LibraryExport = string[] | string; export type LibraryType = | ( @@ -191,6 +201,7 @@ export type UmdNamedDefine = boolean; export type EnabledLibraryTypes = LibraryType[]; export type GlobalObject = string; export type ImportFunctionName = string; +export type EnabledWasmLoadingTypes = WasmLoadingType[]; export interface OutputNormalized { path?: Path; publicPath?: PublicPath; @@ -206,6 +217,9 @@ export interface OutputNormalized { strictModuleErrorHandling?: StrictModuleErrorHandling; globalObject?: GlobalObject; importFunctionName?: ImportFunctionName; + // enabledWasmLoadingTypes?: EnabledWasmLoadingTypes; + // wasmLoading?: WasmLoading; + webassemblyModuleFilename?: WebassemblyModuleFilename; iife?: Iife; } @@ -488,6 +502,7 @@ export type RspackPluginFunction = (this: Compiler, compiler: Compiler) => void; export interface Experiments { lazyCompilation?: boolean; incrementalRebuild?: boolean; + asyncWebAssembly?: boolean; } ///// Watch ///// diff --git a/packages/rspack/tests/Defaults.unittest.ts b/packages/rspack/tests/Defaults.unittest.ts index 6fcae42dd605..0b706275fc10 100644 --- a/packages/rspack/tests/Defaults.unittest.ts +++ b/packages/rspack/tests/Defaults.unittest.ts @@ -116,6 +116,7 @@ describe("snapshots", () => { }, }, "experiments": { + "asyncWebAssembly": false, "incrementalRebuild": true, "lazyCompilation": false, }, @@ -230,6 +231,7 @@ describe("snapshots", () => { "publicPath": "auto", "strictModuleErrorHandling": false, "uniqueName": "@rspack/core", + "webassemblyModuleFilename": "[hash].module.wasm", }, "plugins": [], "resolve": { @@ -241,6 +243,7 @@ describe("snapshots", () => { ".js", ".json", ".d.ts", + ".wasm", ], "mainFields": [ "browser", @@ -432,18 +435,33 @@ describe("snapshots", () => { + "outputModule": true, `) ); - /** - * not support yet - */ + test("async wasm", { experiments: { asyncWebAssembly: true } }, e => e.toMatchInlineSnapshot(` - Expected + Received @@ ... @@ + - "asyncWebAssembly": false, + "asyncWebAssembly": true, + @@ ... @@ + + }, + + Object { + + "rules": Array [ + + Object { + + "descriptionData": Object { + + "type": "module", + + }, + + "resolve": Object { + + "fullySpecified": true, + + }, + + }, + + ], + + "test": /\\.wasm$/i, + + "type": "webassembly/async", `) ); + test( "both wasm", { experiments: { syncWebAssembly: true, asyncWebAssembly: true } }, @@ -453,9 +471,25 @@ describe("snapshots", () => { + Received @@ ... @@ + - "asyncWebAssembly": false, + "asyncWebAssembly": true, @@ ... @@ + "syncWebAssembly": true, + @@ ... @@ + + }, + + Object { + + "rules": Array [ + + Object { + + "descriptionData": Object { + + "type": "module", + + }, + + "resolve": Object { + + "fullySpecified": true, + + }, + + }, + + ], + + "test": /\\.wasm$/i, + + "type": "webassembly/async", `) ); test("const filename", { output: { filename: "bundle.js" } }, e => diff --git a/packages/rspack/tests/case.template.ts b/packages/rspack/tests/case.template.ts index 0d2d2940893e..e1955719b8d0 100644 --- a/packages/rspack/tests/case.template.ts +++ b/packages/rspack/tests/case.template.ts @@ -26,73 +26,87 @@ export function describeCases(config: { name: string; casePath: string }) { }; }); describe(config.name, () => { - for (const category of categories) { - for (const example of category.tests) { - const testRoot = path.resolve( - casesPath, - `./${category.name}/${example}/` - ); - const outputPath = path.resolve(testRoot, `./dist`); - const bundlePath = path.resolve(outputPath, "main.js"); - if ( - [".js", ".jsx", ".ts", ".tsx"].every(ext => { - return !fs.existsSync(path.resolve(testRoot, "index" + ext)); - }) - ) { - continue; - } - describe(category.name, () => { - describe(example, () => { - it(`${example} should compile`, async () => { - const configFile = path.resolve(testRoot, "webpack.config.js"); - let config = {}; - if (fs.existsSync(configFile)) { - config = require(configFile); - } - const options: RspackOptions = { - target: "node", - context: testRoot, - entry: { - main: "./" - }, - mode: "development", - devServer: { - hot: false - }, - infrastructureLogging: { - debug: false - }, - ...config, // we may need to use deepMerge to handle config merge, but we may fix it until we need it - output: { - publicPath: "/", - // @ts-ignore - ...config.output, - path: outputPath - } - }; - const stats = await util.promisify(rspack)(options); - const statsJson = stats!.toJson(); - if (category.name === "errors") { - assert(statsJson.errors!.length > 0); - } else if (category.name === "warnings") { - assert(statsJson.warnings!.length > 0); - } else { - if (statsJson.errors!.length > 0) { - console.log( - `case: ${example}\nerrors:\n`, - `${statsJson.errors!.map(x => x.message).join("\n")}` - ); - } - assert(statsJson.errors!.length === 0); - } + categories.forEach(category => { + category.tests + .filter(test => { + const testDirectory = path.join(casesPath, category.name, test); + const filterPath = path.join(testDirectory, "test.filter.js"); + if (fs.existsSync(filterPath) && !require(filterPath)(config)) { + describe.skip(test, () => { + it("filtered", () => {}); }); - // this will run the compiled test code to test against itself, a genius idea from webpack - it(`${example} should load the compiled test`, async () => { - const context = {}; - vm.createContext(context); - const code = fs.readFileSync(bundlePath, "utf-8"); - const fn = vm.runInThisContext( - ` + return false; + } + return true; + }) + .forEach(example => { + const testRoot = path.resolve( + casesPath, + `./${category.name}/${example}/` + ); + const outputPath = path.resolve(testRoot, `./dist`); + const bundlePath = path.resolve(outputPath, "main.js"); + + if ( + ![".js", ".jsx", ".ts", ".tsx"].every(ext => { + return !fs.existsSync(path.resolve(testRoot, "index" + ext)); + }) + ) { + describe(category.name, () => { + describe(example, () => { + it(`${example} should compile`, async () => { + const configFile = path.resolve( + testRoot, + "webpack.config.js" + ); + let config = {}; + if (fs.existsSync(configFile)) { + config = require(configFile); + } + const options: RspackOptions = { + target: "node", + context: testRoot, + entry: { + main: "./" + }, + mode: "development", + devServer: { + hot: false + }, + infrastructureLogging: { + debug: false + }, + ...config, // we may need to use deepMerge to handle config merge, but we may fix it until we need it + output: { + publicPath: "/", + // @ts-ignore + ...config.output, + path: outputPath + } + }; + const stats = await util.promisify(rspack)(options); + const statsJson = stats!.toJson(); + if (category.name === "errors") { + assert(statsJson.errors!.length > 0); + } else if (category.name === "warnings") { + assert(statsJson.warnings!.length > 0); + } else { + if (statsJson.errors!.length > 0) { + console.log( + `case: ${example}\nerrors:\n`, + `${statsJson.errors!.map(x => x.message).join("\n")}` + ); + } + assert(statsJson.errors!.length === 0); + } + }); + // this will run the compiled test code to test against itself, a genius idea from webpack + it(`${example} should load the compiled test`, async () => { + const context = {}; + vm.createContext(context); + const code = fs.readFileSync(bundlePath, "utf-8"); + const fn = vm.runInThisContext( + ` (function testWrapper(require,_module,exports,__dirname,__filename,it,expect,jest, define){ global.expect = expect; function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; } @@ -100,34 +114,34 @@ export function describeCases(config: { name: string; casePath: string }) { } ) `, - bundlePath - ); - const m = { - exports: {} - }; - fn.call( - m.exports, - function (p) { - return p && p.startsWith(".") - ? require(path.resolve(outputPath, p)) - : require(p); - }, - m, - m.exports, - outputPath, - bundlePath, - _it, - expect, - jest, - define - ); - return m.exports; + bundlePath + ); + const m = { + exports: {} + }; + fn.call( + m.exports, + function (p) { + return p && p.startsWith(".") + ? require(path.resolve(outputPath, p)) + : require(p); + }, + m, + m.exports, + outputPath, + bundlePath, + _it, + expect, + jest, + define + ); + return m.exports; + }); + }); }); - }); + const { it: _it } = createLazyTestEnv(10000); + } }); - - const { it: _it } = createLazyTestEnv(10000); - } - } + }); }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccfcc2a3edc2..c357afd88141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,12 @@ importers: '@vue/babel-plugin-jsx': 1.1.1_@babel+core@7.21.0 babel-loader: 9.1.2_@babel+core@7.21.0 + examples/wasm-simple: + specifiers: + '@rspack/cli': workspace:* + dependencies: + '@rspack/cli': link:../../packages/rspack-cli + npm/darwin-arm64: specifiers: {}