├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── index.js ├── test.js ├── index.html └── test.html ├── .gitignore ├── .npmignore ├── electroff-head.jpg ├── examples └── oled │ ├── oled.jpg │ ├── package.json │ ├── index.js │ ├── README.md │ └── public │ └── index.html ├── LICENSE ├── package.json ├── client └── index.js ├── esm └── index.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | -------------------------------------------------------------------------------- /electroff-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/electroff/HEAD/electroff-head.jpg -------------------------------------------------------------------------------- /examples/oled/oled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/electroff/HEAD/examples/oled/oled.jpg -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {PORT = 3000} = process.env; 2 | 3 | const express = require('express'); 4 | const electroff = require('../'); 5 | 6 | const app = express(); 7 | app.use(electroff); 8 | app.use(express.static(__dirname)); 9 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 10 | -------------------------------------------------------------------------------- /examples/oled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electroff-oled", 3 | "private": true, 4 | "dependencies": { 5 | "electroff": "latest", 6 | "express": "^4.17.1", 7 | "johnny-five": "^2.0.0", 8 | "oled-font-5x7": "latest", 9 | "oled-js": "latest", 10 | "raspi-io": "^11.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/oled/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {PORT = 3000} = process.env; 4 | 5 | const express = require('express'); 6 | const electroff = require('electroff'); 7 | 8 | const app = express(); 9 | app.use(electroff); 10 | app.use(express.static(`${__dirname}/public`)); 11 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 12 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const {readFileSync} = require('fs'); 2 | const {join} = require('path'); 3 | 4 | const electroff = require('../cjs'); 5 | 6 | const index = readFileSync(join(__dirname, 'index.html')); 7 | 8 | require('http') 9 | .createServer((request, response) => { 10 | if (electroff(request, response)) return; 11 | response.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 12 | response.end(index); 13 | }) 14 | .listen(8080, () => console.log('http://localhost:8080/')); 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |35 | Please 36 |
37 | 38 | 39 | `.trim(); 40 | return {error: e.message}; 41 | }); 42 | 43 | const json = any => stringify(any, (_, any) => { 44 | if (typeof any === 'function') 45 | return `(\xFF${any[secret] || any}\xFF)`; 46 | return any; 47 | }); 48 | 49 | (function poll() { 50 | if (NetworkOK) 51 | setTimeout( 52 | () => exec({UID, channel, code: 'true'}).then(poll), 53 | 60000 54 | ); 55 | }()); 56 | 57 | addEventListener('beforeunload', e => { 58 | navigator.sendBeacon('electroff', stringify({UID, channel, exit: true})); 59 | }); 60 | 61 | let calls = 0; 62 | let once = '{{once}}'; 63 | return function electroff(fn) { 64 | // avoid invoking CommonJS/electroff more than once 65 | // whenever the ELECTROFF_ONCE flag is passed 66 | if (once && calls++) 67 | throw new Error('Unhautorized script'); 68 | 69 | let instances = 0; 70 | let run = Promise.resolve(); 71 | 72 | const evaluate = code => (run = run.then(() => 73 | exec({UID, channel, code}).then(response => { 74 | const {result, error} = response; 75 | if (error) 76 | return Promise.reject(new Error(error)); 77 | if (result && result[channel]) { 78 | const global = result[channel]; 79 | return new Proxy(() => global, $handler); 80 | } 81 | return result; 82 | }) 83 | )); 84 | 85 | const handler = { 86 | apply(target, self, args) { 87 | return new Proxy( 88 | function () { 89 | return apply(target, self, args); 90 | }, 91 | handler 92 | ); 93 | }, 94 | construct(target, args) { 95 | const ref = `global['${UID}']['\x00'][${instances++}]`; 96 | const text = `(${ref}||(${ref}=new(${target()})(...${json(args)})))`; 97 | return new Proxy(() => text, handler); 98 | }, 99 | get(target, key) { 100 | switch (key) { 101 | case secret: 102 | case 'toJSON': 103 | return target(); 104 | case 'apply': 105 | return (self, args) => this.apply(target, self, args); 106 | case 'bind': 107 | return (self, ...args) => (...extras) => new Proxy( 108 | function () { 109 | return apply(target, self, args.concat(extras)); 110 | }, 111 | handler 112 | ); 113 | case 'call': 114 | return (self, ...args) => this.apply(target, self, args); 115 | case 'then': 116 | return fn => evaluate(target()).then(fn); 117 | } 118 | return new Proxy( 119 | function () { 120 | return `${target()}[${stringify(key)}]`; 121 | }, 122 | handler 123 | ); 124 | }, 125 | has(target, key) { 126 | throw new Error(`invalid "${key}" in ${target()} operation`); 127 | }, 128 | set(target, k, v) { 129 | evaluate(`${target()}[${stringify(k)}]=${json(v)}`); 130 | return true; 131 | } 132 | }; 133 | 134 | const $handler = { 135 | ...handler, 136 | get(target, key) { 137 | return key === 'then' ? void 0 : handler.get(target, key); 138 | } 139 | }; 140 | 141 | fn({ 142 | __dirname: '{{__dirname}}', 143 | global: new Proxy( 144 | () => `global['${UID}']`, 145 | handler 146 | ), 147 | remove: target => exec({UID, channel, code: `delete ${target[secret]}`}) 148 | .then(({result}) => result), 149 | require: module => new Proxy( 150 | function () { 151 | return `require(${stringify(module)})`; 152 | }, 153 | handler 154 | ), 155 | until: emitter => new Proxy( 156 | () => `(e=>((e.is||(e.is=t=>new Promise($=>{e.once(t,$)}))),e))(${ 157 | emitter[secret] 158 | })`, 159 | handler 160 | ) 161 | }); 162 | }; 163 | }(fetch)); 164 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | * PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | import crypto from 'crypto'; 19 | import {readFileSync} from 'fs'; 20 | import {basename, dirname, join} from 'path'; 21 | import {env} from 'process'; 22 | import vm from 'vm'; 23 | 24 | import {stringify} from 'flatted'; 25 | 26 | import umeta from 'umeta'; 27 | const {dirName, require: $require} = umeta(import.meta); 28 | 29 | const {isArray} = Array; 30 | const {create, getPrototypeOf} = Object; 31 | const {parse} = JSON; 32 | 33 | const cache = new Map; 34 | const rand = length => crypto.randomBytes(length).toString('hex'); 35 | 36 | const revive = /"\(\xFF([^\2]+?)(\xFF\)")/g; 37 | const callback = (_, $1) => parse(`"${$1}"`); 38 | 39 | const CHANNEL = rand(32); 40 | const EXPIRE = 300000; // expires in 5 minutes 41 | const X00 = '\x00'; 42 | const DEBUG = /^(?:1|y|yes|true)$/i.test(env.DEBUG); 43 | 44 | const ELECTROFF_ONCE = /^(?:1|y|yes|true)$/i.test(env.ELECTROFF_ONCE); 45 | 46 | const cleanup = () => { 47 | const now = Date.now(); 48 | const drop = []; 49 | cache.forEach((value, key) => { 50 | if (value < now) 51 | drop.push(key); 52 | }); 53 | const {length} = drop; 54 | if (0 < length) { 55 | if (DEBUG) 56 | console.log( 57 | `purged ${length} client${ 58 | length === 1 ? '' : 's' 59 | } - total ${cache.size}` 60 | ); 61 | drop.forEach(UID => { 62 | cache.delete(UID); 63 | delete sandbox.global[UID]; 64 | }); 65 | } 66 | }; 67 | 68 | const js = ''.replace.call( 69 | readFileSync(join(dirName, '..', 'client', 'index.js')), 70 | '{{channel}}', 71 | CHANNEL 72 | ).replace( 73 | '{{__dirname}}', 74 | dirName 75 | ).replace( 76 | /('|"|`)\{\{once\}\}\1/, 77 | ELECTROFF_ONCE 78 | ).replace( 79 | '"{{Flatted}}"', 80 | readFileSync( 81 | join(dirname($require.resolve('flatted')), '..', 'es.js') 82 | ).toString() 83 | ); 84 | 85 | const sandbox = vm.createContext({ 86 | global: create(null), 87 | require: $require, 88 | Buffer, 89 | console, 90 | setTimeout, 91 | setInterval, 92 | clearTimeout, 93 | clearInterval, 94 | setImmediate, 95 | clearImmediate 96 | }); 97 | 98 | const ok = (response, content) => { 99 | response.writeHead(200, { 100 | 'Content-Type': 'text/plain;charset=utf-8' 101 | }); 102 | response.end(stringify(content)); 103 | }; 104 | 105 | export default (request, response, next) => { 106 | const {method, url} = request; 107 | if (/^electroff(\?module)?$/.test(basename(url))) { 108 | const {$1: asModule} = RegExp; 109 | if (method === 'POST') { 110 | const data = []; 111 | request.on('data', chunk => { 112 | data.push(chunk); 113 | }); 114 | request.on('end', async () => { 115 | try { 116 | const {UID, channel, code, exit} = parse(data.join('')); 117 | if (channel === CHANNEL && UID) { 118 | cache.set(UID, Date.now() + EXPIRE); 119 | cleanup(); 120 | const exec = (code || '').replace(revive, callback); 121 | if (!(UID in sandbox.global)) { 122 | sandbox.global[UID] = {[X00]: create(null)}; 123 | if (DEBUG) 124 | console.log(`created 1 client - total ${cache.size}`); 125 | } 126 | if (exit) { 127 | cache.delete(UID); 128 | delete sandbox.global[UID]; 129 | ok(response, ''); 130 | if (DEBUG) 131 | console.log(`removed 1 client - total ${cache.size}`); 132 | return; 133 | } 134 | 135 | // YOLO 136 | vm.runInContext( 137 | `try{global['${X00}']={result:(${exec})}} 138 | catch({message}){global['${X00}']={error:message}}`, 139 | sandbox 140 | ); 141 | 142 | const {result, error} = sandbox.global[X00]; 143 | sandbox.global[X00] = null; 144 | if (error) { 145 | ok(response, {error}); 146 | if (DEBUG) 147 | console.error(`unable to evaluate: ${exec}`); 148 | } 149 | else { 150 | try { 151 | result.then( 152 | result => { 153 | if (result instanceof Buffer) 154 | result = result.toString('utf-8'); 155 | ok(response, {result}); 156 | }, 157 | e => { 158 | ok(response, {error: e.message}); 159 | if (DEBUG) 160 | console.error(`unable to resolve: ${exec}`); 161 | } 162 | ); 163 | } 164 | catch (e) { 165 | if (typeof result === 'object') { 166 | switch (!!result) { 167 | case isArray(result): 168 | case !getPrototypeOf(getPrototypeOf(result)): 169 | case result instanceof Date: 170 | break; 171 | default: 172 | const instances = sandbox.global[UID][X00]; 173 | for (const key in instances) { 174 | if (instances[key] === result) { 175 | ok(response, { 176 | result: { 177 | [CHANNEL]: `global['${UID}']['${X00}']['${key}']` 178 | } 179 | }); 180 | return; 181 | } 182 | } 183 | } 184 | } 185 | ok(response, {result}); 186 | } 187 | } 188 | } 189 | else { 190 | response.writeHead(403); 191 | response.end(); 192 | if (DEBUG) 193 | console.error( 194 | channel ? `unauthorized client` : `unauthorized request` 195 | ); 196 | } 197 | } catch (e) { 198 | response.writeHead(500); 199 | response.end(); 200 | if (DEBUG) 201 | console.error(`internal server error`, e); 202 | } 203 | }); 204 | } 205 | else { 206 | if (ELECTROFF_ONCE && !asModule) { 207 | response.writeHead(403); 208 | response.end(); 209 | if (DEBUG) 210 | console.error(`client asked for a global electroff`); 211 | } 212 | else { 213 | response.writeHead(200, { 214 | 'Cache-Control': 'no-store', 215 | 'Content-Type': 'application/javascript;charset=utf-8' 216 | }); 217 | response.end( 218 | js.replace('{{UID}}', rand(16)).concat( 219 | asModule ? 'export default electroff;' : '' 220 | ) 221 | ); 222 | } 223 | } 224 | return true; 225 | } 226 | try { return false; } 227 | finally { 228 | if (next) 229 | next(); 230 | } 231 | }; 232 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 15 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | const crypto = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('crypto')); 20 | const {readFileSync} = require('fs'); 21 | const {basename, dirname, join} = require('path'); 22 | const {env} = require('process'); 23 | const vm = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('vm')); 24 | 25 | const {stringify} = require('flatted'); 26 | 27 | const umeta = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umeta')); 28 | const {dirName, require: $require} = umeta(({url: require('url').pathToFileURL(__filename).href})); 29 | 30 | const {isArray} = Array; 31 | const {create, getPrototypeOf} = Object; 32 | const {parse} = JSON; 33 | 34 | const cache = new Map; 35 | const rand = length => crypto.randomBytes(length).toString('hex'); 36 | 37 | const revive = /"\(\xFF([^\2]+?)(\xFF\)")/g; 38 | const callback = (_, $1) => parse(`"${$1}"`); 39 | 40 | const CHANNEL = rand(32); 41 | const EXPIRE = 300000; // expires in 5 minutes 42 | const X00 = '\x00'; 43 | const DEBUG = /^(?:1|y|yes|true)$/i.test(env.DEBUG); 44 | 45 | const ELECTROFF_ONCE = /^(?:1|y|yes|true)$/i.test(env.ELECTROFF_ONCE); 46 | 47 | const cleanup = () => { 48 | const now = Date.now(); 49 | const drop = []; 50 | cache.forEach((value, key) => { 51 | if (value < now) 52 | drop.push(key); 53 | }); 54 | const {length} = drop; 55 | if (0 < length) { 56 | if (DEBUG) 57 | console.log( 58 | `purged ${length} client${ 59 | length === 1 ? '' : 's' 60 | } - total ${cache.size}` 61 | ); 62 | drop.forEach(UID => { 63 | cache.delete(UID); 64 | delete sandbox.global[UID]; 65 | }); 66 | } 67 | }; 68 | 69 | const js = ''.replace.call( 70 | readFileSync(join(dirName, '..', 'client', 'index.js')), 71 | '{{channel}}', 72 | CHANNEL 73 | ).replace( 74 | '{{__dirname}}', 75 | dirName 76 | ).replace( 77 | /('|"|`)\{\{once\}\}\1/, 78 | ELECTROFF_ONCE 79 | ).replace( 80 | '"{{Flatted}}"', 81 | readFileSync( 82 | join(dirname($require.resolve('flatted')), '..', 'es.js') 83 | ).toString() 84 | ); 85 | 86 | const sandbox = vm.createContext({ 87 | global: create(null), 88 | require: $require, 89 | Buffer, 90 | console, 91 | setTimeout, 92 | setInterval, 93 | clearTimeout, 94 | clearInterval, 95 | setImmediate, 96 | clearImmediate 97 | }); 98 | 99 | const ok = (response, content) => { 100 | response.writeHead(200, { 101 | 'Content-Type': 'text/plain;charset=utf-8' 102 | }); 103 | response.end(stringify(content)); 104 | }; 105 | 106 | module.exports = (request, response, next) => { 107 | const {method, url} = request; 108 | if (/^electroff(\?module)?$/.test(basename(url))) { 109 | const {$1: asModule} = RegExp; 110 | if (method === 'POST') { 111 | const data = []; 112 | request.on('data', chunk => { 113 | data.push(chunk); 114 | }); 115 | request.on('end', async () => { 116 | try { 117 | const {UID, channel, code, exit} = parse(data.join('')); 118 | if (channel === CHANNEL && UID) { 119 | cache.set(UID, Date.now() + EXPIRE); 120 | cleanup(); 121 | const exec = (code || '').replace(revive, callback); 122 | if (!(UID in sandbox.global)) { 123 | sandbox.global[UID] = {[X00]: create(null)}; 124 | if (DEBUG) 125 | console.log(`created 1 client - total ${cache.size}`); 126 | } 127 | if (exit) { 128 | cache.delete(UID); 129 | delete sandbox.global[UID]; 130 | ok(response, ''); 131 | if (DEBUG) 132 | console.log(`removed 1 client - total ${cache.size}`); 133 | return; 134 | } 135 | 136 | // YOLO 137 | vm.runInContext( 138 | `try{global['${X00}']={result:(${exec})}} 139 | catch({message}){global['${X00}']={error:message}}`, 140 | sandbox 141 | ); 142 | 143 | const {result, error} = sandbox.global[X00]; 144 | sandbox.global[X00] = null; 145 | if (error) { 146 | ok(response, {error}); 147 | if (DEBUG) 148 | console.error(`unable to evaluate: ${exec}`); 149 | } 150 | else { 151 | try { 152 | result.then( 153 | result => { 154 | if (result instanceof Buffer) 155 | result = result.toString('utf-8'); 156 | ok(response, {result}); 157 | }, 158 | e => { 159 | ok(response, {error: e.message}); 160 | if (DEBUG) 161 | console.error(`unable to resolve: ${exec}`); 162 | } 163 | ); 164 | } 165 | catch (e) { 166 | if (typeof result === 'object') { 167 | switch (!!result) { 168 | case isArray(result): 169 | case !getPrototypeOf(getPrototypeOf(result)): 170 | case result instanceof Date: 171 | break; 172 | default: 173 | const instances = sandbox.global[UID][X00]; 174 | for (const key in instances) { 175 | if (instances[key] === result) { 176 | ok(response, { 177 | result: { 178 | [CHANNEL]: `global['${UID}']['${X00}']['${key}']` 179 | } 180 | }); 181 | return; 182 | } 183 | } 184 | } 185 | } 186 | ok(response, {result}); 187 | } 188 | } 189 | } 190 | else { 191 | response.writeHead(403); 192 | response.end(); 193 | if (DEBUG) 194 | console.error( 195 | channel ? `unauthorized client` : `unauthorized request` 196 | ); 197 | } 198 | } catch (e) { 199 | response.writeHead(500); 200 | response.end(); 201 | if (DEBUG) 202 | console.error(`internal server error`, e); 203 | } 204 | }); 205 | } 206 | else { 207 | if (ELECTROFF_ONCE && !asModule) { 208 | response.writeHead(403); 209 | response.end(); 210 | if (DEBUG) 211 | console.error(`client asked for a global electroff`); 212 | } 213 | else { 214 | response.writeHead(200, { 215 | 'Cache-Control': 'no-store', 216 | 'Content-Type': 'application/javascript;charset=utf-8' 217 | }); 218 | response.end( 219 | js.replace('{{UID}}', rand(16)).concat( 220 | asModule ? 'export default electroff;' : '' 221 | ) 222 | ); 223 | } 224 | } 225 | return true; 226 | } 227 | try { return false; } 228 | finally { 229 | if (next) 230 | next(); 231 | } 232 | }; 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electroff 2 | 3 |  4 | 5 | A cross browser, electron-less helper, for **IoT** projects and **standalone** applications. 6 | 7 | With this module, you can run arbitrary _Node.js_ code from the client, from any browser, and *without* needing [electron](https://www.electronjs.org/). 8 | 9 | 10 | ## 📣 Announcement 11 | 12 | electroff can be fully replaced by [coincident/server](https://github.com/WebReflection/coincident-oled#readme)! 13 | 14 | - - - 15 | 16 | Looking for a lighter, faster, much safer, yet slightly more limited alternative? Try **[proxied-node](https://github.com/WebReflection/proxied-node#readme)** out instead. 17 | 18 | 19 | ### Community 20 | 21 | Please ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥ 22 | 23 | --- 24 | 25 | ## Getting Started 26 | 27 | Considering the following `app.js` file content: 28 | 29 | ```js 30 | const {PORT = 3000} = process.env; 31 | 32 | const express = require('express'); 33 | const electroff = require('electroff'); 34 | 35 | const app = express(); 36 | app.use(electroff); 37 | app.use(express.static(`${__dirname}/public`)); 38 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 39 | ``` 40 | 41 | The `public/index.html` folder can contain something like this: 42 | 43 | ```html 44 | 45 | 46 | 47 | 48 | 49 |