Last active
April 29, 2024 20:36
-
-
Save GuilhermeRossato/6c7fd0608cd0ded7670a5dfa7e227c51 to your computer and use it in GitHub Desktop.
Single File Framework - An web app combined in a single file for fast prototyping of projects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This is the single file framework. | |
* As a backend script executed by Node.js it creates a http server that send itself to clients. | |
* As a frontend script it executes a startup function on the browser. | |
* | |
* You execute it on the terminal by calling `node index.js`, it will attempt to start your browser at the target url. | |
* The entry point is an empty HTML file that loads this script as the frontend script, and reloads the page when it updates. | |
* | |
* No credit is necessary when using this. | |
* | |
* v1.0 | |
*/ | |
const isBackendNodejsScript = typeof module === 'object'; | |
/** | |
* Function that gets executed once the HTML has finished loading | |
*/ | |
async function onPageLoad(data) { | |
document.body.style.backgroundColor = "rgb(126, 126, 126)"; | |
} | |
/** | |
* Frontend function to load server data.json by issuing a request to the backend | |
*/ | |
async function getData() { | |
const response = await fetch('/data.json') | |
const text = await response.text(); | |
return text ? JSON.parse(text) : null; | |
} | |
/** | |
* Frontend function to save data on data.json file by exchanging it with | |
*/ | |
async function setData(data) { | |
const response = await fetch('/data.json', {method: 'POST', body: JSON.stringify(data)}); | |
if (response.status !== 200) { | |
throw new Error(`Unexpected status response: ${response.status}`); | |
} | |
} | |
/** | |
* Part of the frontend | |
*/ | |
if (!isBackendNodejsScript) { | |
document.addEventListener('DOMContentLoaded', () => { | |
onPageLoad(JSON.parse('{{ data }}')).catch(console.error); | |
let accTime = 0; | |
let lastTime = null; | |
setInterval(() => { | |
const now = new Date(); | |
if (lastTime) { | |
accTime += now.getTime() - lastTime.getTime(); | |
} | |
if (accTime > 20_000) { | |
fetch('/keep-alive/'); | |
accTime -= 20_000; | |
} | |
lastTime = now; | |
}, 1000); | |
fetch('/wait-for-file-change/?id=' + '{{ file-change-date }}').then(r => { | |
window.location.reload(); | |
}).catch(() => {}); | |
}); | |
} | |
/** | |
* Part of the backend | |
*/ | |
if (isBackendNodejsScript) { | |
function getLocaleDateTimeString(date = new Date()) { | |
const hourOffset = new Date().getTimezoneOffset() / 60; | |
return new Date(date.getTime() - hourOffset * 60 * 60 * 1000).toISOString().replace('T', ' ').substring(0, 19); | |
} | |
const http = require('http'); | |
const fs = require('fs'); | |
let deathTimer; | |
const deathTimerInterval = 60000; | |
function deathTimerFunc() { | |
console.log(`[${getLocaleDateTimeString()}] Program will close because of lack of client`); | |
process.exit(0); | |
} | |
if (!fs.existsSync('./data.json')) { | |
fs.writeFileSync('./data.json', 'null', 'utf-8'); | |
} | |
deathTimer = setTimeout(deathTimerFunc, deathTimerInterval); | |
const server = http.createServer(function (req, res) { | |
const isPing = req.method === 'GET' && req.url === '/keep-alive/'; | |
if (!isPing) { | |
console.log(`[${getLocaleDateTimeString()}] ${req.method} ${req.url}`); | |
} | |
try { | |
if (deathTimer) { | |
clearTimeout(deathTimer); | |
deathTimer = setTimeout(deathTimerFunc, deathTimerInterval); | |
} | |
if (isPing) { | |
res.end(); | |
return; | |
} | |
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) { | |
res.setHeader('Content-type', 'text/html;charset=utf-8') | |
res.end( | |
`<!DOCTYPE html><html> | |
<head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"></head> | |
<body><script src="client.js" type="module"></script></body> | |
</html>` | |
); | |
return; | |
} | |
if (req.method === 'GET' && req.url === '/client.js') { | |
res.setHeader('Content-type', 'text/javascript;charset=utf-8'); | |
const scriptContent = fs.readFileSync('./index.js', 'utf-8'); | |
const scriptMtime = fs.statSync('./index.js').mtimeMs; | |
res.end( | |
scriptContent | |
.replace('\'{{ data }}\'', `'${fs.readFileSync('./data.json', 'utf-8')}'`) | |
.replace('\'{{ file-change-date }}\'', `'${Math.floor(scriptMtime)}'`) | |
); | |
return; | |
} | |
if (req.url === '/data.json') { | |
if (req.method === 'GET') { | |
res.setHeader('Content-type', 'application/json;charset=utf-8'); | |
res.end(fs.readFileSync('./data.json', 'utf-8')); | |
} else if (req.method === 'POST') { | |
const buffer = []; | |
req.on('data', data => buffer.push(data)); | |
req.on('end', () => { | |
fs.writeFileSync('./data.json', Buffer.concat(buffer)); | |
res.end(); | |
}); | |
} else { | |
res.end('Unknown method'); | |
} | |
return; | |
} | |
if (req.url.startsWith('/wait-for-file-change/?id=') && req.method === 'GET') { | |
const id = parseInt(req.url.substring(req.url.indexOf('?id=') + 4).trim()); | |
const scriptMtime = Math.floor(fs.statSync('./index.js').mtimeMs); | |
if (scriptMtime !== id) { | |
setTimeout(() => { | |
res.end(); | |
}, 1000); | |
return; | |
} | |
let interval; | |
function checkUpdate() { | |
const scriptMtime = Math.floor(fs.statSync('./index.js').mtimeMs); | |
if (scriptMtime !== id) { | |
if (interval) { | |
clearInterval(interval); | |
} | |
res.end(); | |
} | |
} | |
interval = setInterval(checkUpdate, 100); | |
return; | |
} | |
res.setHeader('Content-type', 'text/plain;charset=utf-8'); | |
res.end(`Error 404: ${req.method} ${req.url}`); | |
} catch (err) { | |
console.error(err); | |
res.writeHead(500).end(); | |
} | |
}); | |
server.listen(() => { | |
const addr = server.address(); | |
let host = addr.address === '::' ? 'localhost' : addr.address; | |
const port = addr.port; | |
console.log(`[${getLocaleDateTimeString()}] Started listening at http://${host}:${port}`); | |
try { | |
require('child_process').spawn('start', [`http://${host}:${port}/`], {shell: true}); | |
} catch (err) { | |
// Ignore open error | |
} | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment