UNPKG

@simple-software/simplebuild

Version:

A build system for JS/TS that simply builds your project in the fastest way possible, with zero effort from you.

195 lines (157 loc) 5.54 kB
const path = require('path'); const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); /** * Generate WASI runner script with template substitution * @param {string} wasmPath - Path to the WASM file * @param {string} workingDir - Working directory * @returns {string} Generated script content */ function generateWasiRunner(wasmPath, workingDir) { return `// Node ≥ 20 (host shim for WASM command execution) import { WASI } from 'node:wasi'; import { spawnSync } from 'node:child_process'; import { readFileSync } from 'fs'; import { chdir } from 'node:process'; // Change to the target working directory chdir('${workingDir}'); const wasi = new WASI({ version: 'preview1', args: process.argv.slice(2), env: process.env, preopens: { '.': '.', '/': '/', '${workingDir}': '${workingDir}' } }); // Memory management for WASM<->JS communication let memory; let wasmInstance; let bump = 0x10000; let lastExitCode = 0; const enc = new TextEncoder(); const dec = new TextDecoder(); function readString(ptr, len) { return dec.decode(new Uint8Array(memory.buffer, ptr, len)); } function allocateMemory(size) { const currentMemorySize = memory.buffer.byteLength; if (bump + size > currentMemorySize) { const pagesNeeded = Math.ceil((bump + size - currentMemorySize) / 65536); memory.grow(pagesNeeded); } const ptr = bump; bump += size; bump = (bump + 3) & ~3; // Align to 4-byte boundary return ptr; } function allocString(str) { const bytes = enc.encode(str); const ptr = allocateMemory(bytes.length); new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); return ptr; } function getExitCode() { return lastExitCode; } function hostExec(cmdPtr, cmdLen, argvPtr, argvLen) { const cmd = readString(cmdPtr, cmdLen); const argvStr = readString(argvPtr, argvLen); const argv = argvStr.split('\0').filter(arg => arg.length > 0); const { status = 255, stdout = '', stderr = '' } = spawnSync(cmd, argv, { encoding: 'utf8', cwd: '${workingDir}' }); lastExitCode = status; const output = stdout + stderr; const outputBytes = enc.encode(output); const structPtr = allocateMemory(4 + outputBytes.length); new DataView(memory.buffer).setUint32(structPtr, status, true); new Uint8Array(memory.buffer, structPtr + 4, outputBytes.length).set(outputBytes); const structLen = 4 + outputBytes.length; return (BigInt(structLen) << 32n) | BigInt(structPtr); } function hostExecWithCwd(cmdPtr, cmdLen, argvPtr, argvLen, cwdPtr, cwdLen) { const cmd = readString(cmdPtr, cmdLen); const argvStr = readString(argvPtr, argvLen); const cwd = readString(cwdPtr, cwdLen); const argv = argvStr.split('\0').filter(arg => arg.length > 0); const { status = 255, stdout = '', stderr = '' } = spawnSync(cmd, argv, { encoding: 'utf8', cwd: cwd }); lastExitCode = status; const output = stdout + stderr; const outputBytes = enc.encode(output); const structPtr = allocateMemory(4 + outputBytes.length); new DataView(memory.buffer).setUint32(structPtr, status, true); new Uint8Array(memory.buffer, structPtr + 4, outputBytes.length).set(outputBytes); const structLen = 4 + outputBytes.length; return (BigInt(structLen) << 32n) | BigInt(structPtr); } const wasm = readFileSync('${wasmPath}'); const wasmModule = new WebAssembly.Module(wasm); wasmInstance = new WebAssembly.Instance(wasmModule, { ...wasi.getImportObject(), host: { allocString, readString, getExitCode, exec: hostExec, execWithCwd: hostExecWithCwd }, }); memory = wasmInstance.exports.memory; wasi.initialize(wasmInstance); `; } /** * Run Simple Build programmatically * @param {Object} options - Build options * @param {string} options.workingDir - Working directory to build * @param {string} [options.simpleBuildDir='.simplebuild'] - Directory name for build artifacts * @returns {Promise<{exitCode: number, output: string}>} */ async function runSimpleBuild(options = {}) { const { workingDir, simpleBuildDir = '.simplebuild' } = options; if (!workingDir) { throw new Error('workingDir is required'); } const resolvedWorkingDir = path.resolve(workingDir); const wasmPath = path.join(__dirname, 'wasm', 'Simple-Build-wasm-wasi.wasm'); const tempDir = os.tmpdir(); const tempScript = path.join(tempDir, `simplebuild-runner-${Date.now()}.mjs`); const wasiScript = generateWasiRunner(wasmPath, resolvedWorkingDir); return new Promise((resolve, reject) => { fs.writeFileSync(tempScript, wasiScript); const wasmArgs = [`--working-dir=${resolvedWorkingDir}`]; if (simpleBuildDir !== '.simplebuild') { wasmArgs.push(`--simplebuild-dir=${simpleBuildDir}`); } const child = spawn('node', ['--no-warnings', tempScript, ...wasmArgs], { cwd: resolvedWorkingDir, stdio: 'pipe' }); let output = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { output += data.toString(); }); child.on('close', (code) => { try { fs.unlinkSync(tempScript); } catch (e) { // Ignore cleanup errors } resolve({ exitCode: code, output }); }); child.on('error', (error) => { try { fs.unlinkSync(tempScript); } catch (e) { // Ignore cleanup errors } reject(error); }); }); } module.exports = { runSimpleBuild };