-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRoute.ts
189 lines (161 loc) · 6.71 KB
/
Route.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
import type {
RouteCompilationResult, RouteDetails, RouteParametersWithQuery, RouteTokens
} from "@/types/Route.types";
import type { Router } from "@/classes/Router";
import { ensureNoTrailingSlash, getLocationAndQuery, isBlank } from "@/helpers/utils.js";
import { parse } from "qs";
/**
* @classdesc A class representing a route.
*/
export class Route {
#name: string;
#details: RouteDetails;
#router: Router;
constructor(name: string, details: RouteDetails, router: Router) {
this.#name = name;
this.#details = details;
this.#router = router;
}
/**
* Retruns the route's origin
*/
get origin(): string {
const hasDomain = !isBlank(this.#details.domain);
// if route has a domain, always return an absolute origin.
if (hasDomain) {
const scheme = this.#router.base.match(/^(http|https):\/\//);
return ensureNoTrailingSlash((scheme?.[0] ?? '') + this.#details.domain);
}
if (!this.#router.config.absolute)
return '';
// no domain, return absolute origin.
return ensureNoTrailingSlash(this.#router.origin);
}
/**
* Retruns the route's template
*/
get template(): string {
const template = ensureNoTrailingSlash(`${this.origin}/${this.#details.uri}`);
return isBlank(template) ? '/' : template;
}
/**
* Retruns the route's template expected parameters
*/
get expects(): RouteTokens {
const tokens: RouteTokens = {};
const matches = this.template.match(/{\w+\??}/g) ?? [];
for (const m of matches) {
const name = m.replace(/\W/g, '');
// if this parameter name appears more than once in the template
// make sure to mark it as required if it is required in all of
// the template occurences.
tokens[name] = m.includes('?') || (tokens[name] ?? false);
}
return tokens;
}
/**
* Return the compiled URI for this route, along with an array of substituted tokens.
*/
compile(params: RouteParametersWithQuery): RouteCompilationResult {
const substituted = new Array<string>();
const tokens = this.expects;
const tokenKeys = Object.keys(tokens);
if (tokenKeys.length < 1)
return { substituted, url: this.template };
let template = this.template;
for (const token of tokenKeys) {
const optional = tokens[token]
let paramValue = params?.[token] ?? this.#router.config.defaults?.[token] ?? '';
if (typeof paramValue == 'boolean') {
paramValue = paramValue ? 1 : 0;
}
const replacement = String(paramValue);
if (!optional) {
if (isBlank(replacement)) {
throw new Error(
`Missing required parameter "${token}" for route "${this.#name}"`
);
}
if (Object.hasOwn(this.#details.wheres, token)) {
const where = this.#details.wheres[token];
const matches = new RegExp(`^${where}$`).test(replacement);
if (!matches) {
throw new Error(
`Parameter "${token}" for route "${this.#name}" ` +
`does not match format "${where}"`
);
}
}
}
const re = new RegExp(`{${token}\\??}`, 'g');
if (re.test(template)) {
const encoded = encodeURIComponent(replacement);
template = ensureNoTrailingSlash(template.replace(re, encoded));
substituted.push(token);
/**
* Not too sure what to do about this.
* For now we will encode everything and warn (or error if strict mode),
* if a parameter contains slashes.
* @see https://github.com/laravel/framework/issues/22125
* @see https://github.com/laravel/framework/blob/11.x/src/Illuminate/Routing/RouteUrlGenerator.php#L36
* @see https://github.com/tighten/ziggy/pull/662
*/
if (/\/|%2F/g.test(encoded)) {
const message = `Character "/" or sequence "%2F" in parameter "${token}" for ` +
`route "${this.#name}" might cause routing issues.`;
if (this.#router.config.strict) {
throw new Error(
message +
'\n\tAn error was thrown because you enabled strict mode.\n'
);
} else {
console.warn(message);
}
}
}
}
return { substituted, url: template };
}
/**
* Determine if the current route template matches the given URL.
*/
matches(url: string): RouteParametersWithQuery | false {
const schemeRegex = /^[a-z]*:\/\//i;
let template = this.template;
// if this URL has no domain or has a domain that doesn not expect parameters
// then remove the origin part from the incoming URL as we do not need to match against it
if (!this.#details.domain?.includes('{')) {
url = url.replace(/^[a-z]*:\/\/([a-z]*\.?)*/i, '');
url += url.startsWith('/') ? '' : '/';
template = template.replace(/^[a-z]*:\/\/([a-z]*\.?)*/i, '');
template += template.startsWith('/') ? '' : '/';
} else {
url = url.replace(schemeRegex, '');
}
const { location, query } = getLocationAndQuery(url);
const escape_regex = /[/\\^$.|?*+()[\]{}]/g;
const token_regex = /\\{(\w+)(\\\?)?\\}/g;
const template_regex = template
.replace(schemeRegex, '')
.replace(escape_regex, '\\$&')
.replace(token_regex, (_, token, isOptional) => {
const tokenMatcher =
this.#details.wheres[token] ?? '[^/]+';
return `${isOptional ? '?' : ''}(?<${token}>${tokenMatcher})${isOptional ? '?' : ''}`
});
const match = new RegExp(`^${template_regex}/?$`).exec(location);
if (match === null)
return false;
for (const token in match.groups) {
if (Object.hasOwn(match.groups, token)) {
if (match.groups[token] === undefined)
continue;
match.groups[token] = decodeURIComponent(match.groups[token]);
}
}
return {
...match.groups,
_query: parse(query),
};
}
}