From 7093fb49a00a36e613f7c96d2e50ecc3e669c0b3 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 12 Jun 2026 18:48:27 +0200 Subject: [PATCH] Initial commit step0 --- step0/.gitignore | 2 + step0/CMakeLists.txt | 11 ++++ step0/README.md | 1 + step0/Web.cmake | 38 ++++++++++++ step0/app.c | 4 ++ step0/favicon.ico | Bin 0 -> 1429 bytes step0/index.html | 33 ++++++++++ step0/main.js | 70 +++++++++++++++++++++ step0/package-lock.json | 33 ++++++++++ step0/package.json | 5 ++ step0/server.js | 133 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 330 insertions(+) create mode 100644 step0/.gitignore create mode 100644 step0/CMakeLists.txt create mode 100644 step0/README.md create mode 100644 step0/Web.cmake create mode 100644 step0/app.c create mode 100644 step0/favicon.ico create mode 100644 step0/index.html create mode 100644 step0/main.js create mode 100644 step0/package-lock.json create mode 100644 step0/package.json create mode 100644 step0/server.js diff --git a/step0/.gitignore b/step0/.gitignore new file mode 100644 index 0000000..b38db2f --- /dev/null +++ b/step0/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/step0/CMakeLists.txt b/step0/CMakeLists.txt new file mode 100644 index 0000000..726418e --- /dev/null +++ b/step0/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 4.0.0) +project(cjs-lite) + +add_executable(app app.c) + +# Set specific executable flags +target_link_options(app PRIVATE + -Wl,--no-entry + -Wl,--no-growable-memory + -Wl,--initial-memory=536870912 +) diff --git a/step0/README.md b/step0/README.md new file mode 100644 index 0000000..2910a4d --- /dev/null +++ b/step0/README.md @@ -0,0 +1 @@ +# C + JavaScript (lite) diff --git a/step0/Web.cmake b/step0/Web.cmake new file mode 100644 index 0000000..1431a9e --- /dev/null +++ b/step0/Web.cmake @@ -0,0 +1,38 @@ +set(CMAKE_SYSTEM_NAME WASI) +set(CMAKE_SYSTEM_PROCESSOR WASM) +set(CMAKE_SYSTEM_PROCESSOR_FAMILY WASM) +set(WASM ON) +set(WEB ON) + +# Find clang/clang++ as compiler +find_program(CMAKE_C_COMPILER clang) +find_program(CMAKE_CXX_COMPILER clang++) +find_program(CMAKE_ASM_COMPILER llvm-mc) + +# Some options that affect the generated output +set(WASM_OPTS -mbulk-memory -matomics -fignore-exceptions -msimd128) + +add_compile_definitions( + $<$:__wasi__> + $<$:__WASM__> +) +add_compile_options(${WASM_OPTS}) +add_link_options(-nostdlib ${WASM_OPTS}) + +set(CMAKE_CROSSCOMPILING TRUE) +set(CMAKE_EXECUTABLE_SUFFIX_CXX ".wasm") +set(CMAKE_EXECUTABLE_SUFFIX_C ".wasm") + +set(BUILD_SHARED_LIBS OFF) + +set(CMAKE_C_COMPILER_TARGET unknown-unknown-wasm32-web) +set(CMAKE_CXX_COMPILER_TARGET unknown-unknown-wasm32-web) +set(CMAKE_ASM_COMPILER_TARGET unknown-unknown-wasm32-web) + +# adjust the default behavior of the FIND_XXX() commands: +# search programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# search headers and libraries in the target environment +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/step0/app.c b/step0/app.c new file mode 100644 index 0000000..292fd5a --- /dev/null +++ b/step0/app.c @@ -0,0 +1,4 @@ +__attribute__((export_name("hello"))) +int hello() { + return 4; +} diff --git a/step0/favicon.ico b/step0/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c1e82a8eed3cb256c8f0042e430d183a71cd4ca1 GIT binary patch literal 1429 zcmb7CeNfV89RL0>glL?!l6l$*%d>504!bqwoMfKi*g0LzUNleCtuzz8#E>);9c`_4 zy6v)2n(pGJb{Z)2R6x|zD|ZKC%b9Pn;kYyvrVd3g)E#hj-Mr52`RBRkp3n37eqJ5` z1P}l~Irm~T01*Jb{v0G|0_)DduE5WuaeE8Ry|KD)ZUV69jvR;}fA>{Sm0 zkquMjz4@wJ;U%YNU8wrxHxo*ZQj9v84e z#^m9RK2+;o`w6W654%&64H;u(mBPK?P$;jN1^96qyCnm zzUiB3_>{c#Im4`*UYUg0M}98}A4|4XGr~gn;PJoN5`TEIT6$ubQ={FMFJ@Pm&a`lR zZWBopmr{3$RZPo!V{-hj7^v#qO&02|KQZqYD8+pfamOXEMTfP$!$kz2ePH)0vM^P{ z){Hw?PYPgt{#-}FlvkR6EV-Muh(wG?8Bn-l9i^s~l>u1a>N^Z^F!I6e@#Cy9Ju{ggJ5X+%YcKtM$xbp^;-mxmMy zE5N`?Mz#GHS#=&@^;`k6#tdXl5D2#dw(vwl&bXzis;fNbm|353Kp~|s79s+gNtRfL z67;SXp;JgCN&jxL(DV=k8YUW2QP8u+>5a+rZjo2Wg8skjC8JjI$FUAI1D-SuttldHA zi3h^<0$R_8^nr%1<}`TJu0ix1>#;$_SpAZPt@Odrs8E!p;1V!x(q=*-@P_WjQTs2oM6 z(pd2#5l6rOV%;d|RV%bx!5$yw=(jyOA6|${`ZmYC^gr>v+Pl(ZQ!{wErhqsaJ+rgA uMH9G51jaJ~^I0aDUyEPwX3&kk96Br{;*Y*PI(TeyxMUuRjgmyBl>P&)K_A`# literal 0 HcmV?d00001 diff --git a/step0/index.html b/step0/index.html new file mode 100644 index 0000000..096ff45 --- /dev/null +++ b/step0/index.html @@ -0,0 +1,33 @@ + + + + + + step0 + + + + + + + diff --git a/step0/main.js b/step0/main.js new file mode 100644 index 0000000..ce2a953 --- /dev/null +++ b/step0/main.js @@ -0,0 +1,70 @@ +const debug = function() { console.debug(...arguments); }; +const log = function() { console.log(...arguments) }; +const error = function() { console.error(...arguments) }; + +async function start() { + try { + const response = await fetch('./bin/app.wasm'); + if (!response.ok) { + throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`); + } + + const wasmBytes = await response.arrayBuffer(); + const wasmCompiled = await WebAssembly.compile(wasmBytes); + const wasmModule = await WebAssembly.instantiate(wasmCompiled, { }); + + // Set canvas size to match display size + function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + var result = wasmModule.exports.hello(); + log("Finished", result); + } catch (error) { + error('runtime error:', error); + } +} + +start(); + +// ------- debug hot-reload code ------- +{ + const debugCtl = { + connect: () => { + const ws = new WebSocket(`${window.location.href}control`); + + ws.onopen = () => { + log('Connected to control WebSocket'); + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + log('Received message:', message); + + if (message.type === 'refresh') { + log('Reloading page...'); + window.location.reload(); + } + } catch (error) { + error('Error parsing WebSocket message:', error); + } + }; + + ws.onclose = () => { + log('WebSocket disconnected, reconnecting in 2s...'); + setTimeout(debugCtl.connect, 2000); + }; + + ws.onerror = (error) => { + error('WebSocket error:', error); + }; + } + }; + debugCtl.connect(); +} +// ------------------------------------- diff --git a/step0/package-lock.json b/step0/package-lock.json new file mode 100644 index 0000000..54c4faf --- /dev/null +++ b/step0/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "step0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "ws": "^8.18.3" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/step0/package.json b/step0/package.json new file mode 100644 index 0000000..31fef03 --- /dev/null +++ b/step0/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "ws": "^8.18.3" + } +} diff --git a/step0/server.js b/step0/server.js new file mode 100644 index 0000000..64dc6d9 --- /dev/null +++ b/step0/server.js @@ -0,0 +1,133 @@ +// ===== Configuration ===== +var config = { + port: 8080, +}; +// +// ========================= +let rootDir = __dirname; +if (!rootDir.endsWith('/')) rootDir += '/'; +const binDir = rootDir + `build`; +const watchPaths = [rootDir, binDir]; + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const WebSocket = require('ws'); + +const mimeTypes = { + '.html': 'text/html', + '.js': 'text/javascript', + '.wasm': 'application/wasm', + '.css': 'text/css', + '.json': 'application/json' +}; + +const server = http.createServer((req, res) => { + let norm = path.normalize(req.url); + console.log(`${req.method} ${norm}`); + + // Serve config endpoint + if (norm === '/config') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(config)); + return; + } + + // Serve files + let filePath; + if (norm.startsWith('/bin/')) { + filePath = path.join(binDir, norm.slice(5)); + } else if (norm == '/') { + filePath = rootDir + 'index.html'; + } else { + filePath = rootDir + norm; + } + + const extname = String(path.extname(filePath)).toLowerCase(); + const contentType = mimeTypes[extname] || 'application/octet-stream'; + + fs.readFile(filePath, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + res.writeHead(404, { 'Content-Type': 'text/html' }); + res.end('

404 - File Not Found

', 'utf-8'); + } else { + res.writeHead(500); + res.end(`Server Error: ${error.code}`, 'utf-8'); + } + } else { + res.writeHead(200, { + 'Content-Type': contentType, + 'Content-Length': content.byteLength, + 'Access-Control-Expose-Headers': 'Content-Length', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }); + res.end(content, 'utf-8'); + } + }); +}); + +const wss = new WebSocket.Server({ noServer: true }); +wss.on('connection', () => { }); +server.on('upgrade', (request, socket, head) => { + if (request.url === '/control') { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } +}); + +function broadcastRefresh() { + const message = JSON.stringify({ type: 'refresh' }); + let clientCount = 0; + + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + clientCount++; + } + }); + + if (clientCount > 0) { + console.log(`Sent refresh to ${clientCount} client(s)`); + } +} + +// Watch files for changes +const watchers = []; +watchPaths.forEach((watchPath) => { + try { + const watcher = fs.watch(watchPath, { recursive: true }, (eventType, filename) => { + console.log(`File changed: ${filename} (${eventType})`); + if (filename) { + console.log(`File changed: ${filename} (${eventType})`); + broadcastRefresh(); + } + }); + watchers.push(watcher); + console.log(`Watching: ${path.resolve(watchPath)}`); + } catch (error) { + console.error(`Failed to watch ${watchPath}:`, error.message); + } +}); + +server.listen(config.port, () => { + console.log(`Server running at http://localhost:${config.port}/`); + console.log(`WebSocket listening on ws://localhost:${config.port}/control`); + console.log(`WASM binary path configured as: ${binDir}`); + + const shouldLaunchBrowser = process.argv.includes('--launch'); + if (shouldLaunchBrowser) { + exec(`librewolf --new-window http://localhost:${config.port}/`, (error) => { + if (error) { + console.error(`Failed to launch LibreWolf: ${error.message}`); + } else { + console.log('LibreWolf launched'); + } + }); + } +});