import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync, spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const rootDir = path.dirname(fileURLToPath(import.meta.url)); const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')); const nodeConfig = packageJson.nodeCore; const EMSDK_CONFIG = { repository: 'https://github.com/emscripten-core/emsdk.git', commit: '3.1.56', checkoutDir: 'emsdk', toolVersion: '3.1.56', }; const nodeCheckoutDir = path.resolve(rootDir, nodeConfig.checkoutDir); const emsdkDir = path.resolve(rootDir, EMSDK_CONFIG.checkoutDir); const wasmBuildDir = path.join(nodeCheckoutDir, 'wasm-build'); const wasmOutputFile = path.join(wasmBuildDir, 'node-worker.wasm'); const wasmModuleFile = path.join(wasmBuildDir, 'node-worker.wasm.js'); const nodeWasmDir = path.join(rootDir, 'src', 'worker', 'node-wasm'); const llhttpShimFile = path.join(nodeWasmDir, 'llhttp-shim.c'); const zlibShimFile = path.join(nodeWasmDir, 'zlib-shim.c'); const brotliShimFile = path.join(nodeWasmDir, 'brotli-shim.c'); const cryptoShimFile = path.join(nodeWasmDir, 'crypto-shim.c'); const nghttp2ShimFile = path.join(nodeWasmDir, 'nghttp2-shim.c'); const patchesDir = path.join(rootDir, 'node-patches'); const nodeRustDir = path.join(rootDir, 'src', 'worker', 'node-rust'); // The wasm-bindgen bits live in the `wasm` crate (which depends on the pure-Rust // `rewriter` core crate); build.sh + the wasm-bindgen output dir live there too. const rewriterWasmCrateDir = path.join(nodeRustDir, 'wasm'); const rewriterOutDir = path.join(rewriterWasmCrateDir, 'out'); const rewriterWasmFile = path.join(rewriterOutDir, 'rewriter_bg.wasm'); const rewriterGlueFile = path.join(rewriterOutDir, 'rewriter.js'); const rewriterDtsFile = path.join(rewriterOutDir, 'rewriter.d.ts'); const rewriterBgDtsFile = path.join(rewriterOutDir, 'rewriter_bg.wasm.d.ts'); const wasmBuildRewriterGlueFile = path.join(wasmBuildDir, 'rewriter.js'); const wasmBuildRewriterDtsFile = path.join(wasmBuildDir, 'rewriter.d.ts'); const wasmBuildRewriterBgDtsFile = path.join(wasmBuildDir, 'rewriter_bg.wasm.d.ts'); const wasmBuildRewriterModuleFile = path.join(wasmBuildDir, 'rewriter.wasm.js'); // OpenSSL libcrypto is built straight from node's vendored tree so the version // (3.5.5) and headers match. `linux-x32` is an ILP32 target that maps cleanly // onto wasm32; `no-asm` is mandatory. The CROSS_COMPILE blanking fixes the // emconfigure-mangled CC path. Only `libcrypto.a` is built (no TLS/libssl). const opensslSourceDir = path.join(nodeCheckoutDir, 'deps', 'openssl', 'openssl'); const opensslBuildDir = path.join(nodeCheckoutDir, 'deps', 'openssl', 'wasm-build'); const opensslLibCrypto = path.join(opensslBuildDir, 'libcrypto.a'); function run(command, args, cwd = rootDir, extra = {}) { const result = spawnSync(command, args, { cwd, stdio: 'inherit', env: { ...process.env, ...extra.env, }, }); if (result.status !== 0) { throw new Error(`Command failed: ${command} ${args.join(' ')}`); } } function runAsync(command, args, extra = {}) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: rootDir, stdio: ['ignore', 'inherit', 'inherit'], env: { ...process.env, ...extra.env }, }); child.on('error', reject); child.on('close', (status) => { if (status === 0) resolve(); else reject(new Error(`Command failed: ${command} ${args.join(' ')}`)); }); }); } // Run an array of thunks with bounded concurrency (one in flight per core). async function runPool(thunks, concurrency) { let next = 0; const worker = async () => { while (next < thunks.length) { await thunks[next++](); } }; await Promise.all( Array.from({ length: Math.min(concurrency, thunks.length) }, worker), ); } function capture(command, args, cwd = rootDir, extra = {}) { const result = spawnSync(command, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...extra.env, }, }); if (result.status !== 0) { const stderr = result.stderr?.trim(); throw new Error(stderr || `Command failed: ${command} ${args.join(' ')}`); } return result.stdout.trim(); } function ensureParentDir(filePath) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } function tryResolveRevision(checkoutDir, revision) { for (const candidate of [revision, `refs/tags/${revision}`, 'FETCH_HEAD']) { const result = spawnSync('git', ['rev-parse', '--verify', candidate], { cwd: checkoutDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }); if (result.status === 0) { return result.stdout.trim(); } } return null; } function ensureGitCheckout(config, checkoutDir) { if (!fs.existsSync(checkoutDir)) { fs.mkdirSync(path.dirname(checkoutDir), { recursive: true }); run('git', ['clone', config.repository, checkoutDir, '--depth=1']); } let target = tryResolveRevision(checkoutDir, config.commit); if (target === null) { run('git', ['fetch', '--depth=1', 'origin', config.commit], checkoutDir); target = tryResolveRevision(checkoutDir, config.commit); } if (target === null) { run('git', ['fetch', '--depth=1', 'origin', 'tag', config.commit], checkoutDir); target = tryResolveRevision(checkoutDir, config.commit); } if (target === null) { throw new Error(`Unable to resolve ${config.commit} in ${checkoutDir}`); } const head = capture('git', ['rev-parse', 'HEAD'], checkoutDir); if (head !== target) { run('git', ['checkout', '--detach', target], checkoutDir); } } function applyNodePatches() { if (!fs.existsSync(patchesDir)) { return; } const patches = fs.readdirSync(patchesDir) .filter((file) => file.endsWith('.patch')) .sort(); for (const patch of patches) { const patchPath = path.join(patchesDir, patch); const forward = spawnSync('git', ['apply', '--check', patchPath], { cwd: nodeCheckoutDir }); if (forward.status === 0) { run('git', ['apply', patchPath], nodeCheckoutDir); continue; } const reverse = spawnSync('git', ['apply', '--reverse', '--check', patchPath], { cwd: nodeCheckoutDir }); if (reverse.status !== 0) { throw new Error(`Patch does not apply cleanly: ${patch}`); } } } function ensureEmsdkInstalled() { const emsdkScript = path.join(emsdkDir, 'emsdk'); run(emsdkScript, ['install', EMSDK_CONFIG.toolVersion], emsdkDir); run(emsdkScript, ['activate', EMSDK_CONFIG.toolVersion], emsdkDir); } function buildOpenSSL() { // Skip if already built — the libcrypto.a compile is the slowest step. if (fs.existsSync(opensslLibCrypto)) { return; } const emConfigure = path.join(emsdkDir, 'upstream', 'emscripten', 'emconfigure'); const emMake = path.join(emsdkDir, 'upstream', 'emscripten', 'emmake'); const emConfig = path.join(emsdkDir, '.emscripten'); fs.mkdirSync(opensslBuildDir, { recursive: true }); const configure = path.join(opensslSourceDir, 'Configure'); run( emConfigure, [ configure, 'linux-x32', // Seed the DRBG via getrandom()/getentropy() instead of the default // OS path (which reads /dev/urandom — unavailable under FILESYSTEM=0). // -D__ELF__ enables the getentropy weak-symbol branch in // providers/.../seeding/rand_unix.c, which crypto-shim.c satisfies. '--with-rand-seed=getrandom', 'no-asm', 'no-shared', 'no-dso', 'no-engine', 'no-afalgeng', 'no-tests', 'no-threads', 'no-ui-console', 'no-comp', 'no-ocsp', 'no-srp', 'no-cms', 'no-ts', 'no-ssl', 'no-tls', 'no-quic', '-D__ELF__', '-DOPENSSL_SYS_NETWARE', '-DHAVE_FORK=0', '-DOPENSSL_NO_DYNAMIC_ENGINE', '-DOPENSSL_NO_SECURE_MEMORY', ], opensslBuildDir, { env: { EM_CONFIG: emConfig } }, ); // emconfigure mangles CROSS_COMPILE into the CC path; blank it so CC is the // bare absolute emcc path. const makefilePath = path.join(opensslBuildDir, 'Makefile'); const makefile = fs.readFileSync(makefilePath, 'utf8') .replace(/^CROSS_COMPILE.*$/m, 'CROSS_COMPILE='); fs.writeFileSync(makefilePath, makefile); run( emMake, ['make', `-j${os.cpus().length}`, 'build_generated', 'libcrypto.a'], opensslBuildDir, { env: { EM_CONFIG: emConfig } }, ); if (!fs.existsSync(opensslLibCrypto)) { throw new Error(`OpenSSL build did not produce ${opensslLibCrypto}`); } } async function compileNodeWorkerWasm() { ensureParentDir(wasmOutputFile); const emcc = path.join(emsdkDir, 'upstream', 'emscripten', 'emcc'); const emConfig = path.join(emsdkDir, '.emscripten'); if (!fs.existsSync(emcc)) { throw new Error(`Missing emcc at ${emcc}`); } if (!fs.existsSync(emConfig)) { throw new Error(`Missing emsdk config at ${emConfig}`); } const llhttpDir = path.join(nodeCheckoutDir, 'deps', 'llhttp'); const llhttpIncludeDir = path.join(llhttpDir, 'include'); const llhttpSourceDir = path.join(llhttpDir, 'src'); const zlibDir = path.join(nodeCheckoutDir, 'deps', 'zlib'); const brotliDir = path.join(nodeCheckoutDir, 'deps', 'brotli', 'c'); const brotliIncludeDir = path.join(brotliDir, 'include'); const nghttp2Dir = path.join(nodeCheckoutDir, 'deps', 'nghttp2', 'lib'); const nghttp2IncludeDir = path.join(nghttp2Dir, 'includes'); // Generated headers (configuration.h, opensslconf.h) live in the build dir; // the rest of the public headers come from the source tree. const opensslBuildIncludeDir = path.join(opensslBuildDir, 'include'); const opensslSrcIncludeDir = path.join(opensslSourceDir, 'include'); // Portable subset — leaves SIMD (adler32_simd, crc32_simd, crc_folding, // slide_hash_simd, cpu_features) and unused file-I/O code (gz*, compress, // uncompr, infback) out of the build. const zlibSources = [ 'adler32.c', 'crc32.c', 'deflate.c', 'inffast.c', 'inflate.c', 'inftrees.c', 'trees.c', 'zutil.c', ].map((name) => path.join(zlibDir, name)); // Mirrors `deps/brotli/brotli.gyp:brotli_sources`. const brotliSources = [ 'common/constants.c', 'common/context.c', 'common/dictionary.c', 'common/platform.c', 'common/shared_dictionary.c', 'common/transform.c', 'dec/bit_reader.c', 'dec/decode.c', 'dec/huffman.c', 'dec/prefix.c', 'dec/state.c', 'dec/static_init.c', 'enc/backward_references.c', 'enc/backward_references_hq.c', 'enc/bit_cost.c', 'enc/block_splitter.c', 'enc/brotli_bit_stream.c', 'enc/cluster.c', 'enc/command.c', 'enc/compound_dictionary.c', 'enc/compress_fragment.c', 'enc/compress_fragment_two_pass.c', 'enc/dictionary_hash.c', 'enc/encode.c', 'enc/encoder_dict.c', 'enc/entropy_encode.c', 'enc/fast_log.c', 'enc/histogram.c', 'enc/literal_cost.c', 'enc/memory.c', 'enc/metablock.c', 'enc/static_dict.c', 'enc/static_dict_lut.c', 'enc/static_init.c', 'enc/utf8_util.c', ].map((name) => path.join(brotliDir, name)); // Mirrors `deps/nghttp2/nghttp2.gyp:nghttp2_sources`. const nghttp2Sources = [ 'nghttp2_buf.c', 'nghttp2_callbacks.c', 'nghttp2_debug.c', 'nghttp2_extpri.c', 'nghttp2_frame.c', 'nghttp2_hd.c', 'nghttp2_hd_huffman.c', 'nghttp2_hd_huffman_data.c', 'nghttp2_helper.c', 'nghttp2_http.c', 'nghttp2_map.c', 'nghttp2_mem.c', 'nghttp2_alpn.c', 'nghttp2_option.c', 'nghttp2_outbound_item.c', 'nghttp2_pq.c', 'nghttp2_priority_spec.c', 'nghttp2_queue.c', 'nghttp2_ratelim.c', 'nghttp2_rcbuf.c', 'nghttp2_session.c', 'nghttp2_stream.c', 'nghttp2_submit.c', 'nghttp2_time.c', 'nghttp2_version.c', 'sfparse.c', ].map((name) => path.join(nghttp2Dir, name)); const exportedFunctions = [ '_malloc', '_free', // llhttp shim '_llhttp_wasm_alloc', '_llhttp_wasm_free', '_llhttp_wasm_init', '_llhttp_wasm_smoke_test', '_llhttp_execute', '_llhttp_finish', '_llhttp_pause', '_llhttp_resume', '_llhttp_resume_after_upgrade', '_llhttp_get_type', '_llhttp_get_http_major', '_llhttp_get_http_minor', '_llhttp_get_method', '_llhttp_get_status_code', '_llhttp_get_upgrade', '_llhttp_reset', '_llhttp_should_keep_alive', '_llhttp_get_errno', '_llhttp_get_error_reason', '_llhttp_get_error_pos', '_llhttp_errno_name', '_llhttp_set_lenient_headers', '_llhttp_set_lenient_chunked_length', '_llhttp_set_lenient_keep_alive', '_llhttp_set_lenient_transfer_encoding', '_llhttp_set_lenient_version', '_llhttp_set_lenient_data_after_close', '_llhttp_set_lenient_optional_lf_after_cr', '_llhttp_set_lenient_optional_crlf_after_chunk', '_llhttp_set_lenient_optional_cr_before_lf', '_llhttp_set_lenient_spaces_after_chunk_size', // zlib shim '_zlib_alloc', '_zlib_init', '_zlib_ensure_in_buf', '_zlib_ensure_out_buf', '_zlib_write', '_zlib_avail_in', '_zlib_avail_out', '_zlib_get_err', '_zlib_get_msg', '_zlib_params', '_zlib_reset', '_zlib_end', '_zlib_crc32_buf', '_zlib_smoke_test', // brotli shim '_brotli_alloc', '_brotli_init', '_brotli_ensure_in_buf', '_brotli_ensure_out_buf', '_brotli_write', '_brotli_avail_in', '_brotli_avail_out', '_brotli_get_err', '_brotli_get_msg', '_brotli_end', // crypto shim (OpenSSL libcrypto) '_crypto_init', '_crypto_last_error', '_crypto_md_oneshot', '_crypto_md_new', '_crypto_md_copy', '_crypto_md_update', '_crypto_md_final', '_crypto_md_free', '_crypto_hmac_new', '_crypto_hmac_update', '_crypto_hmac_final', '_crypto_hmac_free', '_crypto_get_hashes', '_crypto_get_ciphers', '_crypto_get_curves', '_crypto_pbkdf2', '_crypto_scrypt', '_crypto_hkdf', '_crypto_cipher_new', '_crypto_cipher_update', '_crypto_cipher_final', '_crypto_cipher_set_aad', '_crypto_cipher_get_auth_tag', '_crypto_cipher_set_auth_tag', '_crypto_cipher_set_auto_padding', '_crypto_cipher_free', '_crypto_cipher_info', '_crypto_pkey_parse', '_crypto_pkey_free', '_crypto_pkey_up_ref', '_crypto_pkey_type', '_crypto_pkey_export', '_crypto_pkey_detail', '_crypto_generate_rsa', '_crypto_generate_ec', '_crypto_generate_ed', '_crypto_pkey_sign', '_crypto_pkey_verify', '_crypto_generate_secret', '_crypto_x509_parse', '_crypto_x509_free', '_crypto_x509_name', '_crypto_x509_fingerprint', '_crypto_x509_valid', '_crypto_x509_serial', '_crypto_x509_subject_alt_name', '_crypto_x509_info_access', '_crypto_x509_sig_alg', '_crypto_x509_raw', '_crypto_x509_pem', '_crypto_x509_public_key', '_crypto_x509_check_host', '_crypto_x509_check_email', '_crypto_x509_check_ip', '_crypto_x509_check_ca', '_crypto_x509_verify', '_crypto_x509_check_issued', '_crypto_rand_bytes', '_crypto_timing_safe_equal', '_crypto_smoke_test', // nghttp2 shim '_h2_session_new', '_h2_session_del', '_h2_session_want_read', '_h2_session_want_write', '_h2_recv_buf', '_h2_session_mem_recv', '_h2_session_send', '_h2_session_send_ptr', '_h2_submit_request', '_h2_submit_trailers', '_h2_submit_rst_stream', '_h2_submit_priority', '_h2_resume_data', '_h2_submit_settings', '_h2_pack_settings', '_h2_submit_ping', '_h2_submit_goaway', '_h2_set_next_stream_id', '_h2_set_local_window_size', '_h2_terminate', '_h2_refresh_session_state', '_h2_refresh_stream_state', '_h2_get_settings', '_h2_get_next_stream_id', '_h2_get_ping_data', '_h2_get_goaway_code', '_h2_get_goaway_last_stream', '_h2_get_goaway_opaque_ptr', '_h2_get_goaway_opaque_len', '_h2_strerror', '_h2_smoke_test', ]; const sources = [ llhttpShimFile, zlibShimFile, brotliShimFile, cryptoShimFile, nghttp2ShimFile, path.join(llhttpSourceDir, 'api.c'), path.join(llhttpSourceDir, 'http.c'), path.join(llhttpSourceDir, 'llhttp.c'), ...zlibSources, ...brotliSources, ...nghttp2Sources, ]; // Per-file compile flags (shared by every object). Passing the full include // set to every source is harmless and keeps the job uniform. const includes = [ `-I${llhttpIncludeDir}`, `-I${zlibDir}`, `-I${brotliIncludeDir}`, `-I${opensslBuildIncludeDir}`, `-I${opensslSrcIncludeDir}`, ]; const compileFlags = [...includes, '-O3', '-ffunction-sections', '-fdata-sections']; // nghttp2's lib sources + our shim need the vendored headers and the same // defines node's nghttp2.gyp uses. Applied only to those TUs so the shared // object cache for llhttp/zlib/brotli/crypto isn't invalidated. const nghttp2Flags = [ `-I${nghttp2IncludeDir}`, `-I${nghttp2Dir}`, '-DHAVE_CONFIG_H', '-DBUILDING_NGHTTP2', '-DNGHTTP2_STATICLIB', '-D_U_=', ]; const nghttp2SrcSet = new Set([nghttp2ShimFile, ...nghttp2Sources]); const extraFlagsFor = (src) => (nghttp2SrcSet.has(src) ? nghttp2Flags : []); // Compile each source to an object file in parallel (one job per core), // caching by source mtime. The whole object cache is invalidated whenever // the compile flags change. const objDir = path.join(wasmBuildDir, 'obj'); fs.mkdirSync(objDir, { recursive: true }); const flagsFile = path.join(objDir, '.compileflags'); const flagsKey = JSON.stringify([compileFlags, nghttp2Flags]); if (!fs.existsSync(flagsFile) || fs.readFileSync(flagsFile, 'utf8') !== flagsKey) { for (const f of fs.readdirSync(objDir)) { if (f.endsWith('.o')) fs.rmSync(path.join(objDir, f)); } fs.writeFileSync(flagsFile, flagsKey); } const objectFor = (src) => path.join(objDir, path.relative(rootDir, src).replace(/[\\/]/g, '__').replace(/\.c$/, '.o')); const objects = sources.map(objectFor); const jobs = []; sources.forEach((src, i) => { const obj = objects[i]; const fresh = fs.existsSync(obj) && fs.statSync(obj).mtimeMs >= fs.statSync(src).mtimeMs; if (fresh) return; jobs.push(() => runAsync(emcc, ['-c', src, ...compileFlags, ...extraFlagsFor(src), '-o', obj], { env: { EM_CONFIG: emConfig } })); }); const cores = os.cpus().length; if (jobs.length > 0) { console.log(`Compiling ${jobs.length}/${sources.length} wasm objects with ${cores} workers (${sources.length - jobs.length} cached)...`); await runPool(jobs, cores); } else { console.log(`All ${sources.length} wasm objects cached; linking only.`); } // Link the objects + libcrypto.a into the standalone wasm. libcrypto.a is // last so the linker resolves the crypto_* references; --gc-sections drops // the large unreferenced majority of libcrypto. This (binaryen -O3) is the // serial floor, so skip it when the output is already newer than every input // and the link flags are unchanged. const linkFlags = [ '-O3', '-Wl,--gc-sections', '-sSTANDALONE_WASM=1', '-sFILESYSTEM=0', '-sALLOW_MEMORY_GROWTH=1', '-sERROR_ON_UNDEFINED_SYMBOLS=1', `-sEXPORTED_FUNCTIONS=${JSON.stringify(exportedFunctions)}`, '-Wl,--no-entry', ]; const linkFile = path.join(objDir, '.linkflags'); const linkKey = JSON.stringify(linkFlags); const linkFlagsChanged = !fs.existsSync(linkFile) || fs.readFileSync(linkFile, 'utf8') !== linkKey; const newestInput = Math.max( fs.statSync(opensslLibCrypto).mtimeMs, ...objects.map((o) => fs.statSync(o).mtimeMs), ); const wasmFresh = !linkFlagsChanged && fs.existsSync(wasmOutputFile) && fs.statSync(wasmOutputFile).mtimeMs >= newestInput; if (wasmFresh) { console.log('wasm is up to date; skipping link.'); return; } run(emcc, [...objects, opensslLibCrypto, ...linkFlags, '-o', wasmOutputFile], rootDir, { env: { EM_CONFIG: emConfig }, }); fs.writeFileSync(linkFile, linkKey); } // Builds the Rust `wasm` crate to wasm via its own build.sh (cargo + // wasm-bindgen — see src/worker/node-rust/wasm/build.sh), then copies the // wasm-bindgen glue/types and base64-embeds the wasm binary into // node_core/wasm-build/, mirroring emitWasmModule()'s pattern for the // emscripten build. Unlike node-worker.wasm, wasm-bindgen's `--target web` // output isn't a standalone module — src/worker/node-rust/loader.ts uses the // copied glue's own `initSync` to instantiate it instead of a hand-rolled // WebAssembly.Instance. function buildRewriterWasm() { const sourceFiles = [path.join(nodeRustDir, 'Cargo.toml')]; for (const crate of ['rewriter', 'transform', 'wasm']) { const crateDir = path.join(nodeRustDir, crate); sourceFiles.push(path.join(crateDir, 'Cargo.toml')); const srcDir = path.join(crateDir, 'src'); for (const file of fs.readdirSync(srcDir)) { if (file.endsWith('.rs')) sourceFiles.push(path.join(srcDir, file)); } } const newestSource = Math.max(...sourceFiles.map((f) => fs.statSync(f).mtimeMs)); const outputsFresh = fs.existsSync(wasmBuildRewriterModuleFile) && fs.existsSync(wasmBuildRewriterGlueFile) && fs.statSync(wasmBuildRewriterModuleFile).mtimeMs >= newestSource && fs.statSync(wasmBuildRewriterGlueFile).mtimeMs >= newestSource; if (outputsFresh) { console.log('rewriter wasm is up to date; skipping build.'); return; } run(path.join(rewriterWasmCrateDir, 'build.sh'), [], rewriterWasmCrateDir); ensureParentDir(wasmBuildRewriterModuleFile); fs.copyFileSync(rewriterGlueFile, wasmBuildRewriterGlueFile); if (fs.existsSync(rewriterDtsFile)) { fs.copyFileSync(rewriterDtsFile, wasmBuildRewriterDtsFile); } if (fs.existsSync(rewriterBgDtsFile)) { fs.copyFileSync(rewriterBgDtsFile, wasmBuildRewriterBgDtsFile); } const base64 = fs.readFileSync(rewriterWasmFile).toString('base64'); const contents = `const rewriterWasmBase64 = ${JSON.stringify(base64)};\n\nexport default rewriterWasmBase64;\n`; fs.writeFileSync(wasmBuildRewriterModuleFile, contents); } function emitWasmModule() { // Skip re-encoding when the base64 module is already newer than the wasm. if ( fs.existsSync(wasmModuleFile) && fs.statSync(wasmModuleFile).mtimeMs >= fs.statSync(wasmOutputFile).mtimeMs ) { return; } ensureParentDir(wasmModuleFile); const base64 = fs.readFileSync(wasmOutputFile).toString('base64'); const contents = `const nodeWorkerWasmBase64 = ${JSON.stringify(base64)};\n\nexport default nodeWorkerWasmBase64;\n`; fs.writeFileSync(wasmModuleFile, contents); } // WASM_ONLY skips the (already-done) checkout/emsdk steps for fast wasm // rebuilds during shim development. if (!process.env.WASM_ONLY) { ensureGitCheckout(nodeConfig, nodeCheckoutDir); applyNodePatches(); ensureGitCheckout(EMSDK_CONFIG, emsdkDir); ensureEmsdkInstalled(); } buildOpenSSL(); await compileNodeWorkerWasm(); emitWasmModule(); buildRewriterWasm();