Update SpaccDotWebServer to add all currently needed features, update example

This commit is contained in:
2024-02-24 20:26:34 +01:00
parent 618842883d
commit 63dc2e648f
5 changed files with 295 additions and 263 deletions

View File

@ -1,16 +1,16 @@
<!DOCTYPE html><html><head><!-- <!DOCTYPE html><html><head><!--
--><meta charset="utf-8"/><!-- --><meta charset="utf-8"/><!--
--><meta name="viewport" content="width=device-width, initial-scale=1.0"/><!-- --><meta name="viewport" content="width=device-width, initial-scale=1.0"/><!--
--><title>Example</title><!--
--><link rel="stylesheet" href="/static/Example.css"/><!-- --><link rel="stylesheet" href="/static/Example.css"/><!--
--></head><body><!-- --></head><body><!--
--><div id="transition"></div><!-- --><div id="transition"></div><!--
--><div id="app"> --><div id="app">
<script>
window.process = null;
window.require = () => window.SpaccDotWebServer;
window.resFilesData = { "Example.css": "data:text/css;base64,Ym9keSB7CgliYWNrZ3JvdW5kLWNvbG9yOiBsaWdodGdyYXk7Cgljb2xvcjogYmxhY2s7Cn0KCmgyIHsKCXBvc2l0aW9uOiByZWxhdGl2ZTsKCXRvcDogM2VtOwoJcm90YXRlOiAxNWRlZzsKCWNvbG9yOiBwdXJwbGU7Cn0K" };
</script>
<script src="./SpaccDotWeb.Server.js"></script> <script src="./SpaccDotWeb.Server.js"></script>
<script>
window.require = () => window.SpaccDotWebServer;
window.SpaccDotWebServer.resFilesData = { "Example.css": "data:text/css;base64,Ym9keSB7CgliYWNrZ3JvdW5kLWNvbG9yOiBsaWdodGdyYXk7Cgljb2xvcjogYmxhY2s7Cn0KCmgyIHsKCXdpZHRoOiBtYXgtY29udGVudDsKCXJvdGF0ZTogMTVkZWc7Cgljb2xvcjogRGVlcFBpbms7Cn0K" };
</script>
<script src="./Example.Server.js"></script> <script src="./Example.Server.js"></script>
</div><!-- </div><!--
--></body></html> --></body></html>

View File

@ -1,16 +1,20 @@
const SpaccDotWebServer = require('./SpaccDotWeb.Server.js')({ const SpaccDotWebServer = require('./SpaccDotWeb.Server.js');
const server = SpaccDotWebServer.setup({
appName: 'Example', appName: 'Example',
// staticPrefix: '/static/', // staticPrefix: '/static/',
// staticFiles: [], // staticFiles: [],
linkStyles: [ 'Example.css' ], linkStyles: [ 'Example.css' ],
// linkScripts: [], // linkScripts: [],
// htmlPager: htmlPager(content) => `...`, // pageTitler: pageTitler(title) => `...`,
// appPager: appPager(content, title) => `...`,
// htmlPager: htmlPager(content, title) => `...`,
}); });
if (process && process.argv[2] === 'html') { if (SpaccDotWebServer.envIsNode && process.argv[2] === 'html') {
SpaccDotWebServer.writeStaticHtml(__filename); server.writeStaticHtml();
console.log('Dumped Static HTML!');
} else { } else {
SpaccDotWebServer.initServer({ server.initServer({
// defaultResponse: { code: 500, headers: {} }, // defaultResponse: { code: 500, headers: {} },
// endpointsFalltrough: false, // endpointsFalltrough: false,
// port: 3000, // port: 3000,
@ -19,7 +23,7 @@ if (process && process.argv[2] === 'html') {
// appElement: 'div#app', // appElement: 'div#app',
// transitionElement: 'div#transition', // transitionElement: 'div#transition',
endpoints: [ endpoints: [
[ (ctx) => (['GET', 'POST'].includes(ctx.request.method) && ctx.request.url.toLowerCase().startsWith('/main/')), (ctx) => { [ (ctx) => (['GET', 'POST'].includes(ctx.request.method) && ctx.urlSections[0] === 'main'), (ctx) => {
if (ctx.request.method === 'POST') { if (ctx.request.method === 'POST') {
if (ctx.bodyParameters?.add) { if (ctx.bodyParameters?.add) {
ctx.setCookie(`count=${parseInt(ctx.getCookie('count') || 0) + 1}`); ctx.setCookie(`count=${parseInt(ctx.getCookie('count') || 0) + 1}`);
@ -32,13 +36,13 @@ if (process && process.argv[2] === 'html') {
${ctx.request.method === 'POST' ? `<p>POST body parameters:</p><pre>${JSON.stringify(ctx.bodyParameters)}</pre>` : ''} ${ctx.request.method === 'POST' ? `<p>POST body parameters:</p><pre>${JSON.stringify(ctx.bodyParameters)}</pre>` : ''}
<p>This page was rendered at ${Date()}.</p> <p>This page was rendered at ${Date()}.</p>
<p>These were your cookies at time of request:</p> <p>These were your cookies at time of request:</p>
<pre>${ctx.getCookie()}</pre> <pre>${ctx.getCookie() || '[None]'}</pre>
<form method="POST"> <form method="POST">
<input type="submit" name="add" value="Add 1 to cookie"/> <input type="submit" name="add" value="Add 1 to cookie"/>
<input type="submit" name="reset" value="Reset cookies"/> <input type="submit" name="reset" value="Reset cookies"/>
</form> </form>
`; `;
ctx.renderPage(content); ctx.renderPage(content, 'Test');
// return { code: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: content } // return { code: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: content }
} ], } ],
@ -46,4 +50,5 @@ if (process && process.argv[2] === 'html') {
// [ (ctx) => (ctx.request.method === 'GET'), (ctx) => ({ code: 302, headers: { location: '/main/' } }) ], // [ (ctx) => (ctx.request.method === 'GET'), (ctx) => ({ code: 302, headers: { location: '/main/' } }) ],
], ],
}); });
console.log('Running Server...');
}; };

View File

@ -4,8 +4,7 @@ body {
} }
h2 { h2 {
position: relative; width: max-content;
top: 3em;
rotate: 15deg; rotate: 15deg;
color: purple; color: DeepPink;
} }

View File

@ -1,10 +1,15 @@
/* TODO:
* built-in logging
* other things listed in the file
*/
(() => { (() => {
const envIsNode = (typeof module === 'object' && typeof module.exports === 'object'); const envIsNode = (typeof module === 'object' && typeof module.exports === 'object');
const envIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined'); const envIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined');
let fs, path, mime, multipart;
const allOpts = {}; const allOpts = {};
let fs, path, mime, multipart;
const main = (globalOptions={}) => { const setup = (globalOptions={}) => {
allOpts.global = globalOptions; allOpts.global = globalOptions;
allOpts.global.staticPrefix ||= '/static/'; allOpts.global.staticPrefix ||= '/static/';
allOpts.global.staticFiles ||= []; allOpts.global.staticFiles ||= [];
@ -16,20 +21,23 @@
allOpts.global.staticFiles.push(item); allOpts.global.staticFiles.push(item);
}; };
}; };
allOpts.global.htmlPager ||= (content) => `<!DOCTYPE html><html><head><!-- allOpts.global.pageTitler ||= (title) => `${title || ''}${title && allOpts.global.appName ? ' — ' : ''}${allOpts.global.appName || ''}`,
allOpts.global.appPager ||= (content, title) => content,
allOpts.global.htmlPager ||= (content, title) => `<!DOCTYPE html><html><head><!--
--><meta charset="utf-8"/><!-- --><meta charset="utf-8"/><!--
--><meta name="viewport" content="width=device-width, initial-scale=1.0"/><!-- --><meta name="viewport" content="width=device-width, initial-scale=1.0"/><!--
--><title>${allOpts.global.pageTitler(title)}</title><!--
-->${allOpts.global.linkStyles.map((item) => { -->${allOpts.global.linkStyles.map((item) => {
return `<link rel="stylesheet" href="${allOpts.global.staticFiles.includes(item) ? (allOpts.global.staticPrefix + item) : item}"/>`; return `<link rel="stylesheet" href="${allOpts.global.staticFiles.includes(item) ? (allOpts.global.staticPrefix + item) : item}"/>`;
}).join('')}<!-- }).join('')}<!--
--></head><body><!-- --></head><body><!--
--><div id="transition"></div><!-- --><div id="transition"></div><!--
--><div id="app">${content}</div><!-- --><div id="app">${allOpts.global.appPager(content, title)}</div><!--
--></body></html>`; --></body></html>`;
const result = {}; const result = {};
result.initServer = (serverOptions={}) => initServer(serverOptions); result.initServer = (serverOptions={}) => initServer(serverOptions);
if (envIsNode) { if (envIsNode) {
result.writeStaticHtml = (mainScriptPath) => writeStaticHtml(mainScriptPath); result.writeStaticHtml = writeStaticHtml;
}; };
return result; return result;
}; };
@ -56,18 +64,19 @@
}; };
}; };
const writeStaticHtml = (mainScriptPath) => { const writeStaticHtml = () => {
fs.writeFileSync((mainScriptPath.split('.').slice(0, -1).join('.') + '.html'), allOpts.global.htmlPager(` // TODO: fix script paths
// TODO: this should somehow set envIsBrowser to true to maybe allow for correct template rendering, but how to do it without causing race conditions? maybe we should expose another variable
fs.writeFileSync((process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html'), allOpts.global.htmlPager(`
<script src="./${path.basename(__filename)}"></script>
<script> <script>
window.process = null;
window.require = () => window.SpaccDotWebServer; window.require = () => window.SpaccDotWebServer;
window.resFilesData = { ${allOpts.global.staticFiles.map((file) => { window.SpaccDotWebServer.resFilesData = { ${allOpts.global.staticFiles.map((file) => {
const filePath = (mainScriptPath.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file); const filePath = (process.mainModule.filename.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file);
return `"${file}": "data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`; return `"${file}": "data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`;
})} }; })} };
</script> </script>
<script src="./${path.basename(__filename)}"></script> <script src="./${path.basename(process.mainModule.filename)}"></script>
<script src="./${path.basename(mainScriptPath)}"></script>
`)); `));
}; };
@ -77,17 +86,22 @@
const context = { const context = {
request, request,
response, response,
urlSections: request.url.slice(1).toLowerCase().split('?')[0].split('/'),
urlParameters: (new URLSearchParams(request.url.split('?')[1]?.join('?'))), urlParameters: (new URLSearchParams(request.url.split('?')[1]?.join('?'))),
bodyParameters: (request.method === 'POST' && await parseBodyParams(request)), // TODO which other methods need body? bodyParameters: (request.method === 'POST' && await parseBodyParams(request)), // TODO which other methods need body?
getCookie: (cookie) => getCookie(request, cookie), getCookie: (cookie) => getCookie(request, cookie),
setCookie: (cookie) => setCookie(response, cookie), setCookie: (cookie) => setCookie(response, cookie),
renderPage: (content) => renderPage(response, content), renderPage: (content, title) => renderPage(response, content, title),
redirectTo: (url) => redirectTo(response, url), redirectTo: (url) => redirectTo(response, url),
}; };
// client transitions
if (envIsBrowser && document.querySelector(allOpts.server.transitionElement)) {
document.querySelector(allOpts.server.transitionElement).style.display = 'block';
};
// serve static files // serve static files
if (envIsNode && request.method === 'GET' && request.url.toLowerCase().startsWith(allOpts.global.staticPrefix)) { if (envIsNode && request.method === 'GET' && request.url.toLowerCase().startsWith(allOpts.global.staticPrefix)) {
const resPath = request.url.split(allOpts.global.staticPrefix).slice(1).join(allOpts.global.staticPrefix); const resPath = request.url.split(allOpts.global.staticPrefix).slice(1).join(allOpts.global.staticPrefix);
const filePath = (__dirname + path.sep + resPath); // TODO i think we need to read this another way if the module is in a different directory from the importing program const filePath = (process.mainModule.path + path.sep + resPath); // TODO i think we need to read this another way if the module is in a different directory from the importing program
if (allOpts.global.staticFiles.includes(resPath) && fs.existsSync(filePath)) { if (allOpts.global.staticFiles.includes(resPath) && fs.existsSync(filePath)) {
result = { code: 200, headers: { 'content-type': mime.lookup(filePath) }, body: fs.readFileSync(filePath) }; result = { code: 200, headers: { 'content-type': mime.lookup(filePath) }, body: fs.readFileSync(filePath) };
} else { } else {
@ -96,7 +110,7 @@
} else { } else {
// handle custom endpoints // handle custom endpoints
for (const [check, procedure] of allOpts.server.endpoints) { for (const [check, procedure] of allOpts.server.endpoints) {
if (check(context)) { if (await check(context)) {
result = await procedure(context); result = await procedure(context);
if (!allOpts.server.endpointsFalltrough) { if (!allOpts.server.endpointsFalltrough) {
break; break;
@ -114,20 +128,21 @@
}; };
}; };
const renderPage = (response, content) => { const renderPage = (response, content, title) => {
// TODO titles and things // TODO titles and things
// TODO status code could need to be different in different situations and so should be set accordingly? // TODO status code could need to be different in different situations and so should be set accordingly?
if (envIsNode) { if (envIsNode) {
response.setHeader('content-type', 'text/html; charset=utf-8'); response.setHeader('content-type', 'text/html; charset=utf-8');
response.end(allOpts.global.htmlPager(content)); response.end(allOpts.global.htmlPager(content, title));
}; };
if (envIsBrowser) { if (envIsBrowser) {
document.querySelector(allOpts.server.appElement).innerHTML = content; document.title = allOpts.global.pageTitler(title);
document.querySelector(allOpts.server.appElement).innerHTML = allOpts.global.appPager(content, title);
for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) { for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) {
srcElem.src = resFilesData[srcElem.getAttribute('src')]; srcElem.src = window.SpaccDotWebServer.resFilesData[srcElem.getAttribute('src')];
}; };
for (const linkElem of document.querySelectorAll(`link[rel="stylesheet"][href^="${allOpts.global.staticPrefix}"]`)) { for (const linkElem of document.querySelectorAll(`link[rel="stylesheet"][href^="${allOpts.global.staticPrefix}"]`)) {
linkElem.href = resFilesData[linkElem.getAttribute('href').slice(allOpts.global.staticPrefix.length)]; linkElem.href = window.SpaccDotWebServer.resFilesData[linkElem.getAttribute('href').slice(allOpts.global.staticPrefix.length)];
}; };
for (const aElem of document.querySelectorAll('a[href^="/"]')) { for (const aElem of document.querySelectorAll('a[href^="/"]')) {
aElem.href = `#${aElem.getAttribute('href')}`; aElem.href = `#${aElem.getAttribute('href')}`;
@ -145,6 +160,9 @@
}); });
}; };
}; };
if (document.querySelector(allOpts.server.transitionElement)) {
document.querySelector(allOpts.server.transitionElement).style.display = 'none';
};
}; };
}; };
@ -198,7 +216,7 @@
const getCookie = (request, name) => { const getCookie = (request, name) => {
let cookies; let cookies;
if (envIsNode) { if (envIsNode) {
cookies = request.headers?.cookie; cookies = (request.headers?.cookie || '');
}; };
if (envIsBrowser) { if (envIsBrowser) {
cookies = clientCookieApi(); cookies = clientCookieApi();
@ -230,10 +248,11 @@
// try to use the built-in cookie API, fallback to a Storage-based wrapper in case it fails (for example on file:///) // try to use the built-in cookie API, fallback to a Storage-based wrapper in case it fails (for example on file:///)
const clientCookieApi = (envIsBrowser && (document.cookie || (!document.cookie && (document.cookie = '_=_') && document.cookie) ? (set) => (set ? (document.cookie = set) : document.cookie) : (set) => { const clientCookieApi = (envIsBrowser && (document.cookie || (!document.cookie && (document.cookie = '_=_') && document.cookie) ? (set) => (set ? (document.cookie = set) : document.cookie) : (set) => {
const gid = allOpts.global.appName; // TODO: introduce a conf field that is specifically a GID for less potential problems? const gid = allOpts.global.appName; // TODO: introduce a conf field that is specifically a GID for less potential problems?
// also, TODO: what to do when no app name or any id is set?
if (set) { if (set) {
let api = sessionStorage; let api = sessionStorage;
const tokens = set.split(';'); const tokens = set.split(';');
const [key, ...rest] = tokens[0].split('='); let [key, ...rest] = tokens[0].split('=');
for (let token of tokens) { for (let token of tokens) {
token = token.trim(); token = token.trim();
if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) { if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) {
@ -241,7 +260,14 @@
break; break;
}; };
}; };
api.setItem(`${gid}/${key}`, rest.join('=')); key = `${gid}/${key}`;
const value = rest.join('=');
if (value) {
api.setItem(key, value);
} else {
sessionStorage.removeItem(key);
localStorage.removeItem(key);
};
} else /*(get)*/ { } else /*(get)*/ {
let items = ''; let items = '';
for (const item of Object.entries({ ...localStorage, ...sessionStorage })) { for (const item of Object.entries({ ...localStorage, ...sessionStorage })) {
@ -253,14 +279,16 @@
}; };
})); }));
const exportObj = { envIsNode, envIsBrowser, setup };
if (envIsNode) { if (envIsNode) {
fs = require('fs'); fs = require('fs');
path = require('path'); path = require('path');
mime = require('mime-types'); mime = require('mime-types');
multipart = require('parse-multipart-data'); multipart = require('parse-multipart-data');
module.exports = main; module.exports = exportObj;
}; };
if (envIsBrowser) { if (envIsBrowser) {
window.SpaccDotWebServer = main; window.SpaccDotWebServer = exportObj;
}; };
})(); })();

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "SpaccDotWeb", "name": "SpaccDotWeb",
"version": "indev", "version": "0.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "SpaccDotWeb", "name": "SpaccDotWeb",
"version": "indev", "version": "0.2.1",
"dependencies": { "dependencies": {
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"parse-multipart-data": "^1.5.0" "parse-multipart-data": "^1.5.0"