[Server] Handle linkClientScripts, polish static HTML gen with embedded files

This commit is contained in:
octospacc 2024-07-14 21:16:59 +02:00
parent b47baf408b
commit b315fdce00
5 changed files with 87 additions and 35 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
*.tmp *.tmp
/Build/* /Build/*
/node_modules/* /node_modules/*
/Example.Server.html /Example.Server/index.html

View File

@ -1,18 +1,20 @@
#!/usr/bin/env node #!/usr/bin/env node
const SpaccDotWebServer = require('./SpaccDotWeb.Server.js'); const SpaccDotWebServer = require('../SpaccDotWeb.Server.js');
const server = SpaccDotWebServer.setup({ const server = SpaccDotWebServer.setup({
appName: 'Example', appName: 'Example',
// staticPrefix: '/static/', // staticPrefix: '/static/',
// staticRoot: '', // not (yet) implemented
// staticFiles: [], // staticFiles: [],
linkStyles: [ 'Example.css' ], linkStyles: [ 'index.css' ],
// linkScripts: [], // linkRuntimeScripts: [], // not (yet) implemented
linkClientScripts: [ 'particles.js' ],
// pageTitler: (title, opts={}) => `...`, // pageTitler: (title, opts={}) => `...`,
// appPager: (content, title, opts={}) => `...`, // appPager: (content, title, opts={}) => `...`,
// htmlPager: (content, title, opts={}) => `...`, // htmlPager: (content, title, opts={}) => `...`,
}); });
if (SpaccDotWebServer.envIsNode && ['dump', 'html'].includes(process.argv[2])) { if (SpaccDotWebServer.envIsNode && ['dump', 'html', 'writeStaticHtml'].includes(process.argv[2])) {
const fileName = server.writeStaticHtml(); const fileName = server.writeStaticHtml(Number(process.argv[3] || 0));
console.log(`Dumped Static HTML to '${fileName}'!`); console.log(`Dumped Static HTML to '${fileName}'!`);
} else { } else {
const serverData = server.initServer({ const serverData = server.initServer({

View File

@ -0,0 +1,17 @@
window.addEventListener('load', function(){
for (var i=0; i<(window.innerWidth * window.innerHeight / 6000); i++) (function(){
var v = (Math.random() * window.innerHeight), h = (100 * Math.random());
var n = document.createElement('span');
n.textContent = '✨️';
n.style.position = 'absolute';
document.body.appendChild(n);
var e = setInterval(function(){
var r = Math.random();
n.style.top = (v += 1).toString() + 'px';
n.style.left = (h += (r > 0.7 ? r/5 : -r/5)).toString() + '%';
if (v > window.innerHeight) v = -16;
if (h > 100) h = -2;
else if (h < -2) h = 100;
}, 20);
})();
});

View File

@ -1,12 +1,10 @@
/* TODO: /* TODO:
* built-in logging * built-in logging
* configure to embed linked styles and scripts into the HTML output, or just link to file * relative URL root for redirects and internal functions? (or useless since HTML must be custom anyways?)
* handle linkScripts to insert in the HTML, like linkStyles
* differentiate between client-only, served-only, and both-mode scripts?
* relative URL root
* utility functions for rewriting url query parameters? * utility functions for rewriting url query parameters?
* fix hash navigation to prevent no-going-back issue (possible?) * fix hash navigation to prevent no-going-back issue (possible?)
* finish polishing the cookie API * finish polishing the cookie API
* implement some nodejs `fs` functions for client-side
* other things listed in the file * other things listed in the file
*/ */
(() => { (() => {
@ -24,11 +22,14 @@ const cookieMaxAge2Years = (2 * 365 * 24 * 60 * 60);
const setup = (globalOptions={}) => { const setup = (globalOptions={}) => {
allOpts.global = globalOptions; allOpts.global = globalOptions;
//allOpts.global.appName ||= 'Untitled SpaccDotWeb App'; //allOpts.global.appName ||= 'Untitled SpaccDotWeb App';
//allOpts.global.appRoot ||= ''; //TODO
allOpts.global.staticPrefix ||= '/static/'; allOpts.global.staticPrefix ||= '/static/';
//allOpts.global.staticRoot ||= ''; //TODO
allOpts.global.staticFiles ||= []; allOpts.global.staticFiles ||= [];
allOpts.global.linkStyles ||= []; allOpts.global.linkStyles ||= [];
allOpts.global.linkScripts ||= []; //allOpts.global.linkRuntimeScripts ||= []; //TODO
for (const item of [...allOpts.global.linkStyles, ...allOpts.global.linkScripts]) { allOpts.global.linkClientScripts ||= [];
for (const item of [ ...allOpts.global.linkStyles, ...allOpts.global.linkClientScripts ]) {
const itemLow = item.toLowerCase(); const itemLow = item.toLowerCase();
if (!(itemLow.startsWith('http://') || itemLow.startsWith('https://') || itemLow.startsWith('/'))) { if (!(itemLow.startsWith('http://') || itemLow.startsWith('https://') || itemLow.startsWith('/'))) {
allOpts.global.staticFiles.push(item); allOpts.global.staticFiles.push(item);
@ -40,9 +41,8 @@ const setup = (globalOptions={}) => {
--><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>${(opts.pageTitler || allOpts.global.pageTitler)(title, opts)}</title><!-- --><title>${(opts.pageTitler || allOpts.global.pageTitler)(title, opts)}</title><!--
-->${allOpts.global.linkStyles.map((item) => { -->${allOpts.global.linkStyles.map((path) => makeHtmlStyleFragment(path, opts.selfContained)).join('')}<!--
return `<link rel="stylesheet" href="${allOpts.global.staticFiles.includes(item) ? (allOpts.global.staticPrefix + item) : item}"/>`; -->${allOpts.global.linkClientScripts.map((path) => makeHtmlScriptFragment(path, opts.selfContained)).join('')}<!--
}).join('')}<!--
--></head><body><!-- --></head><body><!--
--><div id="transition"></div><!-- --><div id="transition"></div><!--
--><div id="app">${(opts.appPager || allOpts.global.appPager)(content, title, opts)}</div><!-- --><div id="app">${(opts.appPager || allOpts.global.appPager)(content, title, opts)}</div><!--
@ -80,30 +80,50 @@ const initServer = (serverOptions) => {
return allOpts.server; return allOpts.server;
}; };
const writeStaticHtml = () => { const writeStaticHtml = (selfContained=false) => {
// TODO: fix script paths // TODO fix selfContained to embed everything when true, even the runtime files
// 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 // 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
const fileName = (process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html'); const appFilePath = process.mainModule.filename;
fs.writeFileSync(fileName, allOpts.global.htmlPager(` const htmlFilePath = (appFilePath.split('.').slice(0, -1).join('.') + '.html');
<script src="./${path.basename(__filename)}"></script> // path.relative seems to always append an extra '../', so we must slice it
<script src="./SpaccDotWeb.Alt.js"></script> const libraryPath = path.relative(appFilePath, __filename).split(path.sep).slice(1).join(path.sep);
<script> const libraryFolder = libraryPath.split(path.sep).slice(0, -1).join(path.sep);
fs.writeFileSync(htmlFilePath, allOpts.global.htmlPager(`
${makeHtmlScriptFragment(libraryPath, selfContained)}
${makeHtmlScriptFragment(((libraryFolder && (libraryFolder + '/')) + 'SpaccDotWeb.Alt.js'), selfContained)}
<${'script'}>
window.require = () => { window.require = () => {
window.require = async (src, type) => { window.require = async (src, type) => {
await SpaccDotWeb.RequireScript((src.startsWith('./') ? src : ('node_modules/' + src)), type); await SpaccDotWeb.RequireScript((src.startsWith('./') ? src : ('node_modules/' + src)), type);
}; };
return window.SpaccDotWebServer; return window.SpaccDotWebServer;
}; };
window.SpaccDotWebServer.staticFilesData = { ${allOpts.global.staticFiles.map((file) => { window.SpaccDotWebServer.staticFilesData = { ${selfContained ? allOpts.global.staticFiles.map((file) => {
const filePath = (process.mainModule.filename.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file); // TODO check if these paths are correct or must still be fixed
const filePath = (appFilePath.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')}"`;
})} }; }).join() : ''} };
</script> </${'script'}>
<script src="./${path.basename(process.mainModule.filename)}"></script> ${makeHtmlScriptFragment(path.basename(appFilePath), selfContained)}
`)); `, null, { selfContained }));
return fileName; return htmlFilePath;
}; };
const makeHtmlStyleFragment = (path, getContent) => {
const data = getFilePathContent(path, getContent);
return (data[1] ? `<style>${data[1]}</style>` : `<link rel="stylesheet" href="${data[0]}"/>`);
};
const makeHtmlScriptFragment = (path, getContent) => {
const data = getFilePathContent(path, getContent);
return `<${'script'}${data[1] ? `>${data[1]}` : ` src="${data[0]}">`}</${'script'}>`;
};
const getFilePathContent = (path, getContent) => ([
(allOpts.global.staticFiles.includes(path) ? (allOpts.global.staticPrefix + path) : ('./' + path)),
(getContent && fs.existsSync(path) && fs.readFileSync(path)),
]);
const handleRequest = async (request, response={}) => { const handleRequest = async (request, response={}) => {
// build request context and handle special tasks // build request context and handle special tasks
let result = allOpts.server.defaultResponse; let result = allOpts.server.defaultResponse;
@ -193,10 +213,18 @@ const renderPage = (response, content, title, opts={}) => {
document.title = (opts.pageTitler || allOpts.global.pageTitler)(title, opts); document.title = (opts.pageTitler || allOpts.global.pageTitler)(title, opts);
document.querySelector(allOpts.server.appElement).innerHTML = ((opts.appPager || allOpts.global.appPager)(content, title, opts)); document.querySelector(allOpts.server.appElement).innerHTML = ((opts.appPager || allOpts.global.appPager)(content, title, opts));
for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) { for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) {
srcElem.src = window.SpaccDotWebServer.staticFilesData[srcElem.getAttribute('src')]; var fileUrl = makeStaticClientFileUrl(srcElem.getAttribute('src'));
if (srcElem.tagName === 'SCRIPT') {
// script elements die immediately after being first set up,
// so we must re-create them to have them run with correct uri
srcElem.remove();
document.head.appendChild(Object.assign(document.createElement('script'), { src: fileUrl }));
} else {
srcElem.src = fileUrl;
}
} }
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 = window.SpaccDotWebServer.staticFilesData[linkElem.getAttribute('href').slice(allOpts.global.staticPrefix.length)]; linkElem.href = makeStaticClientFileUrl(linkElem.getAttribute('href'));
} }
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')}`;
@ -218,6 +246,11 @@ const renderPage = (response, content, title, opts={}) => {
}; };
}; };
const makeStaticClientFileUrl = (url) => {
url = url.slice(allOpts.global.staticPrefix.length);
return (window.SpaccDotWebServer.staticFilesData[url] || ('./' + url));
};
const setClientTransition = (status) => { const setClientTransition = (status) => {
let transitionElement; let transitionElement;
if (envIsBrowser && (transitionElement = document.querySelector(allOpts.server.transitionElement))) { if (envIsBrowser && (transitionElement = document.querySelector(allOpts.server.transitionElement))) {
@ -295,7 +328,7 @@ const parseCookieString = (cookieString) => {
cookieData[key] = { value: rest.join('=') }; cookieData[key] = { value: rest.join('=') };
} }
if (allOpts.server.metaCookie) { if (allOpts.server.metaCookie) {
const metaData = parseMetaCookie(cookieData[allOpts.server.metaCookie].value); const metaData = parseMetaCookie(cookieData[allOpts.server.metaCookie]?.value);
for (const cookieName in cookieData) { for (const cookieName in cookieData) {
cookieData[cookieName] = { ...cookieData[cookieName], ...metaData[cookieName] }; cookieData[cookieName] = { ...cookieData[cookieName], ...metaData[cookieName] };
} }
@ -457,16 +490,16 @@ const parseclientLanguages = (request) => {
} }
}; };
const exportObj = { envIsNode, envIsBrowser, setup }; const exportObj = { envIsNode, envIsBrowser, setup, makeHtmlStyleFragment, makeHtmlScriptFragment };
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 = exportObj; module.exports = exportObj;
}; }
if (envIsBrowser) { if (envIsBrowser) {
window.SpaccDotWebServer = exportObj; window.SpaccDotWebServer = exportObj;
}; }
})(); })();