[Server] Add client language data, raw URL strings in ctx, HEAD method, handle arrays in HTML form
This commit is contained in:
parent
495c7a8d2e
commit
912534d13d
|
@ -21,8 +21,10 @@ if (SpaccDotWebServer.envIsNode && ['dump', 'html'].includes(process.argv[2])) {
|
||||||
// port: 3000,
|
// port: 3000,
|
||||||
// address: '127.0.0.1',
|
// address: '127.0.0.1',
|
||||||
// maxBodyUploadSize: null,
|
// maxBodyUploadSize: null,
|
||||||
|
// handleHttpHead: true,
|
||||||
// appElement: 'div#app',
|
// appElement: 'div#app',
|
||||||
// transitionElement: 'div#transition',
|
// transitionElement: 'div#transition',
|
||||||
|
// cookieInUrl: 'spaccdotweb-cookie',
|
||||||
|
|
||||||
// endpoints are defined by a discriminator and an action
|
// endpoints are defined by a discriminator and an action
|
||||||
endpoints: [
|
endpoints: [
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
/* 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
|
||||||
|
* utility functions for rewriting url query parameters?
|
||||||
|
* fix hash navigation to prevent no-going-back issue
|
||||||
* other things listed in the file
|
* other things listed in the file
|
||||||
*/
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -11,6 +15,7 @@ let fs, path, mime, multipart;
|
||||||
|
|
||||||
const setup = (globalOptions={}) => {
|
const setup = (globalOptions={}) => {
|
||||||
allOpts.global = globalOptions;
|
allOpts.global = globalOptions;
|
||||||
|
//allOpts.global.appName ||= 'Untitled SpaccDotWeb App';
|
||||||
allOpts.global.staticPrefix ||= '/static/';
|
allOpts.global.staticPrefix ||= '/static/';
|
||||||
allOpts.global.staticFiles ||= [];
|
allOpts.global.staticFiles ||= [];
|
||||||
allOpts.global.linkStyles ||= [];
|
allOpts.global.linkStyles ||= [];
|
||||||
|
@ -50,6 +55,7 @@ const initServer = (serverOptions) => {
|
||||||
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);
|
||||||
|
allOpts.server.handleHttpHead ||= true;
|
||||||
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) {
|
||||||
|
@ -71,11 +77,17 @@ const writeStaticHtml = () => {
|
||||||
const fileName = (process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html');
|
const fileName = (process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html');
|
||||||
fs.writeFileSync(fileName, allOpts.global.htmlPager(`
|
fs.writeFileSync(fileName, allOpts.global.htmlPager(`
|
||||||
<script src="./${path.basename(__filename)}"></script>
|
<script src="./${path.basename(__filename)}"></script>
|
||||||
|
<script src="./SpaccDotWeb.Alt.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.require = () => window.SpaccDotWebServer;
|
window.require = () => {
|
||||||
|
window.require = async (src, type) => {
|
||||||
|
await SpaccDotWeb.RequireScript((src.startsWith('./') ? src : ('node_modules/' + src)), type);
|
||||||
|
};
|
||||||
|
return window.SpaccDotWebServer;
|
||||||
|
};
|
||||||
window.SpaccDotWebServer.staticFilesData = { ${allOpts.global.staticFiles.map((file) => {
|
window.SpaccDotWebServer.staticFilesData = { ${allOpts.global.staticFiles.map((file) => {
|
||||||
const filePath = (process.mainModule.filename.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(process.mainModule.filename)}"></script>
|
<script src="./${path.basename(process.mainModule.filename)}"></script>
|
||||||
|
@ -89,22 +101,23 @@ const handleRequest = async (request, response={}) => {
|
||||||
const context = {
|
const context = {
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
urlSections: request.url.slice(1).toLowerCase().split('?')[0].split('/'),
|
urlPath: request.url.slice(1).toLowerCase().split('?')[0],
|
||||||
urlParameters: Object.fromEntries(new URLSearchParams(request.url.split('?')?.slice(1)?.join('?'))),
|
urlQuery: request.url.split('?')?.slice(1)?.join('?'),
|
||||||
bodyParameters: (['POST', 'PUT', 'PATCH'].includes(request.method) && await parseBodyParams(request)), // TODO which other methods need body?
|
bodyParameters: (['POST', 'PUT', 'PATCH'].includes(request.method) && 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),
|
||||||
//storageApi: (key,value, opts) => storageApi(key, value, opts),
|
//storageApi: (key,value, opts) => storageApi(key, value, opts),
|
||||||
renderPage: (content, title, opts) => renderPage(response, content, title, opts),
|
renderPage: (content, title, opts) => renderPage(response, content, title, opts),
|
||||||
redirectTo: (url) => redirectTo(response, url),
|
redirectTo: (url) => redirectTo(response, url),
|
||||||
|
clientLanguages: parseclientLanguages(request),
|
||||||
};
|
};
|
||||||
// client transitions
|
context.urlSections = context.urlPath.split('/');
|
||||||
if (envIsBrowser) {
|
context.urlParameters = Object.fromEntries(new URLSearchParams(context.urlQuery));
|
||||||
const transitionElement = document.querySelector(allOpts.server.transitionElement);
|
setClientTransition(true);
|
||||||
if (transitionElement) {
|
// TODO check if this is respected even when using renderPage?
|
||||||
transitionElement.hidden = false;
|
const handlingHttpHead = (allOpts.server.handleHttpHead && request.method === 'HEAD')
|
||||||
transitionElement.style.display = 'block';
|
if (handlingHttpHead) {
|
||||||
}
|
request.method = 'GET';
|
||||||
}
|
}
|
||||||
// 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)) {
|
||||||
|
@ -132,7 +145,7 @@ const handleRequest = async (request, response={}) => {
|
||||||
for (const name in result.headers) {
|
for (const name in result.headers) {
|
||||||
response.setHeader(name, result.headers[name]);
|
response.setHeader(name, result.headers[name]);
|
||||||
}
|
}
|
||||||
response.end(result.body);
|
response.end(!handlingHttpHead && result.body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,7 +171,7 @@ const requestCheckFunction = (check, context) => {
|
||||||
|
|
||||||
const renderPage = (response, content, title, opts={}) => {
|
const renderPage = (response, content, title, opts={}) => {
|
||||||
// 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? (but we don't handle it here?)
|
||||||
if (envIsNode) {
|
if (envIsNode) {
|
||||||
response.setHeader('content-type', 'text/html; charset=utf-8');
|
response.setHeader('content-type', 'text/html; charset=utf-8');
|
||||||
response.end((opts.htmlPager || allOpts.global.htmlPager)(content, title));
|
response.end((opts.htmlPager || allOpts.global.htmlPager)(content, title));
|
||||||
|
@ -188,14 +201,18 @@ const renderPage = (response, content, title, opts={}) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const transitionElement = document.querySelector(allOpts.server.transitionElement);
|
setClientTransition(false);
|
||||||
if (transitionElement) {
|
|
||||||
transitionElement.hidden = true;
|
|
||||||
transitionElement.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setClientTransition = (status) => {
|
||||||
|
let transitionElement;
|
||||||
|
if (envIsBrowser && (transitionElement = document.querySelector(allOpts.server.transitionElement))) {
|
||||||
|
transitionElement.hidden = !status;
|
||||||
|
transitionElement.style.display = (status ? 'block' : 'none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redirectTo = (response, url) => {
|
const redirectTo = (response, url) => {
|
||||||
if (envIsNode) {
|
if (envIsNode) {
|
||||||
response.statusCode = 302;
|
response.statusCode = 302;
|
||||||
|
@ -217,15 +234,19 @@ const parseBodyParams = async (request) => {
|
||||||
if (request.body.length > allOpts.server.maxBodyUploadSize) {
|
if (request.body.length > allOpts.server.maxBodyUploadSize) {
|
||||||
request.connection?.destroy();
|
request.connection?.destroy();
|
||||||
// TODO handle this more gracefully? maybe an error callback or something?
|
// TODO handle this more gracefully? maybe an error callback or something?
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => request.on('end', () => resolve()));
|
await new Promise((resolve) => request.on('end', () => resolve()));
|
||||||
}
|
}
|
||||||
const [contentMime, contentEnc] = request.headers['content-type'].split(';');
|
const [contentMime, contentEnc] = request.headers['content-type'].split(';');
|
||||||
if (envIsNode && contentMime === 'application/x-www-form-urlencoded') {
|
if (envIsNode && contentMime === 'application/x-www-form-urlencoded') {
|
||||||
for (const [key, value] of (new URLSearchParams(request.body.toString())).entries()) {
|
for (const [key, value] of (new URLSearchParams(request.body.toString())).entries()) {
|
||||||
|
if (key in params) {
|
||||||
|
params[key] = [].concat(params[key], value);
|
||||||
|
} else {
|
||||||
params[key] = value;
|
params[key] = value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (envIsNode && contentMime === 'multipart/form-data') {
|
} else if (envIsNode && contentMime === 'multipart/form-data') {
|
||||||
for (const param of multipart.parse(request.body, contentEnc.split('boundary=')[1])) {
|
for (const param of multipart.parse(request.body, contentEnc.split('boundary=')[1])) {
|
||||||
params[param.name] = (param.type && param.filename !== undefined ? param : param.data.toString());
|
params[param.name] = (param.type && param.filename !== undefined ? param : param.data.toString());
|
||||||
|
@ -240,7 +261,7 @@ const parseBodyParams = async (request) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
request.connection?.destroy();
|
request.connection?.destroy();
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCookie = (request, name) => {
|
const getCookie = (request, name) => {
|
||||||
|
@ -275,7 +296,9 @@ const setCookie = (response, cookie) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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?
|
// also, TODO: what to do when no app name or any id is set?
|
||||||
if (set) {
|
if (set) {
|
||||||
|
@ -308,6 +331,21 @@ const clientCookieApi = (envIsBrowser && (document.cookie || (!document.cookie &
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const parseclientLanguages = (request) => {
|
||||||
|
if (envIsNode) {
|
||||||
|
const languages = [];
|
||||||
|
const languageTokens = request.headers['accept-language']?.split(',');
|
||||||
|
if (languageTokens) {
|
||||||
|
for (const language of languageTokens) {
|
||||||
|
languages.push(language.split(';')[0].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
} else if (envIsBrowser) {
|
||||||
|
return (navigator.languages || [navigator.language /* || navigator.userLanguage */]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportObj = { envIsNode, envIsBrowser, setup };
|
const exportObj = { envIsNode, envIsBrowser, setup };
|
||||||
if (envIsNode) {
|
if (envIsNode) {
|
||||||
fs = require('fs');
|
fs = require('fs');
|
||||||
|
|
Loading…
Reference in New Issue