mirror of
https://gitlab.com/SpaccInc/SpaccDotWeb.git
synced 2025-06-05 21:29:12 +02:00
[Server] add endpoint matching via strings, improve example, small improvements
This commit is contained in:
@@ -19,26 +19,26 @@ const setup = (globalOptions={}) => {
|
||||
const itemLow = item.toLowerCase();
|
||||
if (!(itemLow.startsWith('http://') || itemLow.startsWith('https://') || itemLow.startsWith('/'))) {
|
||||
allOpts.global.staticFiles.push(item);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
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><!--
|
||||
allOpts.global.htmlPager ||= (content, title, opts={}) => `<!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><!--
|
||||
--><title>${(opts.pageTitler || 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><!--
|
||||
--><div id="app">${(opts.appPager || allOpts.global.appPager)(content, title)}</div><!--
|
||||
--></body></html>`;
|
||||
const result = {};
|
||||
result.initServer = (serverOptions={}) => initServer(serverOptions);
|
||||
if (envIsNode) {
|
||||
result.writeStaticHtml = writeStaticHtml;
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ const initServer = (serverOptions) => {
|
||||
allOpts.server.address ||= '127.0.0.1';
|
||||
allOpts.server.maxBodyUploadSize = (parseInt(allOpts.server.maxBodyUploadSize) || undefined);
|
||||
require('http').createServer(handleRequest).listen(allOpts.server.port, allOpts.server.address);
|
||||
};
|
||||
}
|
||||
if (envIsBrowser) {
|
||||
allOpts.server.appElement ||= 'div#app';
|
||||
allOpts.server.transitionElement ||= 'div#transition';
|
||||
@@ -61,23 +61,26 @@ const initServer = (serverOptions) => {
|
||||
navigatePage();
|
||||
});
|
||||
navigatePage();
|
||||
};
|
||||
}
|
||||
return allOpts.server;
|
||||
};
|
||||
|
||||
const writeStaticHtml = () => {
|
||||
// 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(`
|
||||
const fileName = (process.mainModule.filename.split('.').slice(0, -1).join('.') + '.html');
|
||||
fs.writeFileSync(fileName, allOpts.global.htmlPager(`
|
||||
<script src="./${path.basename(__filename)}"></script>
|
||||
<script>
|
||||
window.require = () => window.SpaccDotWebServer;
|
||||
window.SpaccDotWebServer.resFilesData = { ${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);
|
||||
return `"${file}": "data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`;
|
||||
})} };
|
||||
</script>
|
||||
<script src="./${path.basename(process.mainModule.filename)}"></script>
|
||||
`));
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const handleRequest = async (request, response={}) => {
|
||||
@@ -87,66 +90,91 @@ const handleRequest = async (request, response={}) => {
|
||||
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?
|
||||
urlParameters: Object.fromEntries(new URLSearchParams(request.url.split('?')?.slice(1)?.join('?'))),
|
||||
bodyParameters: (['POST', 'PUT', 'PATCH'].includes(request.method) && 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),
|
||||
//storageApi: (key,value, opts) => storageApi(key, value, opts),
|
||||
renderPage: (content, title, opts) => renderPage(response, content, title, opts),
|
||||
redirectTo: (url) => redirectTo(response, url),
|
||||
};
|
||||
// client transitions
|
||||
if (envIsBrowser && document.querySelector(allOpts.server.transitionElement)) {
|
||||
document.querySelector(allOpts.server.transitionElement).style.display = 'block';
|
||||
};
|
||||
if (envIsBrowser) {
|
||||
const transitionElement = document.querySelector(allOpts.server.transitionElement);
|
||||
if (transitionElement) {
|
||||
transitionElement.hidden = false;
|
||||
transitionElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
// 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 = (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) };
|
||||
} else {
|
||||
result = { code: 404 };
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// handle custom endpoints
|
||||
for (const [check, procedure] of allOpts.server.endpoints) {
|
||||
if (await check(context)) {
|
||||
if (await requestCheckFunction(check, context)) {
|
||||
result = await procedure(context);
|
||||
if (!allOpts.server.endpointsFalltrough) {
|
||||
break;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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) => {
|
||||
const requestCheckFunction = (check, context) => {
|
||||
if (typeof check == 'function') {
|
||||
return check(context);
|
||||
} else if (typeof check == 'string') {
|
||||
let [method, ...urlSections] = check.trim().split(' ');
|
||||
urlSections = urlSections.join(' ').trim().split('/').slice(1, -1);
|
||||
const methodCheck = (method === '*' || method.split('|').includes(context.request.method));
|
||||
let urlCheck = true;
|
||||
for (const sectionIndex in urlSections) {
|
||||
const urlSection = urlSections[sectionIndex];
|
||||
const checkSection = context.urlSections[sectionIndex];
|
||||
if (!['', '*', checkSection].includes(urlSection)) {
|
||||
urlCheck = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (methodCheck && urlCheck);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPage = (response, content, title, opts={}) => {
|
||||
// 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, title));
|
||||
};
|
||||
response.end((opts.htmlPager || allOpts.global.htmlPager)(content, title));
|
||||
}
|
||||
if (envIsBrowser) {
|
||||
document.title = allOpts.global.pageTitler(title);
|
||||
document.querySelector(allOpts.server.appElement).innerHTML = allOpts.global.appPager(content, title);
|
||||
document.title = (opts.pageTitler || allOpts.global.pageTitler)(title);
|
||||
document.querySelector(allOpts.server.appElement).innerHTML = ((opts.appPager || allOpts.global.appPager)(content, title));
|
||||
for (const srcElem of document.querySelectorAll(`[src^="${allOpts.global.staticPrefix}"]`)) {
|
||||
srcElem.src = window.SpaccDotWebServer.resFilesData[srcElem.getAttribute('src')];
|
||||
};
|
||||
srcElem.src = window.SpaccDotWebServer.staticFilesData[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)];
|
||||
};
|
||||
linkElem.href = window.SpaccDotWebServer.staticFilesData[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();
|
||||
@@ -159,10 +187,12 @@ const renderPage = (response, content, title) => {
|
||||
body: formData,
|
||||
});
|
||||
};
|
||||
};
|
||||
if (document.querySelector(allOpts.server.transitionElement)) {
|
||||
document.querySelector(allOpts.server.transitionElement).style.display = 'none';
|
||||
};
|
||||
}
|
||||
const transitionElement = document.querySelector(allOpts.server.transitionElement);
|
||||
if (transitionElement) {
|
||||
transitionElement.hidden = true;
|
||||
transitionElement.style.display = 'none';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -171,10 +201,10 @@ const redirectTo = (response, url) => {
|
||||
response.statusCode = 302;
|
||||
response.setHeader('location', url);
|
||||
response.end();
|
||||
};
|
||||
}
|
||||
if (envIsBrowser) {
|
||||
location.hash = url;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const parseBodyParams = async (request) => {
|
||||
@@ -190,22 +220,22 @@ const parseBodyParams = async (request) => {
|
||||
};
|
||||
});
|
||||
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);
|
||||
@@ -217,32 +247,31 @@ const getCookie = (request, name) => {
|
||||
let cookies;
|
||||
if (envIsNode) {
|
||||
cookies = (request.headers?.cookie || '');
|
||||
};
|
||||
if (envIsBrowser) {
|
||||
} else if (envIsBrowser) {
|
||||
cookies = clientCookieApi();
|
||||
};
|
||||
}
|
||||
if (name) {
|
||||
// get a specific cookie
|
||||
for (const cookie of (cookies?.split(';') || [])) {
|
||||
const [key, ...rest] = cookie.split('=');
|
||||
// TODO ensure this is good, whitespace must be removed at the start but idk about the end
|
||||
const [key, ...rest] = cookie.trim().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) {
|
||||
} else 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:///)
|
||||
@@ -258,8 +287,8 @@ const clientCookieApi = (envIsBrowser && (document.cookie || (!document.cookie &
|
||||
if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) {
|
||||
api = localStorage;
|
||||
break;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
key = `${gid}/${key}`;
|
||||
const value = rest.join('=');
|
||||
if (value) {
|
||||
@@ -267,16 +296,16 @@ const clientCookieApi = (envIsBrowser && (document.cookie || (!document.cookie &
|
||||
} 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 };
|
||||
|
Reference in New Issue
Block a user