mirror of
https://github.com/denoland/deno.git
synced 2025-01-11 16:42:21 -05:00
BREAKING: Remove support for .wasm imports (#5135)
Importing .wasm files is non-standardized therefore deciding to support current functionality past 1.0 release is risky. Besides that .wasm import posed many challenges in our codebase due to complex interactions with TS compiler which spawned thread for each encountered .wasm import. This commit removes: - cli/compilers/wasm.rs - cli/compilers/wasm_wrap.js - two integration tests related to .wasm imports
This commit is contained in:
parent
93cf3bd534
commit
2b66b8a03e
20 changed files with 5 additions and 388 deletions
|
@ -6,14 +6,12 @@ use futures::Future;
|
|||
mod compiler_worker;
|
||||
mod js;
|
||||
mod ts;
|
||||
mod wasm;
|
||||
|
||||
pub use js::JsCompiler;
|
||||
pub use ts::runtime_compile;
|
||||
pub use ts::runtime_transpile;
|
||||
pub use ts::TargetLib;
|
||||
pub use ts::TsCompiler;
|
||||
pub use wasm::WasmCompiler;
|
||||
|
||||
pub type CompilationResultFuture = dyn Future<Output = JsonResult>;
|
||||
|
||||
|
|
|
@ -691,8 +691,6 @@ impl TsCompiler {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO(bartlomieju): exactly same function is in `wasm.rs` - only difference
|
||||
// it created WasmCompiler instead of TsCompiler - deduplicate
|
||||
async fn execute_in_thread(
|
||||
global_state: GlobalState,
|
||||
req: Buf,
|
||||
|
|
|
@ -1,185 +0,0 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
use super::compiler_worker::CompilerWorker;
|
||||
use crate::compilers::CompiledModule;
|
||||
use crate::file_fetcher::SourceFile;
|
||||
use crate::global_state::GlobalState;
|
||||
use crate::startup_data;
|
||||
use crate::state::*;
|
||||
use crate::tokio_util;
|
||||
use crate::web_worker::WebWorkerHandle;
|
||||
use crate::worker::WorkerEvent;
|
||||
use deno_core::Buf;
|
||||
use deno_core::ErrBox;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use url::Url;
|
||||
|
||||
// TODO(ry) The entire concept of spawning a thread, sending data to JS,
|
||||
// compiling WASM there, and moving the data back into the calling thread is
|
||||
// completelly wrong. V8 has native facilities for getting this information.
|
||||
// We might be lacking bindings for this currently in rusty_v8 but ultimately
|
||||
// this "compiler" should be calling into rusty_v8 directly, not spawning
|
||||
// threads.
|
||||
|
||||
// TODO(kevinkassimo): This is a hack to encode/decode data as base64 string.
|
||||
// (Since Deno namespace might not be available, Deno.read can fail).
|
||||
// Binary data is already available through source_file.source_code.
|
||||
// If this is proven too wasteful in practice, refactor this.
|
||||
|
||||
// Ref: https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration
|
||||
// https://github.com/nodejs/node/blob/35ec01097b2a397ad0a22aac536fe07514876e21/lib/internal/modules/esm/translators.js#L190-L210
|
||||
|
||||
// Dynamically construct JS wrapper with custom static imports and named exports.
|
||||
// Boots up an internal worker to resolve imports/exports through query from V8.
|
||||
|
||||
static WASM_WRAP: &str = include_str!("./wasm_wrap.js");
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WasmModuleInfo {
|
||||
import_list: Vec<String>,
|
||||
export_list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WasmCompiler {
|
||||
cache: Arc<Mutex<HashMap<Url, CompiledModule>>>,
|
||||
}
|
||||
|
||||
impl WasmCompiler {
|
||||
/// Create a new V8 worker with snapshot of WASM compiler and setup compiler's runtime.
|
||||
fn setup_worker(global_state: GlobalState) -> CompilerWorker {
|
||||
let entry_point =
|
||||
ModuleSpecifier::resolve_url_or_path("./__$deno$wasm_compiler.ts")
|
||||
.unwrap();
|
||||
let worker_state =
|
||||
State::new(global_state.clone(), None, entry_point, DebugType::Internal)
|
||||
.expect("Unable to create worker state");
|
||||
|
||||
// Count how many times we start the compiler worker.
|
||||
global_state.compiler_starts.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let mut worker = CompilerWorker::new(
|
||||
"WASM".to_string(),
|
||||
startup_data::compiler_isolate_init(),
|
||||
worker_state,
|
||||
);
|
||||
worker.execute("bootstrap.wasmCompilerRuntime()").unwrap();
|
||||
worker
|
||||
}
|
||||
|
||||
pub async fn compile(
|
||||
&self,
|
||||
global_state: GlobalState,
|
||||
source_file: &SourceFile,
|
||||
) -> Result<CompiledModule, ErrBox> {
|
||||
let cache = self.cache.clone();
|
||||
let cache_ = self.cache.clone();
|
||||
let source_file = source_file.clone();
|
||||
|
||||
let maybe_cached = { cache.lock().unwrap().get(&source_file.url).cloned() };
|
||||
if let Some(m) = maybe_cached {
|
||||
return Ok(m);
|
||||
}
|
||||
debug!(">>>>> wasm_compile START");
|
||||
let base64_data = base64::encode(&source_file.source_code);
|
||||
let url = source_file.url.clone();
|
||||
let req_msg = serde_json::to_string(&base64_data)
|
||||
.unwrap()
|
||||
.into_boxed_str()
|
||||
.into_boxed_bytes();
|
||||
let msg = execute_in_thread(global_state.clone(), req_msg).await?;
|
||||
debug!("Received message from worker");
|
||||
let module_info: WasmModuleInfo = serde_json::from_slice(&msg).unwrap();
|
||||
debug!("WASM module info: {:#?}", &module_info);
|
||||
let code = wrap_wasm_code(
|
||||
&base64_data,
|
||||
&module_info.import_list,
|
||||
&module_info.export_list,
|
||||
);
|
||||
debug!("Generated code: {}", &code);
|
||||
let module = CompiledModule {
|
||||
code,
|
||||
name: url.to_string(),
|
||||
};
|
||||
{
|
||||
cache_.lock().unwrap().insert(url.clone(), module.clone());
|
||||
}
|
||||
debug!("<<<<< wasm_compile END");
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_in_thread(
|
||||
global_state: GlobalState,
|
||||
req: Buf,
|
||||
) -> Result<Buf, ErrBox> {
|
||||
let (handle_sender, handle_receiver) =
|
||||
std::sync::mpsc::sync_channel::<Result<WebWorkerHandle, ErrBox>>(1);
|
||||
let builder =
|
||||
std::thread::Builder::new().name("deno-wasm-compiler".to_string());
|
||||
let join_handle = builder.spawn(move || {
|
||||
let worker = WasmCompiler::setup_worker(global_state);
|
||||
handle_sender.send(Ok(worker.thread_safe_handle())).unwrap();
|
||||
drop(handle_sender);
|
||||
tokio_util::run_basic(worker).expect("Panic in event loop");
|
||||
})?;
|
||||
let handle = handle_receiver.recv().unwrap()?;
|
||||
handle.post_message(req)?;
|
||||
let event = handle.get_event().await.expect("Compiler didn't respond");
|
||||
let buf = match event {
|
||||
WorkerEvent::Message(buf) => Ok(buf),
|
||||
WorkerEvent::Error(error) => Err(error),
|
||||
WorkerEvent::TerminalError(error) => Err(error),
|
||||
}?;
|
||||
// Shutdown worker and wait for thread to finish
|
||||
handle.terminate();
|
||||
join_handle.join().unwrap();
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn build_single_import(index: usize, origin: &str) -> String {
|
||||
let origin_json = serde_json::to_string(origin).unwrap();
|
||||
format!(
|
||||
r#"import * as m{} from {};
|
||||
importObject[{}] = m{};
|
||||
"#,
|
||||
index, &origin_json, &origin_json, index
|
||||
)
|
||||
}
|
||||
|
||||
fn build_imports(imports: &[String]) -> String {
|
||||
let mut code = String::from("");
|
||||
for (index, origin) in imports.iter().enumerate() {
|
||||
code.push_str(&build_single_import(index, origin));
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
fn build_single_export(name: &str) -> String {
|
||||
format!("export const {} = instance.exports.{};\n", name, name)
|
||||
}
|
||||
|
||||
fn build_exports(exports: &[String]) -> String {
|
||||
let mut code = String::from("");
|
||||
for e in exports {
|
||||
code.push_str(&build_single_export(e));
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
fn wrap_wasm_code(
|
||||
base64_data: &str,
|
||||
imports: &[String],
|
||||
exports: &[String],
|
||||
) -> String {
|
||||
let imports_code = build_imports(imports);
|
||||
let exports_code = build_exports(exports);
|
||||
String::from(WASM_WRAP)
|
||||
.replace("//IMPORTS\n", &imports_code)
|
||||
.replace("//EXPORTS\n", &exports_code)
|
||||
.replace("BASE64_DATA", base64_data)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
// @ts-nocheck
|
||||
const importObject = Object.create(null);
|
||||
//IMPORTS
|
||||
|
||||
function base64ToUint8Array(data) {
|
||||
const binString = window.atob(data);
|
||||
const size = binString.length;
|
||||
const bytes = new Uint8Array(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
bytes[i] = binString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const buffer = base64ToUint8Array("BASE64_DATA");
|
||||
const compiled = await WebAssembly.compile(buffer);
|
||||
|
||||
const instance = new WebAssembly.Instance(compiled, importObject);
|
||||
|
||||
//EXPORTS
|
|
@ -3,7 +3,6 @@ use crate::compilers::CompiledModule;
|
|||
use crate::compilers::JsCompiler;
|
||||
use crate::compilers::TargetLib;
|
||||
use crate::compilers::TsCompiler;
|
||||
use crate::compilers::WasmCompiler;
|
||||
use crate::deno_dir;
|
||||
use crate::file_fetcher::SourceFileFetcher;
|
||||
use crate::flags;
|
||||
|
@ -36,7 +35,6 @@ pub struct GlobalStateInner {
|
|||
pub file_fetcher: SourceFileFetcher,
|
||||
pub js_compiler: JsCompiler,
|
||||
pub ts_compiler: TsCompiler,
|
||||
pub wasm_compiler: WasmCompiler,
|
||||
pub lockfile: Option<Mutex<Lockfile>>,
|
||||
pub compiler_starts: AtomicUsize,
|
||||
compile_lock: AsyncMutex<()>,
|
||||
|
@ -87,7 +85,6 @@ impl GlobalState {
|
|||
file_fetcher,
|
||||
ts_compiler,
|
||||
js_compiler: JsCompiler {},
|
||||
wasm_compiler: WasmCompiler::default(),
|
||||
lockfile,
|
||||
compiler_starts: AtomicUsize::new(0),
|
||||
compile_lock: AsyncMutex::new(()),
|
||||
|
@ -116,12 +113,6 @@ impl GlobalState {
|
|||
let compile_lock = self.compile_lock.lock().await;
|
||||
|
||||
let compiled_module = match out.media_type {
|
||||
msg::MediaType::Json | msg::MediaType::Unknown => {
|
||||
state1.js_compiler.compile(out).await
|
||||
}
|
||||
msg::MediaType::Wasm => {
|
||||
state1.wasm_compiler.compile(state1.clone(), &out).await
|
||||
}
|
||||
msg::MediaType::TypeScript
|
||||
| msg::MediaType::TSX
|
||||
| msg::MediaType::JSX => {
|
||||
|
@ -152,6 +143,7 @@ impl GlobalState {
|
|||
state1.js_compiler.compile(out).await
|
||||
}
|
||||
}
|
||||
_ => state1.js_compiler.compile(out).await,
|
||||
}?;
|
||||
drop(compile_lock);
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ fn compiler_snapshot() {
|
|||
deno_core::js_check(isolate.execute(
|
||||
"<anon>",
|
||||
r#"
|
||||
if (!(bootstrap.tsCompilerRuntime && bootstrap.wasmCompilerRuntime)) {
|
||||
if (!(bootstrap.tsCompilerRuntime)) {
|
||||
throw Error("bad");
|
||||
}
|
||||
console.log(`ts version: ${ts.version}`);
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
// This module is the entry point for "compiler" isolate, ie. the one
|
||||
// that is created when Deno needs to compile TS/WASM to JS.
|
||||
//
|
||||
// It provides a two functions that should be called by Rust:
|
||||
// It provides a single functions that should be called by Rust:
|
||||
// - `bootstrapTsCompilerRuntime`
|
||||
// - `bootstrapWasmCompilerRuntime`
|
||||
// Either of these functions must be called when creating isolate
|
||||
// This functions must be called when creating isolate
|
||||
// to properly setup runtime.
|
||||
|
||||
// NOTE: this import has side effects!
|
||||
|
@ -22,7 +21,6 @@ import { sendAsync, sendSync } from "./ops/dispatch_json.ts";
|
|||
import { bootstrapWorkerRuntime } from "./runtime_worker.ts";
|
||||
import { assert, log } from "./util.ts";
|
||||
import * as util from "./util.ts";
|
||||
import { atob } from "./web/text_encoding.ts";
|
||||
import { TextDecoder, TextEncoder } from "./web/text_encoding.ts";
|
||||
import { core } from "./core.ts";
|
||||
|
||||
|
@ -1123,16 +1121,6 @@ function commonPath(paths: string[], sep = "/"): string {
|
|||
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
|
||||
}
|
||||
|
||||
function base64ToUint8Array(data: string): Uint8Array {
|
||||
const binString = atob(data);
|
||||
const size = binString.length;
|
||||
const bytes = new Uint8Array(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
bytes[i] = binString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
let rootExports: string[] | undefined;
|
||||
|
||||
function normalizeUrl(rootName: string): string {
|
||||
|
@ -1585,43 +1573,11 @@ async function tsCompilerOnMessage({
|
|||
// Currently Rust shuts down worker after single request
|
||||
}
|
||||
|
||||
async function wasmCompilerOnMessage({
|
||||
data: binary,
|
||||
}: {
|
||||
data: string;
|
||||
}): Promise<void> {
|
||||
const buffer = base64ToUint8Array(binary);
|
||||
// @ts-ignore
|
||||
const compiled = await WebAssembly.compile(buffer);
|
||||
|
||||
util.log(">>> WASM compile start");
|
||||
|
||||
const importList = Array.from(
|
||||
// @ts-ignore
|
||||
new Set(WebAssembly.Module.imports(compiled).map(({ module }) => module))
|
||||
);
|
||||
const exportList = Array.from(
|
||||
// @ts-ignore
|
||||
new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name))
|
||||
);
|
||||
|
||||
globalThis.postMessage({ importList, exportList });
|
||||
|
||||
util.log("<<< WASM compile end");
|
||||
|
||||
// Currently Rust shuts down worker after single request
|
||||
}
|
||||
|
||||
function bootstrapTsCompilerRuntime(): void {
|
||||
bootstrapWorkerRuntime("TS", false);
|
||||
globalThis.onmessage = tsCompilerOnMessage;
|
||||
}
|
||||
|
||||
function bootstrapWasmCompilerRuntime(): void {
|
||||
bootstrapWorkerRuntime("WASM", false);
|
||||
globalThis.onmessage = wasmCompilerOnMessage;
|
||||
}
|
||||
|
||||
// Removes the `__proto__` for security reasons. This intentionally makes
|
||||
// Deno non compliant with ECMA-262 Annex B.2.2.1
|
||||
//
|
||||
|
@ -1632,7 +1588,6 @@ Object.defineProperties(globalThis, {
|
|||
bootstrap: {
|
||||
value: {
|
||||
...globalThis.bootstrap,
|
||||
wasmCompilerRuntime: bootstrapWasmCompilerRuntime,
|
||||
tsCompilerRuntime: bootstrapTsCompilerRuntime,
|
||||
},
|
||||
configurable: true,
|
||||
|
|
|
@ -147,7 +147,6 @@ declare global {
|
|||
workerRuntime: ((name: string) => Promise<void> | void) | undefined;
|
||||
// Assigned to `self` global - compiler
|
||||
tsCompilerRuntime: (() => void) | undefined;
|
||||
wasmCompilerRuntime: (() => void) | undefined;
|
||||
};
|
||||
|
||||
var onerror:
|
||||
|
|
|
@ -3,7 +3,6 @@ use super::dispatch_json::Deserialize;
|
|||
use super::dispatch_json::JsonOp;
|
||||
use super::dispatch_json::Value;
|
||||
use crate::futures::future::try_join_all;
|
||||
use crate::msg;
|
||||
use crate::op_error::OpError;
|
||||
use crate::state::State;
|
||||
use deno_core::CoreIsolate;
|
||||
|
@ -125,21 +124,7 @@ fn op_fetch_source_files(
|
|||
}
|
||||
_ => f,
|
||||
};
|
||||
// Special handling of WASM and JSON files:
|
||||
// compile them into JS first!
|
||||
// This allows TS to do correct export types as well as bundles.
|
||||
let source_code = match file.media_type {
|
||||
msg::MediaType::Wasm => {
|
||||
global_state
|
||||
.wasm_compiler
|
||||
.compile(global_state.clone(), &file)
|
||||
.await
|
||||
.map_err(|e| OpError::other(e.to_string()))?
|
||||
.code
|
||||
}
|
||||
_ => String::from_utf8(file.source_code)
|
||||
.map_err(|_| OpError::invalid_utf8())?,
|
||||
};
|
||||
let source_code = String::from_utf8(file.source_code).map_err(|_| OpError::invalid_utf8())?;
|
||||
Ok::<_, OpError>(json!({
|
||||
"url": file.url.to_string(),
|
||||
"filename": file.filename.to_str().unwrap(),
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { add, addImported, addRemote } from "./051_wasm_import/simple.wasm";
|
||||
import { state } from "./051_wasm_import/wasm-dep.js";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown, msg?: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(state, "WASM Start Executed", "Incorrect state");
|
||||
|
||||
assertEquals(add(10, 20), 30, "Incorrect add");
|
||||
|
||||
assertEquals(addImported(0), 42, "Incorrect addImported");
|
||||
|
||||
assertEquals(state, "WASM JS Function Executed", "Incorrect state");
|
||||
|
||||
assertEquals(addImported(1), 43, "Incorrect addImported");
|
||||
|
||||
assertEquals(addRemote(1), 2020, "Incorrect addRemote");
|
||||
|
||||
console.log("Passed");
|
|
@ -1 +0,0 @@
|
|||
Passed
|
|
@ -1,3 +0,0 @@
|
|||
export function jsRemoteFn(): number {
|
||||
return 2019;
|
||||
}
|
Binary file not shown.
|
@ -1,31 +0,0 @@
|
|||
;; From https://github.com/nodejs/node/blob/bbc254db5db672643aad89a436a4938412a5704e/test/fixtures/es-modules/simple.wat
|
||||
;; MIT Licensed
|
||||
;; $ wat2wasm simple.wat -o simple.wasm
|
||||
|
||||
(module
|
||||
(import "./wasm-dep.js" "jsFn" (func $jsFn (result i32)))
|
||||
(import "./wasm-dep.js" "jsInitFn" (func $jsInitFn))
|
||||
(import "http://127.0.0.1:4545/cli/tests/051_wasm_import/remote.ts" "jsRemoteFn" (func $jsRemoteFn (result i32)))
|
||||
(export "add" (func $add))
|
||||
(export "addImported" (func $addImported))
|
||||
(export "addRemote" (func $addRemote))
|
||||
(start $startFn)
|
||||
(func $startFn
|
||||
call $jsInitFn
|
||||
)
|
||||
(func $add (param $a i32) (param $b i32) (result i32)
|
||||
local.get $a
|
||||
local.get $b
|
||||
i32.add
|
||||
)
|
||||
(func $addImported (param $a i32) (result i32)
|
||||
local.get $a
|
||||
call $jsFn
|
||||
i32.add
|
||||
)
|
||||
(func $addRemote (param $a i32) (result i32)
|
||||
local.get $a
|
||||
call $jsRemoteFn
|
||||
i32.add
|
||||
)
|
||||
)
|
|
@ -1,17 +0,0 @@
|
|||
function assertEquals(actual, expected, msg) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(msg || "");
|
||||
}
|
||||
}
|
||||
|
||||
export function jsFn() {
|
||||
state = "WASM JS Function Executed";
|
||||
return 42;
|
||||
}
|
||||
|
||||
export let state = "JS Function Executed";
|
||||
|
||||
export function jsInitFn() {
|
||||
assertEquals(state, "JS Function Executed", "Incorrect state");
|
||||
state = "WASM Start Executed";
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import * as wasm from "./055_import_wasm_via_network.wasm";
|
||||
console.log(wasm);
|
|
@ -1,5 +0,0 @@
|
|||
Module {
|
||||
add_one: [Function: 0],
|
||||
memory: WebAssembly.Memory {},
|
||||
Symbol(Symbol.toStringTag): "Module"
|
||||
}
|
Binary file not shown.
|
@ -1143,12 +1143,6 @@ itest_ignore!(_049_info_flag_script_jsx {
|
|||
http_server: true,
|
||||
});
|
||||
|
||||
itest!(_051_wasm_import {
|
||||
args: "run --reload --allow-net --allow-read 051_wasm_import.ts",
|
||||
output: "051_wasm_import.ts.out",
|
||||
http_server: true,
|
||||
});
|
||||
|
||||
// TODO(ry) Re-enable flaky test https://github.com/denoland/deno/issues/4049
|
||||
itest_ignore!(_052_no_remote_flag {
|
||||
args:
|
||||
|
@ -1165,12 +1159,6 @@ itest!(_054_info_local_imports {
|
|||
exit_code: 0,
|
||||
});
|
||||
|
||||
itest!(_055_import_wasm_via_network {
|
||||
args: "run --reload http://127.0.0.1:4545/cli/tests/055_import_wasm_via_network.ts",
|
||||
output: "055_import_wasm_via_network.ts.out",
|
||||
http_server: true,
|
||||
});
|
||||
|
||||
itest!(_056_make_temp_file_write_perm {
|
||||
args:
|
||||
"run --allow-read --allow-write=./subdir/ 056_make_temp_file_write_perm.ts",
|
||||
|
|
|
@ -17,15 +17,3 @@ const wasmInstance = new WebAssembly.Instance(wasmModule);
|
|||
console.log(wasmInstance.exports.main().toString());
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### ES Module style imports
|
||||
|
||||
> This is an unstable feature. Learn more about
|
||||
> [unstable features](../../runtime/unstable).
|
||||
|
||||
WASM files can also be loaded using imports:
|
||||
|
||||
```ts
|
||||
import { fib } from "./fib.wasm";
|
||||
console.log(fib(20));
|
||||
```
|
Loading…
Reference in a new issue