js-routes 2.0.8 → 2.2.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
@@ -7,48 +7,26 @@ require 'active_support/core_ext/string/indent'
7
7
 
8
8
  class JsRoutes
9
9
 
10
- #
11
- # OPTIONS
12
- #
10
+ class Configuration
11
+ DEFAULTS = {
12
+ namespace: nil,
13
+ exclude: [],
14
+ include: //,
15
+ file: nil,
16
+ prefix: -> { Rails.application.config.relative_url_root || "" },
17
+ url_links: false,
18
+ camel_case: false,
19
+ default_url_options: {},
20
+ compact: false,
21
+ serializer: nil,
22
+ special_options_key: "_options",
23
+ application: -> { Rails.application },
24
+ module_type: 'ESM',
25
+ documentation: true,
26
+ } #:nodoc:
27
+
28
+ attr_accessor(*DEFAULTS.keys)
13
29
 
14
- DEFAULTS = {
15
- namespace: nil,
16
- exclude: [],
17
- include: //,
18
- file: -> do
19
- webpacker_dir = Rails.root.join('app', 'javascript')
20
- sprockets_dir = Rails.root.join('app','assets','javascripts')
21
- sprockets_file = sprockets_dir.join('routes.js')
22
- webpacker_file = webpacker_dir.join('routes.js')
23
- !Dir.exist?(webpacker_dir) && defined?(::Sprockets) ? sprockets_file : webpacker_file
24
- end,
25
- prefix: -> { Rails.application.config.relative_url_root || "" },
26
- url_links: false,
27
- camel_case: false,
28
- default_url_options: {},
29
- compact: false,
30
- serializer: nil,
31
- special_options_key: "_options",
32
- application: -> { Rails.application },
33
- module_type: 'ESM',
34
- documentation: true,
35
- } #:nodoc:
36
-
37
- NODE_TYPES = {
38
- GROUP: 1,
39
- CAT: 2,
40
- SYMBOL: 3,
41
- OR: 4,
42
- STAR: 5,
43
- LITERAL: 6,
44
- SLASH: 7,
45
- DOT: 8
46
- } #:nodoc:
47
-
48
- FILTERED_DEFAULT_PARTS = [:controller, :action] #:nodoc:
49
- URL_OPTIONS = [:protocol, :domain, :host, :port, :subdomain] #:nodoc:
50
-
51
- class Configuration < Struct.new(*DEFAULTS.keys)
52
30
  def initialize(attributes = nil)
53
31
  assign(DEFAULTS)
54
32
  return unless attributes
@@ -80,6 +58,31 @@ class JsRoutes
80
58
  module_type === 'ESM'
81
59
  end
82
60
 
61
+ def dts?
62
+ self.module_type === 'DTS'
63
+ end
64
+
65
+ def modern?
66
+ esm? || dts?
67
+ end
68
+
69
+ def require_esm
70
+ raise "ESM module type is required" unless modern?
71
+ end
72
+
73
+ def source_file
74
+ File.dirname(__FILE__) + "/" + default_file_name
75
+ end
76
+
77
+ def output_file
78
+ webpacker_dir = Rails.root.join('app', 'javascript')
79
+ sprockets_dir = Rails.root.join('app','assets','javascripts')
80
+ file_name = file || default_file_name
81
+ sprockets_file = sprockets_dir.join(file_name)
82
+ webpacker_file = webpacker_dir.join(file_name)
83
+ !Dir.exist?(webpacker_dir) && defined?(::Sprockets) ? sprockets_file : webpacker_file
84
+ end
85
+
83
86
  def normalize_and_verify
84
87
  normalize
85
88
  verify
@@ -87,6 +90,10 @@ class JsRoutes
87
90
 
88
91
  protected
89
92
 
93
+ def default_file_name
94
+ dts? ? "routes.d.ts" : "routes.js"
95
+ end
96
+
90
97
  def normalize
91
98
  self.module_type = module_type&.upcase || 'NIL'
92
99
  end
@@ -112,16 +119,21 @@ class JsRoutes
112
119
  @configuration ||= Configuration.new
113
120
  end
114
121
 
115
- def generate(opts = {})
122
+ def generate(**opts)
116
123
  new(opts).generate
117
124
  end
118
125
 
119
- def generate!(file_name=nil, opts = {})
120
- if file_name.is_a?(Hash)
121
- opts = file_name
122
- file_name = opts[:file]
123
- end
124
- new(opts).generate!(file_name)
126
+ def generate!(file_name=nil, **opts)
127
+ new(file: file_name, **opts).generate!
128
+ end
129
+
130
+ def definitions(**opts)
131
+ generate(module_type: 'DTS', **opts)
132
+ end
133
+
134
+ def definitions!(file_name = nil, **opts)
135
+ file_name ||= configuration.file&.sub(%r{(\.d)?\.(j|t)s\Z}, ".d.ts")
136
+ generate!(file_name, module_type: 'DTS', **opts)
125
137
  end
126
138
 
127
139
  def json(string)
@@ -139,51 +151,58 @@ class JsRoutes
139
151
 
140
152
  def generate
141
153
  # Ensure routes are loaded. If they're not, load them.
142
- if named_routes.to_a.empty? && application.respond_to?(:reload_routes!)
154
+ if named_routes.empty? && application.respond_to?(:reload_routes!)
143
155
  application.reload_routes!
144
156
  end
157
+ content = File.read(@configuration.source_file)
145
158
 
146
- {
147
- 'GEM_VERSION' => JsRoutes::VERSION,
148
- 'ROUTES_OBJECT' => routes_object,
149
- 'RAILS_VERSION' => ActionPack.version,
150
- 'DEPRECATED_GLOBBING_BEHAVIOR' => ActionPack::VERSION::MAJOR == 4 && ActionPack::VERSION::MINOR == 0,
151
-
152
- 'APP_CLASS' => application.class.to_s,
153
- 'NAMESPACE' => json(@configuration.namespace),
154
- 'DEFAULT_URL_OPTIONS' => json(@configuration.default_url_options),
155
- 'PREFIX' => json(@configuration.prefix),
156
- 'SPECIAL_OPTIONS_KEY' => json(@configuration.special_options_key),
157
- 'SERIALIZER' => @configuration.serializer || json(nil),
158
- 'MODULE_TYPE' => json(@configuration.module_type),
159
- 'WRAPPER' => @configuration.esm? ? 'const __jsr = ' : '',
160
- }.inject(File.read(File.dirname(__FILE__) + "/routes.js")) do |js, (key, value)|
161
- js.gsub!("RubyVariables.#{key}", value.to_s) ||
159
+ if !@configuration.dts?
160
+ content = js_variables.inject(content) do |js, (key, value)|
161
+ js.gsub!("RubyVariables.#{key}", value.to_s) ||
162
162
  raise("Missing key #{key} in JS template")
163
- end + routes_export
163
+ end
164
+ end
165
+ content + routes_export + prevent_types_export
164
166
  end
165
167
 
166
- def generate!(file_name = nil)
167
- # Some libraries like Devise do not yet loaded their routes so we will wait
168
- # until initialization process finish
168
+ def generate!
169
+ # Some libraries like Devise did not load their routes yet
170
+ # so we will wait until initialization process finishes
169
171
  # https://github.com/railsware/js-routes/issues/7
170
172
  Rails.configuration.after_initialize do
171
- file_name ||= self.class.configuration['file']
172
- file_path = Rails.root.join(file_name)
173
- js_content = generate
173
+ file_path = Rails.root.join(@configuration.output_file)
174
+ source_code = generate
174
175
 
175
176
  # We don't need to rewrite file if it already exist and have same content.
176
177
  # It helps asset pipeline or webpack understand that file wasn't changed.
177
- next if File.exist?(file_path) && File.read(file_path) == js_content
178
+ next if File.exist?(file_path) && File.read(file_path) == source_code
178
179
 
179
180
  File.open(file_path, 'w') do |f|
180
- f.write js_content
181
+ f.write source_code
181
182
  end
182
183
  end
183
184
  end
184
185
 
185
186
  protected
186
187
 
188
+ def js_variables
189
+ {
190
+ 'GEM_VERSION' => JsRoutes::VERSION,
191
+ 'ROUTES_OBJECT' => routes_object,
192
+ 'RAILS_VERSION' => ActionPack.version,
193
+ 'DEPRECATED_GLOBBING_BEHAVIOR' => ActionPack::VERSION::MAJOR == 4 && ActionPack::VERSION::MINOR == 0,
194
+
195
+ 'APP_CLASS' => application.class.to_s,
196
+ 'NAMESPACE' => json(@configuration.namespace),
197
+ 'DEFAULT_URL_OPTIONS' => json(@configuration.default_url_options),
198
+ 'PREFIX' => json(@configuration.prefix),
199
+ 'SPECIAL_OPTIONS_KEY' => json(@configuration.special_options_key),
200
+ 'SERIALIZER' => @configuration.serializer || json(nil),
201
+ 'MODULE_TYPE' => json(@configuration.module_type),
202
+ 'WRAPPER' => @configuration.esm? ? 'const __jsr = ' : '',
203
+ }
204
+ end
205
+
187
206
  def application
188
207
  @configuration.application
189
208
  end
@@ -197,32 +216,52 @@ class JsRoutes
197
216
  end
198
217
 
199
218
  def routes_object
200
- return json({}) if @configuration.esm?
219
+ return json({}) if @configuration.modern?
201
220
  properties = routes_list.map do |comment, name, body|
202
221
  "#{comment}#{name}: #{body}".indent(2)
203
222
  end
204
223
  "{\n" + properties.join(",\n\n") + "}\n"
205
224
  end
206
225
 
207
- STATIC_EXPORTS = [:configure, :config, :serialize].map do |name|
208
- ["", name, "__jsr.#{name}"]
226
+ def static_exports
227
+ [:configure, :config, :serialize].map do |name|
228
+ [
229
+ "", name,
230
+ @configuration.dts? ?
231
+ "RouterExposedMethods['#{name}']" :
232
+ "__jsr.#{name}"
233
+ ]
234
+ end
209
235
  end
210
236
 
211
237
  def routes_export
212
- return "" unless @configuration.esm?
213
- [*STATIC_EXPORTS, *routes_list].map do |comment, name, body|
214
- "#{comment}export const #{name} = #{body};"
215
- end.join("\n\n")
238
+ return "" unless @configuration.modern?
239
+ [*static_exports, *routes_list].map do |comment, name, body|
240
+ "#{comment}export const #{name}#{export_separator}#{body};\n\n"
241
+ end.join
242
+ end
243
+
244
+ def prevent_types_export
245
+ return "" unless @configuration.dts?
246
+ <<-JS
247
+ // By some reason this line prevents all types in a file
248
+ // from being automatically exported
249
+ export {};
250
+ JS
251
+ end
252
+
253
+ def export_separator
254
+ @configuration.dts? ? ': ' : ' = '
216
255
  end
217
256
 
218
257
  def routes_list
219
258
  named_routes.sort_by(&:first).flat_map do |_, route|
220
259
  route_helpers_if_match(route) + mounted_app_routes(route)
221
- end.compact
260
+ end
222
261
  end
223
262
 
224
263
  def mounted_app_routes(route)
225
- rails_engine_app = get_app_from_route(route)
264
+ rails_engine_app = app_from_route(route)
226
265
  if rails_engine_app.respond_to?(:superclass) &&
227
266
  rails_engine_app.superclass == Rails::Engine && !route.path.anchored
228
267
  rails_engine_app.routes.named_routes.flat_map do |_, engine_route|
@@ -233,13 +272,14 @@ class JsRoutes
233
272
  end
234
273
  end
235
274
 
236
- def get_app_from_route(route)
275
+ def app_from_route(route)
276
+ app = route.app
237
277
  # rails engine in Rails 4.2 use additional
238
278
  # ActionDispatch::Routing::Mapper::Constraints, which contain app
239
- if route.app.respond_to?(:app) && route.app.respond_to?(:constraints)
240
- route.app.app
279
+ if app.respond_to?(:app) && app.respond_to?(:constraints)
280
+ app.app
241
281
  else
242
- route.app
282
+ app
243
283
  end
244
284
  end
245
285
 
@@ -248,6 +288,19 @@ class JsRoutes
248
288
  end
249
289
 
250
290
  class JsRoute #:nodoc:
291
+ FILTERED_DEFAULT_PARTS = [:controller, :action]
292
+ URL_OPTIONS = [:protocol, :domain, :host, :port, :subdomain]
293
+ NODE_TYPES = {
294
+ GROUP: 1,
295
+ CAT: 2,
296
+ SYMBOL: 3,
297
+ OR: 4,
298
+ STAR: 5,
299
+ LITERAL: 6,
300
+ SLASH: 7,
301
+ DOT: 8
302
+ }
303
+
251
304
  attr_reader :configuration, :route, :parent_route
252
305
 
253
306
  def initialize(configuration, route, parent_route = nil)
@@ -257,24 +310,36 @@ class JsRoutes
257
310
  end
258
311
 
259
312
  def helpers
260
- unless match_configuration?
261
- []
262
- else
263
- [false, true].map do |absolute|
264
- absolute && !@configuration[:url_links] ?
265
- nil : [ documentation, helper_name(absolute), body(absolute) ]
266
- end
313
+ helper_types.map do |absolute|
314
+ [ documentation, helper_name(absolute), body(absolute) ]
267
315
  end
268
316
  end
269
317
 
270
- protected
318
+ def helper_types
319
+ return [] unless match_configuration?
320
+ @configuration[:url_links] ? [true, false] : [false]
321
+ end
271
322
 
272
323
  def body(absolute)
273
- "__jsr.r(#{arguments(absolute).map{|a| json(a)}.join(', ')})"
324
+ @configuration.dts? ?
325
+ definition_body : "__jsr.r(#{arguments(absolute).map{|a| json(a)}.join(', ')})"
274
326
  end
275
327
 
328
+ def definition_body
329
+ args = required_parts.map{|p| "#{apply_case(p)}: RequiredRouteParameter"}
330
+ args << "options?: #{optional_parts_type} & RouteOptions"
331
+ "((\n#{args.join(",\n").indent(2)}\n) => string) & RouteHelperExtras"
332
+ end
333
+
334
+ def optional_parts_type
335
+ @optional_parts_type ||=
336
+ "{" + optional_parts.map {|p| "#{p}?: OptionalRouteParameter"}.join(', ') + "}"
337
+ end
338
+
339
+ protected
340
+
276
341
  def arguments(absolute)
277
- absolute ? base_arguments + [true] : base_arguments
342
+ absolute ? [*base_arguments, true] : base_arguments
278
343
  end
279
344
 
280
345
  def match_configuration?
@@ -319,6 +384,10 @@ JS
319
384
  route.required_parts
320
385
  end
321
386
 
387
+ def optional_parts
388
+ route.path.optional_names
389
+ end
390
+
322
391
  def base_arguments
323
392
  @base_arguments ||= [parts_table, serialize(spec, parent_spec)]
324
393
  end
@@ -388,4 +457,10 @@ JS
388
457
  ].compact
389
458
  end
390
459
  end
460
+ module Generators
461
+ end
391
462
  end
463
+
464
+ require "js_routes/middleware"
465
+ require "js_routes/generators/webpacker"
466
+ require "js_routes/generators/middleware"
data/lib/routes.d.ts CHANGED
@@ -2,14 +2,38 @@
2
2
  * File generated by js-routes RubyVariables.GEM_VERSION
3
3
  * Based on Rails RubyVariables.RAILS_VERSION routes of RubyVariables.APP_CLASS
4
4
  */
5
- declare type RouteParameter = unknown;
6
- declare type RouteParameters = Record<string, RouteParameter>;
7
- declare type Serializer = (value: unknown) => string;
8
- declare type RouteHelper = {
9
- (...args: RouteParameter[]): string;
5
+ declare type Optional<T> = {
6
+ [P in keyof T]?: T[P] | null;
7
+ };
8
+ declare type BaseRouteParameter = string | boolean | Date | number;
9
+ declare type MethodRouteParameter = BaseRouteParameter | (() => BaseRouteParameter);
10
+ declare type ModelRouteParameter = {
11
+ id: MethodRouteParameter;
12
+ } | {
13
+ to_param: MethodRouteParameter;
14
+ } | {
15
+ toParam: MethodRouteParameter;
16
+ };
17
+ declare type RequiredRouteParameter = BaseRouteParameter | ModelRouteParameter;
18
+ declare type OptionalRouteParameter = undefined | null | RequiredRouteParameter;
19
+ declare type QueryRouteParameter = OptionalRouteParameter | QueryRouteParameter[] | {
20
+ [k: string]: QueryRouteParameter;
21
+ };
22
+ declare type RouteParameters = Record<string, QueryRouteParameter>;
23
+ declare type Serializable = Record<string, unknown>;
24
+ declare type Serializer = (value: Serializable) => string;
25
+ declare type RouteHelperExtras = {
10
26
  requiredParams(): string[];
11
27
  toString(): string;
12
28
  };
29
+ declare type RequiredParameters<T extends number> = T extends 1 ? [RequiredRouteParameter] : T extends 2 ? [RequiredRouteParameter, RequiredRouteParameter] : T extends 3 ? [RequiredRouteParameter, RequiredRouteParameter, RequiredRouteParameter] : T extends 4 ? [
30
+ RequiredRouteParameter,
31
+ RequiredRouteParameter,
32
+ RequiredRouteParameter,
33
+ RequiredRouteParameter
34
+ ] : RequiredRouteParameter[];
35
+ declare type RouteHelperOptions<T extends string> = RouteOptions & Optional<Record<T, OptionalRouteParameter>>;
36
+ declare type RouteHelper<T extends number = number, U extends string = string> = ((...args: [...RequiredParameters<T>, RouteHelperOptions<U>]) => string) & RouteHelperExtras;
13
37
  declare type RouteHelpers = Record<string, RouteHelper>;
14
38
  declare type Configuration = {
15
39
  prefix: string;
@@ -17,9 +41,6 @@ declare type Configuration = {
17
41
  special_options_key: string;
18
42
  serializer: Serializer;
19
43
  };
20
- declare type Optional<T> = {
21
- [P in keyof T]?: T[P] | null;
22
- };
23
44
  interface RouterExposedMethods {
24
45
  config(): Configuration;
25
46
  configure(arg: Partial<Configuration>): Configuration;
@@ -29,15 +50,16 @@ declare type KeywordUrlOptions = Optional<{
29
50
  host: string;
30
51
  protocol: string;
31
52
  subdomain: string;
32
- port: string;
53
+ port: string | number;
33
54
  anchor: string;
34
55
  trailing_slash: boolean;
35
56
  }>;
57
+ declare type RouteOptions = KeywordUrlOptions & RouteParameters;
36
58
  declare type PartsTable = Record<string, {
37
59
  r?: boolean;
38
- d?: unknown;
60
+ d?: OptionalRouteParameter;
39
61
  }>;
40
- declare type ModuleType = "CJS" | "AMD" | "UMD" | "ESM" | "NIL";
62
+ declare type ModuleType = "CJS" | "AMD" | "UMD" | "ESM" | "DTS" | "NIL";
41
63
  declare const RubyVariables: {
42
64
  PREFIX: string;
43
65
  DEPRECATED_GLOBBING_BEHAVIOR: boolean;
data/lib/routes.js CHANGED
@@ -16,6 +16,7 @@ RubyVariables.WRAPPER((that) => {
16
16
  NodeTypes[NodeTypes["DOT"] = 8] = "DOT";
17
17
  })(NodeTypes || (NodeTypes = {}));
18
18
  const Root = that;
19
+ const isBroswer = typeof window !== "undefined";
19
20
  const ModuleReferences = {
20
21
  CJS: {
21
22
  define(routes) {
@@ -73,7 +74,16 @@ RubyVariables.WRAPPER((that) => {
73
74
  Utils.namespace(Root, RubyVariables.NAMESPACE, routes);
74
75
  },
75
76
  isSupported() {
76
- return !!Root;
77
+ return !RubyVariables.NAMESPACE || !!Root;
78
+ },
79
+ },
80
+ DTS: {
81
+ // Acts the same as ESM
82
+ define(routes) {
83
+ ModuleReferences.ESM.define(routes);
84
+ },
85
+ isSupported() {
86
+ return ModuleReferences.ESM.isSupported();
77
87
  },
78
88
  },
79
89
  };
@@ -162,7 +172,7 @@ RubyVariables.WRAPPER((that) => {
162
172
  }
163
173
  looks_like_serialized_model(object) {
164
174
  return (this.is_object(object) &&
165
- !object[this.configuration.special_options_key] &&
175
+ !(this.configuration.special_options_key in object) &&
166
176
  ("id" in object || "to_param" in object || "toParam" in object));
167
177
  }
168
178
  path_identifier(object) {
@@ -195,7 +205,9 @@ RubyVariables.WRAPPER((that) => {
195
205
  throw new Error("Too many parameters provided for path");
196
206
  }
197
207
  let use_all_parts = args.length > required_params.length;
198
- const parts_options = {};
208
+ const parts_options = {
209
+ ...this.configuration.default_url_options,
210
+ };
199
211
  for (const key in options) {
200
212
  const value = options[key];
201
213
  if (!hasProp(options, key))
@@ -320,9 +332,7 @@ RubyVariables.WRAPPER((that) => {
320
332
  }
321
333
  }
322
334
  encode_segment(segment) {
323
- return segment.replace(UriEncoderSegmentRegex, function (str) {
324
- return encodeURIComponent(str);
325
- });
335
+ return segment.replace(UriEncoderSegmentRegex, (str) => encodeURIComponent(str));
326
336
  }
327
337
  is_optional_node(node) {
328
338
  return [NodeTypes.STAR, NodeTypes.SYMBOL, NodeTypes.CAT].includes(node);
@@ -410,32 +420,17 @@ RubyVariables.WRAPPER((that) => {
410
420
  port = port ? ":" + port : "";
411
421
  return protocol + "://" + subdomain + hostname + port;
412
422
  }
413
- has_location() {
414
- return this.is_not_nullable(window) && !!window.location;
415
- }
416
423
  current_host() {
417
- if (this.has_location()) {
418
- return window.location.hostname;
419
- }
420
- else {
421
- return null;
422
- }
424
+ var _a;
425
+ return (isBroswer && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.hostname)) || "";
423
426
  }
424
427
  current_protocol() {
425
- if (this.has_location() && window.location.protocol !== "") {
426
- return window.location.protocol.replace(/:$/, "");
427
- }
428
- else {
429
- return "http";
430
- }
428
+ var _a, _b;
429
+ return ((isBroswer && ((_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");
431
430
  }
432
431
  current_port() {
433
- if (this.has_location() && window.location.port !== "") {
434
- return window.location.port;
435
- }
436
- else {
437
- return "";
438
- }
432
+ var _a;
433
+ return (isBroswer && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.port)) || "";
439
434
  }
440
435
  is_object(value) {
441
436
  return (typeof value === "object" &&
@@ -453,7 +448,7 @@ RubyVariables.WRAPPER((that) => {
453
448
  namespace(object, namespace, routes) {
454
449
  const parts = (namespace === null || namespace === void 0 ? void 0 : namespace.split(".")) || [];
455
450
  if (parts.length === 0) {
456
- return routes;
451
+ return;
457
452
  }
458
453
  for (let index = 0; index < parts.length; index++) {
459
454
  const part = parts[index];
@@ -461,7 +456,7 @@ RubyVariables.WRAPPER((that) => {
461
456
  object = object[part] || (object[part] = {});
462
457
  }
463
458
  else {
464
- return (object[part] = routes);
459
+ object[part] = routes;
465
460
  }
466
461
  }
467
462
  }