js-routes 2.3.7 → 2.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/Readme.md +51 -1
- data/lib/js_routes/configuration.rb +62 -5
- data/lib/js_routes/generators/base.rb +5 -4
- data/lib/js_routes/instance.rb +96 -25
- data/lib/js_routes/route.rb +40 -8
- data/lib/js_routes/utils.rb +6 -1
- data/lib/js_routes/version.rb +1 -1
- data/lib/js_routes.rb +11 -1
- data/lib/router.d.ts +63 -0
- data/lib/router.js +449 -0
- data/lib/router.ts +665 -0
- data/lib/routes.d.ts +1 -78
- data/lib/routes.js +103 -502
- data/lib/routes.ts +125 -703
- data/lib/templates/initializer.rb +12 -2
- metadata +7 -4
data/lib/router.ts
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
type Optional<T> = { [P in keyof T]?: T[P] | null };
|
|
2
|
+
export type Collection<T> = Record<string, T>;
|
|
3
|
+
|
|
4
|
+
type BaseRouteParameter = string | boolean | Date | number | bigint;
|
|
5
|
+
type MethodRouteParameter = BaseRouteParameter | (() => BaseRouteParameter);
|
|
6
|
+
type ModelRouteParameter =
|
|
7
|
+
| { id: MethodRouteParameter }
|
|
8
|
+
| { to_param: MethodRouteParameter }
|
|
9
|
+
| { toParam: MethodRouteParameter };
|
|
10
|
+
type RequiredRouteParameter = BaseRouteParameter | ModelRouteParameter;
|
|
11
|
+
type OptionalRouteParameter = undefined | null | RequiredRouteParameter;
|
|
12
|
+
type QueryRouteParameter =
|
|
13
|
+
| OptionalRouteParameter
|
|
14
|
+
| QueryRouteParameter[]
|
|
15
|
+
| { [k: string]: QueryRouteParameter };
|
|
16
|
+
export type RouteParameters = Collection<QueryRouteParameter>;
|
|
17
|
+
|
|
18
|
+
type Serializable = Collection<unknown>;
|
|
19
|
+
export type Serializer = (value: Serializable) => string;
|
|
20
|
+
|
|
21
|
+
type RouteHelperExtras = {
|
|
22
|
+
requiredParams(): string[];
|
|
23
|
+
toString(): string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RequiredParameters<T extends number> = T extends 1
|
|
27
|
+
? [RequiredRouteParameter]
|
|
28
|
+
: T extends 2
|
|
29
|
+
? [RequiredRouteParameter, RequiredRouteParameter]
|
|
30
|
+
: T extends 3
|
|
31
|
+
? [RequiredRouteParameter, RequiredRouteParameter, RequiredRouteParameter]
|
|
32
|
+
: T extends 4
|
|
33
|
+
? [
|
|
34
|
+
RequiredRouteParameter,
|
|
35
|
+
RequiredRouteParameter,
|
|
36
|
+
RequiredRouteParameter,
|
|
37
|
+
RequiredRouteParameter,
|
|
38
|
+
]
|
|
39
|
+
: RequiredRouteParameter[];
|
|
40
|
+
|
|
41
|
+
type RouteHelperOptions = RouteOptions & Collection<OptionalRouteParameter>;
|
|
42
|
+
|
|
43
|
+
export type RouteHelper<T extends number = number> = ((
|
|
44
|
+
...args: [...RequiredParameters<T>, RouteHelperOptions]
|
|
45
|
+
) => string) &
|
|
46
|
+
RouteHelperExtras;
|
|
47
|
+
|
|
48
|
+
type Configuration = {
|
|
49
|
+
prefix: string;
|
|
50
|
+
default_url_options: RouteParameters;
|
|
51
|
+
special_options_key: string;
|
|
52
|
+
serializer: Serializer;
|
|
53
|
+
deprecated_false_parameter_behavior: boolean;
|
|
54
|
+
deprecated_nil_query_parameter_behavior: boolean;
|
|
55
|
+
include_undefined_query_parameters: boolean;
|
|
56
|
+
};
|
|
57
|
+
export interface RouterExposedMethods {
|
|
58
|
+
config(): Configuration;
|
|
59
|
+
configure(arg: Partial<Configuration>): Configuration;
|
|
60
|
+
serialize: Serializer;
|
|
61
|
+
__route(...args: unknown[]): RouteHelper;
|
|
62
|
+
}
|
|
63
|
+
export interface RouterConstructor {
|
|
64
|
+
new (config?: Partial<Configuration>): RouterExposedMethods;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type KeywordUrlOptions = Optional<{
|
|
68
|
+
host: string;
|
|
69
|
+
protocol: string;
|
|
70
|
+
subdomain: string;
|
|
71
|
+
port: string | number;
|
|
72
|
+
anchor: string;
|
|
73
|
+
trailing_slash: boolean;
|
|
74
|
+
script_name: string;
|
|
75
|
+
params: RouteParameters;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
type RouteOptions = KeywordUrlOptions & RouteParameters;
|
|
79
|
+
|
|
80
|
+
type PartsTable = Collection<{
|
|
81
|
+
r?: boolean;
|
|
82
|
+
d?: OptionalRouteParameter;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
86
|
+
const Router: RouterConstructor = (() => {
|
|
87
|
+
enum NodeTypes {
|
|
88
|
+
GROUP = 1,
|
|
89
|
+
CAT = 2,
|
|
90
|
+
SYMBOL = 3,
|
|
91
|
+
OR = 4,
|
|
92
|
+
STAR = 5,
|
|
93
|
+
LITERAL = 6,
|
|
94
|
+
SLASH = 7,
|
|
95
|
+
DOT = 8,
|
|
96
|
+
}
|
|
97
|
+
type RouteNodes = {
|
|
98
|
+
[NodeTypes.GROUP]: { left: RouteTree; right: never };
|
|
99
|
+
[NodeTypes.STAR]: { left: RouteTree; right: never };
|
|
100
|
+
[NodeTypes.LITERAL]: { left: string; right: never };
|
|
101
|
+
[NodeTypes.SLASH]: { left: "/"; right: never };
|
|
102
|
+
[NodeTypes.DOT]: { left: "."; right: never };
|
|
103
|
+
[NodeTypes.CAT]: { left: RouteTree; right: RouteTree };
|
|
104
|
+
[NodeTypes.SYMBOL]: { left: string; right: never };
|
|
105
|
+
};
|
|
106
|
+
type RouteNode<T extends keyof RouteNodes> = [
|
|
107
|
+
T,
|
|
108
|
+
RouteNodes[T]["left"],
|
|
109
|
+
RouteNodes[T]["right"],
|
|
110
|
+
];
|
|
111
|
+
type RouteTree = {
|
|
112
|
+
[T in keyof RouteNodes]: RouteNode<T>;
|
|
113
|
+
}[keyof RouteNodes];
|
|
114
|
+
|
|
115
|
+
const isBrowser = typeof window !== "undefined";
|
|
116
|
+
|
|
117
|
+
const UnescapedSpecials = "-._~!$&'()*+,;=:@"
|
|
118
|
+
.split("")
|
|
119
|
+
.map((s) => s.charCodeAt(0));
|
|
120
|
+
const UnescapedRanges = [
|
|
121
|
+
["a", "z"],
|
|
122
|
+
["A", "Z"],
|
|
123
|
+
["0", "9"],
|
|
124
|
+
].map((range) => range.map((s) => s.charCodeAt(0)));
|
|
125
|
+
|
|
126
|
+
class ParametersMissing extends Error {
|
|
127
|
+
readonly keys: string[];
|
|
128
|
+
constructor(...keys: string[]) {
|
|
129
|
+
super(`Route missing required keys: ${keys.join(", ")}`);
|
|
130
|
+
this.keys = keys;
|
|
131
|
+
Object.setPrototypeOf(this, Object.getPrototypeOf(this));
|
|
132
|
+
this.name = ParametersMissing.name;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ReservedOptions = [
|
|
137
|
+
"anchor",
|
|
138
|
+
"trailing_slash",
|
|
139
|
+
"subdomain",
|
|
140
|
+
"host",
|
|
141
|
+
"port",
|
|
142
|
+
"protocol",
|
|
143
|
+
"script_name",
|
|
144
|
+
] as const;
|
|
145
|
+
|
|
146
|
+
type ReservedOption = (typeof ReservedOptions)[any];
|
|
147
|
+
|
|
148
|
+
class Router implements RouterExposedMethods {
|
|
149
|
+
private configuration: Configuration = {
|
|
150
|
+
prefix: "",
|
|
151
|
+
default_url_options: {},
|
|
152
|
+
special_options_key: "__options__",
|
|
153
|
+
serializer: this.default_serializer.bind(this),
|
|
154
|
+
deprecated_false_parameter_behavior: false,
|
|
155
|
+
deprecated_nil_query_parameter_behavior: false,
|
|
156
|
+
include_undefined_query_parameters: true,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
constructor(config: Partial<Configuration> = {}) {
|
|
160
|
+
this.configure(config);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
configure(new_config: Partial<Configuration>): Configuration {
|
|
164
|
+
if (new_config.prefix) {
|
|
165
|
+
console.warn(
|
|
166
|
+
"JsRoutes configuration prefix option is deprecated in favor of default_url_options.script_name.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
new_config.serializer ??= this.default_serializer.bind(this);
|
|
170
|
+
this.configuration = { ...this.configuration, ...new_config };
|
|
171
|
+
return this.configuration;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
config(): Configuration {
|
|
175
|
+
return { ...this.configuration };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
serialize(object: Serializable): string {
|
|
179
|
+
return this.configuration.serializer(object);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
__route(
|
|
183
|
+
parts_table: PartsTable,
|
|
184
|
+
route_spec: RouteTree,
|
|
185
|
+
absolute = false,
|
|
186
|
+
): RouteHelper {
|
|
187
|
+
const required_params: string[] = [];
|
|
188
|
+
const parts: string[] = [];
|
|
189
|
+
const default_options: RouteParameters = {};
|
|
190
|
+
for (const [part, { r: required, d: value }] of Object.entries(
|
|
191
|
+
parts_table,
|
|
192
|
+
)) {
|
|
193
|
+
parts.push(part);
|
|
194
|
+
if (required) {
|
|
195
|
+
required_params.push(part);
|
|
196
|
+
}
|
|
197
|
+
if (this.is_not_nullable(value)) {
|
|
198
|
+
default_options[part] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const result = (...args: OptionalRouteParameter[]): string => {
|
|
202
|
+
return this.build_route(
|
|
203
|
+
parts,
|
|
204
|
+
required_params,
|
|
205
|
+
default_options,
|
|
206
|
+
route_spec,
|
|
207
|
+
absolute,
|
|
208
|
+
args,
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
result.requiredParams = () => required_params;
|
|
212
|
+
result.toString = () => {
|
|
213
|
+
return this.build_path_spec(route_spec);
|
|
214
|
+
};
|
|
215
|
+
return result as any;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private default_serializer(
|
|
219
|
+
value: unknown,
|
|
220
|
+
prefix?: string | null,
|
|
221
|
+
array_element = false,
|
|
222
|
+
): string {
|
|
223
|
+
if (!prefix && !this.is_object(value)) {
|
|
224
|
+
throw new Error("URL parameters should be a javascript hash");
|
|
225
|
+
}
|
|
226
|
+
prefix = prefix || "";
|
|
227
|
+
const result: string[] = [];
|
|
228
|
+
if (this.is_array(value)) {
|
|
229
|
+
// Rails serializes an empty array nested inside another array as a nil
|
|
230
|
+
// query value, while an empty object contributes no query parameter.
|
|
231
|
+
if (value.length === 0) {
|
|
232
|
+
return array_element
|
|
233
|
+
? this.default_serializer(null, prefix + "[]")
|
|
234
|
+
: "";
|
|
235
|
+
}
|
|
236
|
+
for (const element of value) {
|
|
237
|
+
const subvalue = this.default_serializer(
|
|
238
|
+
element,
|
|
239
|
+
prefix + "[]",
|
|
240
|
+
true,
|
|
241
|
+
);
|
|
242
|
+
// Skip empty object results so arrays do not produce leading,
|
|
243
|
+
// trailing, or doubled "&" separators.
|
|
244
|
+
if (subvalue.length) {
|
|
245
|
+
result.push(subvalue);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (this.is_object(value)) {
|
|
249
|
+
for (let key in value) {
|
|
250
|
+
if (!this.hasProp(value, key)) continue;
|
|
251
|
+
let prop = value[key];
|
|
252
|
+
if (
|
|
253
|
+
!this.configuration.include_undefined_query_parameters &&
|
|
254
|
+
prop === undefined
|
|
255
|
+
) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (prefix) {
|
|
259
|
+
key = prefix + "[" + key + "]";
|
|
260
|
+
}
|
|
261
|
+
const subvalue = this.default_serializer(prop, key);
|
|
262
|
+
if (subvalue.length) {
|
|
263
|
+
result.push(subvalue);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
const key = encodeURIComponent(prefix);
|
|
268
|
+
result.push(
|
|
269
|
+
this.is_not_nullable(value) ||
|
|
270
|
+
this.configuration.deprecated_nil_query_parameter_behavior
|
|
271
|
+
? key + "=" + encodeURIComponent("" + (value ?? ""))
|
|
272
|
+
: key,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return result.join("&");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private extract_options(
|
|
279
|
+
number_of_params: number,
|
|
280
|
+
args: OptionalRouteParameter[],
|
|
281
|
+
): {
|
|
282
|
+
args: OptionalRouteParameter[];
|
|
283
|
+
options: RouteOptions;
|
|
284
|
+
} {
|
|
285
|
+
const last_el = args[args.length - 1];
|
|
286
|
+
if (
|
|
287
|
+
(args.length > number_of_params && last_el === 0) ||
|
|
288
|
+
(this.is_object(last_el) && !this.looks_like_serialized_model(last_el))
|
|
289
|
+
) {
|
|
290
|
+
if (this.is_object(last_el)) {
|
|
291
|
+
delete last_el[this.configuration.special_options_key];
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
args: args.slice(0, args.length - 1),
|
|
295
|
+
options: last_el as unknown as RouteOptions,
|
|
296
|
+
};
|
|
297
|
+
} else {
|
|
298
|
+
return { args, options: {} };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private looks_like_serialized_model(
|
|
303
|
+
object: unknown,
|
|
304
|
+
): object is ModelRouteParameter {
|
|
305
|
+
return (
|
|
306
|
+
this.is_object(object) &&
|
|
307
|
+
!(this.configuration.special_options_key in object) &&
|
|
308
|
+
("id" in object || "to_param" in object || "toParam" in object)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private path_identifier(object: QueryRouteParameter): string {
|
|
313
|
+
const result = this.unwrap_path_identifier(object);
|
|
314
|
+
return this.is_nullable(result) ||
|
|
315
|
+
(this.configuration.deprecated_false_parameter_behavior &&
|
|
316
|
+
result === false)
|
|
317
|
+
? ""
|
|
318
|
+
: "" + result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private unwrap_path_identifier(object: QueryRouteParameter): unknown {
|
|
322
|
+
let result: unknown = object;
|
|
323
|
+
if (!this.is_object(object)) {
|
|
324
|
+
return object;
|
|
325
|
+
}
|
|
326
|
+
if ("to_param" in object) {
|
|
327
|
+
result = object.to_param;
|
|
328
|
+
} else if ("toParam" in object) {
|
|
329
|
+
result = object.toParam;
|
|
330
|
+
} else if ("id" in object) {
|
|
331
|
+
result = object.id;
|
|
332
|
+
} else {
|
|
333
|
+
result = object;
|
|
334
|
+
}
|
|
335
|
+
return this.is_callable(result) ? result.call(object) : result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private partition_parameters(
|
|
339
|
+
parts: string[],
|
|
340
|
+
required_params: string[],
|
|
341
|
+
default_options: RouteParameters,
|
|
342
|
+
call_arguments: OptionalRouteParameter[],
|
|
343
|
+
): {
|
|
344
|
+
keyword_parameters: KeywordUrlOptions;
|
|
345
|
+
query_parameters: RouteParameters;
|
|
346
|
+
} {
|
|
347
|
+
let { args, options } = this.extract_options(
|
|
348
|
+
parts.length,
|
|
349
|
+
call_arguments,
|
|
350
|
+
);
|
|
351
|
+
if (args.length > parts.length) {
|
|
352
|
+
throw new Error("Too many parameters provided for path");
|
|
353
|
+
}
|
|
354
|
+
let use_all_parts = args.length > required_params.length;
|
|
355
|
+
const parts_options: RouteParameters = {
|
|
356
|
+
...this.configuration.default_url_options,
|
|
357
|
+
};
|
|
358
|
+
for (const key in options) {
|
|
359
|
+
const value = options[key];
|
|
360
|
+
if (!this.hasProp(options, key)) continue;
|
|
361
|
+
use_all_parts = true;
|
|
362
|
+
if (parts.includes(key)) {
|
|
363
|
+
parts_options[key] = value;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
options = {
|
|
367
|
+
...this.configuration.default_url_options,
|
|
368
|
+
...default_options,
|
|
369
|
+
...options,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const keyword_parameters: KeywordUrlOptions = {};
|
|
373
|
+
let query_parameters: RouteParameters = {};
|
|
374
|
+
for (const key in options) {
|
|
375
|
+
if (!this.hasProp(options, key)) continue;
|
|
376
|
+
const value = options[key];
|
|
377
|
+
if (key === "params") {
|
|
378
|
+
if (this.is_object(value)) {
|
|
379
|
+
query_parameters = {
|
|
380
|
+
...query_parameters,
|
|
381
|
+
...(value as RouteParameters),
|
|
382
|
+
};
|
|
383
|
+
} else {
|
|
384
|
+
throw new Error("params value should always be an object");
|
|
385
|
+
}
|
|
386
|
+
} else if (this.is_reserved_option(key)) {
|
|
387
|
+
keyword_parameters[key] = value as any;
|
|
388
|
+
} else {
|
|
389
|
+
if (
|
|
390
|
+
!this.is_nullable(value) &&
|
|
391
|
+
(value !== default_options[key] || required_params.includes(key))
|
|
392
|
+
) {
|
|
393
|
+
query_parameters[key] = value;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const route_parts = use_all_parts ? parts : required_params;
|
|
398
|
+
let i = 0;
|
|
399
|
+
for (const part of route_parts) {
|
|
400
|
+
if (i < args.length) {
|
|
401
|
+
const value = args[i];
|
|
402
|
+
if (!this.hasProp(parts_options, part)) {
|
|
403
|
+
query_parameters[part] = value;
|
|
404
|
+
++i;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { keyword_parameters, query_parameters };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private build_route(
|
|
412
|
+
parts: string[],
|
|
413
|
+
required_params: string[],
|
|
414
|
+
default_options: RouteParameters,
|
|
415
|
+
route: RouteTree,
|
|
416
|
+
absolute: boolean,
|
|
417
|
+
args: OptionalRouteParameter[],
|
|
418
|
+
): string {
|
|
419
|
+
const { keyword_parameters, query_parameters } =
|
|
420
|
+
this.partition_parameters(
|
|
421
|
+
parts,
|
|
422
|
+
required_params,
|
|
423
|
+
default_options,
|
|
424
|
+
args,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
let { trailing_slash, anchor, script_name } = keyword_parameters;
|
|
428
|
+
const missing_params = required_params.filter(
|
|
429
|
+
(param) =>
|
|
430
|
+
!this.hasProp(query_parameters, param) ||
|
|
431
|
+
this.is_nullable(query_parameters[param]),
|
|
432
|
+
);
|
|
433
|
+
if (missing_params.length) {
|
|
434
|
+
throw new ParametersMissing(...missing_params);
|
|
435
|
+
}
|
|
436
|
+
let result = this.get_prefix() + this.visit(route, query_parameters);
|
|
437
|
+
if (trailing_slash) {
|
|
438
|
+
result = result.replace(/(.*?)[/]?$/, "$1/");
|
|
439
|
+
}
|
|
440
|
+
const url_params = this.serialize(query_parameters);
|
|
441
|
+
if (url_params.length) {
|
|
442
|
+
result += "?" + url_params;
|
|
443
|
+
}
|
|
444
|
+
if (anchor) {
|
|
445
|
+
result += "#" + anchor;
|
|
446
|
+
}
|
|
447
|
+
if (script_name) {
|
|
448
|
+
const last_index = script_name.length - 1;
|
|
449
|
+
if (script_name[last_index] == "/" && result[0] == "/") {
|
|
450
|
+
script_name = script_name.slice(0, last_index);
|
|
451
|
+
}
|
|
452
|
+
result = script_name + result;
|
|
453
|
+
}
|
|
454
|
+
if (absolute) {
|
|
455
|
+
result = this.route_url(keyword_parameters) + result;
|
|
456
|
+
}
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private visit(
|
|
461
|
+
route: RouteTree,
|
|
462
|
+
parameters: RouteParameters,
|
|
463
|
+
optional = false,
|
|
464
|
+
): string {
|
|
465
|
+
switch (route[0]) {
|
|
466
|
+
case NodeTypes.GROUP:
|
|
467
|
+
return this.visit(route[1], parameters, true);
|
|
468
|
+
case NodeTypes.CAT:
|
|
469
|
+
return this.visit_cat(route, parameters, optional);
|
|
470
|
+
case NodeTypes.SYMBOL:
|
|
471
|
+
return this.visit_symbol(route, parameters, optional);
|
|
472
|
+
case NodeTypes.STAR:
|
|
473
|
+
return this.visit_globbing(route[1], parameters, true);
|
|
474
|
+
case NodeTypes.LITERAL:
|
|
475
|
+
case NodeTypes.SLASH:
|
|
476
|
+
case NodeTypes.DOT:
|
|
477
|
+
return route[1];
|
|
478
|
+
default:
|
|
479
|
+
throw new Error("Unknown Rails node type");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private is_not_nullable<T>(object: T): object is NonNullable<T> {
|
|
484
|
+
return !this.is_nullable(object);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private is_nullable(object: unknown): object is null | undefined {
|
|
488
|
+
return object === undefined || object === null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private visit_cat(
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
493
|
+
[_type, left, right]: RouteNode<NodeTypes.CAT>,
|
|
494
|
+
parameters: RouteParameters,
|
|
495
|
+
optional: boolean,
|
|
496
|
+
): string {
|
|
497
|
+
const left_part = this.visit(left, parameters, optional);
|
|
498
|
+
let right_part = this.visit(right, parameters, optional);
|
|
499
|
+
if (
|
|
500
|
+
optional &&
|
|
501
|
+
((this.is_optional_node(left[0]) && !left_part) ||
|
|
502
|
+
(this.is_optional_node(right[0]) && !right_part))
|
|
503
|
+
) {
|
|
504
|
+
return "";
|
|
505
|
+
}
|
|
506
|
+
// if left_part ends on '/' and right_part starts on '/'
|
|
507
|
+
if (left_part[left_part.length - 1] === "/" && right_part[0] === "/") {
|
|
508
|
+
// strip slash from right_part
|
|
509
|
+
// to prevent double slash
|
|
510
|
+
right_part = right_part.substring(1);
|
|
511
|
+
}
|
|
512
|
+
return left_part + right_part;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private visit_symbol(
|
|
516
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
517
|
+
[_type, key]: RouteNode<NodeTypes.SYMBOL>,
|
|
518
|
+
parameters: RouteParameters,
|
|
519
|
+
optional: boolean,
|
|
520
|
+
): string {
|
|
521
|
+
const value = this.path_identifier(parameters[key]);
|
|
522
|
+
delete parameters[key];
|
|
523
|
+
if (value.length) {
|
|
524
|
+
return this.encode_segment(value);
|
|
525
|
+
}
|
|
526
|
+
if (optional) {
|
|
527
|
+
return "";
|
|
528
|
+
} else {
|
|
529
|
+
throw new ParametersMissing(key);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private encode_segment(segment: string): string {
|
|
534
|
+
if (segment.match(/^[a-zA-Z0-9-]$/)) {
|
|
535
|
+
// Performance optimization for 99% of cases
|
|
536
|
+
return segment;
|
|
537
|
+
}
|
|
538
|
+
return (segment.match(/./gu) || [])
|
|
539
|
+
.map((ch) => {
|
|
540
|
+
const code = ch.charCodeAt(0);
|
|
541
|
+
if (
|
|
542
|
+
UnescapedRanges.find(
|
|
543
|
+
(range) => code >= range[0] && code <= range[1],
|
|
544
|
+
) ||
|
|
545
|
+
UnescapedSpecials.includes(code)
|
|
546
|
+
) {
|
|
547
|
+
return ch;
|
|
548
|
+
} else {
|
|
549
|
+
return encodeURIComponent(ch);
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
.join("");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private is_optional_node(node: NodeTypes): boolean {
|
|
556
|
+
return [NodeTypes.STAR, NodeTypes.SYMBOL, NodeTypes.CAT].includes(node);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private build_path_spec(route: RouteTree, wildcard = false): string {
|
|
560
|
+
let key: string;
|
|
561
|
+
switch (route[0]) {
|
|
562
|
+
case NodeTypes.GROUP:
|
|
563
|
+
return `(${this.build_path_spec(route[1])})`;
|
|
564
|
+
case NodeTypes.CAT:
|
|
565
|
+
return (
|
|
566
|
+
this.build_path_spec(route[1]) + this.build_path_spec(route[2])
|
|
567
|
+
);
|
|
568
|
+
case NodeTypes.STAR:
|
|
569
|
+
return this.build_path_spec(route[1], true);
|
|
570
|
+
case NodeTypes.SYMBOL:
|
|
571
|
+
key = route[1];
|
|
572
|
+
if (wildcard) {
|
|
573
|
+
return (key.startsWith("*") ? "" : "*") + key;
|
|
574
|
+
} else {
|
|
575
|
+
return ":" + key;
|
|
576
|
+
}
|
|
577
|
+
case NodeTypes.SLASH:
|
|
578
|
+
case NodeTypes.DOT:
|
|
579
|
+
case NodeTypes.LITERAL:
|
|
580
|
+
return route[1];
|
|
581
|
+
default:
|
|
582
|
+
throw new Error("Unknown Rails node type");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private visit_globbing(
|
|
587
|
+
route: RouteTree,
|
|
588
|
+
parameters: RouteParameters,
|
|
589
|
+
optional: boolean,
|
|
590
|
+
): string {
|
|
591
|
+
const key = route[1] as string;
|
|
592
|
+
let value = parameters[key];
|
|
593
|
+
delete parameters[key];
|
|
594
|
+
if (this.is_nullable(value)) {
|
|
595
|
+
return this.visit(route, parameters, optional);
|
|
596
|
+
}
|
|
597
|
+
if (this.is_array(value)) {
|
|
598
|
+
value = value.join("/");
|
|
599
|
+
}
|
|
600
|
+
const result = this.path_identifier(value as any);
|
|
601
|
+
return encodeURI(result);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private get_prefix(): string {
|
|
605
|
+
const prefix = this.configuration.prefix;
|
|
606
|
+
return prefix.match("/$")
|
|
607
|
+
? prefix.substring(0, prefix.length - 1)
|
|
608
|
+
: prefix;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private route_url(route_defaults: KeywordUrlOptions): string {
|
|
612
|
+
const hostname = route_defaults.host || this.current_host();
|
|
613
|
+
if (!hostname) {
|
|
614
|
+
return "";
|
|
615
|
+
}
|
|
616
|
+
const subdomain = route_defaults.subdomain
|
|
617
|
+
? route_defaults.subdomain + "."
|
|
618
|
+
: "";
|
|
619
|
+
const protocol = route_defaults.protocol || this.current_protocol();
|
|
620
|
+
let port =
|
|
621
|
+
route_defaults.port ||
|
|
622
|
+
(!route_defaults.host ? this.current_port() : undefined);
|
|
623
|
+
port = port ? ":" + port : "";
|
|
624
|
+
return protocol + "://" + subdomain + hostname + port;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private current_host(): string {
|
|
628
|
+
return (isBrowser && window?.location?.hostname) || "";
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private current_protocol(): string {
|
|
632
|
+
return (
|
|
633
|
+
(isBrowser && window?.location?.protocol?.replace(/:$/, "")) || "http"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private current_port(): string {
|
|
638
|
+
return (isBrowser && window?.location?.port) || "";
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private is_object(value: unknown): value is Collection<unknown> {
|
|
642
|
+
return (
|
|
643
|
+
typeof value === "object" &&
|
|
644
|
+
Object.prototype.toString.call(value) === "[object Object]"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private is_array<T>(object: unknown | T[]): object is T[] {
|
|
649
|
+
return object instanceof Array;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private is_callable(object: unknown): object is Function {
|
|
653
|
+
return typeof object === "function" && !!object.call;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private is_reserved_option(key: unknown): key is ReservedOption {
|
|
657
|
+
return ReservedOptions.includes(key as any);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private hasProp(value: unknown, key: string): boolean {
|
|
661
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return Router;
|
|
665
|
+
})();
|