webBuildScriptTemplate function
The em++ invocation per web-targeting module. Flag set verified by the nitro web e2e suite:
- -fwasm-exceptions — the bridge's try/catch → NitroError contract
- MODULARIZE + EXPORT_NAME
create<LibPascal>Module— what NitroRuntime.loadWebModule calls (nitroWebExportName convention) - addFunction/wasmMemory/HEAPU8 runtime methods — heap I/O + the post callback replacing Dart_PostCObject
Implementation
String webBuildScriptTemplate(
List<String> webLibs, {
Map<String, String>? bridgeChecksums,
Set<String> webSpecificImpls = const {},
}) {
// One line per lib, carrying the GENERATOR's compile-time bridge checksum —
// the same constant baked into the .bridge.g.cpp and verified at runtime by
// NitroRuntime.checkLinkChecksum. It hashes the ABI surface, so reformatting
// a spec does not move it and a real signature change always does.
final stamp = webLibs
.map((lib) => '$_bridgeChecksumPrefix$lib ${bridgeChecksums?[lib] ?? 'unavailable'}')
.join('\n');
final b = StringBuffer('''
#!/usr/bin/env bash
# Generated by `nitrogen link`. Module list is kept in sync — re-run
# `nitrogen link` after adding a web-targeting module; edit flags freely.
#
# Builds the plugin's WASM module(s) into assets/web/ (bundled as Flutter
# assets — declared under `flutter: assets:` in pubspec.yaml).
# Requires the Emscripten SDK on PATH: https://emscripten.org/docs/getting_started
#
# Records the generated bridge each module was built against; `nitrogen doctor`
# warns when a bridge has been regenerated since this script was written.
$stamp
set -euo pipefail
cd "\$(dirname "\$0")/.."
GEN="lib/src/generated/cpp"
OUT="assets/web"
mkdir -p "\$OUT"
# The plugin's C++ impl sources: prefer the Hybrid*.cpp convention (only the
# nitro impl files — other modules' native code must not be linked into this
# wasm), falling back to all of src/*.cpp for single-impl layouts.
# dart_api_dl.c is VM-only — never compile it for web (nitro_wasm_compat.h
# shims the Dart API under __EMSCRIPTEN__).
IMPL_SOURCES=\$(ls src/Hybrid*.cpp 2>/dev/null || ls src/*.cpp 2>/dev/null | grep -v dart_api_dl || true)
# NITRO_WEB_THREADS=1 builds with wasm threads: impls may spawn std::thread and
# native-async work leaves the main thread. Needs SharedArrayBuffer — serve the
# app cross-origin isolated (COOP/COEP), or test in Chrome with
# --enable-features=SharedArrayBuffer. Workers are same-origin: the module must
# be served from the app's own origin.
THREAD_FLAGS=""
if [ "\${NITRO_WEB_THREADS:-0}" = "1" ]; then
THREAD_FLAGS="-pthread -sPTHREAD_POOL_SIZE=4 -sPTHREAD_POOL_SIZE_STRICT=0 -sMAXIMUM_MEMORY=1gb"
fi
''');
for (final lib in webLibs) {
final pascal = lib.split(RegExp('[_-]+')).where((s) => s.isNotEmpty).map((s) => s[0].toUpperCase() + s.substring(1)).join();
// Decided at link time, like the desktop CMakeLists: a web/src/ file with
// real code replaces the shared impl for THIS module only.
final (impl, implNote) = webSpecificImpls.contains(lib)
? ('web/src/Hybrid$pascal.cpp', 'web-specific impl')
: ('\$IMPL_SOURCES', 'shared src/ impl');
b.write('''
# $lib: $implNote
em++ -O2 --no-entry -fwasm-exceptions \$THREAD_FLAGS \\
-Isrc -Isrc/native -I"\$GEN" \\
"\$GEN/$lib.bridge.g.cpp" $impl \\
-sMODULARIZE=1 -sEXPORT_NAME=create${pascal}Module \\
-sALLOW_MEMORY_GROWTH=1 -sALLOW_TABLE_GROWTH=1 \\
-sWASM_BIGINT=1 -sENVIRONMENT=web \\
-sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction,wasmExports,wasmMemory,HEAPU8 \\
-sEXPORTED_FUNCTIONS=_malloc,_free \\
-o "\$OUT/$lib.js"
echo "built \$OUT/$lib.js + \$OUT/$lib.wasm"
''');
}
return b.toString();
}