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