@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
JavaScript
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 };