mirror of
				https://gitlab.com/SpaccInc/SpaccDotWeb.git
				synced 2025-06-05 21:29:12 +02:00 
			
		
		
		
	[Server] Handle linkClientScripts, polish static HTML gen with embedded files
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| *.tmp | *.tmp | ||||||
| /Build/* | /Build/* | ||||||
| /node_modules/* | /node_modules/* | ||||||
| /Example.Server.html | /Example.Server/index.html | ||||||
|   | |||||||
| @@ -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({ | ||||||
							
								
								
									
										17
									
								
								Example.Server/particles.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Example.Server/particles.js
									
									
									
									
									
										Normal 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); | ||||||
|  | 	})(); | ||||||
|  | }); | ||||||
| @@ -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; | ||||||
| }; | } | ||||||
|  |  | ||||||
| })(); | })(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user