js-routes 2.0.6 → 2.1.1

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
@@ -60,6 +38,7 @@ class JsRoutes
60
38
  value = value.call if value.is_a?(Proc)
61
39
  send(:"#{attribute}=", value)
62
40
  end
41
+ normalize_and_verify
63
42
  self
64
43
  end
65
44
 
@@ -76,7 +55,53 @@ class JsRoutes
76
55
  end
77
56
 
78
57
  def esm?
79
- self.module_type === 'ESM'
58
+ module_type === 'ESM'
59
+ end
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
+
86
+ def normalize_and_verify
87
+ normalize
88
+ verify
89
+ end
90
+
91
+ protected
92
+
93
+ def default_file_name
94
+ dts? ? "routes.d.ts" : "routes.js"
95
+ end
96
+
97
+ def normalize
98
+ self.module_type = module_type&.upcase || 'NIL'
99
+ end
100
+
101
+ def verify
102
+ if module_type != 'NIL' && namespace
103
+ raise "JsRoutes namespace option can only be used if module_type is nil"
104
+ end
80
105
  end
81
106
  end
82
107
 
@@ -87,22 +112,28 @@ class JsRoutes
87
112
  class << self
88
113
  def setup(&block)
89
114
  configuration.tap(&block) if block
115
+ configuration.normalize_and_verify
90
116
  end
91
117
 
92
118
  def configuration
93
119
  @configuration ||= Configuration.new
94
120
  end
95
121
 
96
- def generate(opts = {})
122
+ def generate(**opts)
97
123
  new(opts).generate
98
124
  end
99
125
 
100
- def generate!(file_name=nil, opts = {})
101
- if file_name.is_a?(Hash)
102
- opts = file_name
103
- file_name = opts[:file]
104
- end
105
- 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{\.(j|t)s\Z}, ".d.ts")
136
+ generate!(file_name, module_type: 'DTS', **opts)
106
137
  end
107
138
 
108
139
  def json(string)
@@ -123,48 +154,55 @@ class JsRoutes
123
154
  if named_routes.to_a.empty? && application.respond_to?(:reload_routes!)
124
155
  application.reload_routes!
125
156
  end
157
+ content = File.read(@configuration.source_file)
126
158
 
127
- {
128
- 'GEM_VERSION' => JsRoutes::VERSION,
129
- 'ROUTES_OBJECT' => routes_object,
130
- 'RAILS_VERSION' => ActionPack.version,
131
- 'DEPRECATED_GLOBBING_BEHAVIOR' => ActionPack::VERSION::MAJOR == 4 && ActionPack::VERSION::MINOR == 0,
132
-
133
- 'APP_CLASS' => application.class.to_s,
134
- 'NAMESPACE' => json(@configuration.namespace),
135
- 'DEFAULT_URL_OPTIONS' => json(@configuration.default_url_options),
136
- 'PREFIX' => json(@configuration.prefix),
137
- 'SPECIAL_OPTIONS_KEY' => json(@configuration.special_options_key),
138
- 'SERIALIZER' => @configuration.serializer || json(nil),
139
- 'MODULE_TYPE' => json(@configuration.module_type),
140
- 'WRAPPER' => @configuration.esm? ? 'const __jsr = ' : '',
141
- }.inject(File.read(File.dirname(__FILE__) + "/routes.js")) do |js, (key, value)|
142
- 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) ||
143
162
  raise("Missing key #{key} in JS template")
144
- end + routes_export
163
+ end
164
+ end
165
+ content + routes_export + prevent_types_export
145
166
  end
146
167
 
147
- def generate!(file_name = nil)
148
- # Some libraries like Devise do not yet loaded their routes so we will wait
149
- # 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
150
171
  # https://github.com/railsware/js-routes/issues/7
151
172
  Rails.configuration.after_initialize do
152
- file_name ||= self.class.configuration['file']
153
- file_path = Rails.root.join(file_name)
154
- js_content = generate
173
+ file_path = Rails.root.join(@configuration.output_file)
174
+ source_code = generate
155
175
 
156
176
  # We don't need to rewrite file if it already exist and have same content.
157
177
  # It helps asset pipeline or webpack understand that file wasn't changed.
158
- 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
159
179
 
160
180
  File.open(file_path, 'w') do |f|
161
- f.write js_content
181
+ f.write source_code
162
182
  end
163
183
  end
164
184
  end
165
185
 
166
186
  protected
167
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
+
168
206
  def application
169
207
  @configuration.application
170
208
  end
@@ -178,32 +216,52 @@ class JsRoutes
178
216
  end
179
217
 
180
218
  def routes_object
181
- return json({}) if @configuration.esm?
219
+ return json({}) if @configuration.modern?
182
220
  properties = routes_list.map do |comment, name, body|
183
221
  "#{comment}#{name}: #{body}".indent(2)
184
222
  end
185
223
  "{\n" + properties.join(",\n\n") + "}\n"
186
224
  end
187
225
 
188
- STATIC_EXPORTS = [:configure, :config, :serialize].map do |name|
189
- ["", 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
190
235
  end
191
236
 
192
237
  def routes_export
193
- return "" unless @configuration.esm?
194
- [*STATIC_EXPORTS, *routes_list].map do |comment, name, body|
195
- "#{comment}export const #{name} = #{body};"
196
- 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? ? ': ' : ' = '
197
255
  end
198
256
 
199
257
  def routes_list
200
258
  named_routes.sort_by(&:first).flat_map do |_, route|
201
259
  route_helpers_if_match(route) + mounted_app_routes(route)
202
- end.compact
260
+ end
203
261
  end
204
262
 
205
263
  def mounted_app_routes(route)
206
- rails_engine_app = get_app_from_route(route)
264
+ rails_engine_app = app_from_route(route)
207
265
  if rails_engine_app.respond_to?(:superclass) &&
208
266
  rails_engine_app.superclass == Rails::Engine && !route.path.anchored
209
267
  rails_engine_app.routes.named_routes.flat_map do |_, engine_route|
@@ -214,7 +272,7 @@ class JsRoutes
214
272
  end
215
273
  end
216
274
 
217
- def get_app_from_route(route)
275
+ def app_from_route(route)
218
276
  # rails engine in Rails 4.2 use additional
219
277
  # ActionDispatch::Routing::Mapper::Constraints, which contain app
220
278
  if route.app.respond_to?(:app) && route.app.respond_to?(:constraints)
@@ -229,6 +287,19 @@ class JsRoutes
229
287
  end
230
288
 
231
289
  class JsRoute #:nodoc:
290
+ FILTERED_DEFAULT_PARTS = [:controller, :action]
291
+ URL_OPTIONS = [:protocol, :domain, :host, :port, :subdomain]
292
+ NODE_TYPES = {
293
+ GROUP: 1,
294
+ CAT: 2,
295
+ SYMBOL: 3,
296
+ OR: 4,
297
+ STAR: 5,
298
+ LITERAL: 6,
299
+ SLASH: 7,
300
+ DOT: 8
301
+ }
302
+
232
303
  attr_reader :configuration, :route, :parent_route
233
304
 
234
305
  def initialize(configuration, route, parent_route = nil)
@@ -238,22 +309,36 @@ class JsRoutes
238
309
  end
239
310
 
240
311
  def helpers
241
- unless match_configuration?
242
- []
243
- else
244
- [false, true].map do |absolute|
245
- absolute && !@configuration[:url_links] ?
246
- nil : [ documentation, helper_name(absolute), body(absolute) ]
247
- end
312
+ helper_types.map do |absolute|
313
+ [ documentation, helper_name(absolute), body(absolute) ]
248
314
  end
249
315
  end
250
316
 
317
+ def helper_types
318
+ return [] unless match_configuration?
319
+ @configuration[:url_links] ? [true, false] : [false]
320
+ end
321
+
251
322
  def body(absolute)
252
- "__jsr.r(#{arguments(absolute).join(', ')})"
323
+ @configuration.dts? ?
324
+ definition_body : "__jsr.r(#{arguments(absolute).map{|a| json(a)}.join(', ')})"
325
+ end
326
+
327
+ def definition_body
328
+ args = required_parts.map{|p| "#{apply_case(p)}: RequiredRouteParameter"}
329
+ args << "options?: #{optional_parts_type} & RouteOptions"
330
+ "((\n#{args.join(",\n").indent(2)}\n) => string) & RouteHelperExtras"
253
331
  end
254
332
 
333
+ def optional_parts_type
334
+ @optional_parts_type ||=
335
+ "{" + optional_parts.map {|p| "#{p}?: OptionalRouteParameter"}.join(', ') + "}"
336
+ end
337
+
338
+ protected
339
+
255
340
  def arguments(absolute)
256
- absolute ? base_arguments + [json(true)] : base_arguments
341
+ absolute ? [*base_arguments, true] : base_arguments
257
342
  end
258
343
 
259
344
  def match_configuration?
@@ -298,10 +383,15 @@ JS
298
383
  route.required_parts
299
384
  end
300
385
 
301
- protected
386
+ def optional_parts
387
+ route.path.optional_names
388
+ end
302
389
 
303
390
  def base_arguments
304
- return @base_arguments if defined?(@base_arguments)
391
+ @base_arguments ||= [parts_table, serialize(spec, parent_spec)]
392
+ end
393
+
394
+ def parts_table
305
395
  parts_table = {}
306
396
  route.parts.each do |part, hash|
307
397
  parts_table[part] ||= {}
@@ -318,11 +408,7 @@ JS
318
408
  parts_table[part][:d] = value
319
409
  end
320
410
  end
321
- @base_arguments = [
322
- parts_table, serialize(spec, parent_spec)
323
- ].map do |argument|
324
- json(argument)
325
- end
411
+ parts_table
326
412
  end
327
413
 
328
414
  def documentation_params
@@ -371,3 +457,5 @@ JS
371
457
  end
372
458
  end
373
459
  end
460
+
461
+ require "js_routes/generators/webpacker"
data/lib/routes.d.ts CHANGED
@@ -2,63 +2,78 @@
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;
10
- requiredParams(): string[];
11
- toString(): 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 = {
26
+ requiredParams(): string[];
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
- prefix: string;
16
- default_url_options: RouteParameters;
17
- special_options_key: string;
18
- serializer: Serializer;
19
- };
20
- declare type Optional<T> = {
21
- [P in keyof T]?: T[P] | null;
39
+ prefix: string;
40
+ default_url_options: RouteParameters;
41
+ special_options_key: string;
42
+ serializer: Serializer;
22
43
  };
23
44
  interface RouterExposedMethods {
24
- config(): Configuration;
25
- configure(arg: Partial<Configuration>): Configuration;
26
- serialize: Serializer;
45
+ config(): Configuration;
46
+ configure(arg: Partial<Configuration>): Configuration;
47
+ serialize: Serializer;
27
48
  }
28
49
  declare type KeywordUrlOptions = Optional<{
29
- host: string;
30
- protocol: string;
31
- subdomain: string;
32
- port: string;
33
- anchor: string;
34
- trailing_slash: boolean;
50
+ host: string;
51
+ protocol: string;
52
+ subdomain: string;
53
+ port: string | number;
54
+ anchor: string;
55
+ trailing_slash: boolean;
35
56
  }>;
36
- declare type PartsTable = Record<
37
- string,
38
- {
57
+ declare type RouteOptions = KeywordUrlOptions & RouteParameters;
58
+ declare type PartsTable = Record<string, {
39
59
  r?: boolean;
40
- d?: unknown;
41
- }
42
- >;
43
- declare type ModuleType = "CJS" | "AMD" | "UMD" | "ESM";
60
+ d?: OptionalRouteParameter;
61
+ }>;
62
+ declare type ModuleType = "CJS" | "AMD" | "UMD" | "ESM" | "DTS" | "NIL";
44
63
  declare const RubyVariables: {
45
- PREFIX: string;
46
- DEPRECATED_GLOBBING_BEHAVIOR: boolean;
47
- SPECIAL_OPTIONS_KEY: string;
48
- DEFAULT_URL_OPTIONS: RouteParameters;
49
- SERIALIZER: Serializer;
50
- NAMESPACE: string;
51
- ROUTES_OBJECT: RouteHelpers;
52
- MODULE_TYPE: ModuleType | null;
53
- WRAPPER: <T>(callback: T) => T;
64
+ PREFIX: string;
65
+ DEPRECATED_GLOBBING_BEHAVIOR: boolean;
66
+ SPECIAL_OPTIONS_KEY: string;
67
+ DEFAULT_URL_OPTIONS: RouteParameters;
68
+ SERIALIZER: Serializer;
69
+ NAMESPACE: string;
70
+ ROUTES_OBJECT: RouteHelpers;
71
+ MODULE_TYPE: ModuleType;
72
+ WRAPPER: <T>(callback: T) => T;
54
73
  };
55
- declare const define:
56
- | undefined
57
- | (((arg: unknown[], callback: () => unknown) => void) & {
58
- amd?: unknown;
59
- });
60
- declare const module:
61
- | {
62
- exports: any;
63
- }
64
- | undefined;
74
+ declare const define: undefined | (((arg: unknown[], callback: () => unknown) => void) & {
75
+ amd?: unknown;
76
+ });
77
+ declare const module: {
78
+ exports: any;
79
+ } | undefined;