diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4aa8198..3dbc317 100755 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,24 +5,101 @@ on: branches: [main] jobs: - publish: + build: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: submodules: recursive - - uses: mymindstorm/setup-emsdk@v14 + + - name: Setup EMSDK + uses: mymindstorm/setup-emsdk@v14 + with: + version: 4.0.1 + - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - - run: npm ci - - run: npm run lint:nofix - - run: npm run build:wasm - - run: npm run build - - run: npm test - - run: npm run luatests - - uses: JS-DevTools/npm-publish@v3 + + - name: Install dependencies + run: npm ci + + - name: Run lint (no fix) + run: npm run lint:nofix + + - name: Build WASM + run: npm run build:wasm + + - name: Build project + run: npm run build + + - name: Run tests + run: npm test + + - name: Run Lua tests + run: npm run luatests + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build-artifact + path: | + package.json + package-lock.json + dist/ + bin/ + LICENSE + + publish_npm: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build-artifact + path: build-artifact + + - name: Restore build artifact + run: cp -r build-artifact/* . + + - name: Publish to npm + uses: JS-DevTools/npm-publish@v3 with: token: ${{ secrets.NPM_TOKEN }} + + # Emscripten still doesn't support deno + # publish_jsr: + # runs-on: ubuntu-latest + # needs: build + # permissions: + # contents: read + # id-token: write + # steps: + # - name: Download build artifact + # uses: actions/download-artifact@v4 + # with: + # name: build-artifact + # path: build-artifact + + # - name: Restore build artifact + # run: cp -r build-artifact/* . + + # - name: Generate jsr.json from package.json + # run: | + # node -e "const pkg = require('./package.json'); \ + # const jsr = { \ + # name: '@ceifa/' + pkg.name, \ + # version: pkg.version, \ + # exports: pkg.main, \ + # include: ['dist/**/*', 'bin/**/*'] + # }; \ + # require('fs').writeFileSync('jsr.json', JSON.stringify(jsr, null, 2));" + + # - name: Publish to JSR + # run: npx jsr publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50e5cc3..119e830 100755 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,13 +10,13 @@ jobs: strategy: matrix: - node-version: [18, 20, 22] + node-version: [22] steps: - uses: actions/checkout@v4 with: submodules: recursive - - uses: mymindstorm/setup-emsdk@v12 + - uses: mymindstorm/setup-emsdk@v14 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/bench/comparisons.js b/bench/comparisons.js index 49c7b1b..1a72e9a 100644 --- a/bench/comparisons.js +++ b/bench/comparisons.js @@ -1,30 +1,62 @@ -const { readFileSync } = require('fs') -const path = require('path') +import { readFileSync } from 'fs' +import path from 'path' +import { performance } from 'perf_hooks' +import { Lua } from '../dist/index.js' +import fengari from 'fengari' -const fengari = require('fengari') -const wasmoon = require('../dist/index') +const heapsort = readFileSync(path.resolve(import.meta.dirname, 'heapsort.lua'), 'utf-8') -const heapsort = readFileSync(path.resolve(__dirname, 'heapsort.lua'), 'utf-8') +function calculateStats(times) { + const n = times.length + const avg = times.reduce((sum, t) => sum + t, 0) / n + const stdDev = Math.sqrt(times.reduce((sum, t) => sum + (t - avg) ** 2, 0) / n) + return { avg, stdDev } +} + +async function benchmark(name, iterations, warmup, fn) { + console.log(`\nBenchmarking ${name}...`) + + for (let i = 0; i < warmup; i++) { + await fn() + } -const startFengari = () => { - const state = fengari.lauxlib.luaL_newstate() - fengari.lualib.luaL_openlibs(state) + const times = [] + for (let i = 0; i < iterations; i++) { + const start = performance.now() + await fn() + const end = performance.now() + times.push(end - start) + } - console.time('Fengari') - fengari.lauxlib.luaL_loadstring(state, fengari.to_luastring(heapsort)) - fengari.lua.lua_callk(state, 0, 1, 0, null) - fengari.lua.lua_callk(state, 0, 0, 0, null) - console.timeEnd('Fengari') + const { avg, stdDev } = calculateStats(times) + console.log(`${name}: ${iterations} iterations | avg: ${avg.toFixed(3)} ms | std dev: ${stdDev.toFixed(3)} ms`) } -const startWasmoon = async () => { - const state = await new wasmoon.LuaFactory().createEngine() +async function benchmarkFengari(iterations, warmup) { + function runFengariIteration() { + const state = fengari.lauxlib.luaL_newstate() + fengari.lualib.luaL_openlibs(state) + fengari.lauxlib.luaL_loadstring(state, fengari.to_luastring(heapsort)) + fengari.lua.lua_callk(state, 0, 1, 0, null) + fengari.lua.lua_callk(state, 0, 0, 0, null) + } + await benchmark('Fengari', iterations, warmup, runFengariIteration) +} + +async function benchmarkWasmoon(iterations, warmup) { + const lua = await Lua.load() - console.time('Wasmoon') - state.global.lua.luaL_loadstring(state.global.address, heapsort) - state.global.lua.lua_callk(state.global.address, 0, 1, 0, null) - state.global.lua.lua_callk(state.global.address, 0, 0, 0, null) - console.timeEnd('Wasmoon') + async function runWasmoonIteration() { + const state = lua.createState() + state.global.lua.luaL_loadstring(state.global.address, heapsort) + state.global.lua.lua_callk(state.global.address, 0, 1, 0, null) + state.global.lua.lua_callk(state.global.address, 0, 0, 0, null) + } + await benchmark('Wasmoon', iterations, warmup, runWasmoonIteration) } -Promise.resolve().then(startFengari).then(startWasmoon).catch(console.error) +const iterations = 100 +const warmup = 10 + +await benchmarkFengari(iterations, warmup) +await benchmarkWasmoon(iterations, warmup) diff --git a/bench/heapsort.lua b/bench/heapsort.lua index 5e7ce00..026f651 100644 --- a/bench/heapsort.lua +++ b/bench/heapsort.lua @@ -57,4 +57,6 @@ return function() assert(a[i] <= a[i + 1]) end end + + return Num end diff --git a/bench/steps.js b/bench/steps.js index 9c670d5..9066fcd 100644 --- a/bench/steps.js +++ b/bench/steps.js @@ -1,44 +1,39 @@ -const { readFileSync } = require('fs') -const path = require('path') +import { readFileSync } from 'fs' +import path from 'path' +import { Lua } from '../dist/index.js' +import assert from 'node:assert' -const wasmoon = require('../dist/index') +const heapsort = readFileSync(path.resolve(import.meta.dirname, 'heapsort.lua'), 'utf-8') -const heapsort = readFileSync(path.resolve(__dirname, 'heapsort.lua'), 'utf-8') - -const createFactory = () => { +const createFactory = async () => { console.time('Create factory') - _ = new wasmoon.LuaFactory() + await Lua.load() console.timeEnd('Create factory') } -const loadWasm = async () => { - console.time('Load wasm') - await new wasmoon.LuaFactory().getLuaModule() - console.timeEnd('Load wasm') -} - -const createEngine = async () => { - const factory = new wasmoon.LuaFactory() +const createState = async () => { + const lua = await Lua.load() - console.time('Create engine') - await factory.createEngine() - console.timeEnd('Create engine') + console.time('Create state') + lua.createState() + console.timeEnd('Create state') } -const createEngineWithoutSuperpowers = async () => { - const factory = new wasmoon.LuaFactory() +const createStateWithoutSuperpowers = async () => { + const lua = await Lua.load() - console.time('Create engine without superpowers') - await factory.createEngine({ + console.time('Create state without superpowers') + lua.createState({ injectObjects: false, enableProxy: false, openStandardLibs: false, }) - console.timeEnd('Create engine without superpowers') + console.timeEnd('Create state without superpowers') } const runHeapsort = async () => { - const state = await new wasmoon.LuaFactory().createEngine() + const lua = await Lua.load() + const state = lua.createState() console.time('Run plain heapsort') state.global.lua.luaL_loadstring(state.global.address, heapsort) @@ -48,16 +43,18 @@ const runHeapsort = async () => { } const runInteropedHeapsort = async () => { - const state = await new wasmoon.LuaFactory().createEngine() + const lua = await Lua.load() + const state = lua.createState() console.time('Run interoped heapsort') const runHeapsort = await state.doString(heapsort) - runHeapsort() + assert(runHeapsort() === 10) console.timeEnd('Run interoped heapsort') } const insertComplexObjects = async () => { - const state = await new wasmoon.LuaFactory().createEngine() + const lua = await Lua.load() + const state = lua.createState() const obj1 = { hello: 'world', } @@ -77,7 +74,8 @@ const insertComplexObjects = async () => { } const insertComplexObjectsWithoutProxy = async () => { - const state = await new wasmoon.LuaFactory().createEngine({ + const lua = await Lua.load() + const state = lua.createState({ enableProxy: false, }) const obj1 = { @@ -99,7 +97,8 @@ const insertComplexObjectsWithoutProxy = async () => { } const getComplexObjects = async () => { - const state = await new wasmoon.LuaFactory().createEngine() + const lua = await Lua.load() + const state = lua.createState() await state.doString(` local obj1 = { hello = 'world', @@ -124,9 +123,8 @@ const getComplexObjects = async () => { Promise.resolve() .then(createFactory) - .then(loadWasm) - .then(createEngine) - .then(createEngineWithoutSuperpowers) + .then(createState) + .then(createStateWithoutSuperpowers) .then(runHeapsort) .then(runInteropedHeapsort) .then(insertComplexObjects) diff --git a/bin/wasmoon b/bin/wasmoon index 9100384..db9cbb7 100755 --- a/bin/wasmoon +++ b/bin/wasmoon @@ -1,114 +1,254 @@ #!/usr/bin/env node -const { LuaFactory, LuaReturn, LuaType, LUA_MULTRET, decorate } = require('../dist') -const fs = require('fs') -const path = require('path') -const readline = require('readline') - -async function* walk(dir) { - const dirents = await fs.promises.readdir(dir, { withFileTypes: true }) - for (const dirent of dirents) { - const res = path.resolve(dir, dirent.name) - if (dirent.isDirectory()) { - yield* walk(res) - } else { - yield res - } - } +import { Lua, LuaReturn, LuaType, LUA_MULTRET, decorate } from '../dist/index.js' +import pkg from '../package.json' with { type: 'json' } +import fs from 'node:fs' +import readline from 'node:readline' + +function printUsage() { + console.log( + ` +usage: wasmoon [options] [script [args]] +Available options are: + -e stat execute string 'stat' + -i enter interactive mode after executing 'script' + -l mod require library 'mod' into global 'mod' + -l g=mod require library 'mod' into global 'g' + -v show version information + -E ignore environment variables + -W turn warnings on + -- stop handling options + - stop handling options and execute stdin +`.trim(), + ) + process.exit(1) } -async function main() { - const factory = new LuaFactory() - const luamodule = await factory.getLuaModule() - const lua = await factory.createEngine() +function parseArgs(args) { + const executeSnippets = [] + const includeModules = [] + let forceInteractive = false + let warnings = false + let ignoreEnv = false + let showVersion = false + let scriptFile = null + + const outArgs = [] - let snippets = process.argv.splice(2) + let i = 0 + for (; i < args.length; i++) { + const arg = args[i] - const consumeOption = (option, single) => { - let i = -1 - const values = [] - while ((i = snippets.indexOf(option)) >= 0) { - values.push(snippets.splice(i, single ? 1 : 2).reverse()[0]) + if (arg === '--') { + i++ + break + } + + if (arg.startsWith('-') && arg.length > 1) { + switch (arg) { + case '-v': + showVersion = true + break + case '-W': + warnings = true + break + case '-E': + ignoreEnv = true + break + case '-i': + forceInteractive = true + break + case '-e': + i++ + if (i >= args.length) { + console.error('Missing argument after -e') + printUsage() + } + executeSnippets.push(args[i]) + break + case '-l': + i++ + if (i >= args.length) { + console.error('Missing argument after -l') + printUsage() + } + includeModules.push(args[i]) + break + case '-': + scriptFile = '-' + i++ + break + default: + console.log(`unrecognized option: '${arg}'`) + printUsage() + break + } + } else { + scriptFile = arg + i++ + break } - return values } - const includes = consumeOption('-l') - const forceInteractive = consumeOption('-i', true).length > 0 - const runFile = process.stdin.isTTY && consumeOption(snippets[0], true)[0] - const args = snippets + outArgs.push(...args.slice(i)) + + return { + executeSnippets, + includeModules, + forceInteractive, + warnings, + showVersion, + scriptFile, + scriptArgs: outArgs, + ignoreEnv, + } +} - for (const include of includes) { - const relativeInclude = path.resolve(process.cwd(), include) - const stat = await fs.promises.lstat(relativeInclude) - if (stat.isFile()) { - await factory.mountFile(relativeInclude, await fs.promises.readFile(relativeInclude)) - } else { - for await (const file of walk(relativeInclude)) { - await factory.mountFile(file, await fs.promises.readFile(file)) +const { executeSnippets, includeModules, ignoreEnv, forceInteractive, warnings, showVersion, scriptFile, scriptArgs } = parseArgs( + process.argv.slice(2), +) + +// When running interactively, process.stdin is used by readline. +// To allow Lua’s io.read to work (even in interactive mode), we open +// a separate file descriptor to the terminal (e.g. '/dev/tty' on Unix). +let inputFD = 0 +if (process.stdin.isTTY) { + try { + inputFD = fs.openSync('/dev/tty', 'r') + } catch (e) { + // If opening /dev/tty fails (or on non-Unix systems), fallback to fd 0. + inputFD = 0 + } +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true, + removeHistoryDuplicates: true, + prompt: '> ', +}) + +const lua = await Lua.load({ + env: ignoreEnv ? undefined : process.env, + fs: 'node', + stdin: () => { + try { + rl.pause() + + let buffer = Buffer.alloc(0xff) + let content = '' + let current = 0 + + while (true) { + const bytesRead = fs.readSync(inputFD, buffer, current, buffer.length - current) + if (bytesRead > 0) { + const charcode = buffer[current++] + if (charcode === 127) { + current = Math.max(0, current - 2) + } else { + if (charcode === 13) { + break + } + } + + content = buffer.subarray(0, current).toString('utf8') + process.stdout.write('\x1b[2K') + process.stdout.write('\x1b[0G') + process.stdout.write(content) + } else { + break + } } + + rl.resume() + return content + } catch (err) { + // the error will not be thrown to the top, its better to log it + console.error(err) } - } + }, +}) - lua.global.set('arg', decorate(args, { disableProxy: true })) +const state = lua.createState() - const interactive = process.stdin.isTTY && (forceInteractive || !runFile) +if (showVersion) { + console.log(`wasmoon ${pkg.version} (${state.global.get('_VERSION')})`) + process.exit(0) +} - if (runFile) { - const relativeRunFile = path.resolve(process.cwd(), runFile) - await factory.mountFile(relativeRunFile, await fs.promises.readFile(relativeRunFile)) +if (warnings) { + lua.module.lua_warning(state.global.address, '@on', 0) +} - await lua.doFile(relativeRunFile) - console.log(lua.global.indexToString(-1)) +for (const module of includeModules) { + let [global, mod] = module.split('=') + if (!mod) { + mod = global } + const require = state.global.get('require') + state.global.set(global, require(mod)) +} - if (!interactive && runFile) { - return - } +for (const snippet of executeSnippets) { + await state.doString(snippet) +} - if (interactive) { - // Call directly from module to bypass the result verification - const loadcode = (code) => !lua.global.setTop(0) && luamodule.luaL_loadstring(lua.global.address, code) === LuaReturn.Ok +state.global.set('arg', decorate(scriptArgs, { disableProxy: true })) - const version = require('../package.json').version - console.log('Welcome to Wasmoon v' + version) +const isTTY = process.stdin.isTTY - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - removeHistoryDuplicates: true, - }) +if (scriptFile === '-') { + const input = fs.readFileSync(0, 'utf-8') + await state.doString(input) +} else if (scriptFile) { + await state.doFile(scriptFile) +} - rl.prompt() +const shouldInteractive = isTTY && (forceInteractive || (!scriptFile && !executeSnippets.length)) +if (shouldInteractive) { + // Bypass result verification for interactive mode + const loadcode = (code) => { + state.global.setTop(0) + return lua.module.luaL_loadstring(state.global.address, code) === LuaReturn.Ok + } - for await (const line of rl) { - const loaded = loadcode(line) || loadcode(`return ${line}`) - if (!loaded) { - console.log(lua.global.getValue(-1, LuaType.String)) - rl.prompt() - continue - } + const version = pkg.version + const luaversion = state.global.get('_VERSION') + console.log(`Welcome to Wasmoon ${version} (${luaversion})`) + console.log('Type Lua code and press Enter to execute. Ctrl+C to exit.\n') + + rl.prompt() - const result = luamodule.lua_pcallk(lua.global.address, 0, LUA_MULTRET, 0, 0, null) - if (result === LuaReturn.Ok) { - const returnValues = Array.from({ length: lua.global.getTop() }).map((_, i) => lua.global.indexToString(i + 1)) + for await (const line of rl) { + // try to load (compile) it first as an expression (return ) and second as a statement + const loaded = loadcode(`return ${line}`) || loadcode(line) + if (!loaded) { + // Failed to parse + const err = state.global.getValue(-1, LuaType.String) + console.log(err) + rl.prompt() + continue + } - if (returnValues.length) { - console.log(...returnValues) + const result = lua.module.lua_pcallk(state.global.address, 0, LUA_MULTRET, 0, 0, null) + if (result === LuaReturn.Ok) { + const count = state.global.getTop() + if (count > 0) { + const returnValues = [] + for (let i = 1; i <= count; i++) { + returnValues.push(state.global.indexToString(i)) } - } else { - console.log(lua.global.getValue(-1, LuaType.String)) + console.log(...returnValues) } - - rl.prompt() + } else { + console.log(state.global.getValue(-1, LuaType.String)) } - } else { - await lua.doString(fs.readFileSync(0, 'utf-8')) - console.log(lua.global.indexToString(-1)) + + rl.prompt() } +} else if (!scriptFile) { + // If we're not interactive, and we did NOT run a file, + // read from stdin until EOF. (Non-TTY or no -i, no file). + const input = fs.readFileSync(0, 'utf-8') + await state.doString(input) } - -main().catch((err) => { - console.error(err) - process.exit(1) -}) diff --git a/eslint.config.js b/eslint.config.js index 1610949..23e7807 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,7 +22,7 @@ export default [ }, }, { - files: ['**/*.js', '**/*.mjs', '**/*.ts'], + files: ['**/*.js', '**/*.mjs', '**/*.ts', './bin/*'], ignores: ['**/test/*', '**/bench/*'], plugins: { 'simple-import-sort': simpleImportSort, diff --git a/package-lock.json b/package-lock.json index a9b3163..4e37b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,29 +9,29 @@ "version": "1.16.0", "license": "MIT", "dependencies": { - "@types/emscripten": "1.39.10" + "@types/emscripten": "1.40.0" }, "bin": { "wasmoon": "bin/wasmoon" }, "devDependencies": { - "@eslint/js": "9.17.0", - "@types/node": "22.10.2", - "@typescript-eslint/parser": "8.18.2", - "chai": "5.1.2", + "@eslint/js": "9.20.0", + "@types/node": "22.13.4", + "@typescript-eslint/parser": "8.24.1", + "chai": "5.2.0", "chai-as-promised": "8.0.1", - "eslint": "9.17.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-prettier": "5.2.1", + "eslint": "9.20.1", + "eslint-config-prettier": "10.0.1", + "eslint-plugin-prettier": "5.2.3", "eslint-plugin-simple-import-sort": "12.1.1", "fengari": "0.1.4", - "mocha": "11.0.1", - "prettier": "3.4.2", - "rolldown": "1.0.0-beta.1-commit.7c52c94", + "mocha": "11.1.0", + "prettier": "3.5.1", + "rolldown": "1.0.0-beta.3", "rollup-plugin-copy": "3.5.0", "tslib": "2.8.1", - "typescript": "5.7.2", - "typescript-eslint": "8.18.2" + "typescript": "5.7.3", + "typescript-eslint": "8.24.1" } }, "node_modules/@emnapi/core": { @@ -137,9 +137,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -198,9 +198,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -218,12 +218,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -366,9 +367,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.45.0.tgz", - "integrity": "sha512-s1xCyuYV024s4Jh9l3a9/gSyIG5qr6P0gdwz03UMx6UqaXRkhD2INeRSNxGM/XXKfYVbAqUBy3q/QEMkTNio9Q==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.46.0.tgz", + "integrity": "sha512-BHU261xrLasw04d2cToR36F6VV0T7t62rtQUprvBRL4Uru9P23moMkDmZUMSZSQj0fIUTA3oTOTwQ7cc4Av/iw==", "dev": true, "license": "MIT", "funding": { @@ -400,9 +401,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-02iMa/cL+kLkS6xC98MBZSkpInFtAK0gKxjQhmdvplF+WMr/i4VUDrwEIP+N0ydOiUw3rfXcz+Vykh2Srw2ioQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.3.tgz", + "integrity": "sha512-qB1ofY+09nDYYaEi5kVsjqy4cKsVPI9E5bkV46CRrQsTF/BBM29wpvaj8qTRQ41qwInFA5kmqnVVr35yfH7ddw==", "cpu": [ "arm64" ], @@ -414,9 +415,9 @@ ] }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-14jCHf59q/SeqMvhq5cah3qwC6/G7Z/64Z77jgwofFLyGbdYmFFv4ct92vXG3Wno88MwAbuilKIaOiu0HhpBmQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.3.tgz", + "integrity": "sha512-Fk+rqyeszMaZK12wItqFDXdUadg+TVQqOPh0fdaCefVebd29N+9fpFrARyo8gReyt/lcnEN4nWgdn7l99R70QA==", "cpu": [ "x64" ], @@ -428,9 +429,9 @@ ] }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-iICCEIauX0xGByel25JkMG8jTN3zF70XHaon6ylbkCsqpZzXTn9qx/KTUKEv0aeoxeAU6JU7Sxk9aK6KHPHkHQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.3.tgz", + "integrity": "sha512-B7QzJKu53MB/hvwO276AsyxN+p9lfgCkIO94TQB6t3auq3pDCC6u6gdRI1Ydwn6/gpMLiUNCW4mnpxCE5fE5tg==", "cpu": [ "x64" ], @@ -442,9 +443,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-xS/2DVlMhi9BcJ15uAvIFdMuGS4bKpOsHkwoVIYxkGNKcLD8J4kDK0Uaa9Wkb191pygqmZZYe+y+m5dp8WyEQQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.3.tgz", + "integrity": "sha512-NB5JrXP5dAigDTbvVc6VWiOY3Rr/0u1pi/9LYoBtMYiST7hYOrBPO9lvDF9w/23yKCr1+8PF4wFGR/YxKTNN5Q==", "cpu": [ "arm" ], @@ -456,9 +457,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-4Wtczg6ZjIqYZ+r5s6KxqUJ1XTcMHk9b0PunedmRsx1Po9lXLhLtD7xMSLrPHT2s6upj/ast5Nc1CocBlGH2kQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.3.tgz", + "integrity": "sha512-bYyZLXzJ2boZ7CdUuCSAaTcWkVKcBUOL+B86zv+tRyrtk4BIpHF+L+vOg5uPD/PHwrIglxAno5MN4NnpkUj5fQ==", "cpu": [ "arm64" ], @@ -470,9 +471,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-WMSnRcek6BRXnOiZisTjmKD93BrugSLkJngbZZvXYoPTXLb19pPntnN7hV9J/V7UkgjdXAdwJwtzUfHfqUzWrg==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.3.tgz", + "integrity": "sha512-t/jaaFrCSvwX2075jRfa2bwAcsuTtY1/sIT4XqsDg2MVxWQtaUyBx5Mi0pqZKTjdOPnL+f/zoUC9dxT2lUpNmw==", "cpu": [ "arm64" ], @@ -484,9 +485,9 @@ ] }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-M5kXmTgi8aP9GKzMcLtbpQ5xPic2xzuilazT0Q8oCbU3rcQg39OTs2A/1pNGqqVzertVWmMw473jDs+39MF4KQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.3.tgz", + "integrity": "sha512-EeDNLPU0Xw8ByRWxNLO30AF0fKYkdb/6rH5G073NFBDkj7ggYR/CvsNBjtDeCJ7+I6JG4xUjete2+VeV+GQjiA==", "cpu": [ "x64" ], @@ -498,9 +499,9 @@ ] }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-4i2hLAjupLmxTRqk6YZORs7CKCdXmzymNTy64rfoSmiL5iN4Ike9erB++pUpmmqG8UmOvrZXzbMWwvVd9GIPPw==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.3.tgz", + "integrity": "sha512-iTcAj8FKac3nyQhvFuqKt6Xqu9YNDbe1ew6US2OSN4g3zwfujgylaRCitEG+Uzd7AZfSVVLAfqrxKMa36Sj9Mg==", "cpu": [ "x64" ], @@ -512,9 +513,9 @@ ] }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-EekMm2D41TDmpqdhVXeXM0dU4SHFe2tZBY9ondJhA2lOHW0No5Y/i2D5dXauaGDBYljZldhUL/oRINc0/1uF8A==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.3.tgz", + "integrity": "sha512-sYgbsbyspvVZ2zplqsTxjf2N3e8UQGQnSsN5u4bMX461gY5vAsjUiA4nf1/ztDBMHWT79lF2QNx4csjnjSxMlA==", "cpu": [ "wasm32" ], @@ -529,9 +530,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-rubF+iwgtmeZfyvR+1y3qYsRWqi0qtDqI6vrDjbyXC7i4NU6/Lpcd5aS60eMJc7chQ9E64SNxddi6V7H38er/g==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.3.tgz", + "integrity": "sha512-qszMtrWybBLTFaew2WgEBRMlz1B/V8XxU87uezXlKcLW36aoRWR8LspZvqqoBkvJzbQtfOgm1HdTIk/v3Rn7QQ==", "cpu": [ "arm64" ], @@ -543,9 +544,9 @@ ] }, "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-qo4Vd1i+tsHQ1AYMgy1vTYcDwcb9Pnzjve1ni97PUdzMc2EtR8BvamX0QxEdlYRZGNiv7PXFVUlTzIpPLimL8w==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.3.tgz", + "integrity": "sha512-J+mzAO68VK91coLVuUln/XN0ummIEOODyupZ2BmXY8suBHPVAyLLAP54rlucBPQmzU8fI6DXM2bl2whZ+KEXpQ==", "cpu": [ "ia32" ], @@ -557,9 +558,9 @@ ] }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-vdhlPFeNk1UNF4t52Lg1Y1FEvjFbYqtbpxz2w8M+HozdJLSRaVJdXPI/tMMFhdC/YlMFRNrZN5W+PwHUhbFxSQ==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.3.tgz", + "integrity": "sha512-r06rAi+1eStgavGnw+2y4F7gpb0w9ocnKk0Ir7LmegLAkMZ/v4Fjo9jZUrLTLtmI36108v1uvUPrIAFzFOWE7g==", "cpu": [ "x64" ], @@ -582,9 +583,9 @@ } }, "node_modules/@types/emscripten": { - "version": "1.39.10", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", - "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.0.tgz", + "integrity": "sha512-MD2JJ25S4tnjnhjWyalMS6K6p0h+zQV6+Ylm+aGbiS8tSn/aHLSGNzBgduj6FB4zH0ax2GRMGYi/8G1uOxhXWA==", "license": "MIT" }, "node_modules/@types/estree": { @@ -630,9 +631,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "dev": true, "license": "MIT", "dependencies": { @@ -640,21 +641,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", - "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/type-utils": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -670,16 +671,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", - "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "engines": { @@ -695,14 +696,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", - "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2" + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -713,16 +714,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", - "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -737,9 +738,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", - "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -751,20 +752,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", - "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -778,16 +779,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", - "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2" + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -802,13 +803,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", - "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -833,9 +834,9 @@ } }, "node_modules/@valibot/to-json-schema": { - "version": "1.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.0.0-beta.3.tgz", - "integrity": "sha512-20XQh1u5sOLwS3NOB7oHCo3clQ9h4GlavXgLKMux2PYpHowb7P97cND0dg8T3+fE1WoKVACcLppvzAPpSx0F+Q==", + "version": "1.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.0.0-beta.4.tgz", + "integrity": "sha512-wXBdCyoqec+NLCl5ihitXzZXD4JAjPK3+HfskSXzfhiNFvKje0A/v1LygqKidUgIbaJtREmq/poJGbaS/0MKuQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1049,9 +1050,9 @@ } }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -1144,15 +1145,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/cliui/node_modules/ansi-regex": { @@ -1376,19 +1380,19 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -1436,22 +1440,22 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, "license": "MIT", "dependencies": { @@ -1519,6 +1523,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2352,9 +2369,9 @@ } }, "node_modules/mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2375,8 +2392,8 @@ "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -2608,9 +2625,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", "dev": true, "license": "MIT", "bin": { @@ -2745,32 +2762,32 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-beta.1-commit.7c52c94", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.1-commit.7c52c94.tgz", - "integrity": "sha512-WSkfhxZ/LMc6FkXhdMoOlyY7YsvxEC1ioqTIwceT7edoA1cIqkGY4pcaNtk1Ve/0hhTGFFPksbGlWW0avVwGQg==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.3.tgz", + "integrity": "sha512-DBpF1K8tSwU/0dQ7zL9BYcje0/GjO5lgfdEW0rHHFfGjGDh8TBVNlokfEXtdt/IoJOiTdtySfsrgarLJkZmZTQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "0.45.0", - "@valibot/to-json-schema": "1.0.0-beta.3", - "valibot": "1.0.0-beta.9" + "@oxc-project/types": "0.46.0", + "@valibot/to-json-schema": "1.0.0-beta.4", + "valibot": "1.0.0-beta.12" }, "bin": { "rolldown": "bin/cli.js" }, "optionalDependencies": { - "@rolldown/binding-darwin-arm64": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-darwin-x64": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.1-commit.7c52c94", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.1-commit.7c52c94" + "@rolldown/binding-darwin-arm64": "1.0.0-beta.3", + "@rolldown/binding-darwin-x64": "1.0.0-beta.3", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.3", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.3", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.3", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.3", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.3", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.3", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.3" }, "peerDependencies": { "@babel/runtime": ">=7" @@ -2844,9 +2861,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -3093,16 +3110,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/tslib": { @@ -3126,9 +3143,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3140,15 +3157,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", - "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz", + "integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.2", - "@typescript-eslint/parser": "8.18.2", - "@typescript-eslint/utils": "8.18.2" + "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/parser": "8.24.1", + "@typescript-eslint/utils": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3190,9 +3207,9 @@ } }, "node_modules/valibot": { - "version": "1.0.0-beta.9", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.9.tgz", - "integrity": "sha512-yEX8gMAZ2R1yI2uwOO4NCtVnJQx36zn3vD0omzzj9FhcoblvPukENIiRZXKZwCnqSeV80bMm8wNiGhQ0S8fiww==", + "version": "1.0.0-beta.12", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.12.tgz", + "integrity": "sha512-j3WIxJ0pmUFMfdfUECn3YnZPYOiG0yHYcFEa/+RVgo0I+MXE3ToLt7gNRLtY5pwGfgNmsmhenGZfU5suu9ijUA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3350,32 +3367,32 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { diff --git a/package.json b/package.json index 6ce6e81..746bc87 100755 --- a/package.json +++ b/package.json @@ -3,12 +3,11 @@ "version": "1.16.0", "description": "A real lua VM with JS bindings made with webassembly", "main": "./dist/index.js", + "types": "./dist/index.d.ts", "type": "module", "scripts": { - "build:wasm:dev": "./build.sh dev", - "build:wasm": "./build.sh", - "build:wasm:docker:dev": "docker run --rm -v $(pwd):/wasmoon emscripten/emsdk /wasmoon/build.sh dev", - "build:wasm:docker": "docker run --rm -v $(pwd):/wasmoon emscripten/emsdk /wasmoon/build.sh", + "build:wasm:dev": "node utils/build-wasm dev", + "build:wasm": "node utils/build-wasm", "start": "rolldown -w -c", "test": "mocha --parallel --require ./test/boot.js test/*.test.js", "luatests": "node --experimental-import-meta-resolve test/luatests.js", @@ -41,25 +40,25 @@ "webassembly" ], "devDependencies": { - "@eslint/js": "9.17.0", - "@types/node": "22.10.2", - "@typescript-eslint/parser": "8.18.2", - "chai": "5.1.2", + "@eslint/js": "9.20.0", + "@types/node": "22.13.4", + "@typescript-eslint/parser": "8.24.1", + "chai": "5.2.0", "chai-as-promised": "8.0.1", - "eslint": "9.17.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-prettier": "5.2.1", + "eslint": "9.20.1", + "eslint-config-prettier": "10.0.1", + "eslint-plugin-prettier": "5.2.3", "eslint-plugin-simple-import-sort": "12.1.1", "fengari": "0.1.4", - "mocha": "11.0.1", - "prettier": "3.4.2", - "rolldown": "1.0.0-beta.1-commit.7c52c94", + "mocha": "11.1.0", + "prettier": "3.5.1", + "rolldown": "1.0.0-beta.3", "rollup-plugin-copy": "3.5.0", "tslib": "2.8.1", - "typescript": "5.7.2", - "typescript-eslint": "8.18.2" + "typescript": "5.7.3", + "typescript-eslint": "8.24.1" }, "dependencies": { - "@types/emscripten": "1.39.10" + "@types/emscripten": "1.40.0" } } diff --git a/rolldown.config.ts b/rolldown.config.ts index 2dfbc28..5186a59 100644 --- a/rolldown.config.ts +++ b/rolldown.config.ts @@ -2,17 +2,14 @@ import { defineConfig } from 'rolldown' import copy from 'rollup-plugin-copy' import pkg from './package.json' with { type: 'json' } -const production = !process.env.ROLLUP_WATCH - export default defineConfig({ input: './src/index.ts', output: { file: 'dist/index.js', format: 'esm', sourcemap: true, - minify: production, }, - external: ['module'], + external: ['module', 'node:fs', 'node:child_process'], define: { // Webpack workaround: https://github.com/webpack/webpack/issues/16878 'import.meta': 'Object(import.meta)', diff --git a/src/engine.ts b/src/engine.ts index 59ab415..440e91d 100755 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,6 +1,6 @@ import { CreateEngineOptions } from './types' import Global from './global' -import type LuaWasm from './luawasm' +import type LuaModule from './module' import Thread from './thread' import createErrorType from './type-extensions/error' import createFunctionType from './type-extensions/function' @@ -14,7 +14,7 @@ export default class LuaEngine { public global: Global public constructor( - private cmodule: LuaWasm, + private module: LuaModule, { openStandardLibs = true, injectObjects = false, @@ -23,7 +23,7 @@ export default class LuaEngine { functionTimeout = undefined as number | undefined, }: CreateEngineOptions = {}, ) { - this.global = new Global(this.cmodule, traceAllocations) + this.global = new Global(this.module, traceAllocations) // Generic handlers - These may be required to be registered for additional types. this.global.registerTypeExtension(0, createTableType(this.global)) @@ -52,7 +52,7 @@ export default class LuaEngine { this.global.registerTypeExtension(4, createUserdataType(this.global)) if (openStandardLibs) { - this.cmodule.luaL_openlibs(this.global.address) + this.module.luaL_openlibs(this.global.address) } } diff --git a/src/factory.ts b/src/factory.ts deleted file mode 100755 index 63449f5..0000000 --- a/src/factory.ts +++ /dev/null @@ -1,93 +0,0 @@ -// A rollup plugin will resolve this to the current version on package.json -import version from 'package-version' -import LuaEngine from './engine' -import LuaWasm from './luawasm' -import { CreateEngineOptions, EnvironmentVariables } from './types' - -/** - * Represents a factory for creating and configuring Lua engines. - */ -export default class LuaFactory { - private luaWasmPromise: Promise - - /** - * Constructs a new LuaFactory instance. - * @param [customWasmUri] - Custom URI for the Lua WebAssembly module. - * @param [environmentVariables] - Environment variables for the Lua engine. - */ - public constructor(customWasmUri?: string, environmentVariables?: EnvironmentVariables) { - if (customWasmUri === undefined) { - const isBrowser = - (typeof window === 'object' && typeof window.document !== 'undefined') || - (typeof self === 'object' && self?.constructor?.name === 'DedicatedWorkerGlobalScope') - - if (isBrowser) { - customWasmUri = `https://unpkg.com/wasmoon@${version}/dist/glue.wasm` - } - } - - this.luaWasmPromise = LuaWasm.initialize(customWasmUri, environmentVariables) - } - - /** - * Mounts a file in the Lua environment asynchronously. - * @param path - Path to the file in the Lua environment. - * @param content - Content of the file to be mounted. - * @returns - A Promise that resolves once the file is mounted. - */ - public async mountFile(path: string, content: string | ArrayBufferView): Promise { - this.mountFileSync(await this.getLuaModule(), path, content) - } - - /** - * Mounts a file in the Lua environment synchronously. - * @param luaWasm - Lua WebAssembly module. - * @param path - Path to the file in the Lua environment. - * @param content - Content of the file to be mounted. - */ - public mountFileSync(luaWasm: LuaWasm, path: string, content: string | ArrayBufferView): void { - const fileSep = path.lastIndexOf('/') - const file = path.substring(fileSep + 1) - const body = path.substring(0, path.length - file.length - 1) - - if (body.length > 0) { - const parts = body.split('/').reverse() - let parent = '' - - while (parts.length) { - const part = parts.pop() - if (!part) { - continue - } - - const current = `${parent}/${part}` - try { - luaWasm.module.FS.mkdir(current) - } catch { - // ignore EEXIST - } - - parent = current - } - } - - luaWasm.module.FS.writeFile(path, content) - } - - /** - * Creates a Lua engine with the specified options. - * @param [options] - Configuration options for the Lua engine. - * @returns - A Promise that resolves to a new LuaEngine instance. - */ - public async createEngine(options: CreateEngineOptions = {}): Promise { - return new LuaEngine(await this.getLuaModule(), options) - } - - /** - * Gets the Lua WebAssembly module. - * @returns - A Promise that resolves to the Lua WebAssembly module. - */ - public async getLuaModule(): Promise { - return this.luaWasmPromise - } -} diff --git a/src/global.ts b/src/global.ts index e006d5e..fd28272 100755 --- a/src/global.ts +++ b/src/global.ts @@ -1,4 +1,4 @@ -import type LuaWasm from './luawasm' +import type LuaModule from './module' import Thread from './thread' import LuaTypeExtension from './type-extension' import { LuaLibraries, LuaType } from './types' @@ -17,18 +17,18 @@ export default class Global extends Thread { /** * Constructs a new Global instance. - * @param cmodule - The Lua WebAssembly module. + * @param cmodule - The Lua module. * @param shouldTraceAllocations - Whether to trace memory allocations. */ - public constructor(cmodule: LuaWasm, shouldTraceAllocations: boolean) { + public constructor(cmodule: LuaModule, shouldTraceAllocations: boolean) { if (shouldTraceAllocations) { const memoryStats: LuaMemoryStats = { memoryUsed: 0 } - const allocatorFunctionPointer = cmodule.module.addFunction( + const allocatorFunctionPointer = cmodule._emscripten.addFunction( (_userData: number, pointer: number, oldSize: number, newSize: number): number => { if (newSize === 0) { if (pointer) { memoryStats.memoryUsed -= oldSize - cmodule.module._free(pointer) + cmodule._emscripten._free(pointer) } return 0 } @@ -40,7 +40,7 @@ export default class Global extends Thread { return 0 } - const reallocated = cmodule.module._realloc(pointer, newSize) + const reallocated = cmodule._emscripten._realloc(pointer, newSize) if (reallocated) { memoryStats.memoryUsed = endMemory } @@ -51,7 +51,7 @@ export default class Global extends Thread { const address = cmodule.lua_newstate(allocatorFunctionPointer, null) if (!address) { - cmodule.module.removeFunction(allocatorFunctionPointer) + cmodule._emscripten.removeFunction(allocatorFunctionPointer) throw new Error('lua_newstate returned a null pointer') } super(cmodule, [], address) @@ -84,7 +84,7 @@ export default class Global extends Thread { this.lua.lua_close(this.address) if (this.allocatorFunctionPointer) { - this.lua.module.removeFunction(this.allocatorFunctionPointer) + this.lua._emscripten.removeFunction(this.allocatorFunctionPointer) } for (const wrapper of this.typeExtensions) { @@ -177,7 +177,7 @@ export default class Global extends Thread { } finally { // +1 for the table if (this.getTop() !== startStackTop + 1) { - console.warn(`getTable: expected stack size ${startStackTop} got ${this.getTop()}`) + console.warn(`getTable: expected stack size ${startStackTop + 1} got ${this.getTop()}`) } this.setTop(startStackTop) } diff --git a/src/index.ts b/src/index.ts index 243c251..11cb4a6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { default as LuaEngine } from './engine' -export { default as LuaFactory } from './factory' +export { default as Lua } from './lua' export { default as LuaGlobal } from './global' export { default as LuaMultiReturn } from './multireturn' export { default as LuaRawResult } from './raw-result' @@ -7,9 +7,12 @@ export { default as LuaThread } from './thread' // Export the underlying bindings to allow users to just // use the bindings rather than the wrappers. export { decorate, Decoration } from './decoration' -export { default as LuaWasm } from './luawasm' +export { default as LuaModule } from './module' export { default as LuaTypeExtension } from './type-extension' export { decorateFunction } from './type-extensions/function' export { decorateProxy } from './type-extensions/proxy' export { decorateUserdata } from './type-extensions/userdata' export * from './types' + +import Lua from './lua' +export default Lua diff --git a/src/lua.ts b/src/lua.ts new file mode 100755 index 0000000..ac799de --- /dev/null +++ b/src/lua.ts @@ -0,0 +1,46 @@ +import LuaModule from './module' +import LuaEngine from './engine' +import { CreateEngineOptions } from './types' + +/** + * Represents a factory for creating and configuring Lua engines. + */ +export default class Lua { + /** + * Constructs a new LuaFactory instance. + * @param opts.wasmFile - Custom URI for the Lua WebAssembly module. + * @param opts.env - Environment variables for the Lua engine. + * @param opts.stdin - Standard input for the Lua engine. + * @param opts.fs - File system that should be used for the Lua engine. + * @param opts.stdout - Standard output for the Lua engine. + * @param opts.stderr - Standard error for the Lua engine. + */ + public static async load(luaModuleOpts: Parameters[0] = {}): Promise { + return new Lua(await LuaModule.initialize(luaModuleOpts)) + } + + public constructor(public readonly module: LuaModule) {} + + public createState(stateOpts: CreateEngineOptions = {}): LuaEngine { + return new LuaEngine(this.module, stateOpts) + } + + /** + * Mounts a file in the Lua environment synchronously. + * @param path - Path to the file in the Lua environment. + * @param content - Content of the file to be mounted. + */ + public mountFile(path: string, content: string | ArrayBufferView): void { + const dirname = this.module._emscripten.PATH.dirname(path) + this.module._emscripten.FS.mkdirTree(dirname) + this.module._emscripten.FS.writeFile(path, content) + } + + public get filesystem(): typeof this.module._emscripten.FS { + return this.module._emscripten.FS + } + + public get path(): typeof this.module._emscripten.PATH { + return this.module._emscripten.PATH + } +} diff --git a/src/luawasm.ts b/src/module.ts similarity index 84% rename from src/luawasm.ts rename to src/module.ts index 98ed2cd..1f0d8ad 100755 --- a/src/luawasm.ts +++ b/src/module.ts @@ -1,5 +1,9 @@ import initWasmModule from '../build/glue.js' -import { EnvironmentVariables, LUA_REGISTRYINDEX, LuaReturn, LuaState, LuaType } from './types' +import { LUA_REGISTRYINDEX, LuaReturn, LuaState, LuaType } from './types.js' +// A rolldown plugin will resolve this to the current version on package.json +import version from 'package-version' + +type EnvironmentVariables = Record interface LuaEmscriptenModule extends EmscriptenModule { ccall: typeof ccall @@ -7,10 +11,21 @@ interface LuaEmscriptenModule extends EmscriptenModule { removeFunction: typeof removeFunction setValue: typeof setValue getValue: typeof getValue - FS: typeof FS + FS: typeof FS & { + mkdirTree: (path: string) => void + filesystems: { + NODEFS: Emscripten.FileSystemType + MEMFS: Emscripten.FileSystemType + } + } + PATH: { + dirname: (typeof import('node:path'))['dirname'] + } stringToNewUTF8: typeof allocateUTF8 lengthBytesUTF8: typeof lengthBytesUTF8 stringToUTF8: typeof stringToUTF8 + intArrayFromString: typeof intArrayFromString + UTF8ToString: typeof UTF8ToString ENV: EnvironmentVariables _realloc: (pointer: number, size: number) => number } @@ -20,22 +35,111 @@ interface ReferenceMetadata { refCount: number } -export default class LuaWasm { - public static async initialize(customWasmFileLocation?: string, environmentVariables?: EnvironmentVariables): Promise { +export default class LuaModule { + public static async initialize(opts: { + wasmFile?: string + env?: EnvironmentVariables + fs?: 'node' | 'memory' + stdin?: () => string + stdout?: (content: string) => void + stderr?: (content: string) => void + }): Promise { + const isBrowser = + (typeof window === 'object' && typeof window.document !== 'undefined') || + (typeof self === 'object' && self?.constructor?.name === 'DedicatedWorkerGlobalScope') + + if (opts.wasmFile === undefined && isBrowser) { + opts.wasmFile = `https://unpkg.com/wasmoon@${version}/dist/glue.wasm` + } + + const fs = !isBrowser && opts.fs === 'node' && typeof process !== 'undefined' ? await import('node:fs') : null + const child_process = !isBrowser && opts.fs === 'node' && typeof process !== 'undefined' ? await import('node:child_process') : null + const module: LuaEmscriptenModule = await initWasmModule({ + print: opts.stdout, + printErr: opts.stderr, locateFile: (path: string, scriptDirectory: string) => { - return customWasmFileLocation || scriptDirectory + path + return opts.wasmFile || scriptDirectory + path }, preRun: (initializedModule: LuaEmscriptenModule) => { - if (typeof environmentVariables === 'object') { - Object.entries(environmentVariables).forEach(([k, v]) => (initializedModule.ENV[k] = v)) + if (typeof opts?.env === 'object') { + Object.assign(initializedModule.ENV, opts.env) + } + + if (fs && child_process) { + let rootdirs: string[] + if (process.platform === 'win32') { + const stdout = child_process.execSync('wmic logicaldisk get name', { encoding: 'utf8' }) + const drives = stdout + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && line !== 'Name') + .map((line) => `${line}\\`) + + rootdirs = [] + for (const drive of drives) { + rootdirs.push( + ...fs + .readdirSync(drive) + .filter((dir) => !['dev', 'lib', 'proc'].includes(dir)) + .map((dir) => `${drive}${dir}`.replace(/\\|\\\\/g, '/')), + ) + } + } else { + rootdirs = fs + .readdirSync('/') + .filter((dir) => !['dev', 'lib', 'proc'].includes(dir)) + .map((dir) => `/${dir}`) + } + + for (const dir of rootdirs) { + try { + const moduleFS = initializedModule.FS + moduleFS.mkdirTree(dir) + moduleFS.mount(moduleFS.filesystems.NODEFS, { root: dir }, dir) + } catch { + // silently fail to mount (generally due to EPERM) + } + } + + initializedModule.FS.chdir(process.cwd().replace(/\\|\\\\/g, '/')) + } + + if (opts.stdin) { + let bufferedInput: number[] | undefined + initializedModule.FS.init( + () => { + if (!opts.stdin) { + throw new Error('stdin is not defined, it was probably mutated from original options') + } + + if (!bufferedInput) { + const stdin = opts.stdin() + if (typeof stdin === 'string') { + bufferedInput = initializedModule.intArrayFromString(stdin, true).concat([0]) + } else { + throw new Error('stdin must return a string') + } + } + + if (bufferedInput.length === 0) { + bufferedInput = undefined + return null + } + + const item = bufferedInput.shift() + return !item || item === 0 ? null : item + }, + null, + null, + ) } }, }) - return new LuaWasm(module) + return new LuaModule(module) } - public module: LuaEmscriptenModule + public _emscripten: LuaEmscriptenModule public luaL_checkversion_: (L: LuaState, ver: number, sz: number) => void public luaL_getmetafield: (L: LuaState, obj: number, e: string | null) => LuaType @@ -111,7 +215,7 @@ export default class LuaWasm { public lua_tointegerx: (L: LuaState, idx: number, isnum: number | null) => bigint public lua_toboolean: (L: LuaState, idx: number) => number public lua_tolstring: (L: LuaState, idx: number, len: number | null) => string - public lua_rawlen: (L: LuaState, idx: number) => number + public lua_rawlen: (L: LuaState, idx: number) => bigint public lua_tocfunction: (L: LuaState, idx: number) => number public lua_touserdata: (L: LuaState, idx: number) => number public lua_tothread: (L: LuaState, idx: number) => LuaState @@ -198,7 +302,7 @@ export default class LuaWasm { private lastRefIndex?: number public constructor(module: LuaEmscriptenModule) { - this.module = module + this._emscripten = module this.luaL_checkversion_ = this.cwrap('luaL_checkversion_', null, ['number', 'number', 'number']) this.luaL_getmetafield = this.cwrap('luaL_getmetafield', 'number', ['number', 'number', 'string']) @@ -436,7 +540,7 @@ export default class LuaWasm { const hasStringOrNumber = argTypes.some((argType) => argType === 'string|number') if (!hasStringOrNumber) { return (...args: any[]) => - this.module.ccall(name, returnType, argTypes as Emscripten.JSType[], args as Emscripten.TypeCompatibleWithC[]) + this._emscripten.ccall(name, returnType, argTypes as Emscripten.JSType[], args as Emscripten.TypeCompatibleWithC[]) } return (...args: any[]) => { @@ -448,7 +552,7 @@ export default class LuaWasm { } else { // because it will be freed later, this can only be used on functions that lua internally copies the string if (args[i]?.length > 1024) { - const bufferPointer = this.module.stringToNewUTF8(args[i] as string) + const bufferPointer = this._emscripten.stringToNewUTF8(args[i] as string) args[i] = bufferPointer pointersToBeFreed.push(bufferPointer) return 'number' @@ -461,10 +565,10 @@ export default class LuaWasm { }) try { - return this.module.ccall(name, returnType, resolvedArgTypes, args as Emscripten.TypeCompatibleWithC[]) + return this._emscripten.ccall(name, returnType, resolvedArgTypes, args as Emscripten.TypeCompatibleWithC[]) } finally { for (const pointer of pointersToBeFreed) { - this.module._free(pointer) + this._emscripten._free(pointer) } } } diff --git a/src/thread.ts b/src/thread.ts index f02ee49..bd9d832 100755 --- a/src/thread.ts +++ b/src/thread.ts @@ -1,5 +1,5 @@ import { Decoration } from './decoration' -import type LuaWasm from './luawasm' +import type LuaModule from './module' import MultiReturn from './multireturn' import { Pointer } from './pointer' import LuaTypeExtension from './type-extension' @@ -26,14 +26,14 @@ const INSTRUCTION_HOOK_COUNT = 1000 export default class Thread { public readonly address: LuaState - public readonly lua: LuaWasm + public readonly lua: LuaModule protected readonly typeExtensions: OrderedExtension[] private closed = false private hookFunctionPointer: number | undefined private timeout?: number private readonly parent?: Thread - public constructor(lua: LuaWasm, typeExtensions: OrderedExtension[], address: number, parent?: Thread) { + public constructor(lua: LuaModule, typeExtensions: OrderedExtension[], address: number, parent?: Thread) { this.lua = lua this.typeExtensions = typeExtensions this.address = address @@ -53,14 +53,14 @@ export default class Thread { } public loadString(luaCode: string, name?: string): void { - const size = this.lua.module.lengthBytesUTF8(luaCode) + const size = this.lua._emscripten.lengthBytesUTF8(luaCode) const pointerSize = size + 1 - const bufferPointer = this.lua.module._malloc(pointerSize) + const bufferPointer = this.lua._emscripten._malloc(pointerSize) try { - this.lua.module.stringToUTF8(luaCode, bufferPointer, pointerSize) + this.lua._emscripten.stringToUTF8(luaCode, bufferPointer, pointerSize) this.assertOk(this.lua.luaL_loadbufferx(this.address, bufferPointer, size, name ?? bufferPointer, null)) } finally { - this.lua.module._free(bufferPointer) + this.lua._emscripten._free(bufferPointer) } } @@ -69,16 +69,16 @@ export default class Thread { } public resume(argCount = 0): LuaResumeResult { - const dataPointer = this.lua.module._malloc(PointerSize) + const dataPointer = this.lua._emscripten._malloc(PointerSize) try { - this.lua.module.setValue(dataPointer, 0, 'i32') + this.lua._emscripten.setValue(dataPointer, 0, 'i32') const luaResult = this.lua.lua_resume(this.address, null, argCount, dataPointer) return { result: luaResult, - resultCount: this.lua.module.getValue(dataPointer, 'i32'), + resultCount: this.lua._emscripten.getValue(dataPointer, 'i32'), } } finally { - this.lua.module._free(dataPointer) + this.lua._emscripten._free(dataPointer) } } @@ -312,7 +312,7 @@ export default class Thread { } if (this.hookFunctionPointer) { - this.lua.module.removeFunction(this.hookFunctionPointer) + this.lua._emscripten.removeFunction(this.hookFunctionPointer) } this.closed = true @@ -322,7 +322,7 @@ export default class Thread { public setTimeout(timeout: number | undefined): void { if (timeout && timeout > 0) { if (!this.hookFunctionPointer) { - this.hookFunctionPointer = this.lua.module.addFunction((): void => { + this.hookFunctionPointer = this.lua._emscripten.addFunction((): void => { if (Date.now() > timeout) { this.pushValue(new LuaTimeoutError(`thread timeout exceeded`)) this.lua.lua_error(this.address) diff --git a/src/type-extension.ts b/src/type-extension.ts index 4543cd3..e4929cc 100644 --- a/src/type-extension.ts +++ b/src/type-extension.ts @@ -25,7 +25,7 @@ export default abstract class LuaTypeExtension { public constructor(thread: Global, injectObject: boolean) { super(thread, 'js_error') - this.gcPointer = thread.lua.module.addFunction((functionStateAddress: LuaState) => { + this.gcPointer = thread.lua._emscripten.addFunction((functionStateAddress: LuaState) => { // Throws a lua error which does a jump if it does not match. const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -72,7 +72,7 @@ class ErrorTypeExtension extends TypeExtension { } public close(): void { - this.thread.lua.module.removeFunction(this.gcPointer) + this.thread.lua._emscripten.removeFunction(this.gcPointer) } } diff --git a/src/type-extensions/function.ts b/src/type-extensions/function.ts index 753ee90..28b5283 100644 --- a/src/type-extensions/function.ts +++ b/src/type-extensions/function.ts @@ -23,14 +23,11 @@ export interface FunctionTypeExtensionOptions { } class FunctionTypeExtension extends TypeExtension { - private readonly functionRegistry = - typeof FinalizationRegistry !== 'undefined' - ? new FinalizationRegistry((func: number) => { - if (!this.thread.isClosed()) { - this.thread.lua.luaL_unref(this.thread.address, LUA_REGISTRYINDEX, func) - } - }) - : undefined + private readonly functionRegistry = new FinalizationRegistry((func: number) => { + if (!this.thread.isClosed()) { + this.thread.lua.luaL_unref(this.thread.address, LUA_REGISTRYINDEX, func) + } + }) private gcPointer: number private functionWrapper: number @@ -53,12 +50,12 @@ class FunctionTypeExtension extends TypeExtension { + this.gcPointer = thread.lua._emscripten.addFunction((calledL: LuaState) => { // Throws a lua error which does a jump if it does not match. thread.lua.luaL_checkudata(calledL, 1, this.name) const userDataPointer = thread.lua.luaL_checkudata(calledL, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -77,11 +74,11 @@ class FunctionTypeExtension extends TypeExtension { + this.functionWrapper = thread.lua._emscripten.addFunction((calledL: LuaState) => { const calledThread = thread.stateToThread(calledL) const refUserdata = thread.lua.luaL_checkudata(calledL, thread.lua.lua_upvalueindex(1), this.name) - const refPointer = thread.lua.module.getValue(refUserdata, '*') + const refPointer = thread.lua._emscripten.getValue(refUserdata, '*') const { target, options } = thread.lua.getRef(refPointer) as Decoration const argsQuantity = calledThread.getTop() @@ -130,8 +127,8 @@ class FunctionTypeExtension extends TypeExtension { public constructor(thread: Global) { super(thread, 'js_null') - this.gcPointer = thread.lua.module.addFunction((functionStateAddress: LuaState) => { + this.gcPointer = thread.lua._emscripten.addFunction((functionStateAddress: LuaState) => { // Throws a lua error which does a jump if it does not match. const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -69,7 +69,7 @@ class NullTypeExtension extends TypeExtension { } public close(): void { - this.thread.lua.module.removeFunction(this.gcPointer) + this.thread.lua._emscripten.removeFunction(this.gcPointer) } } diff --git a/src/type-extensions/promise.ts b/src/type-extensions/promise.ts index 0aa2c51..ea8da1b 100644 --- a/src/type-extensions/promise.ts +++ b/src/type-extensions/promise.ts @@ -14,10 +14,10 @@ class PromiseTypeExtension extends TypeExtension> { public constructor(thread: Global, injectObject: boolean) { super(thread, 'js_promise') - this.gcPointer = thread.lua.module.addFunction((functionStateAddress: LuaState) => { + this.gcPointer = thread.lua._emscripten.addFunction((functionStateAddress: LuaState) => { // Throws a lua error which does a jump if it does not match. const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -63,7 +63,7 @@ class PromiseTypeExtension extends TypeExtension> { promiseResult = { status: 'rejected', value: err } }) - const continuance = this.thread.lua.module.addFunction((continuanceState: LuaState) => { + const continuance = this.thread.lua._emscripten.addFunction((continuanceState: LuaState) => { // If this yield has been called from within a coroutine and so manually resumed // then there may not yet be any results. In that case yield again. if (!promiseResult) { @@ -75,7 +75,7 @@ class PromiseTypeExtension extends TypeExtension> { return thread.lua.lua_yieldk(functionThread.address, 0, 0, continuance) } - this.thread.lua.module.removeFunction(continuance) + this.thread.lua._emscripten.removeFunction(continuance) const continuanceThread = thread.stateToThread(continuanceState) @@ -128,7 +128,7 @@ class PromiseTypeExtension extends TypeExtension> { } public close(): void { - this.thread.lua.module.removeFunction(this.gcPointer) + this.thread.lua._emscripten.removeFunction(this.gcPointer) } public pushValue(thread: Thread, decoration: Decoration>): boolean { diff --git a/src/type-extensions/proxy.ts b/src/type-extensions/proxy.ts index 9be9b1d..9d4d76b 100644 --- a/src/type-extensions/proxy.ts +++ b/src/type-extensions/proxy.ts @@ -22,10 +22,10 @@ class ProxyTypeExtension extends TypeExtension { public constructor(thread: Global) { super(thread, 'js_proxy') - this.gcPointer = thread.lua.module.addFunction((functionStateAddress: LuaState) => { + this.gcPointer = thread.lua._emscripten.addFunction((functionStateAddress: LuaState) => { // Throws a lua error which does a jump if it does not match. const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -131,7 +131,7 @@ class ProxyTypeExtension extends TypeExtension { public getValue(thread: Thread, index: number): any { const refUserdata = thread.lua.lua_touserdata(thread.address, index) - const referencePointer = thread.lua.module.getValue(refUserdata, '*') + const referencePointer = thread.lua._emscripten.getValue(refUserdata, '*') return thread.lua.getRef(referencePointer) } @@ -169,7 +169,7 @@ class ProxyTypeExtension extends TypeExtension { } public close(): void { - this.thread.lua.module.removeFunction(this.gcPointer) + this.thread.lua._emscripten.removeFunction(this.gcPointer) } } diff --git a/src/type-extensions/userdata.ts b/src/type-extensions/userdata.ts index f09ae7e..89a3811 100644 --- a/src/type-extensions/userdata.ts +++ b/src/type-extensions/userdata.ts @@ -18,10 +18,10 @@ class UserdataTypeExtension extends TypeExtension { + this.gcPointer = thread.lua._emscripten.addFunction((functionStateAddress: LuaState) => { // Throws a lua error which does a jump if it does not match. const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name) - const referencePointer = thread.lua.module.getValue(userDataPointer, '*') + const referencePointer = thread.lua._emscripten.getValue(userDataPointer, '*') thread.lua.unref(referencePointer) return LuaReturn.Ok @@ -49,7 +49,7 @@ class UserdataTypeExtension extends TypeExtension - export interface CreateEngineOptions { /** Injects all the lua standard libraries (math, coroutine, debug) */ openStandardLibs?: boolean diff --git a/test/debug.js b/test/debug.js index 00daa23..1373e84 100644 --- a/test/debug.js +++ b/test/debug.js @@ -1,10 +1,10 @@ -import { getEngine } from './utils.js' +import { getState } from './utils.js' // This file was created as a sandbox to test and debug on vscode -const engine = await getEngine() -engine.global.set('potato', { +const state = await getState() +state.global.set('potato', { test: true, hello: ['world'], }) -engine.global.get('potato') -engine.doStringSync('print(potato.hello[1])') +state.global.get('potato') +state.doStringSync('print(potato.hello[1])') diff --git a/test/engine.test.js b/test/engine.test.js index 23e6361..8b1fdd1 100644 --- a/test/engine.test.js +++ b/test/engine.test.js @@ -1,7 +1,7 @@ import { EventEmitter } from 'events' import { LuaLibraries, LuaReturn, LuaThread, LuaType, decorate, decorateProxy, decorateUserdata } from '../dist/index.js' import { expect } from 'chai' -import { getEngine, getFactory } from './utils.js' +import { getState, getLua } from './utils.js' import { setTimeout } from 'node:timers/promises' import { mock } from 'node:test' @@ -19,7 +19,7 @@ class TestClass { } } -describe('Engine', () => { +describe('State', () => { let intervals = [] const setIntervalSafe = (callback, interval) => { intervals.push(setInterval(() => callback(), interval)) @@ -33,32 +33,32 @@ describe('Engine', () => { }) it('receive lua table on JS function should succeed', async () => { - const engine = await getEngine() - engine.global.set('stringify', (table) => { + const state = await getState() + state.global.set('stringify', (table) => { return JSON.stringify(table) }) - await engine.doString('value = stringify({ test = 1 })') + await state.doString('value = stringify({ test = 1 })') - expect(engine.global.get('value')).to.be.equal(JSON.stringify({ test: 1 })) + expect(state.global.get('value')).to.be.equal(JSON.stringify({ test: 1 })) }) it('get a global table inside a JS function called by lua should succeed', async () => { - const engine = await getEngine() - engine.global.set('t', { test: 1 }) - engine.global.set('test', () => { - return engine.global.get('t') + const state = await getState() + state.global.set('t', { test: 1 }) + state.global.set('test', () => { + return state.global.get('t') }) - const value = await engine.doString('return test(2)') + const value = await state.doString('return test(2)') expect(value).to.be.eql({ test: 1 }) }) it('receive JS object on lua should succeed', async () => { - const engine = await getEngine() + const state = await getState() - engine.global.set('test', () => { + state.global.set('test', () => { return { aaaa: 1, bbb: 'hey', @@ -67,27 +67,27 @@ describe('Engine', () => { }, } }) - const value = await engine.doString('return test().test()') + const value = await state.doString('return test().test()') expect(value).to.be.equal(22) }) it('receive JS object with circular references on lua should succeed', async () => { - const engine = await getEngine() + const state = await getState() const obj = { hello: 'world', } obj.self = obj - engine.global.set('obj', obj) + state.global.set('obj', obj) - const value = await engine.doString('return obj.self.self.self.hello') + const value = await state.doString('return obj.self.self.self.hello') expect(value).to.be.equal('world') }) it('receive Lua object with circular references on JS should succeed', async () => { - const engine = await getEngine() - const value = await engine.doString(` + const state = await getState() + const value = await state.doString(` local obj1 = { hello = 'world', } @@ -122,8 +122,8 @@ describe('Engine', () => { }) it('receive lua array with circular references on JS should succeed', async () => { - const engine = await getEngine() - const value = await engine.doString(` + const state = await getState() + const value = await state.doString(` obj = { "hello", "world" @@ -138,7 +138,7 @@ describe('Engine', () => { }) it('receive JS object with multiple circular references on lua should succeed', async () => { - const engine = await getEngine() + const state = await getState() const obj1 = { hello: 'world', } @@ -147,45 +147,45 @@ describe('Engine', () => { hello: 'everybody', } obj2.self = obj2 - engine.global.set('obj', { obj1, obj2 }) + state.global.set('obj', { obj1, obj2 }) - await engine.doString(` + await state.doString(` assert(obj.obj1.self.self.hello == "world") assert(obj.obj2.self.self.hello == "everybody") `) }) it('receive JS object with null prototype on lua should succeed', async () => { - const engine = await getEngine() + const state = await getState() const obj = Object.create(null) obj.hello = 'world' - engine.global.set('obj', obj) + state.global.set('obj', obj) - const value = await engine.doString(`return obj.hello`) + const value = await state.doString(`return obj.hello`) expect(value).to.be.equal('world') }) it('a lua error should throw on JS', async () => { - const engine = await getEngine() + const state = await getState() - await expect(engine.doString(`x -`)).to.eventually.be.rejected + await expect(state.doString(`x -`)).to.eventually.be.rejected }) it('call a lua function from JS should succeed', async () => { - const engine = await getEngine() + const state = await getState() - await engine.doString(`function sum(x, y) return x + y end`) - const sum = engine.global.get('sum') + await state.doString(`function sum(x, y) return x + y end`) + const sum = state.global.get('sum') expect(sum(10, 50)).to.be.equal(60) }) it('scheduled lua calls should succeed', async () => { - const engine = await getEngine() - engine.global.set('setInterval', setIntervalSafe) + const state = await getState() + state.global.set('setInterval', setIntervalSafe) - await engine.doString(` + await state.doString(` test = "" setInterval(function() test = test .. "i" @@ -193,33 +193,33 @@ describe('Engine', () => { `) await setTimeout(20) - const test = engine.global.get('test') + const test = state.global.get('test') expect(test).length.above(3) expect(test).length.below(21) expect(test).to.be.equal(''.padEnd(test.length, 'i')) }) it('scheduled lua calls should fail silently if invalid', async () => { - const engine = await getEngine() - engine.global.set('setInterval', setIntervalSafe) + const state = await getState() + state.global.set('setInterval', setIntervalSafe) const originalConsoleWarn = console.warn console.warn = mock.fn() - await engine.doString(` + await state.doString(` test = 0 setInterval(function() test = test + 1 end, 5) `) - engine.global.close() + state.global.close() await setTimeout(5 + 5) console.warn = originalConsoleWarn }) it('call lua function from JS passing an array argument should succeed', async () => { - const engine = await getEngine() + const state = await getState() - const sum = await engine.doString(` + const sum = await state.doString(` return function(arr) local sum = 0 for k, v in ipairs(arr) do @@ -233,24 +233,24 @@ describe('Engine', () => { }) it('call a global function with multiple returns should succeed', async () => { - const engine = await getEngine() + const state = await getState() - await engine.doString(` + await state.doString(` function f(x,y) return 1,x,y,"Hello World",{},function() end end `) - const returns = engine.global.call('f', 10, 25) + const returns = state.global.call('f', 10, 25) expect(returns).to.have.length(6) expect(returns.slice(0, -1)).to.eql([1, 10, 25, 'Hello World', {}]) expect(returns.at(-1)).to.be.a('function') }) it('get a lua thread should succeed', async () => { - const engine = await getEngine() + const state = await getState() - const thread = await engine.doString(` + const thread = await state.doString(` return coroutine.create(function() print("hey") end) @@ -261,15 +261,15 @@ describe('Engine', () => { }) it('a JS error should pause lua execution', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) - engine.global.set('throw', () => { + state.global.set('check', check) + state.global.set('throw', () => { throw new Error('expected error') }) await expect( - engine.doString(` + state.doString(` throw() check() `), @@ -278,14 +278,14 @@ describe('Engine', () => { }) it('catch a JS error with pcall should succeed', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) - engine.global.set('throw', () => { + state.global.set('check', check) + state.global.set('throw', () => { throw new Error('expected error') }) - await engine.doString(` + await state.doString(` local success, err = pcall(throw) assert(success == false) assert(tostring(err) == "Error: expected error") @@ -296,11 +296,11 @@ describe('Engine', () => { }) it('call a JS function in a different thread should succeed', async () => { - const engine = await getEngine() + const state = await getState() const sum = mock.fn((x, y) => x + y) - engine.global.set('sum', sum) + state.global.set('sum', sum) - await engine.doString(` + await state.doString(` coroutine.resume(coroutine.create(function() sum(10, 20) end)) @@ -311,8 +311,8 @@ describe('Engine', () => { }) it('get callable table as function should succeed', async () => { - const engine = await getEngine() - await engine.doString(` + const state = await getState() + await state.doString(` _G['sum'] = setmetatable({}, { __call = function(self, x, y) return x + y @@ -320,15 +320,15 @@ describe('Engine', () => { }) `) - engine.global.lua.lua_getglobal(engine.global.address, 'sum') - const sum = engine.global.getValue(-1, LuaType.Function) + state.global.lua.lua_getglobal(state.global.address, 'sum') + const sum = state.global.getValue(-1, LuaType.Function) expect(sum(10, 30)).to.be.equal(40) }) it('lua_resume with yield succeeds', async () => { - const engine = await getEngine() - const thread = engine.global.newThread() + const state = await getState() + const thread = state.global.newThread() thread.loadString(` local yieldRes = coroutine.yield(10) return yieldRes @@ -353,34 +353,34 @@ describe('Engine', () => { }) it('get memory with allocation tracing should succeeds', async () => { - const engine = await getEngine({ traceAllocations: true }) - expect(engine.global.getMemoryUsed()).to.be.greaterThan(0) + const state = await getState({ traceAllocations: true }) + expect(state.global.getMemoryUsed()).to.be.greaterThan(0) }) it('get memory should return correct', async () => { - const engine = await getEngine({ traceAllocations: true }) + const state = await getState({ traceAllocations: true }) - const totalMemory = await engine.doString(` + const totalMemory = await state.doString(` collectgarbage() local x = 10 local batata = { dawdwa = 1 } return collectgarbage('count') * 1024 `) - expect(engine.global.getMemoryUsed()).to.be.equal(totalMemory) + expect(state.global.getMemoryUsed()).to.be.equal(totalMemory) }) it('get memory without tracing should throw', async () => { - const engine = await getEngine({ traceAllocations: false }) + const state = await getState({ traceAllocations: false }) - expect(() => engine.global.getMemoryUsed()).to.throw() + expect(() => state.global.getMemoryUsed()).to.throw() }) it('limit memory use causes program loading failure succeeds', async () => { - const engine = await getEngine({ traceAllocations: true }) - engine.global.setMemoryMax(engine.global.getMemoryUsed()) + const state = await getState({ traceAllocations: true }) + state.global.setMemoryMax(state.global.getMemoryUsed()) expect(() => { - engine.global.loadString(` + state.global.loadString(` local a = 10 local b = 20 return a + b @@ -388,8 +388,8 @@ describe('Engine', () => { }).to.throw('not enough memory') // Remove the limit and retry - engine.global.setMemoryMax(undefined) - engine.global.loadString(` + state.global.setMemoryMax(undefined) + state.global.loadString(` local a = 10 local b = 20 return a + b @@ -397,35 +397,35 @@ describe('Engine', () => { }) it('limit memory use causes program runtime failure succeeds', async () => { - const engine = await getEngine({ traceAllocations: true }) - engine.global.loadString(` + const state = await getState({ traceAllocations: true }) + state.global.loadString(` local tab = {} for i = 1, 50, 1 do tab[i] = i end `) - engine.global.setMemoryMax(engine.global.getMemoryUsed()) + state.global.setMemoryMax(state.global.getMemoryUsed()) - await expect(engine.global.run()).to.eventually.be.rejectedWith('not enough memory') + await expect(state.global.run()).to.eventually.be.rejectedWith('not enough memory') }) it('table supported circular dependencies', async () => { - const engine = await getEngine() + const state = await getState() const a = { name: 'a' } const b = { name: 'b' } b.a = a a.b = b - engine.global.pushValue(a) - const res = engine.global.getValue(-1) + state.global.pushValue(a) + const res = state.global.getValue(-1) expect(res.b.a).to.be.eql(res) }) it('wrap a js object (with metatable)', async () => { - const engine = await getEngine() - engine.global.set('TestClass', { + const state = await getState() + state.global.set('TestClass', { create: (name) => { return decorate( { @@ -446,7 +446,7 @@ describe('Engine', () => { }, }) - const res = await engine.doString(` + const res = await state.doString(` local instance = TestClass.create("demo name") return instance.name `) @@ -454,11 +454,11 @@ describe('Engine', () => { }) it('wrap a js object using proxy', async () => { - const engine = await getEngine() - engine.global.set('TestClass', { + const state = await getState() + state.global.set('TestClass', { create: (name) => new TestClass(name), }) - const res = await engine.doString(` + const res = await state.doString(` local instance = TestClass.create("demo name 2") return instance:getName() `) @@ -466,11 +466,11 @@ describe('Engine', () => { }) it('wrap a js object using proxy and apply metatable in lua', async () => { - const engine = await getEngine() - engine.global.set('TestClass', { + const state = await getState() + state.global.set('TestClass', { create: (name) => new TestClass(name), }) - const res = await engine.doString(` + const res = await state.doString(` local instance = TestClass.create("demo name 2") -- Based in the simple lua classes tutotial @@ -495,10 +495,10 @@ describe('Engine', () => { }) it('classes should be a userdata when proxied', async () => { - const engine = await getEngine() - engine.global.set('obj', { TestClass }) + const state = await getState() + state.global.set('obj', { TestClass }) - const testClass = await engine.doString(` + const testClass = await state.doString(` return obj.TestClass `) @@ -506,27 +506,27 @@ describe('Engine', () => { }) it('timeout blocking lua program', async () => { - const engine = await getEngine() - engine.global.loadString(` + const state = await getState() + state.global.loadString(` local i = 0 while true do i = i + 1 end `) - await expect(engine.global.run(0, { timeout: 5 })).eventually.to.be.rejectedWith('thread timeout exceeded') + await expect(state.global.run(0, { timeout: 5 })).eventually.to.be.rejectedWith('thread timeout exceeded') }) it('overwrite lib function', async () => { - const engine = await getEngine() + const state = await getState() let output = '' - engine.global.getTable(LuaLibraries.Base, (index) => { - engine.global.setField(index, 'print', (val) => { + state.global.getTable(LuaLibraries.Base, (index) => { + state.global.setField(index, 'print', (val) => { // Not a proper print implementation. output += `${val}\n` }) }) - await engine.doString(` + await state.doString(` print("hello") print("world") `) @@ -535,28 +535,28 @@ describe('Engine', () => { }) it('inject a userdata with a metatable should succeed', async () => { - const engine = await getEngine() + const state = await getState() const obj = decorate( {}, { metatable: { __index: (_, k) => `Hello ${k}!` }, }, ) - engine.global.set('obj', obj) + state.global.set('obj', obj) - const res = await engine.doString('return obj.World') + const res = await state.doString('return obj.World') expect(res).to.be.equal('Hello World!') }) it('a userdata should be collected', async () => { - const engine = await getEngine() + const state = await getState() const obj = {} - engine.global.set('obj', obj) - const refIndex = engine.global.lua.getLastRefIndex() - const oldRef = engine.global.lua.getRef(refIndex) + state.global.set('obj', obj) + const refIndex = state.global.lua.getLastRefIndex() + const oldRef = state.global.lua.getRef(refIndex) - await engine.doString(` + await state.doString(` local weaktable = {} setmetatable(weaktable, { __mode = "v" }) table.insert(weaktable, obj) @@ -566,43 +566,43 @@ describe('Engine', () => { `) expect(oldRef).to.be.equal(obj) - const newRef = engine.global.lua.getRef(refIndex) + const newRef = state.global.lua.getRef(refIndex) expect(newRef).to.be.equal(undefined) }) it('environment variables should be set', async () => { - const factory = getFactory({ TEST: 'true' }) - const engine = await factory.createEngine() + const lua = await getLua({ TEST: 'true' }) + const state = lua.createState() - const testEnvVar = await engine.doString(`return os.getenv('TEST')`) + const testEnvVar = await state.doString(`return os.getenv('TEST')`) expect(testEnvVar).to.be.equal('true') }) it('static methods should be callable on classes', async () => { - const engine = await getEngine() - engine.global.set('TestClass', TestClass) + const state = await getState() + state.global.set('TestClass', TestClass) - const testHello = await engine.doString(`return TestClass.hello()`) + const testHello = await state.doString(`return TestClass.hello()`) expect(testHello).to.be.equal('world') }) it('should be possible to access function properties', async () => { - const engine = await getEngine() + const state = await getState() const testFunction = () => undefined testFunction.hello = 'world' - engine.global.set('TestFunction', decorateProxy(testFunction, { proxy: true })) + state.global.set('TestFunction', decorateProxy(testFunction, { proxy: true })) - const testHello = await engine.doString(`return TestFunction.hello`) + const testHello = await state.doString(`return TestFunction.hello`) expect(testHello).to.be.equal('world') }) it('throw error includes stack trace', async () => { - const engine = await getEngine() + const state = await getState() try { - await engine.doString(` + await state.doString(` local function a() error("function a threw error") end @@ -622,12 +622,12 @@ describe('Engine', () => { }) it('should get only the last result on run', async () => { - const engine = await getEngine() + const state = await getState() - const a = await engine.doString(`return 1`) - const b = await engine.doString(`return 3`) - const c = engine.doStringSync(`return 2`) - const d = engine.doStringSync(`return 5`) + const a = await state.doString(`return 1`) + const b = await state.doString(`return 3`) + const c = state.doStringSync(`return 2`) + const d = state.doStringSync(`return 5`) expect(a).to.be.equal(1) expect(b).to.be.equal(3) @@ -636,12 +636,12 @@ describe('Engine', () => { }) it('should get only the return values on call function', async () => { - const engine = await getEngine() - engine.global.set('hello', (name) => `Hello ${name}!`) + const state = await getState() + state.global.set('hello', (name) => `Hello ${name}!`) - const a = await engine.doString(`return 1`) - const b = engine.doStringSync(`return 5`) - const values = engine.global.call('hello', 'joao') + const a = await state.doString(`return 1`) + const b = state.doStringSync(`return 5`) + const values = state.global.call('hello', 'joao') expect(a).to.be.equal(1) expect(b).to.be.equal(5) @@ -650,69 +650,69 @@ describe('Engine', () => { }) it('create a large string variable should succeed', async () => { - const engine = await getEngine() + const state = await getState() const str = 'a'.repeat(1000000) - engine.global.set('str', str) + state.global.set('str', str) - const res = await engine.doString('return str') + const res = await state.doString('return str') expect(res).to.be.equal(str) }) it('execute a large string should succeed', async () => { - const engine = await getEngine() + const state = await getState() const str = 'a'.repeat(1000000) - const res = await engine.doString(`return [[${str}]]`) + const res = await state.doString(`return [[${str}]]`) expect(res).to.be.equal(str) }) it('negative integers should be pushed and retrieved as string', async () => { - const engine = await getEngine() - engine.global.set('value', -1) + const state = await getState() + state.global.set('value', -1) - const res = await engine.doString(`return tostring(value)`) + const res = await state.doString(`return tostring(value)`) expect(res).to.be.equal('-1') }) it('negative integers should be pushed and retrieved as number', async () => { - const engine = await getEngine() - engine.global.set('value', -1) + const state = await getState() + state.global.set('value', -1) - const res = await engine.doString(`return value`) + const res = await state.doString(`return value`) expect(res).to.be.equal(-1) }) it('number greater than 32 bit int should be pushed and retrieved as string', async () => { - const engine = await getEngine() + const state = await getState() const value = 1689031554550 - engine.global.set('value', value) + state.global.set('value', value) - const res = await engine.doString(`return tostring(value)`) + const res = await state.doString(`return tostring(value)`) expect(res).to.be.equal(`${String(value)}`) }) it('number greater than 32 bit int should be pushed and retrieved as number', async () => { - const engine = await getEngine() + const state = await getState() const value = 1689031554550 - engine.global.set('value', value) + state.global.set('value', value) - const res = await engine.doString(`return value`) + const res = await state.doString(`return value`) expect(res).to.be.equal(value) }) it('number greater than 32 bit int should be usable as a format argument', async () => { - const engine = await getEngine() + const state = await getState() const value = 1689031554550 - engine.global.set('value', value) + state.global.set('value', value) - const res = await engine.doString(`return ("%d"):format(value)`) + const res = await state.doString(`return ("%d"):format(value)`) expect(res).to.be.equal('1689031554550') }) @@ -721,10 +721,10 @@ describe('Engine', () => { // When yielding within a callback the error 'attempt to yield across a C-call boundary'. // This test just checks that throwing that error still allows the lua global to be // re-used and doesn't cause JS to abort or some nonsense. - const engine = await getEngine() + const state = await getState() const testEmitter = new EventEmitter() - engine.global.set('yield', () => new Promise((resolve) => testEmitter.once('resolve', resolve))) - const resPromise = engine.doString(` + state.global.set('yield', () => new Promise((resolve) => testEmitter.once('resolve', resolve))) + const resPromise = state.doString(` local res = yield():next(function () coroutine.yield() return 15 @@ -735,13 +735,13 @@ describe('Engine', () => { testEmitter.emit('resolve') await expect(resPromise).to.eventually.be.rejectedWith('Error: attempt to yield across a C-call boundary') - expect(await engine.doString(`return 42`)).to.equal(42) + expect(await state.doString(`return 42`)).to.equal(42) }) it('forced yield within JS callback from Lua doesnt cause vm to crash', async () => { - const engine = await getEngine({ functionTimeout: 10 }) - engine.global.set('promise', Promise.resolve()) - const thread = engine.global.newThread() + const state = await getState({ functionTimeout: 10 }) + state.global.set('promise', Promise.resolve()) + const thread = state.global.newThread() thread.loadString(` promise:next(function () while true do @@ -751,13 +751,13 @@ describe('Engine', () => { `) await expect(thread.run(0, { timeout: 5 })).to.eventually.be.rejectedWith('thread timeout exceeded') - expect(await engine.doString(`return 42`)).to.equal(42) + expect(await state.doString(`return 42`)).to.equal(42) }) it('function callback timeout still allows timeout of caller thread', async () => { - const engine = await getEngine() - engine.global.set('promise', Promise.resolve()) - const thread = engine.global.newThread() + const state = await getState() + state.global.set('promise', Promise.resolve()) + const thread = state.global.newThread() thread.loadString(` promise:next(function () -- nothing @@ -768,33 +768,33 @@ describe('Engine', () => { }) it('null injected and valid', async () => { - const engine = await getEngine() - engine.global.loadString(` + const state = await getState() + state.global.loadString(` local args = { ... } assert(args[1] == null, string.format("expected first argument to be null, got %s", tostring(args[1]))) return null, args[1], tostring(null) `) - engine.global.pushValue(null) - const res = await engine.global.run(1) + state.global.pushValue(null) + const res = await state.global.run(1) expect(res).to.deep.equal([null, null, 'null']) }) it('null injected as nil', async () => { - const engine = await getEngine({ injectObjects: false }) - engine.global.loadString(` + const state = await getState({ injectObjects: false }) + state.global.loadString(` local args = { ... } assert(type(args[1]) == "nil", string.format("expected first argument to be nil, got %s", type(args[1]))) return nil, args[1], tostring(nil) `) - engine.global.pushValue(null) - const res = await engine.global.run(1) + state.global.pushValue(null) + const res = await state.global.run(1) expect(res).to.deep.equal([null, null, 'nil']) }) it('Nested callback from JS to Lua', async () => { - const engine = await getEngine() - engine.global.set('call', (fn) => fn()) - const res = await engine.doString(` + const state = await getState() + state.global.set('call', (fn) => fn()) + const res = await state.doString(` return call(function () return call(function () return 10 @@ -805,14 +805,14 @@ describe('Engine', () => { }) it('lots of doString calls should succeed', async () => { - const engine = await getEngine() - const length = 10000; + const state = await getState() + const length = 10000 for (let i = 0; i < length; i++) { - const a = Math.floor(Math.random() * 100); - const b = Math.floor(Math.random() * 100); - const result = await engine.doString(`return ${a} + ${b};`); - expect(result).to.equal(a + b); + const a = Math.floor(Math.random() * 100) + const b = Math.floor(Math.random() * 100) + const result = await state.doString(`return ${a} + ${b};`) + expect(result).to.equal(a + b) } }) }) diff --git a/test/filesystem.test.js b/test/filesystem.test.js index 849f933..d1c5f54 100644 --- a/test/filesystem.test.js +++ b/test/filesystem.test.js @@ -1,66 +1,66 @@ import { expect } from 'chai' -import { getEngine, getFactory } from './utils.js' +import { getState, getLua } from './utils.js' describe('Filesystem', () => { it('mount a file and require inside lua should succeed', async () => { - const factory = getFactory() - await factory.mountFile('test.lua', 'answerToLifeTheUniverseAndEverything = 42') - const engine = await factory.createEngine() + const lua = await getLua() + lua.mountFile('test.lua', 'answerToLifeTheUniverseAndEverything = 42') + const state = lua.createState() - await engine.doString('require("test")') + await state.doString('require("test")') - expect(engine.global.get('answerToLifeTheUniverseAndEverything')).to.be.equal(42) + expect(state.global.get('answerToLifeTheUniverseAndEverything')).to.be.equal(42) }) it('mount a file in a complex directory and require inside lua should succeed', async () => { - const factory = getFactory() - await factory.mountFile('yolo/sofancy/test.lua', 'return 42') - const engine = await factory.createEngine() + const lua = await getLua() + lua.mountFile('yolo/sofancy/test.lua', 'return 42') + const state = lua.createState() - const value = await engine.doString('return require("yolo/sofancy/test")') + const value = await state.doString('return require("yolo/sofancy/test")') expect(value).to.be.equal(42) }) it('mount a init file and require the module inside lua should succeed', async () => { - const factory = getFactory() - await factory.mountFile('hello/init.lua', 'return 42') - const engine = await factory.createEngine() + const lua = await getLua() + lua.mountFile('hello/init.lua', 'return 42') + const state = lua.createState() - const value = await engine.doString('return require("hello")') + const value = await state.doString('return require("hello")') expect(value).to.be.equal(42) }) it('require a file which is not mounted should throw', async () => { - const engine = await getEngine() + const state = await getState() - await expect(engine.doString('require("nothing")')).to.eventually.be.rejected + await expect(state.doString('require("nothing")')).to.eventually.be.rejected }) it('mount a file and run it should succeed', async () => { - const factory = getFactory() - const engine = await factory.createEngine() + const lua = await getLua() + const state = lua.createState() - await factory.mountFile('init.lua', `return 42`) - const value = await engine.doFile('init.lua') + lua.mountFile('init.lua', `return 42`) + const value = await state.doFile('init.lua') expect(value).to.be.equal(42) }) it('run a file which is not mounted should throw', async () => { - const engine = await getEngine() + const state = await getState() - await expect(engine.doFile('init.lua')).to.eventually.be.rejected + await expect(state.doFile('init.lua')).to.eventually.be.rejected }) it('mount a file with a large content should succeed', async () => { - const factory = getFactory() - const engine = await factory.createEngine() + const lua = await getLua() + const state = lua.createState() const content = 'a'.repeat(1000000) - await factory.mountFile('init.lua', `local a = "${content}" return a`) - const value = await engine.doFile('init.lua') + lua.mountFile('init.lua', `local a = "${content}" return a`) + const value = await state.doFile('init.lua') expect(value).to.be.equal(content) }) diff --git a/test/initialization.test.js b/test/initialization.test.js index 72d1684..90952b4 100644 --- a/test/initialization.test.js +++ b/test/initialization.test.js @@ -1,13 +1,23 @@ -import { LuaFactory } from '../dist/index.js' +import { Lua } from '../dist/index.js' import { expect } from 'chai' describe('Initialization', () => { - it('create engine should succeed', async () => { - await new LuaFactory().createEngine() + it('create state should succeed', async () => { + const lua = await Lua.load() + lua.createState() }) - it('create engine with options should succeed', async () => { - await new LuaFactory().createEngine({ + it('create multiple states should succeed', async () => { + const lua = await Lua.load() + const state1 = lua.createState() + const state2 = lua.createState() + + expect(state1.global.address).to.not.be.equal(state2.global.address) + }) + + it('create state with options should succeed', async () => { + const lua = await Lua.load() + lua.createState({ enableProxy: true, injectObjects: true, openStandardLibs: true, @@ -19,9 +29,10 @@ describe('Initialization', () => { const env = { ENV_TEST: 'test', } - const engine = await new LuaFactory(undefined, env).createEngine() + const lua = await Lua.load({ env }) + const state = lua.createState() - const value = await engine.doString('return os.getenv("ENV_TEST")') + const value = await state.doString('return os.getenv("ENV_TEST")') expect(value).to.be.equal(env.ENV_TEST) }) diff --git a/test/luatests.js b/test/luatests.js index 3b08dd2..cfe2df7 100644 --- a/test/luatests.js +++ b/test/luatests.js @@ -1,37 +1,23 @@ -import { LuaFactory } from '../dist/index.js' +import { Lua } from '../dist/index.js' import { fileURLToPath } from 'node:url' -import { readFile, readdir } from 'node:fs/promises' -import { resolve } from 'node:path' +import { readFile, glob } from 'node:fs/promises' -async function* walk(dir) { - const dirents = await readdir(dir, { withFileTypes: true }) - for (const dirent of dirents) { - const res = resolve(dir, dirent.name) - if (dirent.isDirectory()) { - yield* walk(res) - } else { - yield res - } - } -} - -const factory = new LuaFactory() +const lua = await Lua.load() const testsPath = import.meta.resolve('../lua/testes') const filePath = fileURLToPath(typeof testsPath === 'string' ? testsPath : await Promise.resolve(testsPath)) -for await (const file of walk(filePath)) { +for await (const file of glob(`${filePath}/**/*.lua`)) { const relativeFile = file.replace(`${filePath}/`, '') - await factory.mountFile(relativeFile, await readFile(file)) + lua.mountFile(relativeFile, await readFile(file)) } -const lua = await factory.createEngine() -const luamodule = await factory.getLuaModule() -luamodule.lua_warning(lua.global.address, '@on', 0) -lua.global.set('arg', ['lua', 'all.lua']) -lua.global.set('_port', true) -lua.global.getTable('os', (i) => { - lua.global.setField(i, 'setlocale', (locale) => { +const state = lua.createState() +lua.module.lua_warning(state.global.address, '@on', 0) +state.global.set('arg', ['lua', 'all.lua']) +state.global.set('_port', true) +state.global.getTable('os', (i) => { + state.global.setField(i, 'setlocale', (locale) => { return locale && locale !== 'C' ? false : 'C' }) }) -lua.doFileSync('all.lua') +state.doFileSync('all.lua') diff --git a/test/promises.test.js b/test/promises.test.js index d7b1d60..6227707 100644 --- a/test/promises.test.js +++ b/test/promises.test.js @@ -1,16 +1,16 @@ import { expect } from 'chai' -import { getEngine, tick } from './utils.js' +import { getState, tick } from './utils.js' import { mock } from 'node:test' describe('Promises', () => { it('use promise next should succeed', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) + state.global.set('check', check) const promise = new Promise((resolve) => setTimeout(() => resolve(60), 5)) - engine.global.set('promise', promise) + state.global.set('promise', promise) - const res = engine.doString(` + const res = state.doString(` promise:next(check) `) @@ -21,13 +21,13 @@ describe('Promises', () => { }) it('chain promises with next should succeed', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) + state.global.set('check', check) const promise = new Promise((resolve) => resolve(60)) - engine.global.set('promise', promise) + state.global.set('promise', promise) - const res = engine.doString(` + const res = state.doString(` promise:next(function(value) return value * 2 end):next(check):next(check) @@ -42,12 +42,12 @@ describe('Promises', () => { }) it('call an async function should succeed', async () => { - const engine = await getEngine() - engine.global.set('asyncFunction', async () => Promise.resolve(60)) + const state = await getState() + state.global.set('asyncFunction', async () => Promise.resolve(60)) const check = mock.fn() - engine.global.set('check', check) + state.global.set('check', check) - const res = engine.doString(` + const res = state.doString(` asyncFunction():next(check) `) @@ -57,10 +57,10 @@ describe('Promises', () => { }) it('return an async function should succeed', async () => { - const engine = await getEngine() - engine.global.set('asyncFunction', async () => Promise.resolve(60)) + const state = await getState() + state.global.set('asyncFunction', async () => Promise.resolve(60)) - const asyncFunction = await engine.doString(` + const asyncFunction = await state.doString(` return asyncFunction `) const value = await asyncFunction() @@ -69,10 +69,10 @@ describe('Promises', () => { }) it('return a chained promise should succeed', async () => { - const engine = await getEngine() - engine.global.set('asyncFunction', async () => Promise.resolve(60)) + const state = await getState() + state.global.set('asyncFunction', async () => Promise.resolve(60)) - const asyncFunction = await engine.doString(` + const asyncFunction = await state.doString(` return asyncFunction():next(function(x) return x * 2 end) `) const value = await asyncFunction @@ -81,13 +81,13 @@ describe('Promises', () => { }) it('await an promise inside coroutine should succeed', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) + state.global.set('check', check) const promise = new Promise((resolve) => setTimeout(() => resolve(60), 5)) - engine.global.set('promise', promise) + state.global.set('promise', promise) - const res = engine.doString(` + const res = state.doString(` local co = coroutine.create(function() local value = promise:await() check(value) @@ -108,13 +108,13 @@ describe('Promises', () => { }) it('awaited coroutines should ignore resume until it resolves the promise', async () => { - const engine = await getEngine() + const state = await getState() const check = mock.fn() - engine.global.set('check', check) + state.global.set('check', check) const promise = new Promise((resolve) => setTimeout(() => resolve(60), 5)) - engine.global.set('promise', promise) + state.global.set('promise', promise) - const res = engine.doString(` + const res = state.doString(` local co = coroutine.create(function() local value = promise:await() check(value) @@ -133,9 +133,9 @@ describe('Promises', () => { }) it('await a thread run with async calls should succeed', async () => { - const engine = await getEngine() - engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) - const asyncThread = engine.global.newThread() + const state = await getState() + state.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) + const asyncThread = state.global.newThread() asyncThread.loadString(` sleep(1):await() @@ -147,9 +147,9 @@ describe('Promises', () => { }) it('run thread with async calls and yields should succeed', async () => { - const engine = await getEngine() - engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) - const asyncThread = engine.global.newThread() + const state = await getState() + state.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) + const asyncThread = state.global.newThread() asyncThread.loadString(` coroutine.yield() @@ -165,9 +165,9 @@ describe('Promises', () => { }) it('reject a promise should succeed', async () => { - const engine = await getEngine() - engine.global.set('throw', () => new Promise((_, reject) => reject(new Error('expected test error')))) - const asyncThread = engine.global.newThread() + const state = await getState() + state.global.set('throw', () => new Promise((_, reject) => reject(new Error('expected test error')))) + const asyncThread = state.global.newThread() asyncThread.loadString(` throw():await() @@ -178,9 +178,9 @@ describe('Promises', () => { }) it('pcall a promise await should succeed', async () => { - const engine = await getEngine() - engine.global.set('throw', () => new Promise((_, reject) => reject(new Error('expected test error')))) - const asyncThread = engine.global.newThread() + const state = await getState() + state.global.set('throw', () => new Promise((_, reject) => reject(new Error('expected test error')))) + const asyncThread = state.global.newThread() asyncThread.loadString(` local succeed, err = pcall(function() throw():await() end) @@ -192,13 +192,13 @@ describe('Promises', () => { }) it('catch a promise rejection should succeed', async () => { - const engine = await getEngine() + const state = await getState() const fulfilled = mock.fn() const rejected = mock.fn() - engine.global.set('handlers', { fulfilled, rejected }) - engine.global.set('throw', new Promise((_, reject) => reject(new Error('expected test error')))) + state.global.set('handlers', { fulfilled, rejected }) + state.global.set('throw', new Promise((_, reject) => reject(new Error('expected test error')))) - const res = engine.doString(` + const res = state.doString(` throw:next(handlers.fulfilled, handlers.rejected):catch(function() end) `) @@ -209,10 +209,10 @@ describe('Promises', () => { }) it('run with async callback', async () => { - const engine = await getEngine() - const thread = engine.global.newThread() + const state = await getState() + const thread = state.global.newThread() - engine.global.set('asyncCallback', async (input) => { + state.global.set('asyncCallback', async (input) => { return Promise.resolve(input * 2) }) @@ -232,8 +232,8 @@ describe('Promises', () => { }) it('promise creation from js', async () => { - const engine = await getEngine() - const res = await engine.doString(` + const state = await getState() + const res = await state.doString(` local promise = Promise.create(function (resolve) resolve(10) end) @@ -248,8 +248,8 @@ describe('Promises', () => { }) it('reject promise creation from js', async () => { - const engine = await getEngine() - const res = await engine.doString(` + const state = await getState() + const res = await state.doString(` local rejection = Promise.create(function (resolve, reject) reject("expected rejection") end) @@ -261,9 +261,9 @@ describe('Promises', () => { }) it('resolve multiple promises with promise.all', async () => { - const engine = await getEngine() - engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) - const resPromise = engine.doString(` + const state = await getState() + state.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) + const resPromise = state.doString(` local promises = {} for i = 1, 10 do table.insert(promises, sleep(5):next(function () @@ -278,9 +278,9 @@ describe('Promises', () => { }) it('error in promise next catchable', async () => { - const engine = await getEngine() - engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) - const resPromise = engine.doString(` + const state = await getState() + state.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) + const resPromise = state.doString(` return sleep(1):next(function () error("sleep done") end):await() @@ -289,11 +289,11 @@ describe('Promises', () => { }) it('should not be possible to await in synchronous run', async () => { - const engine = await getEngine() - engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) + const state = await getState() + state.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input))) expect(() => { - engine.doStringSync(`sleep(5):await()`) + state.doStringSync(`sleep(5):await()`) }).to.throw('cannot await in the main thread') }) }) diff --git a/test/utils.js b/test/utils.js index 0bd5368..8a0e97d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,11 +1,12 @@ -import { LuaFactory } from '../dist/index.js' +import { Lua } from '../dist/index.js' -export const getFactory = (env) => { - return new LuaFactory(undefined, env) +export const getLua = (env) => { + return Lua.load({ env }) } -export const getEngine = (config = {}) => { - return new LuaFactory().createEngine({ +export const getState = async (config = {}) => { + const lua = await Lua.load() + return lua.createState({ injectObjects: true, ...config, }) diff --git a/tsconfig.json b/tsconfig.json index f911e66..625eae6 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { "incremental": false, - "moduleResolution": "node", + "module": "ES2022", + "moduleResolution": "Bundler", "inlineSources": false, "removeComments": false, "sourceMap": false, - "target": "ES2018", + "target": "ES2022", "skipLibCheck": true, "lib": ["ESNEXT", "DOM"], "forceConsistentCasingInFileNames": true, @@ -16,8 +17,7 @@ "noUnusedLocals": true, "importHelpers": true, "strict": true, - "resolveJsonModule": true, - + "resolveJsonModule": true }, "include": ["src/**/*", "test/**/*", "bench/**/*", "eslint.config.js"], "exclude": ["node_modules"] diff --git a/utils/build-wasm.js b/utils/build-wasm.js new file mode 100644 index 0000000..ff97b26 --- /dev/null +++ b/utils/build-wasm.js @@ -0,0 +1,50 @@ +import { execSync } from 'node:child_process' +import { resolve } from 'node:path' + +const isUnix = process.platform !== 'win32' +const rootdir = resolve(import.meta.dirname, '..') +const args = process.argv.slice(2) + +const execute = (command) => { + console.log(`Running: ${command}`) + try { + execSync(command, { stdio: 'inherit' }) + } catch (error) { + console.error(`Error running command: ${command}`) + process.exit(1) + } +} + +execute('git submodule update --init --recursive') + +if (isUnix) { + let emccInstalled = false + try { + const version = execSync('emcc --version', { encoding: 'utf-8' }) + console.log('Emscripten is installed:', version) + + emccInstalled = true + } catch (error) { + console.error('Emscripten is not installed or not in your PATH. Will try to build using Docker.') + } + + if (emccInstalled) { + const command = `${resolve(rootdir, 'utils/build-wasm.sh')} ${args.join(' ')}` + execute(command) + + process.exit(0) + } +} + +try { + execSync('docker --version', { encoding: 'utf-8' }) +} +catch (error) { + console.error('Docker is not installed or not in your PATH. Please install Docker to build the WASM file.') + process.exit(1) +} + +const dockerVolume = `${rootdir}:/wasmoon` +const command = `docker run --rm -v "${dockerVolume}" emscripten/emsdk /wasmoon/utils/build-wasm.sh ${args.join(' ')}` + +execute(command) diff --git a/build.sh b/utils/build-wasm.sh similarity index 93% rename from build.sh rename to utils/build-wasm.sh index 9d54f96..4818b30 100755 --- a/build.sh +++ b/utils/build-wasm.sh @@ -1,8 +1,8 @@ #!/bin/bash -e cd $(dirname $0) -mkdir -p build +mkdir -p ../build -LUA_SRC=$(ls ./lua/*.c | grep -v "luac.c" | grep -v "lua.c" | tr "\n" " ") +LUA_SRC=$(ls ../lua/*.c | grep -v "luac.c" | grep -v "lua.c" | tr "\n" " ") extension="" if [ "$1" == "dev" ]; @@ -13,22 +13,29 @@ else fi emcc \ - -s WASM=1 $extension -o ./build/glue.js \ + -lnodefs.js \ + -s WASM=1 $extension -o ../build/glue.js \ -s EXPORTED_RUNTIME_METHODS="[ 'ccall', \ 'addFunction', \ 'removeFunction', \ 'FS', \ + 'PATH', \ 'ENV', \ 'getValue', \ 'setValue', \ 'lengthBytesUTF8', \ 'stringToUTF8', \ - 'stringToNewUTF8' + 'stringToNewUTF8', \ + 'intArrayFromString', \ + 'UTF8ToString', \ + 'HEAPU32' ]" \ -s INCOMING_MODULE_JS_API="[ 'locateFile', \ - 'preRun' + 'preRun', \ + 'print', \ + 'printErr' \ ]" \ -s ENVIRONMENT="web,worker,node" \ -s MODULARIZE=1 \ diff --git a/utils/create-bindings.js b/utils/create-bindings.cjs old mode 100755 new mode 100644 similarity index 100% rename from utils/create-bindings.js rename to utils/create-bindings.cjs