├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── demoon ├── example ├── main.lua ├── sleep.js └── sleep.lua ├── package-lock.json ├── package.json └── src ├── index.js ├── legacyclasses.js └── std.lua /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 16.x 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: 16.x 17 | - run: npm ci 18 | - uses: JS-DevTools/npm-publish@v1 19 | with: 20 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bench/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | .vscode/ 3 | .github/ 4 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": false 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gabriel Francisco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demoon 2 | 3 | You love Lua but the runtime API is very weak and you don't want to handle and learn a lot of different luarock libraries? You came at the right place! This project aims to offer the bests of **Lua** and **NodeJS** together. 4 | 5 | > Don't use this in production! 6 | 7 | ## Usage 8 | 9 | You don't need `lua` installed to run demoon, but you need `node` and npm as well, firstly, install demoon globally: 10 | 11 | ```sh 12 | $: npm i -g demoon 13 | ``` 14 | 15 | Then run it passing your entry lua file: 16 | 17 | ```sh 18 | $: demoon app.lua 19 | ``` 20 | 21 | ## Example 22 | 23 | This is a little sample code to demonstrate how demoon is powerful and bridges well with nodeJS: 24 | ```lua 25 | -- you can require node modules (package.json/node_modules works as well) 26 | local http = require('http') 27 | 28 | -- you can require js modules and lua files 29 | -- require('./myjsmodule.js') 30 | -- require('./myluamodule.lua') 31 | 32 | local port = os.getenv('PORT') or 8080 33 | 34 | function sleep(ms) 35 | -- you can use and create promises 36 | return Promise.create(function(resolve) 37 | setTimeout(resolve, ms) 38 | end) 39 | end 40 | 41 | -- top level await works! 42 | sleep(1000):await() 43 | 44 | http.createServer(async(function (req, res) 45 | -- you can await inside async bounded functions 46 | sleep(1000):await() 47 | 48 | res:write('Hello World!') 49 | res['end']() 50 | end)):listen(port) 51 | 52 | print('Your server is running on port ' .. port .. '!') 53 | ``` -------------------------------------------------------------------------------- /bin/demoon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Demoon = require('../src/index'); 4 | 5 | const snippets = process.argv.splice(2); 6 | const [entryFile, ...arg] = snippets 7 | 8 | const demoon = new Demoon(); 9 | demoon.getLuaEngine() 10 | .then(engine => { 11 | engine.global.set('arg', arg) 12 | }).then(async () => { 13 | await demoon.runFile(entryFile) 14 | }) -------------------------------------------------------------------------------- /example/main.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env demoon 2 | -- you can require node modules (package.json/node_modules works as well) 3 | local http = require('http') 4 | 5 | -- you can require js modules and lua files 6 | local jssleep = require('./sleep.js') 7 | local luasleep = require('sleep.lua') 8 | 9 | local port = os.getenv('PORT') or 8080 10 | 11 | -- top level await works! 12 | luasleep(1000):await() 13 | 14 | http.createServer(async(function (req, res) 15 | -- you can await inside async bounded functions 16 | jssleep(1000):await() 17 | 18 | res:write('Hello World!') 19 | res['end']() 20 | end)):listen(port) 21 | 22 | print('Your server is running on port ' .. port .. '!') -------------------------------------------------------------------------------- /example/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = ms => new Promise(resolve => setTimeout(resolve, ms)) -------------------------------------------------------------------------------- /example/sleep.lua: -------------------------------------------------------------------------------- 1 | return function(ms) 2 | return Promise.create(function(resolve) 3 | setTimeout(resolve, ms) 4 | end) 5 | end -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demoon", 3 | "version": "0.0.9", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "demoon", 9 | "version": "0.0.9", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wasmoon": "1.15.0" 13 | }, 14 | "bin": { 15 | "demoon": "bin/demoon" 16 | } 17 | }, 18 | "node_modules/@types/emscripten": { 19 | "version": "1.39.5", 20 | "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.5.tgz", 21 | "integrity": "sha512-DIOOg+POSrYl+OlNRHQuIEqCd8DCtynG57H862UCce16nXJX7J8eWxNGgOcf8Eyge8zXeSs27mz1UcFu8L/L7g==" 22 | }, 23 | "node_modules/wasmoon": { 24 | "version": "1.15.0", 25 | "resolved": "https://registry.npmjs.org/wasmoon/-/wasmoon-1.15.0.tgz", 26 | "integrity": "sha512-QU33AnnMTgbcGOJLzcqM2UBcSksmLvwkvB/Bcgkf5hS+EFoHxB6nyiQiDG+ZnALN8mn/ezeeMSm6eY1zs0cKTg==", 27 | "dependencies": { 28 | "@types/emscripten": "1.39.5" 29 | }, 30 | "bin": { 31 | "wasmoon": "bin/wasmoon" 32 | } 33 | } 34 | }, 35 | "dependencies": { 36 | "@types/emscripten": { 37 | "version": "1.39.5", 38 | "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.5.tgz", 39 | "integrity": "sha512-DIOOg+POSrYl+OlNRHQuIEqCd8DCtynG57H862UCce16nXJX7J8eWxNGgOcf8Eyge8zXeSs27mz1UcFu8L/L7g==" 40 | }, 41 | "wasmoon": { 42 | "version": "1.15.0", 43 | "resolved": "https://registry.npmjs.org/wasmoon/-/wasmoon-1.15.0.tgz", 44 | "integrity": "sha512-QU33AnnMTgbcGOJLzcqM2UBcSksmLvwkvB/Bcgkf5hS+EFoHxB6nyiQiDG+ZnALN8mn/ezeeMSm6eY1zs0cKTg==", 45 | "requires": { 46 | "@types/emscripten": "1.39.5" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demoon", 3 | "version": "0.0.9", 4 | "description": "Lua + Node", 5 | "main": "src/index.js", 6 | "bin": { 7 | "demoon": "bin/demoon" 8 | }, 9 | "author": "ceifa", 10 | "repository": "github:ceifa/demoon", 11 | "license": "ISC", 12 | "keywords": [ 13 | "lua" 14 | ], 15 | "dependencies": { 16 | "wasmoon": "1.15.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { LuaFactory, LuaEngine } = require('wasmoon') 2 | const path = require('path') 3 | const FunctionClassTypeExtension = require('./legacyclasses') 4 | const fs = require('fs/promises') 5 | 6 | module.exports = class { 7 | /** @returns {Promise} */ 8 | async getLuaEngine() { 9 | await this.#setupIfNeeded() 10 | return this.#engine 11 | } 12 | 13 | /** 14 | * @param {string} file 15 | * @returns {Promise} 16 | */ 17 | async runFile(file) { 18 | await this.#setupIfNeeded() 19 | 20 | this.#factory.mountFileSync(await this.#factory.getLuaModule(), file, await fs.readFile(file)) 21 | 22 | try { 23 | const thread = this.#engine.global.newThread() 24 | thread.loadFile(file) 25 | 26 | this.#engine.global.set('jsRequire', (modulename, metaDirectory) => { 27 | if (metaDirectory) { 28 | if (modulename.startsWith('.')) { 29 | modulename = path.resolve(metaDirectory, '..', modulename) 30 | } 31 | 32 | modulename = require.resolve(modulename, { paths: [file] }) 33 | } 34 | 35 | return module.require(modulename) 36 | }) 37 | 38 | await thread.run(0) 39 | } catch (e) { 40 | console.error(e) 41 | } 42 | } 43 | 44 | /** @type {LuaEngine | undefined} */ 45 | #engine 46 | 47 | /** @type {LuaFactory | undefined} */ 48 | #factory 49 | 50 | async #setupIfNeeded() { 51 | if (this.#factory) return 52 | 53 | this.#factory = new LuaFactory(undefined, process.env) 54 | const luamodule = await this.#factory.getLuaModule() 55 | 56 | const fullStdFile = path.resolve(__dirname, "std.lua") 57 | this.#factory.mountFileSync(luamodule, fullStdFile, await fs.readFile(fullStdFile)) 58 | 59 | this.#engine = await this.#factory.createEngine({ injectObjects: true }) 60 | 61 | this.#engine.global.registerTypeExtension(10, new FunctionClassTypeExtension) 62 | this.#engine.global.set('typeof', value => typeof value) 63 | this.#engine.global.set('instanceof', (value, type) => value instanceof type) 64 | this.#engine.global.set('new', (constructor, ...args) => new constructor(...args)) 65 | this.#engine.global.set('global', global) 66 | this.#engine.global.set('mountFile', (path, content) => this.#factory.mountFileSync(luamodule, path, content)) 67 | this.#engine.global.set('jsRequire', (modulename, metaDirectory) => { 68 | if (metaDirectory) { 69 | if (modulename.startsWith('.')) { 70 | modulename = path.resolve(metaDirectory, '..', modulename) 71 | } 72 | 73 | modulename = require.resolve(modulename) 74 | } 75 | 76 | return module.require(modulename) 77 | }) 78 | this.#engine.doFileSync(fullStdFile) 79 | } 80 | } -------------------------------------------------------------------------------- /src/legacyclasses.js: -------------------------------------------------------------------------------- 1 | const { decorateProxy, LuaTypeExtension } = require("wasmoon"); 2 | 3 | const functionClasses = [ 4 | Buffer, 5 | Object, 6 | Array, 7 | String, 8 | Number, 9 | ] 10 | 11 | module.exports = class extends LuaTypeExtension { 12 | constructor(thread, injectObject) { 13 | super(thread, 'js_functionclass') 14 | } 15 | 16 | pushValue(thread, { target, options }) { 17 | // If is a function not bounded yet 18 | if (typeof target === 'function' && !options?.proxy) { 19 | const isLegacyNativeClass = functionClasses.includes(target) 20 | // If it has a prototype, probably is something important 21 | const hasPrototype = target.prototype && Object.keys(target.prototype).length > 0 22 | 23 | if (isLegacyNativeClass || hasPrototype) { 24 | thread.pushValue(decorateProxy(target, { proxy: true })) 25 | return true 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/std.lua: -------------------------------------------------------------------------------- 1 | local path = jsRequire("path") 2 | local fs = jsRequire("fs") 3 | 4 | -- async function to bound awaits 5 | function async(callback) 6 | return function(...) 7 | local co = coroutine.create(callback) 8 | local safe, result = coroutine.resume(co, ...) 9 | 10 | return Promise.create(function(resolve, reject) 11 | local checkresult 12 | local step = function() 13 | if coroutine.status(co) == "dead" then 14 | local send = safe and resolve or reject 15 | return send(result) 16 | end 17 | 18 | safe, result = coroutine.resume(co) 19 | checkresult() 20 | end 21 | 22 | checkresult = function() 23 | if safe and result == Promise.resolve(result) then 24 | result:finally(step) 25 | else 26 | step() 27 | end 28 | end 29 | 30 | checkresult() 31 | end) 32 | end 33 | end 34 | 35 | -- package searcher to handle lua files on the fly 36 | table.insert(package.searchers, function(moduleName) 37 | if moduleName:sub(-4) == ".lua" then 38 | local calledDirectory = debug.getinfo(3).short_src 39 | local luafile = path.resolve(calledDirectory, "..", moduleName) 40 | 41 | local success, content = pcall(fs.readFileSync, luafile) 42 | if success and content then 43 | mountFile(luafile, content) 44 | return loadfile(luafile) 45 | end 46 | end 47 | end) 48 | 49 | -- package searcher to handle JS modules 50 | table.insert(package.searchers, function(moduleName) 51 | local success, result = pcall(jsRequire, moduleName, debug.getinfo(3).short_src) 52 | if success then return function() return result end end 53 | end) 54 | 55 | -- set the nodejs global as fallback to default lua global 56 | setmetatable(_G, { 57 | __index = function(t, k) return global[k] end 58 | }) --------------------------------------------------------------------------------