-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
191 lines (171 loc) · 6.91 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import hook from 'require-in-the-middle';
import shimmer from 'shimmer';
/** Used to track function is patched or not to avoid patching it multiple times. */
const wrapSymbol = Symbol('px5wrapped');
/** Used to store path stack in req. */
const PX5_PATH_STACK = "__px5PathStack";
/** Used to store current mount path in req. */
const PX5_PATH = "__px5Path";
/** Used to determine if stack is frozen or not. */
const PX5_PATH_STACK_FREEZE = "__px5PathStackFreeze";
type PatchInfo = {
patched: boolean;
};
type WrapInfo = {
path: string;
};
type WrappedFunction = {
[wrapSymbol]?: boolean;
(...args: any[]): any;
}
/** Type for a request which has px5 properties added to it */
export type Px5Request = Express.Request & { [PX5_PATH]?: string, [PX5_PATH_STACK]?: (string|RegExp)[], [PX5_PATH_STACK_FREEZE]?: boolean };
/** Type for a response with some extra properties */
export type Px5Response = Express.Response & { __wrapped: boolean, send: (...args: any[]) => any };
/** Used to store info related to current patch of express. */
const patchInfo: PatchInfo = { patched: false };
/** express's mounting methods which are needed to be patched. */
const methods = ['use', 'get', 'post', 'put', 'delete', 'patch'];
/** This function pacthes express. */
export function patch() {
hook(['express'], function (exports: any, name: any, basedir: any) {
if (patchInfo.patched) return exports;
for(const method of methods) {
shimmer.wrap(exports.application, method, wrapMethod);
shimmer.wrap(exports.Router, method, wrapMethod);
}
patchInfo.patched = true;
return exports;
});
}
/**
* Wraps function such that all the arguments passed to are checked for string/regex or function or array of either those.
* - If it is string/regex or array of those then its considered mount path after converting to string
* - if it is function or array of functions then functions are warpped using wrapArgs
*
* finally original function is called and its return value is returned.
*/
function wrapMethod(original: () => any) {
if(original.__wrapped) return original;
const wrapped = function (...args: any[]) {
let path = args[0];
const isRouteArrayOfFunctions = path instanceof Array && typeof path[0] == 'function';
if(typeof path != 'function' && !isRouteArrayOfFunctions) path = String(path);
//@ts-ignore
if(!path) return original.apply(this, args);
const info: WrapInfo = { path };
wrapArgs(args, info);
// @ts-ignore
return original.apply(this, args);
}
mimicFunction(original, wrapped);
return wrapped;
}
/** Checks and wraps middleware functions */
function wrapArgs(args: any[], info: WrapInfo) {
for(let i = 1; i < args.length; i++) {
const middleware = args[i];
if(typeof middleware == 'function') {
const wrapped = wrapMiddlewares(middleware, info);
if(wrapped != middleware) args[i] = wrapped;
} else if(middleware instanceof Array && typeof middleware[0] == 'function') {
wrapArgs(middleware, info);
}
}
}
/**
* Wraps function to find position for req, res, next in function arguments and performs these actions
* - req: adds stack if not present and pushes the mount path if stack not frozen
* - res: wraps it
* - next: wraps it
*/
function wrapMiddlewares(original: WrappedFunction, info: WrapInfo) {
if(original[wrapSymbol]) return original;
const arity = original.length;
let reqIdx: number|undefined;
let nextIdx: number|undefined;
let resIdx: number|undefined;
if(arity == 2) {
reqIdx = 0;
resIdx = 1;
} else if (arity == 3) {
reqIdx = 0;
resIdx = 1;
nextIdx = 2;
} else if (arity == 4) {
reqIdx = 1;
resIdx = 2;
nextIdx = 3;
}
if(typeof reqIdx == undefined) return original;
let wrapped: WrappedFunction = function (...args) {
const req: Px5Request|null = typeof reqIdx == 'number' ? args[reqIdx] : null;
const next = typeof nextIdx == 'number' ? args[nextIdx] : null;
const res: Px5Response|null = typeof resIdx == 'number' ? args[resIdx] : null;
//@ts-ignore
if(!req) return original.apply(this, args);
req[PX5_PATH] = info.path;
if(!req[PX5_PATH_STACK_FREEZE]) {
if(!req[PX5_PATH_STACK]) req[PX5_PATH_STACK] = [];
req[PX5_PATH_STACK].push(info.path);
}
if(next && nextIdx) {
const wrappedNext = wrapNext(next, req, info);
if(next != wrappedNext) args[nextIdx] = wrappedNext;
}
if(res) wrapResSend(res, req, info);
//@ts-ignore
const returnValue = original.apply(this, args);
return returnValue;
}
mimicFunction(original, wrapped);
wrapped[wrapSymbol] = true;
return wrapped;
}
/** Wraps it such that whenever next is called and if path stack is not frozen then pops the path */
function wrapNext(original: WrappedFunction, req: Px5Request, info: WrapInfo) {
if(original[wrapSymbol]) return original;
const wrapped: WrappedFunction = function (...args: any[]) {
const isError = args[0] instanceof Error;
if(!req[PX5_PATH_STACK_FREEZE] && !isError) req[PX5_PATH_STACK]?.pop();
//@ts-ignore
return original.apply(this, args);
}
mimicFunction(original, wrapped);
wrapped[wrapSymbol] = true;
return wrapped;
}
/** Wraps send to freeze path stack whenever it is called */
function wrapResSend(res: Px5Response, req: Px5Request, info: WrapInfo) {
shimmer.wrap(res, 'send', function (original: (...args: any[]) => any) {
if(original.__wrapped) return original;
const wrapped = function (...args: any[]) {
freezeStack(req);
// @ts-ignore
return original.apply(this, args);
}
mimicFunction(original, wrapped);
return wrapped;
});
}
/** Copies name and length properties from source to destination */
function mimicFunction(original: () => any, mime: () => any) {
const originalNameDesc = Object.getOwnPropertyDescriptor(original, 'name');
if(originalNameDesc) Object.defineProperty(mime, 'name', originalNameDesc);
const originalLengthDesc = Object.getOwnPropertyDescriptor(original, 'length');
if(originalLengthDesc) Object.defineProperty(mime, 'length', originalLengthDesc);
}
/** Combines paths in the stack to form complete route. This will fix any extra leading and trailing slash '/'. */
export function getNormalizedPath(req: Px5Request) {
if(req[PX5_PATH_STACK] && req[PX5_PATH_STACK].length > 0) {
let path = req[PX5_PATH_STACK].join('');
path = '/' + path.replace(/^(?:\/+)([^/])/, '$1').replace(/\/+$/, '');
return path;
}
}
export function isStackFrozen(req: Px5Request) {
return !!req[PX5_PATH_STACK_FREEZE];
}
export function freezeStack(req: Px5Request) {
req[PX5_PATH_STACK_FREEZE] = true;
}