jetpath
Version:
A performance-first cross-runtime API framework without the boilerplate
175 lines (174 loc) • 8.01 kB
JavaScript
import { _jet_middleware, _JetPath_paths, _JetPath_paths_trie, assignMiddleware, codeGen, compileAPI, compileUI, corsMiddleware, fs, getHandlers, getHandlersEdge, getLocalIP, server, } from './primitives/functions.js';
import { JetPlugin, LOG } from './primitives/classes.js';
import { readFile } from 'node:fs/promises';
import { cwd } from 'node:process';
import path from 'node:path';
const html_path = path.join(cwd(), "/node_modules/jetpath/dist/jetpath-doc.html");
export class Jetpath {
server = { listen: () => { }, edge: false };
listening = false;
/**
* an object you can set values to per request
*/
plugins = [];
options = {
port: 8080,
apiDoc: { display: 'UI' },
cors: true,
strictMode: 'OFF',
source: '.',
};
plugs = [];
constructor(options = {}) {
//? setting up default values
Object.assign(this.options, options);
// ? setting up app configs
corsMiddleware({
exposeHeaders: [],
allowMethods: ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'],
origin: ['*'],
allowHeaders: ['*'],
maxAge: '86400',
keepHeadersOnError: true,
...(typeof options?.cors === 'object' ? options.cors : {}),
});
//?
if (!this.options.port)
this.options.port = 8080;
}
derivePlugins(...plugins) {
if (this.listening) {
throw new Error("Your app is listening new plugins can't be added.");
}
plugins.forEach((plugin) => {
if (typeof plugin.executor === 'function' ||
typeof plugin.name === 'string') {
// ? add plugin to the server
this.plugs.push(new JetPlugin(plugin));
}
else {
throw new Error('Plugin executor and name is required');
}
});
return this;
}
async listen() {
// ? {-view-} here is replaced at build time to html
const UI = await readFile(html_path, {
encoding: "utf-8",
});
if (!this.options.source) {
LOG.log('Jetpath: Provide a source directory to avoid scanning the root directory', 'warn');
}
LOG.log('Compiling...', 'info');
const startTime = performance.now();
const localIP = getLocalIP();
// ? Load all jetpath functions described in user code
const errorsCount = await getHandlers(this.options?.source || '.', true);
const endTime = performance.now();
// LOG.log("Compiled!");
//? compile API
const [handlersCount, compiledAPI] = compileAPI(this.options);
// ? render API in UI
if (this.options?.apiDoc?.display === 'UI') {
this.api_UI_req(UI);
LOG.log(`Compiled ${handlersCount} Functions\nTime: ${Math.round(endTime - startTime)}ms`, 'info');
//? generate types
if (/(ON|WARN)/.test(this.options?.strictMode || 'OFF')) {
await codeGen(this.options.source || '.', this.options.strictMode, { local: `http://localhost:${this.options.port}`, external: `http://${localIP}:${this.options.port}` }, this.options.generatedRoutesFilePath);
}
LOG.log(`APIs: Viewable at http://localhost:${this.options.port}${this.options?.apiDoc?.path || '/api-doc'}`, 'info');
}
else if (this.options?.apiDoc?.display === 'HTTP') {
//? generate types
await codeGen(this.options.source || '.', this.options?.strictMode, { local: `http://localhost:${this.options.port}`, external: `http://${localIP}:${this.options.port}` }, this.options.generatedRoutesFilePath);
// ? render API in a .HTTP file
await fs().writeFile('api-doc.http', compiledAPI);
LOG.log(`Compiled ${handlersCount} Functions\nTime: ${Math.round(endTime - startTime)}ms`, 'info');
LOG.log(`APIs: written to ${fs().sep}api-doc.http`, 'info');
}
if (errorsCount) {
for (let i = 0; i < errorsCount.length; i++) {
LOG.log(`\nReport: ${errorsCount[i].file} file was not loaded due to \n "${errorsCount[i].error}" error; \n please resolve!`, 'warn');
}
}
//
assignMiddleware(_JetPath_paths, _jet_middleware);
// ? start server
// ? check if server is already listening
this.server = server(this.plugs, this.options);
// ? add plugins to the server
if (this.server.edge &&
typeof this.options.edgeGrabber?.length === 'number') {
await getHandlersEdge(this.options.edgeGrabber);
if (this.options?.apiDoc?.display === 'UI') {
this.api_UI_req(UI);
}
this.server.listen();
LOG.log('Jetpath: Edge is enabled', 'success');
return;
}
else if (this.server.edge && !this.options.edgeGrabber?.length) {
// ? edge is enabled but no edgeGrabber provided
throw new Error('Jetpath: the runtime is Edge is enabled but no edgeGrabber provided. Please provide edgeGrabber in options.');
}
this.listening = true;
this.server.listen(this.options.port);
LOG.log(`Open http://localhost:${this.options.port}`, 'info');
// ? show external IP
if (localIP) {
LOG.log(`External: http://${localIP}:${this.options.port}`, 'info');
}
}
api_UI_req(UI) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, compiledAPI] = compileAPI(this.options);
const name = this.options?.apiDoc?.path || '/api-doc';
_JetPath_paths_trie['GET'].insert(name, (ctx) => {
UI = compileUI(UI, this.options, compiledAPI);
if (this.options.apiDoc?.username && this.options.apiDoc?.password) {
const authHeader = ctx.get('authorization');
if (authHeader && authHeader.startsWith('Basic ')) {
const [authType, encodedToken] = authHeader.trim().split(' ');
if (authType !== 'Basic' || !encodedToken) {
ctx.set('WWW-Authenticate', 'Basic realm=Jetpath API Doc');
ctx.send('<h1>Unauthorized</h1>', 401, 'text/html');
return;
}
let username, password;
try {
const decodedToken = new TextDecoder().decode(Uint8Array.from(atob(encodedToken), (c) => c.charCodeAt(0)));
[username, password] = decodedToken.split(':');
}
catch (error) {
ctx.set('WWW-Authenticate', 'Basic realm=Jetpath API Doc');
ctx.send('<h1>Unauthorized</h1>', 401, 'text/html');
return;
}
if (password === this.options?.apiDoc?.password &&
username === this.options?.apiDoc?.username) {
ctx.send(UI, 200, 'text/html');
return;
}
else {
ctx.set('WWW-Authenticate', 'Basic realm=Jetpath API Doc');
ctx.send('<h1>Unauthorized</h1>', 401, 'text/html');
return;
}
}
else {
ctx.set('WWW-Authenticate', 'Basic realm=Jetpath API Doc');
ctx.send('<h1>Unauthorized</h1>', 401, 'text/html');
return;
}
}
else {
ctx.send(UI, 200, 'text/html');
return;
}
});
}
}
export { JetServer } from './primitives/classes.js';
export { use } from './primitives/functions.js';
export { mime } from './extracts/mimejs-extract.js';