|
|
@@ -0,0 +1,133 @@
|
|
|
+const HTTP_METHODS = [
|
|
|
+ 'get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all'
|
|
|
+];
|
|
|
+
|
|
|
+export function createRouteTracker(app) {
|
|
|
+ const routes = [];
|
|
|
+
|
|
|
+ /* ---- Hook app.METHOD() ---- */
|
|
|
+ for (const method of HTTP_METHODS) {
|
|
|
+ const original = app[method].bind(app);
|
|
|
+
|
|
|
+ app[method] = (path, ...handlers) => {
|
|
|
+ const meta = extractMeta(handlers);
|
|
|
+
|
|
|
+ routes.push({
|
|
|
+ path,
|
|
|
+ methods: method === 'all' ? ['ALL'] : [method.toUpperCase()],
|
|
|
+ input: meta.input,
|
|
|
+ output: meta.output,
|
|
|
+ cookies: meta.cookies
|
|
|
+ });
|
|
|
+
|
|
|
+ return original(path, ...handlers);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---- Hook app.use() ---- */
|
|
|
+ const originalUse = app.use.bind(app);
|
|
|
+
|
|
|
+ app.use = (path, ...handlers) => {
|
|
|
+ if (typeof path === 'string') {
|
|
|
+ for (const h of handlers) {
|
|
|
+ if (h?.stack) {
|
|
|
+ trackRouter(h, path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return originalUse(path, ...handlers);
|
|
|
+ };
|
|
|
+
|
|
|
+ function trackRouter(router, prefix) {
|
|
|
+ for (const layer of router.stack) {
|
|
|
+ if (!layer.route) continue;
|
|
|
+
|
|
|
+ const methods = Object.keys(layer.route.methods)
|
|
|
+ .filter(Boolean)
|
|
|
+ .map(m => m.toUpperCase());
|
|
|
+
|
|
|
+ const meta = extractMeta(layer.route.stack.map(l => l.handle));
|
|
|
+
|
|
|
+ routes.push({
|
|
|
+ path: normalize(prefix + layer.route.path),
|
|
|
+ methods,
|
|
|
+ input: meta.input,
|
|
|
+ output: meta.output,
|
|
|
+ cookies: meta.cookies
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ list: () => routes,
|
|
|
+ visualize
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Extract metadata from handler comments ---- */
|
|
|
+function extractMeta(handlers) {
|
|
|
+ let input = null;
|
|
|
+ let output = null;
|
|
|
+ let cookies = null;
|
|
|
+
|
|
|
+ for (const fn of handlers) {
|
|
|
+ if (typeof fn !== 'function') continue;
|
|
|
+
|
|
|
+ const src = fn.toString();
|
|
|
+
|
|
|
+ const inMatch = src.match(/\/\/\s*@input\s+\{([^}]+)\}/);
|
|
|
+ const outMatch = src.match(/\/\/\s*@output\s+\{([^}]+)\}/);
|
|
|
+ const cookieMatch = src.match(/\/\/\s*@cookies\s+\{([^}]+)\}/);
|
|
|
+
|
|
|
+ if (inMatch) input = inMatch[1].split(',').map(v => v.trim());
|
|
|
+ if (outMatch) output = outMatch[1].split(',').map(v => v.trim());
|
|
|
+ if (cookieMatch) cookies = cookieMatch[1].split(',').map(v => v.trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ return { input, output, cookies };
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Normalize paths ---- */
|
|
|
+function normalize(path) {
|
|
|
+ return path.replace(/\/+/g, '/');
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Developer-friendly table ---- */
|
|
|
+function visualize() {
|
|
|
+ const rows = this.list().map(r => {
|
|
|
+ const formatArray = (arr) => {
|
|
|
+ if (!arr || arr.length === 0) return '-';
|
|
|
+ return arr.map(item => {
|
|
|
+ if (typeof item === 'string' && item.startsWith('in:')) return `\x1b[32m${item}\x1b[0m`; // green
|
|
|
+ if (typeof item === 'string' && item.startsWith('out:')) return `\x1b[34m${item}\x1b[0m`; // blue
|
|
|
+ return item;
|
|
|
+ }).join(', ');
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ METHOD: r.methods.join(','),
|
|
|
+ PATH: r.path,
|
|
|
+ INPUT: formatArray(r.input),
|
|
|
+ OUTPUT: formatArray(r.output),
|
|
|
+ COOKIES: formatArray(r.cookies)
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // compute column widths
|
|
|
+ const headers = ['METHOD','PATH','INPUT','OUTPUT','COOKIES'];
|
|
|
+ const widths = headers.map(h => Math.max(
|
|
|
+ h.length,
|
|
|
+ ...rows.map(r => r[h].length)
|
|
|
+ ));
|
|
|
+
|
|
|
+ // print header
|
|
|
+ let line = headers.map((h,i)=>h.padEnd(widths[i])).join(' | ');
|
|
|
+ console.log(line);
|
|
|
+ console.log(widths.map(w=>'─'.repeat(w)).join('-|-'));
|
|
|
+
|
|
|
+ // print rows
|
|
|
+ for (const r of rows) {
|
|
|
+ console.log(headers.map((h,i)=>r[h].padEnd(widths[i])).join(' | '));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|