diff --git a/Cargo.lock b/Cargo.lock index 752b5246d..eef9054fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2846,6 +2846,7 @@ dependencies = [ "tungstenite", "twox-hash", "url", + "wasmparser", ] [[package]] @@ -7071,6 +7072,19 @@ version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +[[package]] +name = "wasmparser" +version = "0.207.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.6.0", + "hashbrown 0.14.5", + "indexmap 2.5.0", + "semver 1.0.23", +] + [[package]] name = "wayland-client" version = "0.29.5" diff --git a/crates/mako/Cargo.toml b/crates/mako/Cargo.toml index 21361681c..47301e37e 100644 --- a/crates/mako/Cargo.toml +++ b/crates/mako/Cargo.toml @@ -114,6 +114,7 @@ tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tungstenite = "0.19.0" twox-hash = "1.6.3" +wasmparser = "0.207.0" [dev-dependencies] insta = { version = "1.30.0", features = ["yaml"] } diff --git a/crates/mako/src/build/load.rs b/crates/mako/src/build/load.rs index 401fd9e52..80561f24a 100644 --- a/crates/mako/src/build/load.rs +++ b/crates/mako/src/build/load.rs @@ -36,7 +36,6 @@ const CSS_EXTENSIONS: [&str; 1] = ["css"]; const JSON_EXTENSIONS: [&str; 2] = ["json", "json5"]; const YAML_EXTENSIONS: [&str; 2] = ["yaml", "yml"]; const XML_EXTENSIONS: [&str; 1] = ["xml"]; -const WASM_EXTENSIONS: [&str; 1] = ["wasm"]; const TOML_EXTENSIONS: [&str; 1] = ["toml"]; const SVG_EXTENSIONS: [&str; 1] = ["svg"]; const MD_EXTENSIONS: [&str; 2] = ["md", "mdx"]; @@ -180,27 +179,6 @@ export function moduleToDom(css) { })); } - // wasm - if WASM_EXTENSIONS.contains(&file.extname.as_str()) { - let final_file_name = format!( - "{}.{}.{}", - file.get_file_stem(), - file.get_content_hash()?, - file.extname - ); - context.emit_assets( - file.pathname.to_string_lossy().to_string(), - final_file_name.clone(), - ); - return Ok(Content::Js(JsContent { - content: format!( - "module.exports = require._interopreRequireWasm(exports, \"{}\")", - final_file_name - ), - ..Default::default() - })); - } - // xml if XML_EXTENSIONS.contains(&file.extname.as_str()) { let content = FileSystem::read_file(&file.pathname)?; diff --git a/crates/mako/src/plugins/wasm_runtime.rs b/crates/mako/src/plugins/wasm_runtime.rs index 9feb48973..8087bd106 100644 --- a/crates/mako/src/plugins/wasm_runtime.rs +++ b/crates/mako/src/plugins/wasm_runtime.rs @@ -1,12 +1,19 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; use std::sync::Arc; use anyhow; +use wasmparser::{Import, Parser, Payload}; +use crate::ast::file::{Content, JsContent}; use crate::compiler::Context; -use crate::plugin::Plugin; +use crate::plugin::{Plugin, PluginLoadParam}; pub struct WasmRuntimePlugin {} +const WASM_EXTENSIONS: [&str; 1] = ["wasm"]; + impl Plugin for WasmRuntimePlugin { fn name(&self) -> &str { "wasm_runtime" @@ -27,4 +34,139 @@ impl Plugin for WasmRuntimePlugin { Ok(vec![]) } } + + fn load( + &self, + param: &PluginLoadParam, + _context: &Arc, + ) -> anyhow::Result> { + let file = param.file; + + if WASM_EXTENSIONS.contains(&file.extname.as_str()) { + let final_file_name = format!( + "{}.{}.{}", + file.get_file_stem(), + file.get_content_hash()?, + file.extname + ); + _context.emit_assets( + file.pathname.to_string_lossy().to_string(), + final_file_name.clone(), + ); + + let mut buffer = Vec::new(); + File::open(&file.path)?.read_to_end(&mut buffer)?; + // Parse wasm file to get imports + let mut wasm_import_object_map: HashMap<&str, Vec> = HashMap::new(); + Parser::new(0).parse_all(&buffer).for_each(|payload| { + if let Ok(Payload::ImportSection(imports)) = payload { + imports.into_iter_with_offsets().for_each(|import| { + if let Ok(( + _, + Import { + module, + name, + ty: _, + }, + )) = import + { + if let Some(import_object) = wasm_import_object_map.get_mut(module) { + import_object.push(name.to_string()); + } else { + wasm_import_object_map.insert(module, vec![name.to_string()]); + } + } + }); + } + }); + + let mut module_import_code = String::new(); + let mut wasm_import_object_code = String::new(); + + for (index, (key, value)) in wasm_import_object_map.iter().enumerate() { + module_import_code.push_str(&format!( + "import * as module{module_idx} from \"{module}\";\n", + module_idx = index, + module = key + )); + + wasm_import_object_code.push_str(&format!( + "\"{module}\": {{ {names} }}", + module = key, + names = value + .iter() + .map(|name| format!("\"{}\": module{}[\"{}\"]", name, index, name)) + .collect::>() + .join(", ") + )); + } + + let mut content = String::new(); + content.push_str(&module_import_code); + + if wasm_import_object_code.is_empty() { + content.push_str(&format!( + "module.exports = require._interopreRequireWasm(exports, \"{}\")", + final_file_name + )); + } else { + content.push_str(&format!( + "module.exports = require._interopreRequireWasm(exports, \"{}\", {{{}}})", + final_file_name, wasm_import_object_code + )); + } + + return Ok(Some(Content::Js(JsContent { + content, + ..Default::default() + }))); + } + + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::ast::file::File; + use crate::compiler::Context; + + #[test] + fn test_wasm_runtime_load_with_import_object() { + let plugin = WasmRuntimePlugin {}; + let context = Arc::new(Context { + ..Default::default() + }); + let wasm_relative_path = + std::path::Path::new("../../examples/import-resources/minus-wasm-pack/index_bg.wasm"); + let wasm_path = std::fs::canonicalize(wasm_relative_path).unwrap(); + let file = File::new(wasm_path.to_string_lossy().to_string(), context.clone()); + let param = PluginLoadParam { file: &file }; + let result: Option = plugin.load(¶m, &context).unwrap(); + + assert!(result.is_some()); + if let Some(Content::Js(js_content)) = result { + assert!(js_content.content.contains("import * as module0 from")); + } + } + + #[test] + fn test_wasm_runtime_load_without_import_object() { + let plugin = WasmRuntimePlugin {}; + let context = Arc::new(Context { + ..Default::default() + }); + let wasm_relative_path = std::path::Path::new("../../examples/import-resources/add.wasm"); + let wasm_path = std::fs::canonicalize(wasm_relative_path).unwrap(); + let file = File::new(wasm_path.to_string_lossy().to_string(), context.clone()); + let param = PluginLoadParam { file: &file }; + let result = plugin.load(¶m, &context).unwrap(); + assert!(result.is_some()); + if let Some(Content::Js(js_content)) = result { + assert!(!js_content.content.contains("import * as module0 from")) + } + } } diff --git a/examples/import-resources/import-js-wasm/index.d.ts b/examples/import-resources/import-js-wasm/index.d.ts new file mode 100644 index 000000000..be66165bc --- /dev/null +++ b/examples/import-resources/import-js-wasm/index.d.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export function run(): void; diff --git a/examples/import-resources/import-js-wasm/index.js b/examples/import-resources/import-js-wasm/index.js new file mode 100644 index 000000000..76aa1cca7 --- /dev/null +++ b/examples/import-resources/import-js-wasm/index.js @@ -0,0 +1,5 @@ +import * as wasm from './index_bg.wasm'; +export * from './index_bg.js'; +import { __wbg_set_wasm } from './index_bg.js'; +__wbg_set_wasm(wasm); +wasm.__wbindgen_start(); diff --git a/examples/import-resources/import-js-wasm/index_bg.js b/examples/import-resources/import-js-wasm/index_bg.js new file mode 100644 index 000000000..5a0246201 --- /dev/null +++ b/examples/import-resources/import-js-wasm/index_bg.js @@ -0,0 +1,199 @@ +import { + MyClass, + name, +} from './snippets/import_js-ebef8887557a6fbe/defined-in-js.js'; + +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + +const lTextDecoder = + typeof TextDecoder === 'undefined' + ? (0, module.require)('util').TextDecoder + : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if ( + cachedUint8ArrayMemory0 === null || + cachedUint8ArrayMemory0.byteLength === 0 + ) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode( + getUint8ArrayMemory0().subarray(ptr, ptr + len), + ); +} + +let WASM_VECTOR_LEN = 0; + +const lTextEncoder = + typeof TextEncoder === 'undefined' + ? (0, module.require)('util').TextEncoder + : TextEncoder; + +let cachedTextEncoder = new lTextEncoder('utf-8'); + +const encodeString = + typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + }; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedDataViewMemory0 = null; + +function getDataViewMemory0() { + if ( + cachedDataViewMemory0 === null || + cachedDataViewMemory0.buffer.detached === true || + (cachedDataViewMemory0.buffer.detached === undefined && + cachedDataViewMemory0.buffer !== wasm.memory.buffer) + ) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function getObject(idx) { + return heap[idx]; +} + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +export function run() { + wasm.run(); +} + +export function __wbg_log_b2db2c5816f1d07e(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +} + +export function __wbg_name_fca994005bad39be(arg0) { + const ret = name(); + const ptr1 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); +} + +export function __wbg_new_74968321906dc678() { + const ret = new MyClass(); + return addHeapObject(ret); +} + +export function __wbg_number_6ea25e36c02a28e3(arg0) { + const ret = getObject(arg0).number; + return ret; +} + +export function __wbg_render_40d583ad633779da(arg0, arg1) { + const ret = getObject(arg1).render(); + const ptr1 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); +} + +export function __wbg_setnumber_a86efb097c245512(arg0, arg1) { + const ret = (getObject(arg0).number = arg1 >>> 0); + return addHeapObject(ret); +} + +export function __wbindgen_object_drop_ref(arg0) { + takeObject(arg0); +} diff --git a/examples/import-resources/import-js-wasm/index_bg.wasm b/examples/import-resources/import-js-wasm/index_bg.wasm new file mode 100644 index 000000000..7b3192882 Binary files /dev/null and b/examples/import-resources/import-js-wasm/index_bg.wasm differ diff --git a/examples/import-resources/import-js-wasm/index_bg.wasm.d.ts b/examples/import-resources/import-js-wasm/index_bg.wasm.d.ts new file mode 100644 index 000000000..d8cde4a8f --- /dev/null +++ b/examples/import-resources/import-js-wasm/index_bg.wasm.d.ts @@ -0,0 +1,12 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const run: () => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: ( + a: number, + b: number, + c: number, + d: number, +) => number; +export const __wbindgen_start: () => void; diff --git a/examples/import-resources/import-js-wasm/snippets/import_js-ebef8887557a6fbe/defined-in-js.js b/examples/import-resources/import-js-wasm/snippets/import_js-ebef8887557a6fbe/defined-in-js.js new file mode 100644 index 000000000..d948845cd --- /dev/null +++ b/examples/import-resources/import-js-wasm/snippets/import_js-ebef8887557a6fbe/defined-in-js.js @@ -0,0 +1,21 @@ +export function name() { + return 'Rust'; +} + +export class MyClass { + constructor() { + this._number = 42; + } + + get number() { + return this._number; + } + + set number(n) { + return (this._number = n); + } + + render() { + return `My number is: ${this.number}`; + } +} diff --git a/examples/import-resources/index.tsx b/examples/import-resources/index.tsx index f5313c77e..f853d8803 100644 --- a/examples/import-resources/index.tsx +++ b/examples/import-resources/index.tsx @@ -7,6 +7,10 @@ import toml, { title } from './index.toml'; import xml from './index.xml'; import yaml, { pi } from './index.yaml'; import MailchimpUnsplash from './mailchimp-unsplash.jpg'; +import * as wasm from './minus-wasm-pack'; + +// https://github.com/rustwasm/wasm-bindgen/tree/main/examples/import_js +import('./import-js-wasm').catch(console.error); const num1 = 10; const num2 = 20; @@ -32,6 +36,10 @@ function App() {

Test import .wasm file async: {num1} + {num2} = {sum}

+

+ Test import .wasm file(generated by wasm-pack) async: {num1} - {num2} ={' '} + {wasm.minus(num1, num2)} +

Test import .toml file

{JSON.stringify(toml, null, 2)}
diff --git a/examples/import-resources/minus-wasm-pack/index.d.ts b/examples/import-resources/minus-wasm-pack/index.d.ts new file mode 100644 index 000000000..1a32e9455 --- /dev/null +++ b/examples/import-resources/minus-wasm-pack/index.d.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + */ +export function greet(): void; +/** + * @param {string} name + */ +export function greet2(name: string): void; +/** + * @param {number} a + * @param {number} b + * @returns {number} + */ +export function minus(a: number, b: number): number; diff --git a/examples/import-resources/minus-wasm-pack/index.js b/examples/import-resources/minus-wasm-pack/index.js new file mode 100644 index 000000000..870892045 --- /dev/null +++ b/examples/import-resources/minus-wasm-pack/index.js @@ -0,0 +1,4 @@ +import { __wbg_set_wasm } from './index_bg.js'; +import * as wasm from './index_bg.wasm'; +__wbg_set_wasm(wasm); +export * from './index_bg.js'; diff --git a/examples/import-resources/minus-wasm-pack/index_bg.js b/examples/import-resources/minus-wasm-pack/index_bg.js new file mode 100644 index 000000000..e0cd54229 --- /dev/null +++ b/examples/import-resources/minus-wasm-pack/index_bg.js @@ -0,0 +1,133 @@ +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + +const lTextDecoder = + typeof TextDecoder === 'undefined' + ? (0, module.require)('util').TextDecoder + : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if ( + cachedUint8ArrayMemory0 === null || + cachedUint8ArrayMemory0.byteLength === 0 + ) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode( + getUint8ArrayMemory0().subarray(ptr, ptr + len), + ); +} +/** + */ +export function greet() { + wasm.greet(); +} + +let WASM_VECTOR_LEN = 0; + +const lTextEncoder = + typeof TextEncoder === 'undefined' + ? (0, module.require)('util').TextEncoder + : TextEncoder; + +let cachedTextEncoder = new lTextEncoder('utf-8'); + +const encodeString = + typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + }; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} +/** + * @param {string} name + */ +export function greet2(name) { + const ptr0 = passStringToWasm0( + name, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len0 = WASM_VECTOR_LEN; + wasm.greet2(ptr0, len0); +} + +/** + * @param {number} a + * @param {number} b + * @returns {number} + */ +export function minus(a, b) { + const ret = wasm.minus(a, b); + return ret; +} + +export function __wbg_alert_f837f172b2a24942(arg0, arg1) { + alert(getStringFromWasm0(arg0, arg1)); +} + +export function __wbg_prompt_ec584a06a1c7c28b(arg0, arg1) { + prompt(getStringFromWasm0(arg0, arg1)); +} diff --git a/examples/import-resources/minus-wasm-pack/index_bg.wasm b/examples/import-resources/minus-wasm-pack/index_bg.wasm new file mode 100644 index 000000000..a50256cbd Binary files /dev/null and b/examples/import-resources/minus-wasm-pack/index_bg.wasm differ diff --git a/examples/import-resources/minus-wasm-pack/index_bg.wasm.d.ts b/examples/import-resources/minus-wasm-pack/index_bg.wasm.d.ts new file mode 100644 index 000000000..183cf3457 --- /dev/null +++ b/examples/import-resources/minus-wasm-pack/index_bg.wasm.d.ts @@ -0,0 +1,13 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function greet2(a: number, b: number): void; +export function minus(a: number, b: number): number; +export function greet(): void; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_realloc( + a: number, + b: number, + c: number, + d: number, +): number;