mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-06-13 04:51:23 +00:00
Implement the local JS snippets RFC
This commit is an implementation of [RFC 6] which enables crates to inline local JS snippets into the final output artifact of `wasm-bindgen`. This is accompanied with a few minor breaking changes which are intended to be relatively minor in practice: * The `module` attribute disallows paths starting with `./` and `../`. It requires paths starting with `/` to actually exist on the filesystem. * The `--browser` flag no longer emits bundler-compatible code, but rather emits an ES module that can be natively loaded into a browser. Otherwise be sure to check out [the RFC][RFC 6] for more details, and otherwise this should implement at least the MVP version of the RFC! Notably at this time JS snippets with `--nodejs` or `--no-modules` are not supported and will unconditionally generate an error. [RFC 6]: https://github.com/rustwasm/rfcs/pull/6 Closes #1311
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
use crate::decode;
|
||||
use crate::descriptor::{Descriptor, VectorKind};
|
||||
use crate::{Bindgen, EncodeInto};
|
||||
use crate::{Bindgen, EncodeInto, OutputMode};
|
||||
use failure::{bail, Error, ResultExt};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use walrus::{MemoryId, Module};
|
||||
@ -33,7 +33,7 @@ pub struct Context<'a> {
|
||||
/// from, `None` being the global module. The second key is a map of
|
||||
/// identifiers we've already imported from the module to what they're
|
||||
/// called locally.
|
||||
pub imported_names: HashMap<Option<&'a str>, HashMap<&'a str, String>>,
|
||||
pub imported_names: HashMap<ImportModule<'a>, HashMap<&'a str, String>>,
|
||||
|
||||
/// A set of all imported identifiers to the number of times they've been
|
||||
/// imported, used to generate new identifiers.
|
||||
@ -53,6 +53,14 @@ pub struct Context<'a> {
|
||||
pub interpreter: &'a mut Interpreter,
|
||||
pub memory: MemoryId,
|
||||
|
||||
/// A map of all local modules we've found, from the identifier they're
|
||||
/// known as to their actual JS contents.
|
||||
pub local_modules: HashMap<&'a str, &'a str>,
|
||||
|
||||
/// An integer offset of where to start assigning indexes to `inline_js`
|
||||
/// snippets. This is incremented each time a `Program` is processed.
|
||||
pub snippet_offset: usize,
|
||||
|
||||
pub anyref: wasm_bindgen_anyref_xform::Context,
|
||||
}
|
||||
|
||||
@ -100,6 +108,19 @@ enum Import<'a> {
|
||||
name: &'a str,
|
||||
field: Option<&'a str>,
|
||||
},
|
||||
/// Same as `Module`, except we're importing from a local module defined in
|
||||
/// a local JS snippet.
|
||||
LocalModule {
|
||||
module: &'a str,
|
||||
name: &'a str,
|
||||
field: Option<&'a str>,
|
||||
},
|
||||
/// Same as `Module`, except we're importing from an `inline_js` attribute
|
||||
InlineJs {
|
||||
idx: usize,
|
||||
name: &'a str,
|
||||
field: Option<&'a str>,
|
||||
},
|
||||
/// A global import which may have a number of vendor prefixes associated
|
||||
/// with it, like `webkitAudioPrefix`. The `name` is the name to test
|
||||
/// whether it's prefixed.
|
||||
@ -124,28 +145,62 @@ impl<'a> Context<'a> {
|
||||
self.globals.push_str(c);
|
||||
self.typescript.push_str(c);
|
||||
}
|
||||
let global = if self.use_node_require() {
|
||||
if contents.starts_with("class") {
|
||||
format!("{1}\nmodule.exports.{0} = {0};\n", name, contents)
|
||||
} else {
|
||||
format!("module.exports.{} = {};\n", name, contents)
|
||||
let global = match self.config.mode {
|
||||
OutputMode::Node {
|
||||
experimental_modules: false,
|
||||
} => {
|
||||
if contents.starts_with("class") {
|
||||
format!("{1}\nmodule.exports.{0} = {0};\n", name, contents)
|
||||
} else {
|
||||
format!("module.exports.{} = {};\n", name, contents)
|
||||
}
|
||||
}
|
||||
} else if self.config.no_modules {
|
||||
if contents.starts_with("class") {
|
||||
format!("{1}\n__exports.{0} = {0};\n", name, contents)
|
||||
} else {
|
||||
format!("__exports.{} = {};\n", name, contents)
|
||||
OutputMode::NoModules { .. } => {
|
||||
if contents.starts_with("class") {
|
||||
format!("{1}\n__exports.{0} = {0};\n", name, contents)
|
||||
} else {
|
||||
format!("__exports.{} = {};\n", name, contents)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if contents.starts_with("function") {
|
||||
format!("export function {}{}\n", name, &contents[8..])
|
||||
} else if contents.starts_with("class") {
|
||||
format!("export {}\n", contents)
|
||||
} else {
|
||||
format!("export const {} = {};\n", name, contents)
|
||||
OutputMode::Bundler
|
||||
| OutputMode::Node {
|
||||
experimental_modules: true,
|
||||
} => {
|
||||
if contents.starts_with("function") {
|
||||
format!("export function {}{}\n", name, &contents[8..])
|
||||
} else if contents.starts_with("class") {
|
||||
format!("export {}\n", contents)
|
||||
} else {
|
||||
format!("export const {} = {};\n", name, contents)
|
||||
}
|
||||
}
|
||||
OutputMode::Browser => {
|
||||
// In browser mode there's no need to export the internals of
|
||||
// wasm-bindgen as we're not using the module itself as the
|
||||
// import object but rather the `__exports` map we'll be
|
||||
// initializing below.
|
||||
let export = if name.starts_with("__wbindgen")
|
||||
|| name.starts_with("__wbg_")
|
||||
|| name.starts_with("__widl_")
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"export "
|
||||
};
|
||||
if contents.starts_with("function") {
|
||||
format!("{}function {}{}\n", export, name, &contents[8..])
|
||||
} else if contents.starts_with("class") {
|
||||
format!("{}{}\n", export, contents)
|
||||
} else {
|
||||
format!("{}const {} = {};\n", export, name, contents)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.global(&global);
|
||||
|
||||
if self.config.mode.browser() {
|
||||
self.global(&format!("__exports.{} = {0};", name));
|
||||
}
|
||||
}
|
||||
|
||||
fn require_internal_export(&mut self, name: &'static str) -> Result<(), Error> {
|
||||
@ -529,7 +584,7 @@ impl<'a> Context<'a> {
|
||||
})?;
|
||||
|
||||
self.bind("__wbindgen_module", &|me| {
|
||||
if !me.config.no_modules {
|
||||
if !me.config.mode.no_modules() && !me.config.mode.browser() {
|
||||
bail!(
|
||||
"`wasm_bindgen::module` is currently only supported with \
|
||||
--no-modules"
|
||||
@ -596,35 +651,14 @@ impl<'a> Context<'a> {
|
||||
// entirely. Otherwise we want to first add a start function to the
|
||||
// `start` section if one is specified.
|
||||
//
|
||||
// Afterwards, we need to perform what's a bit of a hack. Right after we
|
||||
// added the start function, we remove it again because no current
|
||||
// strategy for bundlers and deployment works well enough with it. For
|
||||
// `--no-modules` output we need to be sure to call the start function
|
||||
// after our exports are wired up (or most imported functions won't
|
||||
// work).
|
||||
//
|
||||
// For ESM outputs bundlers like webpack also don't work because
|
||||
// currently they run the wasm initialization before the JS glue
|
||||
// initialization, meaning that if the wasm start function calls
|
||||
// imported functions the JS glue isn't ready to go just yet.
|
||||
//
|
||||
// To handle `--no-modules` we just unstart the start function and call
|
||||
// it manually. To handle the ESM use case we switch the start function
|
||||
// to calling an imported function which defers the start function via
|
||||
// `Promise.resolve().then(...)` to execute on the next microtask tick.
|
||||
let mut has_start_function = false;
|
||||
// Note that once a start function is added, if any, we immediately
|
||||
// un-start it. This is done because we require that the JS glue
|
||||
// initializes first, so we execute wasm startup manually once the JS
|
||||
// glue is all in place.
|
||||
let mut needs_manual_start = false;
|
||||
if self.config.emit_start {
|
||||
self.add_start_function()?;
|
||||
has_start_function = self.unstart_start_function();
|
||||
|
||||
// In the "we're pretending to be an ES module use case if we've got
|
||||
// a start function then we use an injected shim to actually execute
|
||||
// the real start function on the next tick of the microtask queue
|
||||
// (explained above)
|
||||
if !self.config.no_modules && has_start_function {
|
||||
self.inject_start_shim();
|
||||
}
|
||||
|
||||
needs_manual_start = self.unstart_start_function();
|
||||
}
|
||||
|
||||
self.export_table()?;
|
||||
@ -653,20 +687,94 @@ impl<'a> Context<'a> {
|
||||
// we don't ask for items which we can no longer emit.
|
||||
drop(self.exposed_globals.take().unwrap());
|
||||
|
||||
let mut js = if self.config.threads.is_some() {
|
||||
// TODO: It's not clear right now how to best use threads with
|
||||
// bundlers like webpack. We need a way to get the existing
|
||||
// module/memory into web workers for now and we don't quite know
|
||||
// idiomatically how to do that! In the meantime, always require
|
||||
// `--no-modules`
|
||||
if !self.config.no_modules {
|
||||
bail!("most use `--no-modules` with threads for now")
|
||||
}
|
||||
let mem = self.module.memories.get(self.memory);
|
||||
if mem.import.is_none() {
|
||||
bail!("must impot a shared memory with threads")
|
||||
let mut js = String::new();
|
||||
if self.config.mode.no_modules() {
|
||||
js.push_str("(function() {\n");
|
||||
}
|
||||
|
||||
// Depending on the output mode, generate necessary glue to actually
|
||||
// import the wasm file in one way or another.
|
||||
let mut init = String::new();
|
||||
match &self.config.mode {
|
||||
// In `--no-modules` mode we need to both expose a name on the
|
||||
// global object as well as generate our own custom start function.
|
||||
OutputMode::NoModules { global } => {
|
||||
js.push_str("const __exports = {};\n");
|
||||
js.push_str("let wasm;\n");
|
||||
init = self.gen_init(&module_name, needs_manual_start);
|
||||
self.footer.push_str(&format!(
|
||||
"self.{} = Object.assign(init, __exports);\n",
|
||||
global
|
||||
));
|
||||
}
|
||||
|
||||
// With normal CommonJS node we need to defer requiring the wasm
|
||||
// until the end so most of our own exports are hooked up
|
||||
OutputMode::Node {
|
||||
experimental_modules: false,
|
||||
} => {
|
||||
self.footer
|
||||
.push_str(&format!("wasm = require('./{}_bg');\n", module_name));
|
||||
if needs_manual_start {
|
||||
self.footer.push_str("wasm.__wbindgen_start();\n");
|
||||
}
|
||||
js.push_str("var wasm;\n");
|
||||
}
|
||||
|
||||
// With Bundlers and modern ES6 support in Node we can simply import
|
||||
// the wasm file as if it were an ES module and let the
|
||||
// bundler/runtime take care of it.
|
||||
OutputMode::Bundler
|
||||
| OutputMode::Node {
|
||||
experimental_modules: true,
|
||||
} => {
|
||||
js.push_str(&format!("import * as wasm from './{}_bg';\n", module_name));
|
||||
if needs_manual_start {
|
||||
self.footer.push_str("wasm.__wbindgen_start();\n");
|
||||
}
|
||||
}
|
||||
|
||||
// With a browser-native output we're generating an ES module, but
|
||||
// browsers don't support natively importing wasm right now so we
|
||||
// expose the same initialization function as `--no-modules` as the
|
||||
// default export of the module.
|
||||
OutputMode::Browser => {
|
||||
js.push_str("const __exports = {};\n");
|
||||
self.imports_post.push_str("let wasm;\n");
|
||||
init = self.gen_init(&module_name, needs_manual_start);
|
||||
self.footer.push_str("export default init;\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Emit all the JS for importing all our functionality
|
||||
js.push_str(&self.imports);
|
||||
js.push_str("\n");
|
||||
js.push_str(&self.imports_post);
|
||||
js.push_str("\n");
|
||||
|
||||
// Emit all our exports from this module
|
||||
js.push_str(&self.globals);
|
||||
js.push_str("\n");
|
||||
|
||||
// Generate the initialization glue, if there was any
|
||||
js.push_str(&init);
|
||||
js.push_str("\n");
|
||||
js.push_str(&self.footer);
|
||||
js.push_str("\n");
|
||||
if self.config.mode.no_modules() {
|
||||
js.push_str("})();\n");
|
||||
}
|
||||
|
||||
while js.contains("\n\n\n") {
|
||||
js = js.replace("\n\n\n", "\n\n");
|
||||
}
|
||||
|
||||
Ok((js, self.typescript.clone()))
|
||||
}
|
||||
|
||||
fn gen_init(&mut self, module_name: &str, needs_manual_start: bool) -> String {
|
||||
let mem = self.module.memories.get(self.memory);
|
||||
let (init_memory1, init_memory2) = if mem.import.is_some() {
|
||||
let mut memory = String::from("new WebAssembly.Memory({");
|
||||
memory.push_str(&format!("initial:{}", mem.initial));
|
||||
if let Some(max) = mem.maximum {
|
||||
@ -676,158 +784,64 @@ impl<'a> Context<'a> {
|
||||
memory.push_str(",shared:true");
|
||||
}
|
||||
memory.push_str("})");
|
||||
|
||||
format!(
|
||||
"\
|
||||
(function() {{
|
||||
var wasm;
|
||||
var memory;
|
||||
const __exports = {{}};
|
||||
{globals}
|
||||
function init(module_or_path, maybe_memory) {{
|
||||
let result;
|
||||
const imports = {{ './{module}': __exports }};
|
||||
if (module_or_path instanceof WebAssembly.Module) {{
|
||||
memory = __exports.memory = maybe_memory;
|
||||
result = WebAssembly.instantiate(module_or_path, imports)
|
||||
.then(instance => {{
|
||||
return {{ instance, module: module_or_path }}
|
||||
}});
|
||||
}} else {{
|
||||
memory = __exports.memory = {init_memory};
|
||||
const response = fetch(module_or_path);
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {{
|
||||
result = WebAssembly.instantiateStreaming(response, imports)
|
||||
.catch(e => {{
|
||||
console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \
|
||||
because your server does not serve wasm with \
|
||||
`application/wasm` MIME type. Falling back to \
|
||||
`WebAssembly.instantiate` which is slower. Original \
|
||||
error:\\n\", e);
|
||||
return response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}});
|
||||
}} else {{
|
||||
result = response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}}
|
||||
}}
|
||||
return result.then(({{instance, module}}) => {{
|
||||
wasm = init.wasm = instance.exports;
|
||||
init.__wbindgen_wasm_instance = instance;
|
||||
init.__wbindgen_wasm_module = module;
|
||||
init.__wbindgen_wasm_memory = __exports.memory;
|
||||
{start}
|
||||
}});
|
||||
}};
|
||||
self.{global_name} = Object.assign(init, __exports);
|
||||
}})();",
|
||||
globals = self.globals,
|
||||
module = module_name,
|
||||
global_name = self
|
||||
.config
|
||||
.no_modules_global
|
||||
.as_ref()
|
||||
.map(|s| &**s)
|
||||
.unwrap_or("wasm_bindgen"),
|
||||
init_memory = memory,
|
||||
start = if has_start_function {
|
||||
"wasm.__wbindgen_start();"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
} else if self.config.no_modules {
|
||||
format!(
|
||||
"\
|
||||
(function() {{
|
||||
var wasm;
|
||||
const __exports = {{}};
|
||||
{globals}
|
||||
function init(path_or_module) {{
|
||||
let instantiation;
|
||||
const imports = {{ './{module}': __exports }};
|
||||
if (path_or_module instanceof WebAssembly.Module) {{
|
||||
instantiation = WebAssembly.instantiate(path_or_module, imports)
|
||||
.then(instance => {{
|
||||
return {{ instance, module: path_or_module }}
|
||||
}});
|
||||
}} else {{
|
||||
const data = fetch(path_or_module);
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {{
|
||||
instantiation = WebAssembly.instantiateStreaming(data, imports)
|
||||
.catch(e => {{
|
||||
console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \
|
||||
because your server does not serve wasm with \
|
||||
`application/wasm` MIME type. Falling back to \
|
||||
`WebAssembly.instantiate` which is slower. Original \
|
||||
error:\\n\", e);
|
||||
return data
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}});
|
||||
}} else {{
|
||||
instantiation = data
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(buffer => WebAssembly.instantiate(buffer, imports));
|
||||
}}
|
||||
}}
|
||||
return instantiation.then(({{instance}}) => {{
|
||||
wasm = init.wasm = instance.exports;
|
||||
{start}
|
||||
}});
|
||||
}};
|
||||
self.{global_name} = Object.assign(init, __exports);
|
||||
}})();",
|
||||
globals = self.globals,
|
||||
module = module_name,
|
||||
global_name = self
|
||||
.config
|
||||
.no_modules_global
|
||||
.as_ref()
|
||||
.map(|s| &**s)
|
||||
.unwrap_or("wasm_bindgen"),
|
||||
start = if has_start_function {
|
||||
"wasm.__wbindgen_start();"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
self.imports_post.push_str("let memory;\n");
|
||||
(
|
||||
format!("memory = __exports.memory = maybe_memory;"),
|
||||
format!("memory = __exports.memory = {};", memory),
|
||||
)
|
||||
} else {
|
||||
let import_wasm = if self.globals.len() == 0 {
|
||||
String::new()
|
||||
} else if self.use_node_require() {
|
||||
self.footer
|
||||
.push_str(&format!("wasm = require('./{}_bg');", module_name));
|
||||
format!("var wasm;")
|
||||
} else {
|
||||
format!("import * as wasm from './{}_bg';", module_name)
|
||||
};
|
||||
|
||||
format!(
|
||||
"\
|
||||
/* tslint:disable */\n\
|
||||
{import_wasm}\n\
|
||||
{imports}\n\
|
||||
{imports_post}\n\
|
||||
|
||||
{globals}\n\
|
||||
{footer}",
|
||||
import_wasm = import_wasm,
|
||||
globals = self.globals,
|
||||
imports = self.imports,
|
||||
imports_post = self.imports_post,
|
||||
footer = self.footer,
|
||||
)
|
||||
(String::new(), String::new())
|
||||
};
|
||||
|
||||
while js.contains("\n\n\n") {
|
||||
js = js.replace("\n\n\n", "\n\n");
|
||||
}
|
||||
|
||||
Ok((js, self.typescript.clone()))
|
||||
format!(
|
||||
"\
|
||||
function init(module_or_path, maybe_memory) {{
|
||||
let result;
|
||||
const imports = {{ './{module}': __exports }};
|
||||
if (module_or_path instanceof WebAssembly.Module) {{
|
||||
{init_memory1}
|
||||
result = WebAssembly.instantiate(module_or_path, imports)
|
||||
.then(instance => {{
|
||||
return {{ instance, module: module_or_path }};
|
||||
}});
|
||||
}} else {{
|
||||
{init_memory2}
|
||||
const response = fetch(module_or_path);
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {{
|
||||
result = WebAssembly.instantiateStreaming(response, imports)
|
||||
.catch(e => {{
|
||||
console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \
|
||||
because your server does not serve wasm with \
|
||||
`application/wasm` MIME type. Falling back to \
|
||||
`WebAssembly.instantiate` which is slower. Original \
|
||||
error:\\n\", e);
|
||||
return response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}});
|
||||
}} else {{
|
||||
result = response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}}
|
||||
}}
|
||||
return result.then(({{instance, module}}) => {{
|
||||
wasm = instance.exports;
|
||||
init.__wbindgen_wasm_module = module;
|
||||
{start}
|
||||
return wasm;
|
||||
}});
|
||||
}}
|
||||
",
|
||||
module = module_name,
|
||||
init_memory1 = init_memory1,
|
||||
init_memory2 = init_memory2,
|
||||
start = if needs_manual_start {
|
||||
"wasm.__wbindgen_start();"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn bind(
|
||||
@ -1364,14 +1378,14 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
fn expose_text_processor(&mut self, s: &str) {
|
||||
if self.config.nodejs_experimental_modules {
|
||||
if self.config.mode.nodejs_experimental_modules() {
|
||||
self.imports
|
||||
.push_str(&format!("import {{ {} }} from 'util';\n", s));
|
||||
self.global(&format!("let cached{0} = new {0}('utf-8');", s));
|
||||
} else if self.config.nodejs {
|
||||
} else if self.config.mode.nodejs() {
|
||||
self.global(&format!("const {0} = require('util').{0};", s));
|
||||
self.global(&format!("let cached{0} = new {0}('utf-8');", s));
|
||||
} else if !(self.config.browser || self.config.no_modules) {
|
||||
} else if !self.config.mode.always_run_in_browser() {
|
||||
self.global(&format!(
|
||||
"
|
||||
const l{0} = typeof {0} === 'undefined' ? \
|
||||
@ -2008,7 +2022,7 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
fn use_node_require(&self) -> bool {
|
||||
self.config.nodejs && !self.config.nodejs_experimental_modules
|
||||
self.config.mode.nodejs() && !self.config.mode.nodejs_experimental_modules()
|
||||
}
|
||||
|
||||
fn memory(&mut self) -> &'static str {
|
||||
@ -2052,23 +2066,36 @@ impl<'a> Context<'a> {
|
||||
.or_insert_with(|| {
|
||||
let name = generate_identifier(import.name(), imported_identifiers);
|
||||
match &import {
|
||||
Import::Module { module, .. } => {
|
||||
Import::Module { .. }
|
||||
| Import::LocalModule { .. }
|
||||
| Import::InlineJs { .. } => {
|
||||
// When doing a modular import local snippets (either
|
||||
// inline or not) are routed to a local `./snippets`
|
||||
// directory which the rest of `wasm-bindgen` will fill
|
||||
// in.
|
||||
let path = match import {
|
||||
Import::Module { module, .. } => module.to_string(),
|
||||
Import::LocalModule { module, .. } => format!("./snippets/{}", module),
|
||||
Import::InlineJs { idx, .. } => {
|
||||
format!("./snippets/wbg-inline{}.js", idx)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if use_node_require {
|
||||
imports.push_str(&format!(
|
||||
"const {} = require(String.raw`{}`).{};\n",
|
||||
name,
|
||||
module,
|
||||
path,
|
||||
import.name()
|
||||
));
|
||||
} else if import.name() == name {
|
||||
imports
|
||||
.push_str(&format!("import {{ {} }} from '{}';\n", name, module));
|
||||
imports.push_str(&format!("import {{ {} }} from '{}';\n", name, path));
|
||||
} else {
|
||||
imports.push_str(&format!(
|
||||
"import {{ {} as {} }} from '{}';\n",
|
||||
import.name(),
|
||||
name,
|
||||
module
|
||||
path
|
||||
));
|
||||
}
|
||||
name
|
||||
@ -2290,24 +2317,6 @@ impl<'a> Context<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Injects a `start` function into the wasm module. This start function
|
||||
/// calls a shim in the generated JS which defers the actual start function
|
||||
/// to the next microtask tick of the event queue.
|
||||
///
|
||||
/// See docs above at callsite for why this happens.
|
||||
fn inject_start_shim(&mut self) {
|
||||
let body = "function() {
|
||||
Promise.resolve().then(() => wasm.__wbindgen_start());
|
||||
}";
|
||||
self.export("__wbindgen_defer_start", body, None);
|
||||
let ty = self.module.types.add(&[], &[]);
|
||||
let id =
|
||||
self.module
|
||||
.add_import_func("__wbindgen_placeholder__", "__wbindgen_defer_start", ty);
|
||||
assert!(self.module.start.is_none());
|
||||
self.module.start = Some(id);
|
||||
}
|
||||
|
||||
fn expose_anyref_table(&mut self) {
|
||||
assert!(self.config.anyref);
|
||||
if !self.should_write_global("anyref_table") {
|
||||
@ -2368,6 +2377,14 @@ impl<'a> Context<'a> {
|
||||
|
||||
impl<'a, 'b> SubContext<'a, 'b> {
|
||||
pub fn generate(&mut self) -> Result<(), Error> {
|
||||
for m in self.program.local_modules.iter() {
|
||||
// All local modules we find should be unique, so assert such.
|
||||
assert!(self
|
||||
.cx
|
||||
.local_modules
|
||||
.insert(m.identifier, m.contents)
|
||||
.is_none());
|
||||
}
|
||||
for f in self.program.exports.iter() {
|
||||
self.generate_export(f).with_context(|_| {
|
||||
format!(
|
||||
@ -2752,16 +2769,41 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
) -> Result<Import<'b>, Error> {
|
||||
// First up, imports don't work at all in `--no-modules` mode as we're
|
||||
// not sure how to import them.
|
||||
if self.cx.config.no_modules {
|
||||
if let Some(module) = &import.module {
|
||||
let is_local_snippet = match import.module {
|
||||
decode::ImportModule::Named(s) => self.cx.local_modules.contains_key(s),
|
||||
decode::ImportModule::Inline(_) => true,
|
||||
decode::ImportModule::None => false,
|
||||
};
|
||||
if self.cx.config.mode.no_modules() {
|
||||
if is_local_snippet {
|
||||
bail!(
|
||||
"local JS snippets are not supported with `--no-modules`; \
|
||||
use `--browser` or no flag instead",
|
||||
);
|
||||
}
|
||||
if let decode::ImportModule::Named(module) = &import.module {
|
||||
bail!(
|
||||
"import from `{}` module not allowed with `--no-modules`; \
|
||||
use `--nodejs` or `--browser` instead",
|
||||
use `--nodejs`, `--browser`, or no flag instead",
|
||||
module
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: currently we require that local JS snippets are written in ES
|
||||
// module syntax for imports/exports, but nodejs uses CommonJS to handle
|
||||
// this meaning that local JS snippets are basically guaranteed to be
|
||||
// incompatible. We need to implement a pass that translates the ES
|
||||
// module syntax in the snippet to a CommonJS module, which is in theory
|
||||
// not that hard but is a chunk of work to do.
|
||||
if is_local_snippet && self.cx.config.mode.nodejs() {
|
||||
bail!(
|
||||
"local JS snippets are not supported with `--nodejs`; \
|
||||
see rustwasm/rfcs#6 for more details, but this restriction \
|
||||
will be lifted in the future"
|
||||
);
|
||||
}
|
||||
|
||||
// Similar to `--no-modules`, only allow vendor prefixes basically for web
|
||||
// apis, shouldn't be necessary for things like npm packages or other
|
||||
// imported items.
|
||||
@ -2769,7 +2811,15 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
if let Some(vendor_prefixes) = vendor_prefixes {
|
||||
assert!(vendor_prefixes.len() > 0);
|
||||
|
||||
if let Some(module) = &import.module {
|
||||
if is_local_snippet {
|
||||
bail!(
|
||||
"local JS snippets do not support vendor prefixes for \
|
||||
the import of `{}` with a polyfill of `{}`",
|
||||
item,
|
||||
&vendor_prefixes[0]
|
||||
);
|
||||
}
|
||||
if let decode::ImportModule::Named(module) = &import.module {
|
||||
bail!(
|
||||
"import of `{}` from `{}` has a polyfill of `{}` listed, but
|
||||
vendor prefixes aren't supported when importing from modules",
|
||||
@ -2792,20 +2842,28 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
});
|
||||
}
|
||||
|
||||
let name = import.js_namespace.as_ref().map(|s| &**s).unwrap_or(item);
|
||||
let field = if import.js_namespace.is_some() {
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
let (name, field) = match import.js_namespace {
|
||||
Some(ns) => (ns, Some(item)),
|
||||
None => (item, None),
|
||||
};
|
||||
|
||||
Ok(match import.module {
|
||||
Some(module) => Import::Module {
|
||||
decode::ImportModule::Named(module) if is_local_snippet => Import::LocalModule {
|
||||
module,
|
||||
name,
|
||||
field,
|
||||
},
|
||||
None => Import::Global { name, field },
|
||||
decode::ImportModule::Named(module) => Import::Module {
|
||||
module,
|
||||
name,
|
||||
field,
|
||||
},
|
||||
decode::ImportModule::Inline(idx) => Import::InlineJs {
|
||||
idx: idx as usize + self.cx.snippet_offset,
|
||||
name,
|
||||
field,
|
||||
},
|
||||
decode::ImportModule::None => Import::Global { name, field },
|
||||
})
|
||||
}
|
||||
|
||||
@ -2815,17 +2873,30 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
pub enum ImportModule<'a> {
|
||||
Named(&'a str),
|
||||
Inline(usize),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> Import<'a> {
|
||||
fn module(&self) -> Option<&'a str> {
|
||||
fn module(&self) -> ImportModule<'a> {
|
||||
match self {
|
||||
Import::Module { module, .. } => Some(module),
|
||||
_ => None,
|
||||
Import::Module { module, .. } | Import::LocalModule { module, .. } => {
|
||||
ImportModule::Named(module)
|
||||
}
|
||||
Import::InlineJs { idx, .. } => ImportModule::Inline(*idx),
|
||||
Import::Global { .. } | Import::VendorPrefixed { .. } => ImportModule::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn field(&self) -> Option<&'a str> {
|
||||
match self {
|
||||
Import::Module { field, .. } | Import::Global { field, .. } => *field,
|
||||
Import::Module { field, .. }
|
||||
| Import::LocalModule { field, .. }
|
||||
| Import::InlineJs { field, .. }
|
||||
| Import::Global { field, .. } => *field,
|
||||
Import::VendorPrefixed { .. } => None,
|
||||
}
|
||||
}
|
||||
@ -2833,6 +2904,8 @@ impl<'a> Import<'a> {
|
||||
fn name(&self) -> &'a str {
|
||||
match self {
|
||||
Import::Module { name, .. }
|
||||
| Import::LocalModule { name, .. }
|
||||
| Import::InlineJs { name, .. }
|
||||
| Import::Global { name, .. }
|
||||
| Import::VendorPrefixed { name, .. } => *name,
|
||||
}
|
||||
|
@ -17,11 +17,7 @@ pub mod wasm2es6js;
|
||||
pub struct Bindgen {
|
||||
input: Input,
|
||||
out_name: Option<String>,
|
||||
nodejs: bool,
|
||||
nodejs_experimental_modules: bool,
|
||||
browser: bool,
|
||||
no_modules: bool,
|
||||
no_modules_global: Option<String>,
|
||||
mode: OutputMode,
|
||||
debug: bool,
|
||||
typescript: bool,
|
||||
demangle: bool,
|
||||
@ -39,6 +35,13 @@ pub struct Bindgen {
|
||||
encode_into: EncodeInto,
|
||||
}
|
||||
|
||||
enum OutputMode {
|
||||
Bundler,
|
||||
Browser,
|
||||
NoModules { global: String },
|
||||
Node { experimental_modules: bool },
|
||||
}
|
||||
|
||||
enum Input {
|
||||
Path(PathBuf),
|
||||
Module(Module, String),
|
||||
@ -56,11 +59,7 @@ impl Bindgen {
|
||||
Bindgen {
|
||||
input: Input::None,
|
||||
out_name: None,
|
||||
nodejs: false,
|
||||
nodejs_experimental_modules: false,
|
||||
browser: false,
|
||||
no_modules: false,
|
||||
no_modules_global: None,
|
||||
mode: OutputMode::Bundler,
|
||||
debug: false,
|
||||
typescript: false,
|
||||
demangle: true,
|
||||
@ -92,29 +91,66 @@ impl Bindgen {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn nodejs(&mut self, node: bool) -> &mut Bindgen {
|
||||
self.nodejs = node;
|
||||
self
|
||||
fn switch_mode(&mut self, mode: OutputMode, flag: &str) -> Result<(), Error> {
|
||||
match self.mode {
|
||||
OutputMode::Bundler => self.mode = mode,
|
||||
_ => bail!(
|
||||
"cannot specify `{}` with another output mode already specified",
|
||||
flag
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn nodejs_experimental_modules(&mut self, node: bool) -> &mut Bindgen {
|
||||
self.nodejs_experimental_modules = node;
|
||||
self
|
||||
pub fn nodejs(&mut self, node: bool) -> Result<&mut Bindgen, Error> {
|
||||
if node {
|
||||
self.switch_mode(
|
||||
OutputMode::Node {
|
||||
experimental_modules: false,
|
||||
},
|
||||
"--nodejs",
|
||||
)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn browser(&mut self, browser: bool) -> &mut Bindgen {
|
||||
self.browser = browser;
|
||||
self
|
||||
pub fn nodejs_experimental_modules(&mut self, node: bool) -> Result<&mut Bindgen, Error> {
|
||||
if node {
|
||||
self.switch_mode(
|
||||
OutputMode::Node {
|
||||
experimental_modules: true,
|
||||
},
|
||||
"--nodejs-experimental-modules",
|
||||
)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn no_modules(&mut self, no_modules: bool) -> &mut Bindgen {
|
||||
self.no_modules = no_modules;
|
||||
self
|
||||
pub fn browser(&mut self, browser: bool) -> Result<&mut Bindgen, Error> {
|
||||
if browser {
|
||||
self.switch_mode(OutputMode::Browser, "--browser")?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn no_modules_global(&mut self, name: &str) -> &mut Bindgen {
|
||||
self.no_modules_global = Some(name.to_string());
|
||||
self
|
||||
pub fn no_modules(&mut self, no_modules: bool) -> Result<&mut Bindgen, Error> {
|
||||
if no_modules {
|
||||
self.switch_mode(
|
||||
OutputMode::NoModules {
|
||||
global: "wasm_bindgen".to_string(),
|
||||
},
|
||||
"--no-modules",
|
||||
)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn no_modules_global(&mut self, name: &str) -> Result<&mut Bindgen, Error> {
|
||||
match &mut self.mode {
|
||||
OutputMode::NoModules { global } => *global = name.to_string(),
|
||||
_ => bail!("can only specify `--no-modules-global` with `--no-modules`"),
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn debug(&mut self, debug: bool) -> &mut Bindgen {
|
||||
@ -203,8 +239,10 @@ impl Bindgen {
|
||||
// a module's start function, if any, because we assume start functions
|
||||
// only show up when injected on behalf of wasm-bindgen's passes.
|
||||
if module.start.is_some() {
|
||||
bail!("wasm-bindgen is currently incompatible with modules that \
|
||||
already have a start function");
|
||||
bail!(
|
||||
"wasm-bindgen is currently incompatible with modules that \
|
||||
already have a start function"
|
||||
);
|
||||
}
|
||||
|
||||
let mut program_storage = Vec::new();
|
||||
@ -263,8 +301,10 @@ impl Bindgen {
|
||||
imported_functions: Default::default(),
|
||||
imported_statics: Default::default(),
|
||||
direct_imports: Default::default(),
|
||||
local_modules: Default::default(),
|
||||
start: None,
|
||||
anyref: Default::default(),
|
||||
snippet_offset: 0,
|
||||
};
|
||||
cx.anyref.enabled = self.anyref;
|
||||
cx.anyref.prepare(cx.module)?;
|
||||
@ -275,11 +315,30 @@ impl Bindgen {
|
||||
vendor_prefixes: Default::default(),
|
||||
}
|
||||
.generate()?;
|
||||
|
||||
for (i, js) in program.inline_js.iter().enumerate() {
|
||||
let name = format!("wbg-inline{}.js", i + cx.snippet_offset);
|
||||
let path = out_dir.join("snippets").join(name);
|
||||
fs::create_dir_all(path.parent().unwrap())?;
|
||||
fs::write(&path, js)
|
||||
.with_context(|_| format!("failed to write `{}`", path.display()))?;
|
||||
}
|
||||
cx.snippet_offset += program.inline_js.len();
|
||||
}
|
||||
|
||||
// Write out all local JS snippets to the final destination now that
|
||||
// we've collected them from all the programs.
|
||||
for (path, contents) in cx.local_modules.iter() {
|
||||
let path = out_dir.join("snippets").join(path);
|
||||
fs::create_dir_all(path.parent().unwrap())?;
|
||||
fs::write(&path, contents)
|
||||
.with_context(|_| format!("failed to write `{}`", path.display()))?;
|
||||
}
|
||||
|
||||
cx.finalize(stem)?
|
||||
};
|
||||
|
||||
let extension = if self.nodejs_experimental_modules {
|
||||
let extension = if self.mode.nodejs_experimental_modules() {
|
||||
"mjs"
|
||||
} else {
|
||||
"js"
|
||||
@ -296,7 +355,7 @@ impl Bindgen {
|
||||
|
||||
let wasm_path = out_dir.join(format!("{}_bg", stem)).with_extension("wasm");
|
||||
|
||||
if self.nodejs {
|
||||
if self.mode.nodejs() {
|
||||
let js_path = wasm_path.with_extension(extension);
|
||||
let shim = self.generate_node_wasm_import(&module, &wasm_path);
|
||||
fs::write(&js_path, shim)
|
||||
@ -325,7 +384,7 @@ impl Bindgen {
|
||||
|
||||
let mut shim = String::new();
|
||||
|
||||
if self.nodejs_experimental_modules {
|
||||
if self.mode.nodejs_experimental_modules() {
|
||||
for (i, module) in imports.iter().enumerate() {
|
||||
shim.push_str(&format!("import * as import{} from '{}';\n", i, module));
|
||||
}
|
||||
@ -357,7 +416,7 @@ impl Bindgen {
|
||||
}
|
||||
shim.push_str("let imports = {};\n");
|
||||
for (i, module) in imports.iter().enumerate() {
|
||||
if self.nodejs_experimental_modules {
|
||||
if self.mode.nodejs_experimental_modules() {
|
||||
shim.push_str(&format!("imports['{}'] = import{};\n", module, i));
|
||||
} else {
|
||||
shim.push_str(&format!("imports['{0}'] = require('{0}');\n", module));
|
||||
@ -371,7 +430,7 @@ impl Bindgen {
|
||||
",
|
||||
));
|
||||
|
||||
if self.nodejs_experimental_modules {
|
||||
if self.mode.nodejs_experimental_modules() {
|
||||
for entry in m.exports.iter() {
|
||||
shim.push_str("export const ");
|
||||
shim.push_str(&entry.name);
|
||||
@ -567,3 +626,41 @@ fn demangle(module: &mut Module) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputMode {
|
||||
fn nodejs_experimental_modules(&self) -> bool {
|
||||
match self {
|
||||
OutputMode::Node { experimental_modules } => *experimental_modules,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn nodejs(&self) -> bool {
|
||||
match self {
|
||||
OutputMode::Node { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn no_modules(&self) -> bool {
|
||||
match self {
|
||||
OutputMode::NoModules { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn always_run_in_browser(&self) -> bool {
|
||||
match self {
|
||||
OutputMode::Browser => true,
|
||||
OutputMode::NoModules { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn browser(&self) -> bool {
|
||||
match self {
|
||||
OutputMode::Browser => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user