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.
data/lib/js_routes.rb CHANGED
@@ -40,10 +40,20 @@ module JsRoutes
40
40
  instance = Instance.new(file: file_name, **opts)
41
41
  instance.generate!
42
42
  if typed && instance.configuration.modern?
43
- definitions!(file_name, **opts)
43
+ definitions!(file_name, **opts.except(:package))
44
44
  end
45
45
  end
46
46
 
47
+ sig { params(opts: T.untyped).returns(String) }
48
+ def package(**opts)
49
+ Instance.new(module_type: 'PKG', **opts).package
50
+ end
51
+
52
+ sig { params(file_name: FileName, opts: T.untyped).void }
53
+ def package!(file_name = nil, **opts)
54
+ Instance.new(module_type: 'PKG', file: file_name, **opts).package!
55
+ end
56
+
47
57
  sig { params(file_name: FileName, opts: T.untyped).void }
48
58
  def remove!(file_name = configuration.file, **opts)
49
59
  Instance.new(file: file_name, **opts).remove!
data/lib/router.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ type Optional<T> = {
2
+ [P in keyof T]?: T[P] | null;
3
+ };
4
+ export type Collection<T> = Record<string, T>;
5
+ type BaseRouteParameter = string | boolean | Date | number | bigint;
6
+ type MethodRouteParameter = BaseRouteParameter | (() => BaseRouteParameter);
7
+ type ModelRouteParameter = {
8
+ id: MethodRouteParameter;
9
+ } | {
10
+ to_param: MethodRouteParameter;
11
+ } | {
12
+ toParam: MethodRouteParameter;
13
+ };
14
+ type RequiredRouteParameter = BaseRouteParameter | ModelRouteParameter;
15
+ type OptionalRouteParameter = undefined | null | RequiredRouteParameter;
16
+ type QueryRouteParameter = OptionalRouteParameter | QueryRouteParameter[] | {
17
+ [k: string]: QueryRouteParameter;
18
+ };
19
+ export type RouteParameters = Collection<QueryRouteParameter>;
20
+ type Serializable = Collection<unknown>;
21
+ export type Serializer = (value: Serializable) => string;
22
+ type RouteHelperExtras = {
23
+ requiredParams(): string[];
24
+ toString(): string;
25
+ };
26
+ type RequiredParameters<T extends number> = T extends 1 ? [RequiredRouteParameter] : T extends 2 ? [RequiredRouteParameter, RequiredRouteParameter] : T extends 3 ? [RequiredRouteParameter, RequiredRouteParameter, RequiredRouteParameter] : T extends 4 ? [
27
+ RequiredRouteParameter,
28
+ RequiredRouteParameter,
29
+ RequiredRouteParameter,
30
+ RequiredRouteParameter
31
+ ] : RequiredRouteParameter[];
32
+ type RouteHelperOptions = RouteOptions & Collection<OptionalRouteParameter>;
33
+ export type RouteHelper<T extends number = number> = ((...args: [...RequiredParameters<T>, RouteHelperOptions]) => string) & RouteHelperExtras;
34
+ type Configuration = {
35
+ prefix: string;
36
+ default_url_options: RouteParameters;
37
+ special_options_key: string;
38
+ serializer: Serializer;
39
+ deprecated_false_parameter_behavior: boolean;
40
+ deprecated_nil_query_parameter_behavior: boolean;
41
+ include_undefined_query_parameters: boolean;
42
+ };
43
+ export interface RouterExposedMethods {
44
+ config(): Configuration;
45
+ configure(arg: Partial<Configuration>): Configuration;
46
+ serialize: Serializer;
47
+ __route(...args: unknown[]): RouteHelper;
48
+ }
49
+ export interface RouterConstructor {
50
+ new (config?: Partial<Configuration>): RouterExposedMethods;
51
+ }
52
+ type KeywordUrlOptions = Optional<{
53
+ host: string;
54
+ protocol: string;
55
+ subdomain: string;
56
+ port: string | number;
57
+ anchor: string;
58
+ trailing_slash: boolean;
59
+ script_name: string;
60
+ params: RouteParameters;
61
+ }>;
62
+ type RouteOptions = KeywordUrlOptions & RouteParameters;
63
+ export {};
data/lib/router.js ADDED
@@ -0,0 +1,449 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2
+ const Router = (() => {
3
+ let NodeTypes;
4
+ (function (NodeTypes) {
5
+ NodeTypes[NodeTypes["GROUP"] = 1] = "GROUP";
6
+ NodeTypes[NodeTypes["CAT"] = 2] = "CAT";
7
+ NodeTypes[NodeTypes["SYMBOL"] = 3] = "SYMBOL";
8
+ NodeTypes[NodeTypes["OR"] = 4] = "OR";
9
+ NodeTypes[NodeTypes["STAR"] = 5] = "STAR";
10
+ NodeTypes[NodeTypes["LITERAL"] = 6] = "LITERAL";
11
+ NodeTypes[NodeTypes["SLASH"] = 7] = "SLASH";
12
+ NodeTypes[NodeTypes["DOT"] = 8] = "DOT";
13
+ })(NodeTypes || (NodeTypes = {}));
14
+ const isBrowser = typeof window !== "undefined";
15
+ const UnescapedSpecials = "-._~!$&'()*+,;=:@"
16
+ .split("")
17
+ .map((s) => s.charCodeAt(0));
18
+ const UnescapedRanges = [
19
+ ["a", "z"],
20
+ ["A", "Z"],
21
+ ["0", "9"],
22
+ ].map((range) => range.map((s) => s.charCodeAt(0)));
23
+ class ParametersMissing extends Error {
24
+ constructor(...keys) {
25
+ super(`Route missing required keys: ${keys.join(", ")}`);
26
+ this.keys = keys;
27
+ Object.setPrototypeOf(this, Object.getPrototypeOf(this));
28
+ this.name = ParametersMissing.name;
29
+ }
30
+ }
31
+ const ReservedOptions = [
32
+ "anchor",
33
+ "trailing_slash",
34
+ "subdomain",
35
+ "host",
36
+ "port",
37
+ "protocol",
38
+ "script_name",
39
+ ];
40
+ class Router {
41
+ constructor(config = {}) {
42
+ this.configuration = {
43
+ prefix: "",
44
+ default_url_options: {},
45
+ special_options_key: "__options__",
46
+ serializer: this.default_serializer.bind(this),
47
+ deprecated_false_parameter_behavior: false,
48
+ deprecated_nil_query_parameter_behavior: false,
49
+ include_undefined_query_parameters: true,
50
+ };
51
+ this.configure(config);
52
+ }
53
+ configure(new_config) {
54
+ var _a;
55
+ if (new_config.prefix) {
56
+ console.warn("JsRoutes configuration prefix option is deprecated in favor of default_url_options.script_name.");
57
+ }
58
+ (_a = new_config.serializer) !== null && _a !== void 0 ? _a : (new_config.serializer = this.default_serializer.bind(this));
59
+ this.configuration = { ...this.configuration, ...new_config };
60
+ return this.configuration;
61
+ }
62
+ config() {
63
+ return { ...this.configuration };
64
+ }
65
+ serialize(object) {
66
+ return this.configuration.serializer(object);
67
+ }
68
+ __route(parts_table, route_spec, absolute = false) {
69
+ const required_params = [];
70
+ const parts = [];
71
+ const default_options = {};
72
+ for (const [part, { r: required, d: value }] of Object.entries(parts_table)) {
73
+ parts.push(part);
74
+ if (required) {
75
+ required_params.push(part);
76
+ }
77
+ if (this.is_not_nullable(value)) {
78
+ default_options[part] = value;
79
+ }
80
+ }
81
+ const result = (...args) => {
82
+ return this.build_route(parts, required_params, default_options, route_spec, absolute, args);
83
+ };
84
+ result.requiredParams = () => required_params;
85
+ result.toString = () => {
86
+ return this.build_path_spec(route_spec);
87
+ };
88
+ return result;
89
+ }
90
+ default_serializer(value, prefix, array_element = false) {
91
+ if (!prefix && !this.is_object(value)) {
92
+ throw new Error("URL parameters should be a javascript hash");
93
+ }
94
+ prefix = prefix || "";
95
+ const result = [];
96
+ if (this.is_array(value)) {
97
+ // Rails serializes an empty array nested inside another array as a nil
98
+ // query value, while an empty object contributes no query parameter.
99
+ if (value.length === 0) {
100
+ return array_element
101
+ ? this.default_serializer(null, prefix + "[]")
102
+ : "";
103
+ }
104
+ for (const element of value) {
105
+ const subvalue = this.default_serializer(element, prefix + "[]", true);
106
+ // Skip empty object results so arrays do not produce leading,
107
+ // trailing, or doubled "&" separators.
108
+ if (subvalue.length) {
109
+ result.push(subvalue);
110
+ }
111
+ }
112
+ }
113
+ else if (this.is_object(value)) {
114
+ for (let key in value) {
115
+ if (!this.hasProp(value, key))
116
+ continue;
117
+ let prop = value[key];
118
+ if (!this.configuration.include_undefined_query_parameters &&
119
+ prop === undefined) {
120
+ continue;
121
+ }
122
+ if (prefix) {
123
+ key = prefix + "[" + key + "]";
124
+ }
125
+ const subvalue = this.default_serializer(prop, key);
126
+ if (subvalue.length) {
127
+ result.push(subvalue);
128
+ }
129
+ }
130
+ }
131
+ else {
132
+ const key = encodeURIComponent(prefix);
133
+ result.push(this.is_not_nullable(value) ||
134
+ this.configuration.deprecated_nil_query_parameter_behavior
135
+ ? key + "=" + encodeURIComponent("" + (value !== null && value !== void 0 ? value : ""))
136
+ : key);
137
+ }
138
+ return result.join("&");
139
+ }
140
+ extract_options(number_of_params, args) {
141
+ const last_el = args[args.length - 1];
142
+ if ((args.length > number_of_params && last_el === 0) ||
143
+ (this.is_object(last_el) && !this.looks_like_serialized_model(last_el))) {
144
+ if (this.is_object(last_el)) {
145
+ delete last_el[this.configuration.special_options_key];
146
+ }
147
+ return {
148
+ args: args.slice(0, args.length - 1),
149
+ options: last_el,
150
+ };
151
+ }
152
+ else {
153
+ return { args, options: {} };
154
+ }
155
+ }
156
+ looks_like_serialized_model(object) {
157
+ return (this.is_object(object) &&
158
+ !(this.configuration.special_options_key in object) &&
159
+ ("id" in object || "to_param" in object || "toParam" in object));
160
+ }
161
+ path_identifier(object) {
162
+ const result = this.unwrap_path_identifier(object);
163
+ return this.is_nullable(result) ||
164
+ (this.configuration.deprecated_false_parameter_behavior &&
165
+ result === false)
166
+ ? ""
167
+ : "" + result;
168
+ }
169
+ unwrap_path_identifier(object) {
170
+ let result = object;
171
+ if (!this.is_object(object)) {
172
+ return object;
173
+ }
174
+ if ("to_param" in object) {
175
+ result = object.to_param;
176
+ }
177
+ else if ("toParam" in object) {
178
+ result = object.toParam;
179
+ }
180
+ else if ("id" in object) {
181
+ result = object.id;
182
+ }
183
+ else {
184
+ result = object;
185
+ }
186
+ return this.is_callable(result) ? result.call(object) : result;
187
+ }
188
+ partition_parameters(parts, required_params, default_options, call_arguments) {
189
+ let { args, options } = this.extract_options(parts.length, call_arguments);
190
+ if (args.length > parts.length) {
191
+ throw new Error("Too many parameters provided for path");
192
+ }
193
+ let use_all_parts = args.length > required_params.length;
194
+ const parts_options = {
195
+ ...this.configuration.default_url_options,
196
+ };
197
+ for (const key in options) {
198
+ const value = options[key];
199
+ if (!this.hasProp(options, key))
200
+ continue;
201
+ use_all_parts = true;
202
+ if (parts.includes(key)) {
203
+ parts_options[key] = value;
204
+ }
205
+ }
206
+ options = {
207
+ ...this.configuration.default_url_options,
208
+ ...default_options,
209
+ ...options,
210
+ };
211
+ const keyword_parameters = {};
212
+ let query_parameters = {};
213
+ for (const key in options) {
214
+ if (!this.hasProp(options, key))
215
+ continue;
216
+ const value = options[key];
217
+ if (key === "params") {
218
+ if (this.is_object(value)) {
219
+ query_parameters = {
220
+ ...query_parameters,
221
+ ...value,
222
+ };
223
+ }
224
+ else {
225
+ throw new Error("params value should always be an object");
226
+ }
227
+ }
228
+ else if (this.is_reserved_option(key)) {
229
+ keyword_parameters[key] = value;
230
+ }
231
+ else {
232
+ if (!this.is_nullable(value) &&
233
+ (value !== default_options[key] || required_params.includes(key))) {
234
+ query_parameters[key] = value;
235
+ }
236
+ }
237
+ }
238
+ const route_parts = use_all_parts ? parts : required_params;
239
+ let i = 0;
240
+ for (const part of route_parts) {
241
+ if (i < args.length) {
242
+ const value = args[i];
243
+ if (!this.hasProp(parts_options, part)) {
244
+ query_parameters[part] = value;
245
+ ++i;
246
+ }
247
+ }
248
+ }
249
+ return { keyword_parameters, query_parameters };
250
+ }
251
+ build_route(parts, required_params, default_options, route, absolute, args) {
252
+ const { keyword_parameters, query_parameters } = this.partition_parameters(parts, required_params, default_options, args);
253
+ let { trailing_slash, anchor, script_name } = keyword_parameters;
254
+ const missing_params = required_params.filter((param) => !this.hasProp(query_parameters, param) ||
255
+ this.is_nullable(query_parameters[param]));
256
+ if (missing_params.length) {
257
+ throw new ParametersMissing(...missing_params);
258
+ }
259
+ let result = this.get_prefix() + this.visit(route, query_parameters);
260
+ if (trailing_slash) {
261
+ result = result.replace(/(.*?)[/]?$/, "$1/");
262
+ }
263
+ const url_params = this.serialize(query_parameters);
264
+ if (url_params.length) {
265
+ result += "?" + url_params;
266
+ }
267
+ if (anchor) {
268
+ result += "#" + anchor;
269
+ }
270
+ if (script_name) {
271
+ const last_index = script_name.length - 1;
272
+ if (script_name[last_index] == "/" && result[0] == "/") {
273
+ script_name = script_name.slice(0, last_index);
274
+ }
275
+ result = script_name + result;
276
+ }
277
+ if (absolute) {
278
+ result = this.route_url(keyword_parameters) + result;
279
+ }
280
+ return result;
281
+ }
282
+ visit(route, parameters, optional = false) {
283
+ switch (route[0]) {
284
+ case NodeTypes.GROUP:
285
+ return this.visit(route[1], parameters, true);
286
+ case NodeTypes.CAT:
287
+ return this.visit_cat(route, parameters, optional);
288
+ case NodeTypes.SYMBOL:
289
+ return this.visit_symbol(route, parameters, optional);
290
+ case NodeTypes.STAR:
291
+ return this.visit_globbing(route[1], parameters, true);
292
+ case NodeTypes.LITERAL:
293
+ case NodeTypes.SLASH:
294
+ case NodeTypes.DOT:
295
+ return route[1];
296
+ default:
297
+ throw new Error("Unknown Rails node type");
298
+ }
299
+ }
300
+ is_not_nullable(object) {
301
+ return !this.is_nullable(object);
302
+ }
303
+ is_nullable(object) {
304
+ return object === undefined || object === null;
305
+ }
306
+ visit_cat(
307
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
308
+ [_type, left, right], parameters, optional) {
309
+ const left_part = this.visit(left, parameters, optional);
310
+ let right_part = this.visit(right, parameters, optional);
311
+ if (optional &&
312
+ ((this.is_optional_node(left[0]) && !left_part) ||
313
+ (this.is_optional_node(right[0]) && !right_part))) {
314
+ return "";
315
+ }
316
+ // if left_part ends on '/' and right_part starts on '/'
317
+ if (left_part[left_part.length - 1] === "/" && right_part[0] === "/") {
318
+ // strip slash from right_part
319
+ // to prevent double slash
320
+ right_part = right_part.substring(1);
321
+ }
322
+ return left_part + right_part;
323
+ }
324
+ visit_symbol(
325
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
326
+ [_type, key], parameters, optional) {
327
+ const value = this.path_identifier(parameters[key]);
328
+ delete parameters[key];
329
+ if (value.length) {
330
+ return this.encode_segment(value);
331
+ }
332
+ if (optional) {
333
+ return "";
334
+ }
335
+ else {
336
+ throw new ParametersMissing(key);
337
+ }
338
+ }
339
+ encode_segment(segment) {
340
+ if (segment.match(/^[a-zA-Z0-9-]$/)) {
341
+ // Performance optimization for 99% of cases
342
+ return segment;
343
+ }
344
+ return (segment.match(/./gu) || [])
345
+ .map((ch) => {
346
+ const code = ch.charCodeAt(0);
347
+ if (UnescapedRanges.find((range) => code >= range[0] && code <= range[1]) ||
348
+ UnescapedSpecials.includes(code)) {
349
+ return ch;
350
+ }
351
+ else {
352
+ return encodeURIComponent(ch);
353
+ }
354
+ })
355
+ .join("");
356
+ }
357
+ is_optional_node(node) {
358
+ return [NodeTypes.STAR, NodeTypes.SYMBOL, NodeTypes.CAT].includes(node);
359
+ }
360
+ build_path_spec(route, wildcard = false) {
361
+ let key;
362
+ switch (route[0]) {
363
+ case NodeTypes.GROUP:
364
+ return `(${this.build_path_spec(route[1])})`;
365
+ case NodeTypes.CAT:
366
+ return (this.build_path_spec(route[1]) + this.build_path_spec(route[2]));
367
+ case NodeTypes.STAR:
368
+ return this.build_path_spec(route[1], true);
369
+ case NodeTypes.SYMBOL:
370
+ key = route[1];
371
+ if (wildcard) {
372
+ return (key.startsWith("*") ? "" : "*") + key;
373
+ }
374
+ else {
375
+ return ":" + key;
376
+ }
377
+ case NodeTypes.SLASH:
378
+ case NodeTypes.DOT:
379
+ case NodeTypes.LITERAL:
380
+ return route[1];
381
+ default:
382
+ throw new Error("Unknown Rails node type");
383
+ }
384
+ }
385
+ visit_globbing(route, parameters, optional) {
386
+ const key = route[1];
387
+ let value = parameters[key];
388
+ delete parameters[key];
389
+ if (this.is_nullable(value)) {
390
+ return this.visit(route, parameters, optional);
391
+ }
392
+ if (this.is_array(value)) {
393
+ value = value.join("/");
394
+ }
395
+ const result = this.path_identifier(value);
396
+ return encodeURI(result);
397
+ }
398
+ get_prefix() {
399
+ const prefix = this.configuration.prefix;
400
+ return prefix.match("/$")
401
+ ? prefix.substring(0, prefix.length - 1)
402
+ : prefix;
403
+ }
404
+ route_url(route_defaults) {
405
+ const hostname = route_defaults.host || this.current_host();
406
+ if (!hostname) {
407
+ return "";
408
+ }
409
+ const subdomain = route_defaults.subdomain
410
+ ? route_defaults.subdomain + "."
411
+ : "";
412
+ const protocol = route_defaults.protocol || this.current_protocol();
413
+ let port = route_defaults.port ||
414
+ (!route_defaults.host ? this.current_port() : undefined);
415
+ port = port ? ":" + port : "";
416
+ return protocol + "://" + subdomain + hostname + port;
417
+ }
418
+ current_host() {
419
+ var _a;
420
+ return (isBrowser && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.hostname)) || "";
421
+ }
422
+ current_protocol() {
423
+ var _a, _b;
424
+ return ((isBrowser && ((_b = (_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.protocol) === null || _b === void 0 ? void 0 : _b.replace(/:$/, ""))) || "http");
425
+ }
426
+ current_port() {
427
+ var _a;
428
+ return (isBrowser && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.port)) || "";
429
+ }
430
+ is_object(value) {
431
+ return (typeof value === "object" &&
432
+ Object.prototype.toString.call(value) === "[object Object]");
433
+ }
434
+ is_array(object) {
435
+ return object instanceof Array;
436
+ }
437
+ is_callable(object) {
438
+ return typeof object === "function" && !!object.call;
439
+ }
440
+ is_reserved_option(key) {
441
+ return ReservedOptions.includes(key);
442
+ }
443
+ hasProp(value, key) {
444
+ return Object.prototype.hasOwnProperty.call(value, key);
445
+ }
446
+ }
447
+ return Router;
448
+ })();
449
+ export {};