Update SpaccDotWebServer to add all currently needed features, update example
This commit is contained in:
parent
618842883d
commit
63dc2e648f
|
@ -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>
|
|
@ -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...');
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,8 +4,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
position: relative;
|
width: max-content;
|
||||||
top: 3em;
|
|
||||||
rotate: 15deg;
|
rotate: 15deg;
|
||||||
color: purple;
|
color: DeepPink;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,266 +1,294 @@
|
||||||
|
/* TODO:
|
||||||
|
* built-in logging
|
||||||
|
* other things listed in the file
|
||||||
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
const envIsNode = (typeof module === 'object' && typeof module.exports === 'object');
|
|
||||||
const envIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined');
|
|
||||||
let fs, path, mime, multipart;
|
|
||||||
const allOpts = {};
|
|
||||||
|
|
||||||
const main = (globalOptions={}) => {
|
const envIsNode = (typeof module === 'object' && typeof module.exports === 'object');
|
||||||
allOpts.global = globalOptions;
|
const envIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined');
|
||||||
allOpts.global.staticPrefix ||= '/static/';
|
const allOpts = {};
|
||||||
allOpts.global.staticFiles ||= [];
|
let fs, path, mime, multipart;
|
||||||
allOpts.global.linkStyles ||= [];
|
|
||||||
allOpts.global.linkScripts ||= [];
|
const setup = (globalOptions={}) => {
|
||||||
for (const item of [...allOpts.global.linkStyles, ...allOpts.global.linkScripts]) {
|
allOpts.global = globalOptions;
|
||||||
const itemLow = item.toLowerCase();
|
allOpts.global.staticPrefix ||= '/static/';
|
||||||
if (!(itemLow.startsWith('http://') || itemLow.startsWith('https://') || itemLow.startsWith('/'))) {
|
allOpts.global.staticFiles ||= [];
|
||||||
allOpts.global.staticFiles.push(item);
|
allOpts.global.linkStyles ||= [];
|
||||||
};
|
allOpts.global.linkScripts ||= [];
|
||||||
|
for (const item of [...allOpts.global.linkStyles, ...allOpts.global.linkScripts]) {
|
||||||
|
const itemLow = item.toLowerCase();
|
||||||
|
if (!(itemLow.startsWith('http://') || itemLow.startsWith('https://') || itemLow.startsWith('/'))) {
|
||||||
|
allOpts.global.staticFiles.push(item);
|
||||||
};
|
};
|
||||||
allOpts.global.htmlPager ||= (content) => `<!DOCTYPE html><html><head><!--
|
|
||||||
--><meta charset="utf-8"/><!--
|
|
||||||
--><meta name="viewport" content="width=device-width, initial-scale=1.0"/><!--
|
|
||||||
-->${allOpts.global.linkStyles.map((item) => {
|
|
||||||
return `<link rel="stylesheet" href="${allOpts.global.staticFiles.includes(item) ? (allOpts.global.staticPrefix + item) : item}"/>`;
|
|
||||||
}).join('')}<!--
|
|
||||||
--></head><body><!--
|
|
||||||
--><div id="transition"></div><!--
|
|
||||||
--><div id="app">${content}</div><!--
|
|
||||||
--></body></html>`;
|
|
||||||
const result = {};
|
|
||||||
result.initServer = (serverOptions={}) => initServer(serverOptions);
|
|
||||||
if (envIsNode) {
|
|
||||||
result.writeStaticHtml = (mainScriptPath) => writeStaticHtml(mainScriptPath);
|
|
||||||
};
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
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 name="viewport" content="width=device-width, initial-scale=1.0"/><!--
|
||||||
|
--><title>${allOpts.global.pageTitler(title)}</title><!--
|
||||||
|
-->${allOpts.global.linkStyles.map((item) => {
|
||||||
|
return `<link rel="stylesheet" href="${allOpts.global.staticFiles.includes(item) ? (allOpts.global.staticPrefix + item) : item}"/>`;
|
||||||
|
}).join('')}<!--
|
||||||
|
--></head><body><!--
|
||||||
|
--><div id="transition"></div><!--
|
||||||
|
--><div id="app">${allOpts.global.appPager(content, title)}</div><!--
|
||||||
|
--></body></html>`;
|
||||||
|
const result = {};
|
||||||
|
result.initServer = (serverOptions={}) => initServer(serverOptions);
|
||||||
|
if (envIsNode) {
|
||||||
|
result.writeStaticHtml = writeStaticHtml;
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const initServer = (serverOptions) => {
|
const initServer = (serverOptions) => {
|
||||||
allOpts.server = serverOptions;
|
allOpts.server = serverOptions;
|
||||||
allOpts.server.defaultResponse ||= { code: 500, headers: {} };
|
allOpts.server.defaultResponse ||= { code: 500, headers: {} };
|
||||||
allOpts.server.endpointsFalltrough ||= false;
|
allOpts.server.endpointsFalltrough ||= false;
|
||||||
if (envIsNode) {
|
if (envIsNode) {
|
||||||
allOpts.server.port ||= 3000;
|
allOpts.server.port ||= 3000;
|
||||||
allOpts.server.address ||= '127.0.0.1';
|
allOpts.server.address ||= '127.0.0.1';
|
||||||
allOpts.server.maxBodyUploadSize = (parseInt(allOpts.server.maxBodyUploadSize) || undefined);
|
allOpts.server.maxBodyUploadSize = (parseInt(allOpts.server.maxBodyUploadSize) || undefined);
|
||||||
require('http').createServer(handleRequest).listen(allOpts.server.port, allOpts.server.address);
|
require('http').createServer(handleRequest).listen(allOpts.server.port, allOpts.server.address);
|
||||||
};
|
};
|
||||||
if (envIsBrowser) {
|
if (envIsBrowser) {
|
||||||
allOpts.server.appElement ||= 'div#app';
|
allOpts.server.appElement ||= 'div#app';
|
||||||
allOpts.server.transitionElement ||= 'div#transition';
|
allOpts.server.transitionElement ||= 'div#transition';
|
||||||
const navigatePage = () => handleRequest({ url: window.location.hash.slice(1), method: 'GET' });
|
const navigatePage = () => handleRequest({ url: window.location.hash.slice(1), method: 'GET' });
|
||||||
window.addEventListener('hashchange', () => {
|
window.addEventListener('hashchange', () => {
|
||||||
window.location.hash ||= '/';
|
window.location.hash ||= '/';
|
||||||
navigatePage();
|
|
||||||
});
|
|
||||||
navigatePage();
|
navigatePage();
|
||||||
};
|
});
|
||||||
|
navigatePage();
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const writeStaticHtml = (mainScriptPath) => {
|
const writeStaticHtml = () => {
|
||||||
fs.writeFileSync((mainScriptPath.split('.').slice(0, -1).join('.') + '.html'), allOpts.global.htmlPager(`
|
// TODO: fix script paths
|
||||||
<script>
|
// 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
|
||||||
window.process = null;
|
fs.writeFileSync((process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html'), allOpts.global.htmlPager(`
|
||||||
window.require = () => window.SpaccDotWebServer;
|
<script src="./${path.basename(__filename)}"></script>
|
||||||
window.resFilesData = { ${allOpts.global.staticFiles.map((file) => {
|
<script>
|
||||||
const filePath = (mainScriptPath.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file);
|
window.require = () => window.SpaccDotWebServer;
|
||||||
return `"${file}": "data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`;
|
window.SpaccDotWebServer.resFilesData = { ${allOpts.global.staticFiles.map((file) => {
|
||||||
})} };
|
const filePath = (process.mainModule.filename.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file);
|
||||||
</script>
|
return `"${file}": "data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`;
|
||||||
<script src="./${path.basename(__filename)}"></script>
|
})} };
|
||||||
<script src="./${path.basename(mainScriptPath)}"></script>
|
</script>
|
||||||
`));
|
<script src="./${path.basename(process.mainModule.filename)}"></script>
|
||||||
|
`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequest = async (request, response={}) => {
|
||||||
|
// build request context and handle special tasks
|
||||||
|
let result = allOpts.server.defaultResponse;
|
||||||
|
const context = {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
urlSections: request.url.slice(1).toLowerCase().split('?')[0].split('/'),
|
||||||
|
urlParameters: (new URLSearchParams(request.url.split('?')[1]?.join('?'))),
|
||||||
|
bodyParameters: (request.method === 'POST' && await parseBodyParams(request)), // TODO which other methods need body?
|
||||||
|
getCookie: (cookie) => getCookie(request, cookie),
|
||||||
|
setCookie: (cookie) => setCookie(response, cookie),
|
||||||
|
renderPage: (content, title) => renderPage(response, content, title),
|
||||||
|
redirectTo: (url) => redirectTo(response, url),
|
||||||
};
|
};
|
||||||
|
// client transitions
|
||||||
const handleRequest = async (request, response={}) => {
|
if (envIsBrowser && document.querySelector(allOpts.server.transitionElement)) {
|
||||||
// build request context and handle special tasks
|
document.querySelector(allOpts.server.transitionElement).style.display = 'block';
|
||||||
let result = allOpts.server.defaultResponse;
|
};
|
||||||
const context = {
|
// serve static files
|
||||||
request,
|
if (envIsNode && request.method === 'GET' && request.url.toLowerCase().startsWith(allOpts.global.staticPrefix)) {
|
||||||
response,
|
const resPath = request.url.split(allOpts.global.staticPrefix).slice(1).join(allOpts.global.staticPrefix);
|
||||||
urlParameters: (new URLSearchParams(request.url.split('?')[1]?.join('?'))),
|
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
|
||||||
bodyParameters: (request.method === 'POST' && await parseBodyParams(request)), // TODO which other methods need body?
|
if (allOpts.global.staticFiles.includes(resPath) && fs.existsSync(filePath)) {
|
||||||
getCookie: (cookie) => getCookie(request, cookie),
|
result = { code: 200, headers: { 'content-type': mime.lookup(filePath) }, body: fs.readFileSync(filePath) };
|
||||||
setCookie: (cookie) => setCookie(response, cookie),
|
|
||||||
renderPage: (content) => renderPage(response, content),
|
|
||||||
redirectTo: (url) => redirectTo(response, url),
|
|
||||||
};
|
|
||||||
// serve static files
|
|
||||||
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 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
|
|
||||||
if (allOpts.global.staticFiles.includes(resPath) && fs.existsSync(filePath)) {
|
|
||||||
result = { code: 200, headers: { 'content-type': mime.lookup(filePath) }, body: fs.readFileSync(filePath) };
|
|
||||||
} else {
|
|
||||||
result = { code: 404 };
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// handle custom endpoints
|
result = { code: 404 };
|
||||||
for (const [check, procedure] of allOpts.server.endpoints) {
|
|
||||||
if (check(context)) {
|
|
||||||
result = await procedure(context);
|
|
||||||
if (!allOpts.server.endpointsFalltrough) {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
// finalize a normal response
|
} else {
|
||||||
if (result) {
|
// handle custom endpoints
|
||||||
response.statusCode = result.code;
|
for (const [check, procedure] of allOpts.server.endpoints) {
|
||||||
for (const name in result.headers) {
|
if (await check(context)) {
|
||||||
response.setHeader(name, result.headers[name]);
|
result = await procedure(context);
|
||||||
};
|
if (!allOpts.server.endpointsFalltrough) {
|
||||||
response.end(result.body);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPage = (response, content) => {
|
|
||||||
// TODO titles and things
|
|
||||||
// TODO status code could need to be different in different situations and so should be set accordingly?
|
|
||||||
if (envIsNode) {
|
|
||||||
response.setHeader('content-type', 'text/html; charset=utf-8');
|
|
||||||
response.end(allOpts.global.htmlPager(content));
|
|
||||||
};
|
|
||||||
if (envIsBrowser) {
|
|
||||||
document.querySelector(allOpts.server.appElement).innerHTML = content;
|
|
||||||
for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) {
|
|
||||||
srcElem.src = resFilesData[srcElem.getAttribute('src')];
|
|
||||||
};
|
|
||||||
for (const linkElem of document.querySelectorAll(`link[rel="stylesheet"][href^="${allOpts.global.staticPrefix}"]`)) {
|
|
||||||
linkElem.href = resFilesData[linkElem.getAttribute('href').slice(allOpts.global.staticPrefix.length)];
|
|
||||||
};
|
|
||||||
for (const aElem of document.querySelectorAll('a[href^="/"]')) {
|
|
||||||
aElem.href = `#${aElem.getAttribute('href')}`;
|
|
||||||
};
|
|
||||||
for (const formElem of document.querySelectorAll('form')) {
|
|
||||||
formElem.onsubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const formData = (new FormData(formElem));
|
|
||||||
formData.append(event.submitter.getAttribute('name'), (event.submitter.value || 'Submit'));
|
|
||||||
handleRequest({
|
|
||||||
method: (formElem.getAttribute('method') || 'GET'),
|
|
||||||
url: (formElem.getAttribute('action') || location.hash.slice(1)),
|
|
||||||
headers: { 'content-type': (formElem.getAttribute('enctype') || "application/x-www-form-urlencoded") },
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectTo = (response, url) => {
|
|
||||||
if (envIsNode) {
|
|
||||||
response.statusCode = 302;
|
|
||||||
response.setHeader('location', url);
|
|
||||||
response.end();
|
|
||||||
};
|
|
||||||
if (envIsBrowser) {
|
|
||||||
location.hash = url;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseBodyParams = async (request) => {
|
|
||||||
try {
|
|
||||||
let params = {};
|
|
||||||
if (envIsNode) {
|
|
||||||
request.body = Buffer.alloc(0);
|
|
||||||
request.on('data', (data) => {
|
|
||||||
request.body = Buffer.concat([request.body, data]);
|
|
||||||
if (request.body.length > allOpts.server.maxBodyUploadSize) {
|
|
||||||
request.connection?.destroy();
|
|
||||||
// TODO handle this more gracefully? maybe an error callback or something?
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => request.on('end', () => resolve()));
|
|
||||||
};
|
|
||||||
const [contentMime, contentEnc] = request.headers['content-type'].split(';');
|
|
||||||
if (envIsNode && contentMime === 'application/x-www-form-urlencoded') {
|
|
||||||
for (const [key, value] of (new URLSearchParams(request.body.toString())).entries()) {
|
|
||||||
params[key] = value;
|
|
||||||
};
|
|
||||||
} else if (envIsNode && contentMime === 'multipart/form-data') {
|
|
||||||
for (const param of multipart.parse(request.body, contentEnc.split('boundary=')[1])) {
|
|
||||||
params[param.name] = (param.type && param.filename !== undefined ? param : param.data.toString());
|
|
||||||
};
|
|
||||||
} else if (envIsBrowser && ['application/x-www-form-urlencoded', 'multipart/form-data'].includes(contentMime)) {
|
|
||||||
for (const [key, value] of request.body) {
|
|
||||||
params[key] = value;
|
|
||||||
params[key].filename = params[key].name;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return params;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
request.connection?.destroy();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCookie = (request, name) => {
|
|
||||||
let cookies;
|
|
||||||
if (envIsNode) {
|
|
||||||
cookies = request.headers?.cookie;
|
|
||||||
};
|
|
||||||
if (envIsBrowser) {
|
|
||||||
cookies = clientCookieApi();
|
|
||||||
};
|
|
||||||
if (name) {
|
|
||||||
// get a specific cookie
|
|
||||||
for (const cookie of (cookies?.split(';') || [])) {
|
|
||||||
const [key, ...rest] = cookie.split('=');
|
|
||||||
if (key === name) {
|
|
||||||
return rest.join('=');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// get all cookies
|
|
||||||
return cookies;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCookie = (response, cookie) => {
|
|
||||||
if (envIsNode) {
|
|
||||||
response.setHeader('Set-Cookie', cookie);
|
|
||||||
// TODO update current cookie list in existing request to reflect new assignment
|
|
||||||
};
|
|
||||||
if (envIsBrowser) {
|
|
||||||
clientCookieApi(cookie);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 gid = allOpts.global.appName; // TODO: introduce a conf field that is specifically a GID for less potential problems?
|
|
||||||
if (set) {
|
|
||||||
let api = sessionStorage;
|
|
||||||
const tokens = set.split(';');
|
|
||||||
const [key, ...rest] = tokens[0].split('=');
|
|
||||||
for (let token of tokens) {
|
|
||||||
token = token.trim();
|
|
||||||
if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) {
|
|
||||||
api = localStorage;
|
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
api.setItem(`${gid}/${key}`, rest.join('='));
|
|
||||||
} else /*(get)*/ {
|
|
||||||
let items = '';
|
|
||||||
for (const item of Object.entries({ ...localStorage, ...sessionStorage })) {
|
|
||||||
if (item[0].startsWith(`${gid}/`)) {
|
|
||||||
items += (item.join('=') + ';').slice(gid.length + 1);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return items.slice(0, -1);
|
|
||||||
};
|
};
|
||||||
}));
|
};
|
||||||
|
// finalize a normal response
|
||||||
|
if (result) {
|
||||||
|
response.statusCode = result.code;
|
||||||
|
for (const name in result.headers) {
|
||||||
|
response.setHeader(name, result.headers[name]);
|
||||||
|
};
|
||||||
|
response.end(result.body);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPage = (response, content, title) => {
|
||||||
|
// TODO titles and things
|
||||||
|
// TODO status code could need to be different in different situations and so should be set accordingly?
|
||||||
if (envIsNode) {
|
if (envIsNode) {
|
||||||
fs = require('fs');
|
response.setHeader('content-type', 'text/html; charset=utf-8');
|
||||||
path = require('path');
|
response.end(allOpts.global.htmlPager(content, title));
|
||||||
mime = require('mime-types');
|
|
||||||
multipart = require('parse-multipart-data');
|
|
||||||
module.exports = main;
|
|
||||||
};
|
};
|
||||||
if (envIsBrowser) {
|
if (envIsBrowser) {
|
||||||
window.SpaccDotWebServer = main;
|
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}"]`)) {
|
||||||
|
srcElem.src = window.SpaccDotWebServer.resFilesData[srcElem.getAttribute('src')];
|
||||||
|
};
|
||||||
|
for (const linkElem of document.querySelectorAll(`link[rel="stylesheet"][href^="${allOpts.global.staticPrefix}"]`)) {
|
||||||
|
linkElem.href = window.SpaccDotWebServer.resFilesData[linkElem.getAttribute('href').slice(allOpts.global.staticPrefix.length)];
|
||||||
|
};
|
||||||
|
for (const aElem of document.querySelectorAll('a[href^="/"]')) {
|
||||||
|
aElem.href = `#${aElem.getAttribute('href')}`;
|
||||||
|
};
|
||||||
|
for (const formElem of document.querySelectorAll('form')) {
|
||||||
|
formElem.onsubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = (new FormData(formElem));
|
||||||
|
formData.append(event.submitter.getAttribute('name'), (event.submitter.value || 'Submit'));
|
||||||
|
handleRequest({
|
||||||
|
method: (formElem.getAttribute('method') || 'GET'),
|
||||||
|
url: (formElem.getAttribute('action') || location.hash.slice(1)),
|
||||||
|
headers: { 'content-type': (formElem.getAttribute('enctype') || "application/x-www-form-urlencoded") },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (document.querySelector(allOpts.server.transitionElement)) {
|
||||||
|
document.querySelector(allOpts.server.transitionElement).style.display = 'none';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectTo = (response, url) => {
|
||||||
|
if (envIsNode) {
|
||||||
|
response.statusCode = 302;
|
||||||
|
response.setHeader('location', url);
|
||||||
|
response.end();
|
||||||
|
};
|
||||||
|
if (envIsBrowser) {
|
||||||
|
location.hash = url;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseBodyParams = async (request) => {
|
||||||
|
try {
|
||||||
|
let params = {};
|
||||||
|
if (envIsNode) {
|
||||||
|
request.body = Buffer.alloc(0);
|
||||||
|
request.on('data', (data) => {
|
||||||
|
request.body = Buffer.concat([request.body, data]);
|
||||||
|
if (request.body.length > allOpts.server.maxBodyUploadSize) {
|
||||||
|
request.connection?.destroy();
|
||||||
|
// TODO handle this more gracefully? maybe an error callback or something?
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => request.on('end', () => resolve()));
|
||||||
|
};
|
||||||
|
const [contentMime, contentEnc] = request.headers['content-type'].split(';');
|
||||||
|
if (envIsNode && contentMime === 'application/x-www-form-urlencoded') {
|
||||||
|
for (const [key, value] of (new URLSearchParams(request.body.toString())).entries()) {
|
||||||
|
params[key] = value;
|
||||||
|
};
|
||||||
|
} else if (envIsNode && contentMime === 'multipart/form-data') {
|
||||||
|
for (const param of multipart.parse(request.body, contentEnc.split('boundary=')[1])) {
|
||||||
|
params[param.name] = (param.type && param.filename !== undefined ? param : param.data.toString());
|
||||||
|
};
|
||||||
|
} else if (envIsBrowser && ['application/x-www-form-urlencoded', 'multipart/form-data'].includes(contentMime)) {
|
||||||
|
for (const [key, value] of request.body) {
|
||||||
|
params[key] = value;
|
||||||
|
params[key].filename = params[key].name;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return params;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
request.connection?.destroy();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCookie = (request, name) => {
|
||||||
|
let cookies;
|
||||||
|
if (envIsNode) {
|
||||||
|
cookies = (request.headers?.cookie || '');
|
||||||
|
};
|
||||||
|
if (envIsBrowser) {
|
||||||
|
cookies = clientCookieApi();
|
||||||
|
};
|
||||||
|
if (name) {
|
||||||
|
// get a specific cookie
|
||||||
|
for (const cookie of (cookies?.split(';') || [])) {
|
||||||
|
const [key, ...rest] = cookie.split('=');
|
||||||
|
if (key === name) {
|
||||||
|
return rest.join('=');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// get all cookies
|
||||||
|
return cookies;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCookie = (response, cookie) => {
|
||||||
|
if (envIsNode) {
|
||||||
|
response.setHeader('Set-Cookie', cookie);
|
||||||
|
// TODO update current cookie list in existing request to reflect new assignment
|
||||||
|
};
|
||||||
|
if (envIsBrowser) {
|
||||||
|
clientCookieApi(cookie);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 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) {
|
||||||
|
let api = sessionStorage;
|
||||||
|
const tokens = set.split(';');
|
||||||
|
let [key, ...rest] = tokens[0].split('=');
|
||||||
|
for (let token of tokens) {
|
||||||
|
token = token.trim();
|
||||||
|
if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) {
|
||||||
|
api = localStorage;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
key = `${gid}/${key}`;
|
||||||
|
const value = rest.join('=');
|
||||||
|
if (value) {
|
||||||
|
api.setItem(key, value);
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
};
|
||||||
|
} else /*(get)*/ {
|
||||||
|
let items = '';
|
||||||
|
for (const item of Object.entries({ ...localStorage, ...sessionStorage })) {
|
||||||
|
if (item[0].startsWith(`${gid}/`)) {
|
||||||
|
items += (item.join('=') + ';').slice(gid.length + 1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return items.slice(0, -1);
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
const exportObj = { envIsNode, envIsBrowser, setup };
|
||||||
|
if (envIsNode) {
|
||||||
|
fs = require('fs');
|
||||||
|
path = require('path');
|
||||||
|
mime = require('mime-types');
|
||||||
|
multipart = require('parse-multipart-data');
|
||||||
|
module.exports = exportObj;
|
||||||
|
};
|
||||||
|
if (envIsBrowser) {
|
||||||
|
window.SpaccDotWebServer = exportObj;
|
||||||
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue