Skip to content

Instantly share code, notes, and snippets.

@GuilhermeRossato
Last active April 29, 2024 20:36
Show Gist options
  • Save GuilhermeRossato/6c7fd0608cd0ded7670a5dfa7e227c51 to your computer and use it in GitHub Desktop.
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 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