@contrast/route-coverage 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,125 +25,90 @@ const METHODS = [
25
25
  'options',
26
26
  'head',
27
27
  ];
28
- // eslint-disable-next-line node/no-extraneous-require
28
+
29
29
  const fnInspect = require('@contrast/fn-inspect');
30
30
  const { createSignature, patchType } = require('../utils/route-info');
31
- const { join, replace, toLowerCase } = require('@contrast/common');
31
+ const { join, toLowerCase, isString } = require('@contrast/common');
32
32
 
33
- /**
34
- * @param {import('..').Core & {
35
- * routeCoverage: import('..').RouteCoverage & {
36
- * express?: import('@contrast/common').Installable
37
- * }
38
- * }} core
39
- */
40
33
  module.exports = function init(core) {
41
- const routers = new Map();
42
- const signatureMap = new Map();
43
34
  const { patcher, depHooks, routeCoverage } = core;
44
-
45
- function formatUrl(url) {
46
- if (Array.isArray(url)) {
47
- return `/[${join(url, ', ')}]`;
48
- } else if (url instanceof RegExp) {
49
- return `/{${replace(url.toString(), /(^\/?)|(\/?$)/g, '')}}`;
50
- } else {
51
- return url;
52
- }
35
+ const discover = (route) => routeCoverage.discover(route);
36
+ const observe = (route) => routeCoverage.observe(route);
37
+
38
+ const isRoute = (layer) => !!layer.route;
39
+ const isRouter = (layer) => toLowerCase(layer.name) === 'router';
40
+ const isValidPath = (path) => isString(path) || Array.isArray(path) || path instanceof RegExp;
41
+ const regExpToPath = (regex) => regex?.source?.split('/?')[0].replace(/\\/g, '').replace('^', ''); //TODO: replaceAll when v14 deprecated
42
+ const format = (url) => Array.isArray(url) ? `/[${join(url)}]` : url instanceof RegExp ? `/{${url.toString().slice(1, -1)}}` : url;
43
+ const getHandleMethod = (layer) => fnInspect.funcInfo(layer.__handle)?.file.includes('express-async-errors') ? '__handle' : 'handle';
44
+
45
+ function parseRoute(route) {
46
+ const { path } = route;
47
+ const method = route.methods._all ? 'all' : route.stack[0].method;
48
+ return { url: format(path), method };
53
49
  }
54
50
 
55
- function getLastLayer(router) {
56
- if (router.stack) {
57
- const len = router.stack.length;
58
- return router.stack[len - 1];
59
- }
60
- }
61
-
62
- function getLayerHandleMethod(layer) {
63
- let methodName = 'handle';
64
- const __handleData = fnInspect.funcInfo(layer.__handle);
65
- if (__handleData && __handleData.file.includes('express-async-errors')) {
66
- methodName = '__handle';
67
- }
68
- return methodName;
51
+ function createRouteInfo(url, method, obj) {
52
+ return {
53
+ signature: createSignature(url, method, obj),
54
+ url,
55
+ normalizedUrl: url,
56
+ method
57
+ };
69
58
  }
70
59
 
71
- function patchHandler(layer, route) {
72
- if (!layer) return;
73
- const handle = getLayerHandleMethod(layer);
60
+ function patchHandle(layer, routeInfo) {
61
+ const handle = getHandleMethod(layer);
74
62
  patcher.patch(layer, handle, {
75
- name: 'express.Router.handle',
63
+ name: 'express.Route.handle',
76
64
  patchType,
77
- post(data) {
78
- const [req] = data.args;
79
- const method = req?.method && toLowerCase(req.method);
80
- const url = `${req.baseUrl}${req._parsedUrl.pathname}`;
81
- const { path } = req.route;
82
- const normalizedUrl = path instanceof RegExp ? path.toString() : path;
83
- const { signature } = signatureMap.get(route.signature);
84
- if (method) routeCoverage.observe({ signature, url, method, normalizedUrl });
65
+ post({ args }) {
66
+ const [req] = args;
67
+ const [url] = req.originalUrl.split('?');
68
+ const { method } = req;
69
+ if (url && method) {
70
+ observe({ ...routeInfo, url, method: toLowerCase(method) });
71
+ }
85
72
  }
86
73
  });
87
74
  }
88
75
 
89
- function createRoute(url, method, id) {
90
- const signature = createSignature(url, method);
91
- const route = { signature, url, method, id: id || signature };
92
- signatureMap.set(signature, route);
93
- return route;
94
- }
95
-
96
- function discoverRoute({ signature, url, method }) {
97
- routeCoverage.discover({ signature, url, method, normalizedUrl: url });
98
- }
99
-
100
- function instrumentRoute(router, route) {
101
- if (!router) return;
102
- const layer = getLastLayer(router);
103
- patchHandler(layer, route);
104
- }
105
-
106
- function updateRoutes(prefix, router, updatedRouter) {
107
- const routes = routers.get(router);
108
- const updatedLayers = router === updatedRouter
109
- ? [] : (routers.get(updatedRouter) || []);
110
- if (routes) {
111
- routes.forEach((route) => {
112
- const { url, method, id } = route;
113
- const newRoute = createRoute(`${prefix}${url}`, method, id);
114
- updatedLayers.push(newRoute);
115
- signatureMap.set(id, newRoute);
116
- });
117
- routers.set(router, updatedLayers);
118
- routers.set(updatedRouter, updatedLayers);
119
- }
76
+ function traverse(path, stack, depth = 0) {
77
+ path = format(path);
78
+ stack.forEach((layer) => {
79
+ if (isRoute(layer)) {
80
+ const { url, method } = parseRoute(layer.route);
81
+ const routeInfo = createRouteInfo(path + url, method);
82
+ discover(routeInfo);
83
+ patchHandle(layer, routeInfo);
84
+ } else if (isRouter(layer)) {
85
+ const regexPath = regExpToPath(layer.regexp);
86
+ if (depth < 3) traverse(path + regexPath, layer.handle.stack, depth += 1);
87
+ } else {
88
+ const regexPath = regExpToPath(layer.regexp);
89
+ const routeInfo = createRouteInfo(path + regexPath, 'use');
90
+ discover(routeInfo);
91
+ patchHandle(layer, routeInfo);
92
+ }
93
+ });
120
94
  }
121
-
122
95
  return core.routeCoverage.express = {
123
96
  install() {
124
97
  depHooks.resolve({ name: 'express' }, (express) => {
125
- patcher.patch(express.Router, 'use', {
126
- name: 'express.Router.use',
127
- patchType,
128
- post({ args, result }) {
129
- const [prefix, router] = args;
130
- if (typeof prefix === 'string' && prefix !== '/') {
131
- updateRoutes(prefix, router, result);
132
- }
133
- }
134
- });
135
-
136
98
  patcher.patch(express.application, 'use', {
137
99
  name: 'express.application.use',
138
100
  patchType,
139
- post({ args }) {
140
- const idx = args.length;
141
- const router = args[idx - 1];
142
- const routes = routers.get(router);
143
- if (routes) {
144
- routes.forEach((route) => {
145
- discoverRoute(route);
146
- });
101
+ post({ args, result }) {
102
+ const len = args.length;
103
+ const fn = args[len - 1];
104
+ const path = len > 1 ? args[0] : '';
105
+ if (!isValidPath(path)) return;
106
+ if (isRouter(fn)) {
107
+ traverse(path, fn.stack);
108
+ } else {
109
+ const routeInfo = createRouteInfo(path, 'use', 'App');
110
+ discover(routeInfo);
111
+ patchHandle(result._router, routeInfo);
147
112
  }
148
113
  }
149
114
  });
@@ -154,25 +119,10 @@ module.exports = function init(core) {
154
119
  patchType,
155
120
  post({ args, result }) {
156
121
  const [url, fn] = args;
157
- if (!url || !fn) return;
158
- const route = createRoute(formatUrl(url), method);
159
- instrumentRoute(result?._router, route);
160
- discoverRoute(route);
161
- }
162
- });
163
-
164
- patcher.patch(express.Router, method, {
165
- name: `express.Router.${method}`,
166
- patchType,
167
- post({ args, obj: router }) {
168
- const [url, fn] = args;
169
- if (!url || !fn) return;
170
- const route = createRoute(formatUrl(url), method);
171
- const routes = routers.get(router) || [];
172
-
173
- instrumentRoute(router, route);
174
- routes.push(route);
175
- routers.set(router, routes);
122
+ if (!url || !fn || !isValidPath(url)) return;
123
+ const routeInfo = createRouteInfo(format(url), method, 'App');
124
+ discover(routeInfo);
125
+ patchHandle(result._router, routeInfo);
176
126
  }
177
127
  });
178
128
  });
@@ -34,13 +34,13 @@ module.exports = function init(core) {
34
34
  function registerRouteHandler(routeOptions) {
35
35
  if (!routeOptions || !routeOptions.method || !routeOptions.url) return;
36
36
 
37
- const url = `${routeOptions.prefix || ''}${routeOptions.url}`;
38
- if (Array.isArray(routeOptions.method)) {
39
- routeOptions.method.forEach((method) => {
37
+ const { url, method } = routeOptions;
38
+ if (Array.isArray(method)) {
39
+ method.forEach((method) => {
40
40
  emitRouteCoverage(url, method);
41
41
  });
42
42
  } else {
43
- emitRouteCoverage(url, routeOptions.method);
43
+ emitRouteCoverage(url, method);
44
44
  }
45
45
 
46
46
  // TODO
@@ -22,8 +22,8 @@ const patchType = 'route-coverage';
22
22
  * @param {string} method
23
23
  * @return {string} formatted signature
24
24
  */
25
- function createSignature(path, method = '') {
26
- return `Router.${method}('${path}', [Function])`;
25
+ function createSignature(path, method = '', obj = 'Router') {
26
+ return `${obj}.${method}('${path}', [Function])`;
27
27
  }
28
28
 
29
29
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/route-coverage",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Handles route discovery and observation",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -11,13 +11,13 @@
11
11
  "types": "lib/index.d.ts",
12
12
  "engines": {
13
13
  "npm": ">=6.13.7 <7 || >= 8.3.1",
14
- "node": ">= 14.15.0"
14
+ "node": ">= 14.18.0"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/common": "1.20.0",
20
+ "@contrast/common": "1.20.1",
21
21
  "@contrast/fn-inspect": "^4.0.0"
22
22
  }
23
23
  }