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