js_from_routes 4.0.2 → 5.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13b5b3af46d9a41f4ae0628b9d615fed9e17d564be52db3feabd3d72746cdc8d
4
- data.tar.gz: 509f343953d3b610ce0d5ee41558ad92464cbb28866f49cbb5589112b1638220
3
+ metadata.gz: 1c5e43fa00d907a28931b13ec417afb2d7ef602fdb1b9c47ccd9e00289b305ec
4
+ data.tar.gz: 4bf4c33ffd5adf831f940f82d51e68af66cc7be9199c5fcbcd957ab4cf2858be
5
5
  SHA512:
6
- metadata.gz: b71c17a8b7023eff5918bd35eea370e15cd141db8bc07a196e7b093c048208848cf6df117d04d084e21adb882e7077e283255c02ab162c2169c2ffa18eeadb07
7
- data.tar.gz: 53288df1b91c1bbc2562cb249e659cfb8e4117223e0503eb3db1e79d3b6376d31da161875b884b64ef484aba17c3253cfc3a298bbd09193f1eb9cecf909235cf
6
+ metadata.gz: d55050b0f4b4168db33c7db98fa51323fb469e5b8ffd0f484da07860d6834d1a23db1537fcb05414422ec14d344f31fa15f784aac33d283b5365baa45caebda5
7
+ data.tar.gz: fa99b4558aae4ad1e91fc6ab4792b058f928232565f521c98e32344bd304e548fc8cfcac873e31c13ca1be06a170bbc9a3d13db8ddc88cc89eacfcf367e4612d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ # [5.0.0](https://github.com/ElMassimo/js_from_routes/compare/js_from_routes@4.1.0...js_from_routes@5.0.0) (2026-10-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * remove cache-key headers and watch route changes ([#72](https://github.com/ElMassimo/js_from_routes/issues/72)) ([2d34fc1](https://github.com/ElMassimo/js_from_routes/commit/2d34fc1015157339ce9c39bd0c30cf30a34abdb1))
7
+
8
+
9
+
10
+ # [5.0.0](https://github.com/ElMassimo/js_from_routes/compare/js_from_routes@4.1.0...js_from_routes@5.0.0) (2026-10-17)
11
+
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ * requires Rails 7.2 or newer, which is what CI has been testing against. The development reloader relies on `RoutesReloader#external_routes` and `config.file_watcher`.
16
+
17
+
18
+ ### Features
19
+
20
+ * generated files no longer start with a `// JsFromRoutes CacheKey` header. Files are compared by content and only written when they changed, which removes a frequent source of merge conflicts. Run `bin/rake js_from_routes:generate` once after upgrading to drop the header from existing files.
21
+ * in development, helpers are generated at boot and then only when a routes file or a template changes, instead of on every reload.
22
+ * `JS_FROM_ROUTES_FORCE` is no longer needed and has been removed.
23
+
24
+
25
+ # [4.1.0](https://github.com/ElMassimo/js_from_routes/compare/js_from_routes@4.0.2...js_from_routes@4.1.0) (2026-08-03)
26
+
27
+
28
+ ### Features
29
+
30
+ * support multiple generator instances for sub-app routing ([#70](https://github.com/ElMassimo/js_from_routes/issues/70)) ([49d944b](https://github.com/ElMassimo/js_from_routes/commit/49d944b7fcde885f1e8b26312738450a4a3f9136))
31
+
32
+
33
+
1
34
  ## [4.0.2](https://github.com/ElMassimo/js_from_routes/compare/js_from_routes@4.0.1...js_from_routes@4.0.2) (2025-01-15)
2
35
 
3
36
 
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
3
  require "erubi"
5
4
  require "fileutils"
6
- require "pathname"
7
5
 
8
6
  # Public: Automatically generates JS for Rails routes with { export: true }.
9
7
  # Generates one file per controller, and one function per route.
@@ -24,12 +22,6 @@ module JsFromRoutes
24
22
  }
25
23
  end
26
24
 
27
- # Public: Used to check whether the file should be generated again, changes
28
- # based on the configuration, and route definition.
29
- def cache_key
30
- routes.map(&:inspect).join + [File.read(@config.template_path), @config.helper_mappings.inspect, @config.client_library].join
31
- end
32
-
33
25
  # Public: Exposes the preferred import library to the generator.
34
26
  def client_library
35
27
  @config.client_library
@@ -87,7 +79,7 @@ module JsFromRoutes
87
79
  @mappings.fetch(helper, helper)
88
80
  end
89
81
 
90
- # Internal: Useful as a cache key for the route, and for debugging purposes.
82
+ # Internal: Useful for debugging purposes.
91
83
  def inspect
92
84
  "#{verb} #{helper} #{path}"
93
85
  end
@@ -100,10 +92,10 @@ module JsFromRoutes
100
92
  @compiled_template = Erubi::Engine.new(File.read(template_path), filename: template_path).src
101
93
  end
102
94
 
103
- # Public: Checks if the cache is fresh, or renders the template with the
104
- # specified variables, and writes the updated result to a file.
95
+ # Public: Renders the template with the specified variables, and writes the
96
+ # result to a file unless it already has the same content.
105
97
  def write_if_changed(object)
106
- write_file_if_changed(object.filename, object.cache_key) { render_template(object) }
98
+ write_file_if_changed(object.filename) { render_template(object) }
107
99
  end
108
100
 
109
101
  private
@@ -113,25 +105,16 @@ module JsFromRoutes
113
105
  object.instance_eval(@compiled_template)
114
106
  end
115
107
 
116
- # Internal: Returns true if the cache key has changed since the last codegen.
117
- def stale?(file, cache_key_comment)
118
- ENV["JS_FROM_ROUTES_FORCE"] || file.gets != cache_key_comment
119
- end
120
-
121
- # Internal: Writes if the file does not exist or the cache key has changed.
122
- # The cache strategy consists of a comment on the first line of the file.
108
+ # Internal: Writes the file unless it already has the same content, which
109
+ # keeps mtimes stable so file watchers don't react to no-op generations.
123
110
  #
124
- # Yields to receive the rendered file content when it needs to.
125
- def write_file_if_changed(name, cache_key)
111
+ # Yields to receive the rendered file content.
112
+ def write_file_if_changed(name)
113
+ content = yield
114
+ return if name.exist? && name.read == content
115
+
126
116
  FileUtils.mkdir_p(name.dirname)
127
- cache_key_comment = "// JsFromRoutes CacheKey #{Digest::MD5.hexdigest(cache_key)}\n"
128
- File.open(name, "a+") { |file|
129
- if stale?(file, cache_key_comment)
130
- file.truncate(0)
131
- file.write(cache_key_comment)
132
- file.write(yield)
133
- end
134
- }
117
+ name.write(content)
135
118
  end
136
119
  end
137
120
 
@@ -155,28 +138,40 @@ module JsFromRoutes
155
138
  end
156
139
 
157
140
  class TemplateConfig
158
- attr_reader :cache_key, :filename, :helpers
141
+ attr_reader :filename, :helpers
159
142
 
160
- def initialize(cache_key:, filename:, helpers: nil)
161
- @cache_key = cache_key
143
+ def initialize(filename:, helpers: nil)
162
144
  @filename = filename
163
145
  @helpers = helpers
164
146
  end
165
147
  end
166
148
 
167
- class << self
168
- # Public: Configuration of the code generator.
169
- def config
170
- @config ||= Configuration.new(::Rails.root || Pathname.new(Dir.pwd))
171
- yield(@config) if block_given?
172
- @config
149
+ # Public: A generator instance with its own configuration. Allows generating
150
+ # routes separately for different sub-apps in a Rails monolith.
151
+ #
152
+ # Example:
153
+ # JsFromRoutes.config(:admin) do |config|
154
+ # config.output_folder = Rails.root.join("app/javascript/admin/api")
155
+ # config.export_if = ->(route) { route.defaults[:export] == :admin }
156
+ # end
157
+ #
158
+ # JsFromRoutes.config(:public) do |config|
159
+ # config.output_folder = Rails.root.join("app/javascript/public/api")
160
+ # config.export_if = ->(route) { route.defaults[:export] == :public }
161
+ # end
162
+ class Instance
163
+ attr_reader :name, :config
164
+
165
+ def initialize(name, config)
166
+ @name = name
167
+ @config = config
173
168
  end
174
169
 
175
170
  # Public: Generates code for the specified routes with { export: true }.
176
171
  def generate!(app_or_routes = Rails.application)
177
172
  raise ArgumentError, "A Rails app must be defined, or you must specify a custom `output_folder`" if config.output_folder.to_s.blank?
178
173
  rails_routes = app_or_routes.is_a?(::Rails::Engine) ? app_or_routes.routes.routes : app_or_routes
179
- generate_files exported_routes_by_controller(rails_routes)
174
+ generate_files(exported_routes_by_controller(rails_routes))
180
175
  end
181
176
 
182
177
  private
@@ -198,12 +193,10 @@ module JsFromRoutes
198
193
  index_file = (config.all_helpers_file == true) ? "index#{preferred_extension}" : config.all_helpers_file
199
194
 
200
195
  Template.new(config.template_all_path).write_if_changed TemplateConfig.new(
201
- cache_key: routes.map(&:import_filename).join + File.read(config.template_all_path),
202
196
  filename: config.output_folder.join("all#{preferred_extension}"),
203
197
  helpers: routes,
204
198
  )
205
199
  Template.new(config.template_index_path).write_if_changed TemplateConfig.new(
206
- cache_key: File.read(config.template_index_path),
207
200
  filename: config.output_folder.join(index_file),
208
201
  )
209
202
  end
@@ -221,4 +214,88 @@ module JsFromRoutes
221
214
  .group_by { |route| namespace_for_route(route)&.to_s }
222
215
  end
223
216
  end
217
+
218
+ class << self
219
+ # Public: Configuration of the code generator.
220
+ #
221
+ # Without a name, returns/yields the default global configuration:
222
+ # JsFromRoutes.config do |config|
223
+ # config.file_suffix = "Api.ts"
224
+ # end
225
+ #
226
+ # With a name, defines a named generator instance with its own configuration,
227
+ # allowing separate route generation for different sub-apps:
228
+ # JsFromRoutes.config(:admin) do |config|
229
+ # config.output_folder = Rails.root.join("app/javascript/admin/api")
230
+ # config.export_if = ->(route) { route.defaults[:export] == :admin }
231
+ # end
232
+ def config(name = nil)
233
+ if name
234
+ root = ::Rails.root || Pathname.new(Dir.pwd)
235
+ new_config = Configuration.new(root)
236
+ yield(new_config) if block_given?
237
+ instances[name] = Instance.new(name, new_config)
238
+ else
239
+ @config ||= Configuration.new(::Rails.root || Pathname.new(Dir.pwd))
240
+ if block_given?
241
+ @config_customized = true
242
+ yield(@config)
243
+ end
244
+ @config
245
+ end
246
+ end
247
+
248
+ # Public: Returns all registered named instances.
249
+ def instances
250
+ @instances ||= {}
251
+ end
252
+
253
+ # Public: Generates code for the specified routes with { export: true }.
254
+ #
255
+ # Runs every named instance, plus the default global config whenever it was
256
+ # explicitly configured (or when no named instances exist at all). This keeps
257
+ # mixed usage working: adding a named instance never silently disables the
258
+ # global config that an existing app already relies on.
259
+ def generate!(app_or_routes = Rails.application)
260
+ runnable_instances.each { |inst| inst.generate!(app_or_routes) }
261
+ end
262
+
263
+ # Public: Generates code on the first call, and afterwards only when a
264
+ # routes file or a template changed, so that reloads unrelated to routes
265
+ # don't pay for iterating every route.
266
+ def generate_if_changed(app = Rails.application)
267
+ watcher(app).execute_if_updated
268
+ end
269
+
270
+ # Public: Watches the files Rails loads routes from, and the templates.
271
+ # Generates once when created, so that helpers are fresh after boot.
272
+ #
273
+ # Registered as a Rails reloader by the railtie, so that editing a
274
+ # template triggers a reload even when no autoloaded file changed.
275
+ def watcher(app = Rails.application)
276
+ @watcher ||= app.config.file_watcher.new(watched_files(app), watched_dirs(app)) { generate!(app) }
277
+ .tap { generate!(app) }
278
+ end
279
+
280
+ private
281
+
282
+ def runnable_instances
283
+ runnable = instances.values
284
+ runnable.unshift(Instance.new(:default, config)) if @config_customized || runnable.empty?
285
+ runnable
286
+ end
287
+
288
+ # Internal: The files Rails loads routes from, plus the templates, which
289
+ # also determine the generated output.
290
+ def watched_files(app)
291
+ templates = runnable_instances.flat_map { |inst|
292
+ [inst.config.template_path, inst.config.template_all_path, inst.config.template_index_path]
293
+ }
294
+ (app.routes_reloader.paths + templates).map(&:to_s).uniq
295
+ end
296
+
297
+ def watched_dirs(app)
298
+ app.routes_reloader.external_routes.to_h { |dir| [dir.to_s, %w[rb]] }
299
+ end
300
+ end
224
301
  end
@@ -10,7 +10,11 @@ class JsFromRoutes::Railtie < Rails::Railtie
10
10
  # Allows to automatically trigger code generation after updating routes.
11
11
  initializer "js_from_routes.reloader" do |app|
12
12
  app.config.to_prepare do
13
- JsFromRoutes.generate!(app)
13
+ JsFromRoutes.generate_if_changed(app)
14
+ end
15
+
16
+ app.config.after_initialize do
17
+ app.reloaders << JsFromRoutes.watcher(app)
14
18
  end
15
19
  end
16
20
  end
@@ -1,4 +1,3 @@
1
- //
2
1
  // DO NOT MODIFY: This file was automatically generated by JsFromRoutes.
3
2
  import { definePathHelper } from '<%= client_library %>'
4
3
 
@@ -1,4 +1,3 @@
1
- //
2
1
  // DO NOT MODIFY: This file was automatically generated by JsFromRoutes.
3
2
  <% helpers.each do |helper| %>
4
3
  export { default as <%= helper.js_name %> } from './<%= helper.import_filename %>'
@@ -1,4 +1,3 @@
1
- //
2
1
  // DO NOT MODIFY: This file was automatically generated by JsFromRoutes.
3
2
  import * as helpers from './all'
4
3
  export * from './all'
@@ -4,5 +4,5 @@
4
4
  # Generates one file per controller, and one function per route.
5
5
  module JsFromRoutes
6
6
  # Public: This library adheres to semantic versioning.
7
- VERSION = "4.0.2"
7
+ VERSION = "5.0.0"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: js_from_routes
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.2
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Máximo Mussini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-15 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.1'
19
+ version: '7.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '9'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '5.1'
29
+ version: '7.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9'
@@ -34,16 +34,22 @@ dependencies:
34
34
  name: bundler
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '5'
40
43
  type: :development
41
44
  prerelease: false
42
45
  version_requirements: !ruby/object:Gem::Requirement
43
46
  requirements:
44
- - - "~>"
47
+ - - ">="
45
48
  - !ruby/object:Gem::Version
46
49
  version: '2'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '5'
47
53
  - !ruby/object:Gem::Dependency
48
54
  name: listen
49
55
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +78,20 @@ dependencies:
72
78
  - - "~>"
73
79
  - !ruby/object:Gem::Version
74
80
  version: '3.9'
81
+ - !ruby/object:Gem::Dependency
82
+ name: readline
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
75
95
  - !ruby/object:Gem::Dependency
76
96
  name: rake
77
97
  requirement: !ruby/object:Gem::Requirement
@@ -104,16 +124,16 @@ dependencies:
104
124
  name: simplecov
105
125
  requirement: !ruby/object:Gem::Requirement
106
126
  requirements:
107
- - - "<"
127
+ - - "~>"
108
128
  - !ruby/object:Gem::Version
109
- version: '0.18'
129
+ version: 0.17.1
110
130
  type: :development
111
131
  prerelease: false
112
132
  version_requirements: !ruby/object:Gem::Requirement
113
133
  requirements:
114
- - - "<"
134
+ - - "~>"
115
135
  - !ruby/object:Gem::Version
116
- version: '0.18'
136
+ version: 0.17.1
117
137
  - !ruby/object:Gem::Dependency
118
138
  name: standard
119
139
  requirement: !ruby/object:Gem::Requirement