Skip to content

Commit 77b5eca

Browse files
committed
.d.ts: decorate wasm-bindgen output with emscripten factory shape
Replaces the parked emcc --emit-tsd + textual-merge plumbing with a direct decoration approach. wasm-pack appends a small EmscriptenRuntime interface, a `MainModule = EmscriptenRuntime & BindgenModule` intersection, and a default-exported factory declaration to the wasm-bindgen-generated .d.ts. The decoration makes `import M from "./<name>.mjs"` type-check cleanly without depending on emcc's TSD generator (which currently asserts on wasm-bindgen-style multi-value returns) or any upstream emcc PR. For a pure-Rust wasm-pack package the TS surface is fully knowable to us: wasm-bindgen owns the exports, emcc's runtime is a small curated set, and there's no user C/C++ to type. Users who need additional emscripten runtime members typed can extend the .d.ts after the build. Drops emcc_post_link's emit_tsd parameter, the merge_emscripten_and_bindgen_dts helper, and the parked code path in step_emcc_post_link. Adds an integration test that asserts the decoration's shape.
1 parent 38080d5 commit 77b5eca

3 files changed

Lines changed: 115 additions & 66 deletions

File tree

docs/src/emscripten-target.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,14 @@ A few things differ from the default wasm-pack target:
132132
- **`wasm-pack test` is not supported.** The `wasm-bindgen-test` runner
133133
isn't currently wired up for emscripten; tests must run via
134134
`cargo test` directly.
135-
- **TypeScript declarations come from wasm-bindgen only.** emcc's
136-
`--emit-tsd` mode currently asserts on wasm-bindgen-style multi-value
137-
returns (any Rust function returning `String`, `Vec`, etc.). The `.d.ts`
138-
in `pkg/` describes the wasm-bindgen surface but does not type
139-
emscripten runtime methods.
135+
- **TypeScript declarations are wasm-bindgen-driven and wasm-pack-decorated.**
136+
The `.d.ts` in `pkg/` is wasm-bindgen's output (covering every
137+
`#[wasm_bindgen]` export and class) decorated with a `MainModule`
138+
intersection type and a default-export factory declaration so
139+
`import M from "./<name>.mjs"` type-checks cleanly. A small curated
140+
`EmscriptenRuntime` interface surfaces the heap views and
141+
`_initialize`. If you need more of emscripten's runtime typed (for C/C++
142+
interop, FS, etc.) extend the `.d.ts` in `pkg/` after the build.
140143
- **Optimization is capped at `-O2`.** emcc's `-O3` enables wasm-opt's
141144
`--minify-imports-and-exports` pass, which renames wasm exports to
142145
single letters — but wasm-bindgen's generated JS glue references the

src/command/build.rs

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -709,31 +709,32 @@ impl Build {
709709
emcc_opt_level_for(&self.profile)
710710
};
711711

712-
// TODO: re-enable emcc's `--emit-tsd` once
713-
// https://github.com/emscripten-core/emscripten (TSD multi-value PR)
714-
// lands. emcc currently asserts in its TSD generator on any wasm
715-
// function with multiple return values, and wasm-bindgen emits
716-
// those for every Rust-side String / Vec / Result / struct
717-
// return. The merge code path (`merge_emscripten_and_bindgen_dts`)
718-
// is kept ready below — flip `emscripten_dts` from `None` to
719-
// `Some(...)` once emcc cooperates and the merge will produce a
720-
// union `.d.ts` describing both EmscriptenModule and BindgenModule.
721-
let emscripten_dts: Option<PathBuf> = None;
722-
let _ = (
723-
&bindgen_dts,
724-
merge_emscripten_and_bindgen_dts as fn(&Path, &Path) -> Result<()>,
725-
);
726-
712+
// We deliberately don't ask emcc for `--emit-tsd`. For a pure-Rust
713+
// wasm-pack package the TS surface is fully knowable to us:
714+
// wasm-bindgen owns the user-facing exports (already typed in its
715+
// own .d.ts), and emcc's runtime surface is a small curated set
716+
// wasm-pack chooses to expose. emcc's TSD would only add value if
717+
// we linked arbitrary C/C++ exports, which the wasm-pack flow
718+
// doesn't support today. Avoiding it also sidesteps the emcc
719+
// assertion on wasm-bindgen-style multi-value-return exports.
727720
emcc_post_link(
728721
&in_wasm,
729722
&in_library,
730723
&out_js,
731-
emscripten_dts.as_deref(),
732724
&post_link_settings,
733725
opt_level,
734726
)?;
735727
info!("emcc --post-link produced {out_js:?}.");
736728

729+
// Decorate wasm-bindgen's bare .d.ts with the emscripten-shaped
730+
// factory: an `EmscriptenRuntime` interface for the runtime members
731+
// we surface, a `MainModule` intersection type, and a default-export
732+
// factory declaration. After this `import M from "./<name>.mjs"`
733+
// type-checks against the produced module.
734+
if !self.disable_dts && bindgen_dts.exists() {
735+
decorate_bindgen_dts_for_emscripten(&bindgen_dts)?;
736+
}
737+
737738
// Clean up intermediate artifacts that shouldn't ship in pkg/.
738739
// `_bg.wasm` (pre-post-link) is superseded by the post-linked
739740
// `<name>.wasm`; `library_bindgen.js` was a build-time-only DSL file.
@@ -1106,18 +1107,10 @@ fn emcc_opt_level_for(profile: &BuildProfile) -> &'static str {
11061107

11071108
/// Invoke `emcc --post-link` to merge the wasm-bindgen output with
11081109
/// emscripten's standard JS runtime, producing the final JS module.
1109-
///
1110-
/// If `emit_tsd` is supplied, emcc also tries to emit an EmscriptenModule
1111-
/// `.d.ts` to that path. Currently emcc's TSD generator asserts on
1112-
/// wasm-bindgen-style multi-value returns, so the file may not be
1113-
/// produced — we leave it as best-effort and rely on
1114-
/// `merge_emscripten_and_bindgen_dts` to fall back gracefully when the
1115-
/// file is missing.
11161110
fn emcc_post_link(
11171111
in_wasm: &Path,
11181112
in_library: &Path,
11191113
out_js: &Path,
1120-
emit_tsd: Option<&Path>,
11211114
settings: &EmccPostLinkSettings,
11221115
opt_level: &str,
11231116
) -> Result<()> {
@@ -1134,9 +1127,6 @@ fn emcc_post_link(
11341127
if settings.source_phase_imports {
11351128
cmd.arg("-sSOURCE_PHASE_IMPORTS=1");
11361129
}
1137-
if let Some(tsd) = emit_tsd {
1138-
cmd.arg("--emit-tsd").arg(tsd);
1139-
}
11401130
cmd.arg("-o").arg(out_js);
11411131

11421132
let status = cmd.status().context("running emcc --post-link")?;
@@ -1146,43 +1136,62 @@ fn emcc_post_link(
11461136
Ok(())
11471137
}
11481138

1149-
/// Fuse emscripten's `EmscriptenModule` typings with wasm-bindgen's
1150-
/// `BindgenModule` typings into a single `.d.ts` written at `bindgen_dts`
1151-
/// (overwriting it in place).
1139+
/// Append the emscripten factory shape to wasm-bindgen's `.d.ts`.
11521140
///
1153-
/// emcc's `--emit-tsd` output ends with:
1154-
/// ```ts
1155-
/// export type MainModule = EmscriptenModule;
1156-
/// ```
1157-
/// We strip that line, append wasm-bindgen's typings, and replace it with
1158-
/// an intersection:
1159-
/// ```ts
1160-
/// export type MainModule = EmscriptenModule & BindgenModule;
1161-
/// ```
1162-
/// so consumers see a single typed factory covering both surfaces.
1163-
fn merge_emscripten_and_bindgen_dts(emscripten_dts: &Path, bindgen_dts: &Path) -> Result<()> {
1164-
let em_src = std::fs::read_to_string(emscripten_dts)
1165-
.with_context(|| format!("reading emscripten .d.ts at {emscripten_dts:?}"))?;
1166-
let bg_src = std::fs::read_to_string(bindgen_dts)
1167-
.with_context(|| format!("reading bindgen .d.ts at {bindgen_dts:?}"))?;
1141+
/// wasm-bindgen's emscripten-mode output emits an unexported
1142+
/// `interface BindgenModule { ... }` and a sibling class export per
1143+
/// `#[wasm_bindgen] pub struct`, but doesn't declare the default-exported
1144+
/// module factory that emscripten's `-sMODULARIZE -sEXPORT_ES6` emits in
1145+
/// the JS. Without this decoration, `import M from "./<name>.mjs"`
1146+
/// type-checks against `any`.
1147+
///
1148+
/// We append:
1149+
///
1150+
/// ```ts
1151+
/// export interface EmscriptenRuntime {
1152+
/// // small curated set of emscripten-runtime members
1153+
/// }
1154+
/// export type MainModule = EmscriptenRuntime & BindgenModule;
1155+
/// declare function ModuleFactory(opts?: object): Promise<MainModule>;
1156+
/// export default ModuleFactory;
1157+
/// ```
1158+
///
1159+
/// The runtime members we surface are the ones realistic consumers reach
1160+
/// for (heap views, `_initialize`, optional `ccall`/`cwrap`). emcc's full
1161+
/// runtime is much larger; we deliberately don't enumerate everything
1162+
/// since that would commit us to tracking emcc's evolution.
1163+
fn decorate_bindgen_dts_for_emscripten(bindgen_dts: &Path) -> Result<()> {
1164+
const DECORATION: &str = r#"
1165+
1166+
// --- wasm-pack: emscripten factory shape ---
1167+
// Curated subset of emscripten's MODULARIZE/EXPORT_ES6 runtime surface.
1168+
// Add or remove members as your use case dictates.
1169+
export interface EmscriptenRuntime {
1170+
HEAP8: Int8Array;
1171+
HEAPU8: Uint8Array;
1172+
HEAP16: Int16Array;
1173+
HEAPU16: Uint16Array;
1174+
HEAP32: Int32Array;
1175+
HEAPU32: Uint32Array;
1176+
HEAPF32: Float32Array;
1177+
HEAPF64: Float64Array;
1178+
_initialize?: () => void;
1179+
}
11681180
1169-
let mut merged = String::new();
1170-
for line in em_src.lines() {
1171-
if line.trim_start().starts_with("export type MainModule") {
1172-
continue;
1173-
}
1174-
merged.push_str(line);
1175-
merged.push('\n');
1176-
}
1177-
merged.push_str("\n// --- wasm-bindgen-generated types ---\n");
1178-
merged.push_str(&bg_src);
1179-
if !merged.ends_with('\n') {
1180-
merged.push('\n');
1181-
}
1182-
merged.push_str("\nexport type MainModule = EmscriptenModule & BindgenModule;\n");
1181+
export type MainModule = EmscriptenRuntime & BindgenModule;
1182+
1183+
declare function ModuleFactory(opts?: object): Promise<MainModule>;
1184+
export default ModuleFactory;
1185+
"#;
11831186

1184-
std::fs::write(bindgen_dts, merged)
1185-
.with_context(|| format!("writing merged .d.ts to {bindgen_dts:?}"))?;
1187+
let mut existing = std::fs::read_to_string(bindgen_dts)
1188+
.with_context(|| format!("reading bindgen .d.ts at {bindgen_dts:?}"))?;
1189+
if !existing.ends_with('\n') {
1190+
existing.push('\n');
1191+
}
1192+
existing.push_str(DECORATION);
1193+
std::fs::write(bindgen_dts, existing)
1194+
.with_context(|| format!("writing decorated .d.ts to {bindgen_dts:?}"))?;
11861195
Ok(())
11871196
}
11881197

tests/all/emscripten.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,43 @@ fn emscripten_package_json_points_at_mjs() {
275275
);
276276
}
277277

278+
#[test]
279+
fn emscripten_dts_is_decorated_with_factory_shape() {
280+
skip_without_emcc!();
281+
let fixture = utils::fixture::emscripten_hello_world();
282+
let mut cmd = fixture.wasm_pack();
283+
if let Some(bin) = local_wasm_bindgen_bin() {
284+
cmd.env("WASM_BINDGEN_BIN", bin);
285+
} else {
286+
fixture.install_local_wasm_bindgen();
287+
}
288+
cmd.arg("build")
289+
.arg("--target")
290+
.arg("web")
291+
.assert()
292+
.success();
293+
294+
let dts = std::fs::read_to_string(fixture.path.join("pkg/em_hello_world.d.ts")).unwrap();
295+
// wasm-bindgen's own surface should be present untouched.
296+
assert!(
297+
dts.contains("interface BindgenModule"),
298+
"bindgen-emitted BindgenModule interface should be preserved"
299+
);
300+
// wasm-pack appends the factory shape on top.
301+
for needle in [
302+
"export interface EmscriptenRuntime",
303+
"HEAPU8: Uint8Array",
304+
"export type MainModule = EmscriptenRuntime & BindgenModule",
305+
"declare function ModuleFactory(opts?: object): Promise<MainModule>",
306+
"export default ModuleFactory",
307+
] {
308+
assert!(
309+
dts.contains(needle),
310+
"expected decorated .d.ts to contain {needle:?}; got:\n{dts}"
311+
);
312+
}
313+
}
314+
278315
#[test]
279316
fn emscripten_test_command_is_rejected() {
280317
// No emcc gating — this path doesn't actually invoke emcc, just

0 commit comments

Comments
 (0)