lupine.web
Version:
lupine.web is a extremely fast, small size and lightweight frontend framework, using React TSX syntax.
181 lines (164 loc) • 6.62 kB
text/typescript
import { VNode } from '../jsx';
import { isFrontEnd } from '../lib/is-frontend';
import { PageProps } from './export-lupine';
import { mountInnerComponent } from './mount-component';
import { Logger } from '../lib/logger';
export type PageRouterCallback = (props: PageProps) => Promise<VNode<any> | null>;
export type PageRouterData = {
path: string;
handler: (PageRouterCallback | PageRouter)[];
parameterVariables: string[];
parameterLength: number;
};
export type FramePageProps = {
component: (placeholderClassname: string, vnode: VNode<any>) => Promise<VNode<any>>;
placeholderClassname: string;
};
export class PageRouter {
logger = new Logger('page-router');
private routerData: PageRouterData[] = [];
private filter: PageRouterCallback | undefined;
private framePage: FramePageProps | undefined;
private subDir: string = '';
// if the filter returns null (passed filter), the router will continue.
// it works in the same way as in use method
setFilter(filter: PageRouterCallback) {
this.filter = filter;
}
// if the script is under a sub-dir (without last /), then findRoute needs to remove it from the url
setSubDir(subDir: string) {
this.subDir = subDir;
}
setFramePage(framePage: FramePageProps) {
this.framePage = framePage;
}
// the path should start with / and end without /, and it can be
// /aaa/:bbb/ccc/:ddd (ccc is a fixed section)
// /aaa/:bbb/ccc/?ddd/?eee (from ddd, all sections are optional)
// /aaa/:?bbb/ccc/ (from bbb, all sections are optional)
private storeRouter(path: string, handler: (PageRouterCallback | PageRouter)[]) {
let fixedPath;
if (path === '*' || path === '' || path === '/*') {
// removed path === '/' ||
fixedPath = '*';
} else {
fixedPath = path;
if (!fixedPath.startsWith('/')) {
fixedPath = '/' + fixedPath;
}
if (fixedPath.endsWith('/') && fixedPath.length > 1) {
fixedPath = fixedPath.substring(0, fixedPath.length - 1);
}
}
let parameterLength = 0;
let parameterVariables: string[] = [];
const ind = fixedPath.indexOf('/:');
if (ind >= 0) {
parameterVariables = fixedPath.substring(ind + 1).split('/');
fixedPath = fixedPath.substring(0, ind);
// from optionInd, all will be optional
const optionInd = parameterVariables.findIndex((item) => item.startsWith('?'));
parameterLength = optionInd >= 0 ? optionInd : parameterVariables.length;
}
this.routerData.push({
path: fixedPath,
handler,
parameterVariables,
parameterLength,
});
}
use(path: string, ...handler: (PageRouterCallback | PageRouter)[]) {
this.storeRouter(path, handler);
}
private async callHandle(handle: PageRouterCallback, path: string, props: PageProps): Promise<VNode<any> | null> {
try {
const vNode = await handle(props);
// logger.debug(`Processed path: ${path}`);
return vNode;
} catch (e: any) {
this.logger.error(`Processed path: ${path}, error: ${e.message}`);
// res.write(JSON.stringify({ status: 'error', message: `Processed path: ${path}, error: ${e.message}` }));
}
return null;
}
async findRoute(url: string, props: PageProps, renderPartPage: boolean): Promise<VNode<any> | null> {
for (let i = 0, routerList; (routerList = this.routerData[i]); i++) {
if (routerList.path === '*' || url === routerList.path || url.startsWith(routerList.path + '/')) {
const parameters: { [key: string]: string } = {};
let meet = true;
if (routerList.parameterVariables.length > 0) {
meet = false;
let newUrl = url.substring(routerList.path.length + 1);
if (newUrl.endsWith('/')) {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
const restPath = newUrl.split('/');
// the path must have mandatory parameters but some parameters can be optional
if (
restPath.length >= routerList.parameterLength &&
restPath.length <= routerList.parameterVariables.length
) {
meet = true;
for (const [index, item] of routerList.parameterVariables.entries()) {
if (!item.startsWith(':') && !item.startsWith('?') && item !== restPath[index]) {
meet = false;
break;
} else if ((item.startsWith(':') || item.startsWith('?')) && index < restPath.length) {
parameters[item.replace(/[:?]/g, '')] = restPath[index];
}
}
props.urlParameters = parameters;
}
}
if (meet) {
for (let j = 0, router; (router = routerList.handler[j]); j++) {
if (router instanceof PageRouter) {
// it's a sub-level router
const nextPath =
routerList.path === '*' || (url === '/' && routerList.path === '/')
? url
: url.substring(routerList.path.length);
// TODO: sub-level?
const vNode = await router.handleRoute(nextPath, props, renderPartPage);
if (vNode) {
return vNode;
}
} else {
// it should be a function
// the query's url should match the api's path
const dom = await this.callHandle(router, url, props);
if (dom) {
return dom;
}
}
}
// stop process for this path if no page is found
return null;
}
}
}
return null;
}
async handleRoute(url: string, props: PageProps, renderPartPage: boolean): Promise<VNode<any> | null> {
if (url.startsWith(this.subDir)) {
url = url.substring(this.subDir.length);
}
let vNode = null;
if (this.filter) {
vNode = await this.callHandle(this.filter, url, props);
}
if (!vNode) {
vNode = await this.findRoute(url, props, renderPartPage);
}
if (vNode && this.framePage) {
const selector = '.' + this.framePage.placeholderClassname;
if (renderPartPage && isFrontEnd() && document.querySelector(selector)) {
await mountInnerComponent(selector, vNode);
return null;
} else {
return this.framePage.component(this.framePage.placeholderClassname, vNode);
}
}
return vNode;
}
}