UNPKG

rg-express

Version:

Welcome to the **rg-express** user guide! This document will help you get started with **rg-express**, a powerful route generator for Express that simplifies route management with file-based approach. Whether you're building APIs in TypeScript or JavaScri

496 lines (482 loc) 20.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var express = require('express'); var fs = require('fs'); var path = require('path'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs); var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); // export class ProcessConsole { // private loadingInterval: NodeJS.Timeout | null; // constructor() { // this.loadingInterval = null; // } // private setColor(color: string, message: string): string { // return `\x1b[${color}m${message}\x1b[0m`; // } // start(message: string, frames: string[] = ['⠹', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], speed: number = 100): void { // let i = 0; // process.stdout.write(this.setColor('34', message + ' ')); // this.loadingInterval = setInterval(() => { // process.stdout.write('\r' + this.setColor('34', frames[i] + ' ' + message + ' ')); // i = (i + 1) % frames.length; // }, speed); // } // stop(): void { // if (this.loadingInterval !== null) { // clearInterval(this.loadingInterval); // } // } // complete(successMessage: string): void { // this.stop(); // process.stdout.write(`\r${this.setColor('1;92', '✔')} ${this.setColor('38;5;2', successMessage)}\n`); // } // error(errorMessage: string): void { // this.stop(); // process.stdout.write(`\r${this.setColor('31',errorMessage)} \n`); // } // false(errorMessage: string): void { // this.stop(); // process.stdout.write(`\r${this.setColor('31','✗')} ${this.setColor('30', errorMessage)} \n`); // } // } // // // Example usage // // const console = new ProcessConsole(); // // console.start('Processing'); // // // Perform some task... // // console.complete('Task completed successfully.'); class ProcessConsole { constructor() { this.loadingInterval = null; this.DEFAULT_FRAMES = ['⠹', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; this.DEFAULT_SPEED = 100; // ANSI color codes as constants for better maintainability this.COLORS = { blue: '34', red: '31', green: '38;5;2', brightGreen: '1;92', black: '30' }; this.SYMBOLS = { success: '✔', failure: '✗' }; } formatMessage(color, message, symbol) { const prefix = symbol ? `${this.setColor(color, symbol)} ` : ''; return `\r${prefix}${this.setColor(color, message)}\x1b[0m`; } setColor(color, text) { return `\x1b[${color}m${text}`; } start(message, frames = this.DEFAULT_FRAMES, speed = this.DEFAULT_SPEED) { if (this.loadingInterval) { this.stop(); } let frameIndex = 0; process.stdout.write(this.setColor(this.COLORS.blue, `${message} `)); this.loadingInterval = setInterval(() => { process.stdout.write(this.formatMessage(this.COLORS.blue, `${frames[frameIndex]} ${message}`)); frameIndex = (frameIndex + 1) % frames.length; }, speed); } stop() { if (this.loadingInterval) { clearInterval(this.loadingInterval); this.loadingInterval = null; process.stdout.write('\r'); // Clear the line } } complete(successMessage) { this.stop(); process.stdout.write(this.formatMessage(this.COLORS.green, successMessage, this.setColor(this.COLORS.brightGreen, this.SYMBOLS.success)) + '\n'); } error(errorMessage) { this.stop(); process.stdout.write(this.formatMessage(this.COLORS.red, errorMessage) + '\n'); } fail(errorMessage) { this.stop(); process.stdout.write(this.formatMessage(this.COLORS.black, errorMessage, this.setColor(this.COLORS.red, this.SYMBOLS.failure)) + '\n'); } } // // Example usage with async operation // async function example() { // const console = new ProcessConsole(); // console.start('Processing task'); // try { // await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work // console.complete('Task completed successfully'); // } catch (error) { // console.fail('Task failed unexpectedly'); // } // } const httpMethods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE' ]; const httpMethodsSet = new Set(httpMethods); // export type HttpMethodUpper = typeof httpMethods[number]; function getSpreadParamsByRoute(route) { const matches = route.match(/\[\.\.\.([a-zA-Z0-9_]+)\]/g) || []; return matches.map(match => { var _a; return (_a = match.match(/\w+/)) === null || _a === void 0 ? void 0 : _a[0]; }).filter(Boolean); } function getSlugParamsByRoute(route) { const matches = route.match(/\[(?!\.\.\.)([a-zA-Z0-9_]+)\]/g) || []; return matches.map(match => { var _a; return (_a = match.match(/\w+/)) === null || _a === void 0 ? void 0 : _a[0]; }).filter(Boolean); } function checkForDuplicateParams(route) { const matches = [...route.matchAll(/\[(\.\.\.)?([a-zA-Z0-9_]+)\]/g)]; const params = matches.map(([, , param]) => param); // Using a Set to check for duplicates const uniqueParams = new Set(params); if (uniqueParams.size !== params.length) { throw new Error(`Duplicate parameter(s) detected in route: ${params.filter((param, index, self) => self.indexOf(param) !== index).join(', ')}`); } } function createRoutePath({ name, startDir }, fileExtension) { let route = name .replace(new RegExp(`^${startDir}/?`), '') .replace(new RegExp(`/?route\.${fileExtension}$`), '') || '/'; route = route.replace(/\[(\w+)\]/g, ':$1'); route = route.replace(/\[\.\.\.(\w+)\]/g, "{*$1}").replace(/\\/g, '/'); // if route is not start with / then add / if (!route.startsWith('/')) { route = '/' + route; } route = route.replace(/\/+/g, '/'); // Remove duplicate slashes return { route }; } function filterAndLowercaseHttpMethods(exportedHandlerNames) { return exportedHandlerNames .filter((name) => httpMethodsSet.has(name)) .map(method => method.toLowerCase()); } function isTypeScriptProject() { try { // const files = fs.readdirSync(process.cwd()); // return files.some(file => file.endsWith('.ts') || file.endsWith('.mts')) || fs.existsSync('tsconfig.json'); return fs.existsSync('tsconfig.json'); } catch (_a) { return false; } } /** * Determines the file extension of the current executing file (e.g., .ts, .js, .mjs, etc.) * by inspecting the call stack. * * @returns {string} The file extension (e.g., 'ts', 'js', 'mjs', etc.) */ function determineFileExtension() { var _a; const stack = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n')[3]; // Get the stack trace and grab the 3rd line (caller location) const match = stack === null || stack === void 0 ? void 0 : stack.match(/\((.*?\.([a-zA-Z]+)):\d+:\d+\)/); // Match file extension pattern return match ? match[2] : ''; // Return the file extension (e.g., 'ts', 'js', etc.) } // readFiles.ts class PathParser { static getPathPowers(filePath) { const segments = filePath.split('/'); const pathSplitLength = segments.length; let slugPower = 0; let slugsPower = 0; let slugStartAt = 0; let slugsStartAt = 0; segments.forEach((segment, index) => { if (segment.match(this.SLUG_PATTERN)) { slugPower += pathSplitLength - index; slugStartAt = slugStartAt || index + 1; } else if (segment.match(this.SPREAD_PATTERN)) { slugsPower += pathSplitLength - index; slugsStartAt = slugsStartAt || index + 1; } }); return { slugPower, slugsPower, pathSplitLength, slugStartAt, slugsStartAt }; } static getSlugsCount(filePath) { const matches = filePath.match(this.SLUG_PATTERN); return (matches === null || matches === void 0 ? void 0 : matches.length) || 0; } } PathParser.SLUG_PATTERN = /\[\w+\]/g; PathParser.SPREAD_PATTERN = /\[\.\.\.\w+\]/g; class PathSorter { static sortSpread(a, b) { if (a === b) return 0; const aPowers = PathParser.getPathPowers(a); const bPowers = PathParser.getPathPowers(b); return (bPowers.slugsStartAt - aPowers.slugsStartAt || bPowers.slugsPower - aPowers.slugsPower || bPowers.slugStartAt - aPowers.slugStartAt || bPowers.slugPower - aPowers.slugPower || bPowers.pathSplitLength - aPowers.pathSplitLength || a.localeCompare(b)); } static sortBasic(a, b) { if (a === b) return 0; const hasBracketA = a.includes('['); const hasBracketB = b.includes('['); if (hasBracketA !== hasBracketB) { return hasBracketA ? 1 : -1; } if (hasBracketA && hasBracketB) { const slugsDiff = PathParser.getSlugsCount(b) - PathParser.getSlugsCount(a); return slugsDiff !== 0 ? slugsDiff : b.length - a.length; } return a.localeCompare(b); } static sortFiles(fileList) { const basicFiles = fileList.filter(f => !f.includes('[...')); const spreadFiles = fileList.filter(f => f.includes('[...')); return [ ...basicFiles.sort(this.sortBasic), ...spreadFiles.sort(this.sortSpread) ]; } } class FileReader { static normalizePath(filePath) { return filePath.replace(/\\/g, '/'); } static isRouteFile(fileName, extension) { return fileName === `route.${extension}`; } static readFiles(directoryPath, fileExtension) { try { const normalizedDir = this.normalizePath(directoryPath); const entries = fs__namespace.readdirSync(normalizedDir, { withFileTypes: true }); const fileList = []; for (const entry of entries) { const entryPath = this.normalizePath(path__namespace.join(normalizedDir, entry.name)); if (entry.isDirectory()) { // skip dirs starting with "_" if (entry.name.startsWith("_")) continue; fileList.push(...this.readFiles(entryPath, fileExtension)); continue; } if (entry.isFile()) { if (this.isRouteFile(entry.name, fileExtension)) { checkForDuplicateParams(entryPath); fileList.push(entryPath); } } } return PathSorter.sortFiles(fileList); } catch (error) { console.error("Error reading files:", error); return []; } } } function getValidFunctionName(input) { return input .replace(/[\[\]\/]+/g, '_') .replace(/_+/g, '_') .replace(/^_+|_+$/g, '') .replace(/\.\.\.\w+/g, ''); } /** * Helper function to create the type definition for request parameters. */ function createRequestType(slugParams, spreadParams) { const slugParamTypes = slugParams.map(p_name => `${p_name}: string;`).join(' '); const spreadParamTypes = spreadParams.map(p_name => `${p_name}: string[];`).join(' '); return `type Request = ExpressRequest<{ ${slugParamTypes} ${spreadParamTypes} }>;`; } /** * Generates the content for the route file based on the parameters and language. */ function generateContent(fileExtension, allParams, slugParams, spreadParams, funcName, message) { switch (fileExtension) { case 'ts': case 'mts': return allParams.length ? `import { Request as ExpressRequest, Response } from 'express'; ${createRequestType(slugParams, spreadParams)} export const GET = async (req: Request, res: Response) => { const { ${allParams.join(', ')} } = req.params; res.send({ ${allParams.join(', ')} }); };` : `import { Request, Response } from 'express'; export const GET = async (req: Request, res: Response) => { res.send('${message}'); };`; case 'js': return allParams.length ? `const ${funcName} = async (req, res) => { const { ${spreadParams.join(', ')} } = req.params; res.send({ ${spreadParams.join(', ')} }); }; module.exports = { GET: ${funcName} };` : `const ${funcName} = async (req, res) => { res.send('${message}'); }; module.exports = { GET: ${funcName} };`; case 'mjs': return allParams.length ? `export const GET = async (req, res) => { const { ${spreadParams.join(', ')} } = req.params; res.send({ ${spreadParams.join(', ')} }); };` : `export const GET = async (req, res) => { res.send('${message}'); };`; default: throw new Error('Unsupported file extension'); } } /** * Writes the startup code to a file if it does not exist or if it is empty. */ function writeToFileSyncStartupCode(startDir, filename, { codeSnippet, codeSnippetFn }) { const spreadParams = getSpreadParamsByRoute(filename); // [...slug] const slugParams = getSlugParamsByRoute(filename); // [userId] const allParams = [...slugParams, ...spreadParams]; // Combined const relativePath = filename.substring(startDir.length + 1, filename.lastIndexOf('/')); const funcName = relativePath ? getValidFunctionName(`handleGet${relativePath.replace(/^\w/, c => c.toUpperCase()).replace(/-(\w)/g, (_, g) => g.toUpperCase())}`) : 'handleGetRequest'; const message = relativePath || 'hello'; const ext = filename.split('.').pop(); const content = (codeSnippetFn && codeSnippetFn({ allParams, slugParams, spreadParams })) || codeSnippet || generateContent(ext, allParams, slugParams, spreadParams, funcName, message); try { // Check if the file exists and if its content is empty if (!fs__namespace.existsSync(filename) || !fs__namespace.readFileSync(filename, 'utf8').trim()) { fs__namespace.writeFileSync(filename, content); } } catch (error) { console.error('Error writing startup code:', error); } } const consoleP = new ProcessConsole(); const consoleP2 = new ProcessConsole(); function routes(config) { const normalizedConfig = typeof config === 'string' ? { baseDir: config, autoSetup: false, routeGenIfEmpty: false } : config; // Log a deprecation warning if 'autoSetup' is used if (normalizedConfig.autoSetup) { console.warn('[DEPRECATED] "autoSetup" is deprecated. Please use "routeGenIfEmpty" instead.'); } console.time('✓ Ready in'); const codeSnippet = typeof normalizedConfig.routeGenIfEmpty === 'object' && 'codeSnippet' in normalizedConfig.routeGenIfEmpty ? normalizedConfig.routeGenIfEmpty.codeSnippet : undefined; const codeSnippetFn = typeof normalizedConfig.routeGenIfEmpty === 'object' && 'codeSnippetFn' in normalizedConfig.routeGenIfEmpty ? normalizedConfig.routeGenIfEmpty.codeSnippetFn : undefined; const isRouteGenWIthObject = codeSnippet !== undefined || codeSnippetFn !== undefined; const isRouteGenWithBoolean = typeof normalizedConfig.routeGenIfEmpty === 'boolean' && normalizedConfig.routeGenIfEmpty === true; const isRouteGenIfEmpty = Boolean(isRouteGenWIthObject || isRouteGenWithBoolean || normalizedConfig.autoSetup); const fileExtension = determineFileExtension(); validateFileExtension(fileExtension); consoleP2.complete(`${isTypeScriptProject() ? 'TypeScript' : fileExtension}`); const startDir = `${normalizedConfig.baseDir}/routes`.replace(/\\/g, '/'); const isTsProject = isTypeScriptProject(); const canAutoSetup = isTsProject ? fileExtension === 'ts' : true; const shouldAutoSetup = canAutoSetup && isRouteGenIfEmpty; if (isRouteGenIfEmpty) { shouldAutoSetup ? consoleP2.complete(`AutoSetup: routeGenIfEmpty is enabled`) : consoleP2.fail(`AutoSetup is disabled. To enable, ensure that your project is a '${fileExtension}' project and set 'routeGenIfEmpty' to true or provide a code snippet.`); } consoleP.start('Route processing'); const router = normalizedConfig.app || express.Router(); const fileList = FileReader.readFiles(startDir, fileExtension); if (fileList.length) { processRoutes(fileList, startDir, router, shouldAutoSetup, fileExtension, { codeSnippet, codeSnippetFn }); } consoleP.complete('Route processing complete'); console.timeEnd('✓ Ready in'); if (!normalizedConfig.app) { return router; } } function validateFileExtension(ext) { if (!['ts', 'js', 'mjs', 'mts'].includes(ext)) { throw new Error('Invalid file extension detected'); } } function processRoutes(fileList, startDir, router, autoSetup, lang, { codeSnippet, codeSnippetFn }) { const routeDefinitions = fileList.map(filePath => { const { route: expressRoutePath } = createRoutePath({ name: filePath, startDir }, lang); const exportedHandlers = require(filePath); const supportedMethods = filterAndLowercaseHttpMethods(Object.keys(exportedHandlers)); const methodHandlers = supportedMethods.map(httpMethod => { return { method: httpMethod, handler: exportedHandlers[httpMethod.toUpperCase()] }; }); return { route: expressRoutePath, supportedMethods, filePath, methodHandlers }; }); routeDefinitions.forEach(({ filePath, methodHandlers, route }) => { if (autoSetup) { writeToFileSyncStartupCode(startDir, filePath, { codeSnippet, codeSnippetFn }); } methodHandlers.forEach(({ method, handler }) => { // check validity of handler // handler can be a function or an array of functions (for middleware) let isValidHandler = false; let spacial_case = ''; if (typeof handler === 'function') { isValidHandler = true; } else if (typeof handler === 'object' && Array.isArray(handler)) { // if it's an array, and all elements are functions, then it's valid else store what item/index of element is invalid in spacial_case const allFunctions = handler.every((h, index) => { if (typeof h !== 'function') { spacial_case = `Element at index ${index} is not a function`; return false; } return true; }); if (allFunctions) { isValidHandler = true; } } if (!isValidHandler) { // consoleP.fail(`Handler for ${method.toUpperCase()} ${route} in ${filePath} is not a valid method handler. It should be a function or an array of functions.`); consoleP.fail(`Invalid handler for ${method.toUpperCase()} ${route} in ${filePath}. ${spacial_case ? `Issue: ${spacial_case}` : 'Handler should be a function or an array of functions.'}`); return; } router[method](route, handler); }); }); } const rg = Object.assign(routes, { routes }); exports.default = rg; exports.routes = routes;