js-routes 2.3.6 → 2.3.7

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