js-routes 2.0.8 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }