Skip to content

Commit 68eb86e

Browse files
authored
feat: treeshake imports based on guest content (#117)
1 parent 99997e1 commit 68eb86e

File tree

15 files changed

+175
-48
lines changed

15 files changed

+175
-48
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ export function componentize(jsSource: string, opts: {
152152

153153
Converts a JS source into a component binary.
154154

155+
Imports provides the list of used guest imports only, while the StarlingMonkey engine may pull in additional
156+
imports. Direct component analysis should be used to correctly infer the real imports list.
157+
155158
## Contributing
156159

157160
### Pre-requisites

crates/spidermonkey-embedding-splicer/src/bindgen.rs

+48-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{uwrite, uwriteln};
2+
use anyhow::{bail, Result};
23
use heck::*;
34
use js_component_bindgen::function_bindgen::{
45
ErrHandling, FunctionBindgen, ResourceData, ResourceMap, ResourceTable,
@@ -124,7 +125,13 @@ pub struct Componentization {
124125
pub resource_imports: Vec<(String, String, u32)>,
125126
}
126127

127-
pub fn componentize_bindgen(resolve: &Resolve, id: WorldId, name: &str) -> Componentization {
128+
pub fn componentize_bindgen(
129+
resolve: &Resolve,
130+
id: WorldId,
131+
name: &str,
132+
guest_imports: &Vec<String>,
133+
guest_exports: &Vec<String>,
134+
) -> Result<Componentization> {
128135
let mut bindgen = JsBindgen {
129136
src: Source::default(),
130137
esm_bindgen: EsmBindgen::default(),
@@ -147,9 +154,9 @@ pub fn componentize_bindgen(resolve: &Resolve, id: WorldId, name: &str) -> Compo
147154
.local_names
148155
.exclude_globals(Intrinsic::get_global_names());
149156

150-
bindgen.imports_bindgen();
157+
bindgen.imports_bindgen(&guest_imports);
151158

152-
bindgen.exports_bindgen();
159+
bindgen.exports_bindgen(&guest_exports)?;
153160
bindgen.esm_bindgen.populate_export_aliases();
154161

155162
// consolidate import specifiers and generate wrappers
@@ -348,13 +355,13 @@ pub fn componentize_bindgen(resolve: &Resolve, id: WorldId, name: &str) -> Compo
348355
output.push_str(&js_intrinsics);
349356
output.push_str(&bindgen.src);
350357

351-
Componentization {
358+
Ok(Componentization {
352359
js_bindings: output.to_string(),
353360
exports: bindgen.exports,
354361
imports: bindgen.imports,
355362
import_wrappers,
356363
resource_imports,
357-
}
364+
})
358365
}
359366

360367
impl JsBindgen<'_> {
@@ -363,9 +370,39 @@ impl JsBindgen<'_> {
363370
return intrinsic.name().to_string();
364371
}
365372

366-
fn exports_bindgen(&mut self) {
373+
fn exports_bindgen(&mut self, guest_exports: &Vec<String>) -> Result<()> {
367374
for (key, export) in &self.resolve.worlds[self.world].exports {
368375
let name = self.resolve.name_world_key(key);
376+
377+
// Do not generate exports when the guest export is not implemented.
378+
// We check both the full interface name - "ns:pkg@v/my-interface" and the
379+
// aliased interface name "myInterface". All other names are always
380+
// camel-case in the check.
381+
match key {
382+
WorldKey::Interface(iface) => {
383+
if !guest_exports.contains(&name) {
384+
let iface = &self.resolve.interfaces[*iface];
385+
if let Some(name) = iface.name.as_ref() {
386+
let camel_case_name = name.to_lower_camel_case();
387+
if !guest_exports.contains(&camel_case_name) {
388+
bail!("Expected a JS export definition for '{}'", camel_case_name);
389+
}
390+
// TODO: move populate_export_aliases to a preprocessing
391+
// step that doesn't require esm_bindgen, so that we can
392+
// do alias deduping here as well.
393+
} else {
394+
continue;
395+
}
396+
}
397+
}
398+
WorldKey::Name(export_name) => {
399+
let camel_case_name = export_name.to_lower_camel_case();
400+
if !guest_exports.contains(&camel_case_name) {
401+
bail!("Expected a JS export definition for '{}'", camel_case_name);
402+
}
403+
}
404+
}
405+
369406
match export {
370407
WorldItem::Function(func) => {
371408
let local_name = self.local_names.create_once(&func.name).to_string();
@@ -449,11 +486,15 @@ impl JsBindgen<'_> {
449486
WorldItem::Type(_) => {}
450487
}
451488
}
489+
Ok(())
452490
}
453491

454-
fn imports_bindgen(&mut self) {
492+
fn imports_bindgen(&mut self, guest_imports: &Vec<String>) {
455493
for (key, impt) in &self.resolve.worlds[self.world].imports {
456494
let import_name = self.resolve.name_world_key(key);
495+
if !guest_imports.contains(&import_name) {
496+
continue;
497+
}
457498
match &impt {
458499
WorldItem::Function(f) => {
459500
self.import_bindgen(import_name, f, false, None);

crates/spidermonkey-embedding-splicer/src/lib.rs

+30-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use anyhow::{bail, Context, Result};
22
use bindgen::BindingItem;
3-
use std::path::{Path, PathBuf};
3+
use std::{
4+
path::{Path, PathBuf},
5+
vec,
6+
};
47

58
mod bindgen;
69
mod splice;
@@ -110,6 +113,8 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
110113
wit_source: Option<String>,
111114
wit_path: Option<String>,
112115
world_name: Option<String>,
116+
mut guest_imports: Vec<String>,
117+
guest_exports: Vec<String>,
113118
debug: bool,
114119
) -> Result<SpliceResult, String> {
115120
let source_name = source_name.unwrap_or("source.js".to_string());
@@ -131,7 +136,6 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
131136
.map_err(|e| e.to_string())?;
132137

133138
let mut wasm_bytes = wit_component::dummy_module(&resolve, world);
134-
let componentized = bindgen::componentize_bindgen(&resolve, world, &source_name);
135139

136140
// merge the engine world with the target world, retaining the engine producers
137141
let producers = if let Ok((
@@ -144,6 +148,11 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
144148
},
145149
)) = decode(&engine)
146150
{
151+
// merge the imports from the engine with the imports from the guest content
152+
for (k, _) in &engine_resolve.worlds[engine_world].imports {
153+
guest_imports.push(engine_resolve.name_world_key(k));
154+
}
155+
147156
// we disable the engine run and incoming handler as we recreate these exports
148157
// when needed, so remove these from the world before initiating the merge
149158
let maybe_run = engine_resolve.worlds[engine_world]
@@ -174,19 +183,29 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
174183
resolve
175184
.merge(engine_resolve)
176185
.expect("unable to merge with engine world");
177-
let (engine_world, _) = resolve
178-
.worlds
179-
.iter()
180-
.find(|(world, _)| resolve.worlds[*world].name == "root")
181-
.unwrap();
182-
resolve
183-
.merge_worlds(engine_world, world)
184-
.expect("unable to merge with engine world");
185186
producers
186187
} else {
187188
None
188189
};
189190

191+
let componentized = bindgen::componentize_bindgen(
192+
&resolve,
193+
world,
194+
&source_name,
195+
&guest_imports,
196+
&guest_exports,
197+
)
198+
.map_err(|err| err.to_string())?;
199+
200+
let (engine_world, _) = resolve
201+
.worlds
202+
.iter()
203+
.find(|(world, _)| resolve.worlds[*world].name == "root")
204+
.unwrap();
205+
resolve
206+
.merge_worlds(engine_world, world)
207+
.expect("unable to merge with engine world");
208+
190209
let encoded = wit_component::metadata::encode(
191210
&resolve,
192211
world,
@@ -327,8 +346,8 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
327346
));
328347
}
329348

330-
// println!("{:?}", &imports);
331349
// println!("{:?}", &componentized.imports);
350+
// println!("{:?}", &componentized.resource_imports);
332351
// println!("{:?}", &exports);
333352
let mut wasm =
334353
splice::splice(engine, imports, exports, debug).map_err(|e| format!("{:?}", e))?;

crates/spidermonkey-embedding-splicer/src/splice.rs

+22-12
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,30 @@ pub fn splice(
3838
let mut module = config.parse(&engine)?;
3939

4040
// since StarlingMonkey implements CLI Run and incoming handler,
41-
// we override these in ComponentizeJS, removing them from the
42-
// core function exports
43-
if let Ok(run) = module.exports.get_func("wasi:cli/[email protected]#run") {
44-
let expt = module.exports.get_exported_func(run).unwrap();
45-
module.exports.delete(expt.id());
46-
module.funcs.delete(run);
41+
// we override them only if the guest content exports those functions
42+
if exports
43+
.iter()
44+
.any(|(name, _)| name == "wasi:cli/[email protected]#run")
45+
{
46+
if let Ok(run) = module.exports.get_func("wasi:cli/[email protected]#run") {
47+
let expt = module.exports.get_exported_func(run).unwrap();
48+
module.exports.delete(expt.id());
49+
module.funcs.delete(run);
50+
}
4751
}
48-
if let Ok(serve) = module
49-
.exports
50-
.get_func("wasi:http/[email protected]#handle")
52+
53+
if exports
54+
.iter()
55+
.any(|(name, _)| name == "wasi:http/[email protected]#handle")
5156
{
52-
let expt = module.exports.get_exported_func(serve).unwrap();
53-
module.exports.delete(expt.id());
54-
module.funcs.delete(serve);
57+
if let Ok(serve) = module
58+
.exports
59+
.get_func("wasi:http/[email protected]#handle")
60+
{
61+
let expt = module.exports.get_exported_func(serve).unwrap();
62+
module.exports.delete(expt.id());
63+
module.funcs.delete(serve);
64+
}
5565
}
5666

5767
// we reencode the WASI world component data, so strip it out from the

crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ world spidermonkey-embedding-splicer {
3333

3434
export stub-wasi: func(engine: list<u8>, features: list<features>, wit-world: option<string>, wit-path: option<string>, world-name: option<string>) -> result<list<u8>, string>;
3535

36-
export splice-bindings: func(source-name: option<string>, spidermonkey-engine: list<u8>, wit-world: option<string>, wit-path: option<string>, world-name: option<string>, debug: bool) -> result<splice-result, string>;
36+
export splice-bindings: func(source-name: option<string>, spidermonkey-engine: list<u8>, wit-world: option<string>, wit-path: option<string>, world-name: option<string>, guest-imports: list<string>, guest-exports: list<string>, debug: bool) -> result<splice-result, string>;
3737
}

src/componentize.js

+30-10
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,33 @@ export async function componentize(jsSource, witWorld, opts) {
4747
enableFeatures = [],
4848
} = opts || {};
4949

50+
await lexerInit;
51+
let jsImports = [];
52+
let jsExports = [];
53+
try {
54+
[jsImports, jsExports] = parse(jsSource);
55+
} catch {
56+
// ignore parser errors - will show up as engine parse errors shortly
57+
}
58+
59+
let guestImports = [];
60+
jsImports.map((k) => {
61+
guestImports.push(k.n);
62+
});
63+
64+
let guestExports = [];
65+
jsExports.map((k) => {
66+
guestExports.push(k.n);
67+
});
68+
5069
let { wasm, jsBindings, importWrappers, exports, imports } = spliceBindings(
5170
sourceName,
5271
await readFile(engine),
5372
witWorld,
5473
maybeWindowsPath(witPath),
5574
worldName,
75+
guestImports,
76+
guestExports,
5677
false
5778
);
5879

@@ -103,13 +124,6 @@ export async function componentize(jsSource, witWorld, opts) {
103124
await writeFile(input, Buffer.from(wasm));
104125

105126
// rewrite the JS source import specifiers to reference import wrappers
106-
await lexerInit;
107-
let jsImports = [];
108-
try {
109-
[jsImports] = parse(jsSource);
110-
} catch {
111-
// ignore parser errors - will show up as engine parse errors shortly
112-
}
113127
let source = '',
114128
curIdx = 0;
115129
for (const jsImpt of jsImports) {
@@ -183,7 +197,9 @@ export async function componentize(jsSource, witWorld, opts) {
183197
{
184198
stdio: [null, stdout, stderr],
185199
env,
186-
input: maybeWindowsPath(join(sourceDir, sourceName.slice(0, -3) + '.bindings.js')),
200+
input: maybeWindowsPath(
201+
join(sourceDir, sourceName.slice(0, -3) + '.bindings.js')
202+
),
187203
shell: true,
188204
encoding: 'utf-8',
189205
}
@@ -299,9 +315,13 @@ export async function componentize(jsSource, witWorld, opts) {
299315
}
300316

301317
// after wizering, stub out the wasi imports depending on what features are enabled
302-
const finalBin = stubWasi(bin, features, witWorld,
318+
const finalBin = stubWasi(
319+
bin,
320+
features,
321+
witWorld,
303322
maybeWindowsPath(witPath),
304-
worldName,);
323+
worldName
324+
);
305325

306326
const component = await metadataAdd(
307327
await componentNew(

test/cases/missing-export/source.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function notFunc () {
2+
3+
}

test/cases/missing-export/test.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { strictEqual } from 'node:assert';
2+
3+
export function err(e) {
4+
strictEqual(e.message, 'Expected a JS export definition for \'expected\'');
5+
}

test/cases/missing-export/world.wit

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package local:smoke;
2+
3+
world the-world {
4+
export expected: func();
5+
}

test/cases/smoke/source.js

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { y } from 'imports';
2-
31
export const exports = {
42
hello () {
53
return 'world (' + getNum('world') + ')';
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export function y () {
2-
// console.log('y');
2+
globalThis.y = true;
33
}

test/cases/treeshake/source.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as imports from 'imports';
2+
3+
export function hello () {
4+
imports.y();
5+
}

test/cases/treeshake/test.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { strictEqual, deepStrictEqual } from 'node:assert';
2+
3+
export function test(instance, { imports }) {
4+
deepStrictEqual(imports, [['imports', 'y']]);
5+
instance.hello();
6+
strictEqual(globalThis.y, true);
7+
}

test/cases/treeshake/world.wit

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package local:smoke;
2+
3+
world the-world {
4+
import unused: func();
5+
import imports: interface {
6+
y: func();
7+
}
8+
export hello: func();
9+
}

0 commit comments

Comments
 (0)