react-manifest-rails 0.2.9 → 0.2.13

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: 5a26dc7e8929f42fed07d13c8d38d3390d99d8063c81e5887ac171e52c1d9303
4
- data.tar.gz: defd50858d68d0e22fdd396d6be214af15edb6eb6c5c030c1047c17f1f2ab0e6
3
+ metadata.gz: 78103a797d9951bb67d7e9cd4fcbb0fbf5a6589cd5995f52f745de079b3d6824
4
+ data.tar.gz: 229244d4844f0accdc1c23538f0a5ec8965a35e5246a62abc91dfae3ce16676d
5
5
  SHA512:
6
- metadata.gz: d94c1392eaaa325b56225b618f318d0af156c4696dc806a2ba238796c10c75f128c4a7369475a455d1e6f851edd8ca66a5a10e86ed246a0d15917ba00a198bab
7
- data.tar.gz: 86ddf03428eb1889ea9bcabd463fa761bc5738a724e201197323228ef067cd1d400f85da40d273a69cc06bf7c8016a9bba732ddd36e6c56147d980d14bda74ce
6
+ metadata.gz: 43ba51a2daca72844e5d6606fa4e6c15dda1cef4b42582ab2db4d2a49037675157814a20030e41f60d9bc6c395dd2681f548f0244884b733104f37b66996c8a2
7
+ data.tar.gz: 01a71eda1dc257b0e0c151e34ac227323a2349c02c8d7709edd619975ba9051c9f759f89b440e317e6575d1e37edfba0c9da20ed2f306d57b587ddd239162ca1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.10] - 2026-04-16
9
+
10
+ ### Fixed
11
+ - `ApplicationMigrator` now removes only `ux`-classified directives and preserves non-UX requires in `application*.js`, preventing accidental removal of root-level assets such as `mini-search`.
12
+ - Re-running migration/setup no longer duplicates the managed header comment block in `application*.js`.
13
+
14
+ ### Added
15
+ - Regression coverage for preserving unknown non-UX requires during migration.
16
+ - Regression coverage for idempotent managed-header behavior on repeated migration runs.
17
+ - Dummy app root-level fixture assets (`axios.min.js`, `mini-search.js`) and corresponding requires in `application.js` / `application_dev.js` to verify setup behavior before release.
18
+
8
19
  ## [0.2.9] - 2026-04-15
9
20
 
10
21
  ### Added
data/README.md CHANGED
@@ -88,6 +88,8 @@ Generation is **directory-based** — deterministic and conservative by design.
88
88
 
89
89
  Namespace fallback for nested controllers: `admin/reports/summary` tries `ux_admin_reports_summary`, then `ux_admin_reports`, then `ux_admin`, then `ux_summary`. The most specific match wins.
90
90
 
91
+ When using `react-rails`, `react_component("ComponentName")` is also component-aware: the helper infers the matching controller bundle from the component symbol and includes it at render time. This provides a fallback when controller naming and bundle naming do not align perfectly.
92
+
91
93
  The gem's scanner uses regex to detect which shared symbols are referenced in each controller directory (for the `react_manifest:analyze` report). Generation itself stays directory-based to avoid brittle runtime misses from dynamic component references.
92
94
 
93
95
  ## What Gets Generated
@@ -204,6 +206,16 @@ Check in order:
204
206
  - Restart the Rails server.
205
207
  - Without `listen`, run `react_manifest:generate` manually after making changes.
206
208
 
209
+ ### `ComponentName is not defined` from `react_component`
210
+
211
+ If you see errors like `UserSignInForm is not defined` (often from `eval` inside `react-rails`), ensure your layout does **not** defer the bundle tag:
212
+
213
+ ```erb
214
+ <%= react_bundle_tag %>
215
+ ```
216
+
217
+ Using `defer: true` can cause `react_component` inline scripts to run before your `ux_*.js` bundles are executed.
218
+
207
219
  ## Compatibility
208
220
 
209
221
  - Ruby: 3.2+
@@ -1,6 +1,6 @@
1
1
  module ReactManifest
2
2
  # Rewrites application*.js files to remove UX/app code requires,
3
- # keeping only vendor lib requires.
3
+ # while preserving all non-UX directives.
4
4
  #
5
5
  # Safety:
6
6
  # - Creates a .bak backup before any write; aborts if backup fails
@@ -9,6 +9,12 @@ module ReactManifest
9
9
  # - Adds a managed-by comment at the top
10
10
  class ApplicationMigrator
11
11
  MANAGED_COMMENT = <<~JS.freeze
12
+ // Non-UX libraries — loaded on every page.
13
+ // React app code is now served per-controller via react_bundle_tag.
14
+ // Managed by react-manifest-rails — do not add require_tree.
15
+ JS
16
+
17
+ LEGACY_MANAGED_COMMENT = <<~JS.freeze
12
18
  // Vendor libraries — loaded on every page.
13
19
  // React app code is now served per-controller via react_bundle_tag.
14
20
  // Managed by react-manifest-rails — do not add require_tree.
@@ -70,12 +76,13 @@ module ReactManifest
70
76
 
71
77
  def build_new_content(result)
72
78
  kept_lines = result.directives
73
- .select do |d|
74
- %i[vendor
75
- passthrough].include?(d.classification)
79
+ .reject do |d|
80
+ d.classification == :ux_code
76
81
  end
77
82
  .map(&:original_line)
78
83
 
84
+ strip_managed_header!(kept_lines)
85
+
79
86
  # Remove leading blank lines from kept_lines
80
87
  kept_lines.shift while kept_lines.first&.strip&.empty?
81
88
 
@@ -87,6 +94,25 @@ module ReactManifest
87
94
  lines.join("\n")
88
95
  end
89
96
 
97
+ def strip_managed_header!(lines)
98
+ managed_variants = [MANAGED_COMMENT, LEGACY_MANAGED_COMMENT].map { |comment| comment.lines.map(&:chomp) }
99
+
100
+ loop do
101
+ matched = managed_variants.any? { |managed_lines| starts_with_lines?(lines, managed_lines) }
102
+ break unless matched
103
+
104
+ header_len = managed_variants.find { |managed_lines| starts_with_lines?(lines, managed_lines) }.length
105
+ lines.shift(header_len)
106
+ lines.shift while lines.first&.strip&.empty?
107
+ end
108
+ end
109
+
110
+ def starts_with_lines?(lines, prefix)
111
+ return false if lines.length < prefix.length
112
+
113
+ lines.first(prefix.length) == prefix
114
+ end
115
+
90
116
  def print_diff(file, new_content)
91
117
  old_lines = File.readlines(file, encoding: "utf-8").map(&:chomp)
92
118
  new_lines = new_content.lines.map(&:chomp)
@@ -1,5 +1,4 @@
1
1
  require "digest"
2
- require "time"
3
2
  require "tmpdir"
4
3
 
5
4
  module ReactManifest
@@ -26,7 +25,7 @@ module ReactManifest
26
25
  class Generator
27
26
  HEADER = <<~JS.freeze
28
27
  // AUTO-GENERATED — DO NOT EDIT
29
- // react-manifest-rails %<version>s | %<timestamp>s
28
+ // react-manifest-rails %<version>s
30
29
  // Run `rails react_manifest:generate` to regenerate.
31
30
  JS
32
31
 
@@ -82,8 +81,6 @@ module ReactManifest
82
81
 
83
82
  def build_controller(ctrl)
84
83
  lines = header_lines
85
- lines << "//= require #{@config.shared_bundle}"
86
- lines << ""
87
84
 
88
85
  files = js_files_in(ctrl[:path])
89
86
  if files.empty?
@@ -167,7 +164,7 @@ module ReactManifest
167
164
 
168
165
  def header_lines
169
166
  [
170
- format(HEADER, version: ReactManifest::VERSION, timestamp: Time.now.utc.iso8601),
167
+ format(HEADER, version: ReactManifest::VERSION),
171
168
  ""
172
169
  ].flatten
173
170
  end
@@ -206,7 +203,7 @@ module ReactManifest
206
203
  # Build relative to output_dir (configurable) rather than a hardcoded path.
207
204
  base = @config.abs_output_dir + File::SEPARATOR
208
205
  rel = abs_path.sub(base, "")
209
- # Strip Sprockets-understood extensions: .js.jsx → "", .jsx "", .js → ""
206
+ # Strip Sprockets-understood extensions: .js.jsx/.jsx/.js -> logical path.
210
207
  rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
211
208
  end
212
209
 
@@ -10,8 +10,8 @@ module ReactManifest
10
10
  # ReactManifest::LayoutPatcher.new(config).patch!
11
11
  class LayoutPatcher
12
12
  LAYOUTS_GLOB = "app/views/layouts/*.html.{erb,haml,slim}".freeze
13
- BUNDLE_TAG_ERB = "<%= react_bundle_tag defer: true %>\n".freeze
14
- BUNDLE_TAG_HAML = "= react_bundle_tag defer: true\n".freeze
13
+ BUNDLE_TAG_ERB = "<%= react_bundle_tag %>\n".freeze
14
+ BUNDLE_TAG_HAML = "= react_bundle_tag\n".freeze
15
15
 
16
16
  Result = Struct.new(:file, :status, :detail, keyword_init: true)
17
17
 
@@ -43,6 +43,22 @@ module ReactManifest
43
43
  manifest_dir = ReactManifest.configuration.abs_manifest_dir
44
44
  app.config.assets.paths.delete(manifest_dir)
45
45
  app.config.assets.paths.unshift(manifest_dir)
46
+
47
+ app.config.assets.configure do |env|
48
+ next unless defined?(React::JSX::Processor)
49
+
50
+ begin
51
+ env.register_mime_type("application/jsx", extensions: [".jsx", ".js.jsx", ".es.jsx", ".es6.jsx"])
52
+ rescue StandardError
53
+ nil
54
+ end
55
+
56
+ begin
57
+ env.register_transformer("application/jsx", "application/javascript", React::JSX::Processor)
58
+ rescue StandardError
59
+ nil
60
+ end
61
+ end
46
62
  end
47
63
 
48
64
  # ----------------------------------------------------------------
@@ -177,7 +177,7 @@ module ReactManifest
177
177
  # Build relative to output_dir (configurable) rather than a hardcoded path.
178
178
  base = @config.abs_output_dir + File::SEPARATOR
179
179
  rel = abs_path.sub(base, "")
180
- # Strip Sprockets-understood extensions: .js.jsx → "", .jsx "", .js → ""
180
+ # Strip Sprockets-understood extensions: .js.jsx/.jsx/.js -> logical path.
181
181
  rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
182
182
  end
183
183
 
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.9".freeze
2
+ VERSION = "0.2.13".freeze
3
3
  end
@@ -2,7 +2,7 @@ module ReactManifest
2
2
  # Provides the `react_bundle_tag` view helper, included in ActionView::Base.
3
3
  #
4
4
  # Usage in layouts:
5
- # <%= react_bundle_tag defer: true %>
5
+ # <%= react_bundle_tag %>
6
6
  #
7
7
  # Resolves which ux_*.js bundles to include based on controller_path:
8
8
  # 1. Always includes config.shared_bundle (e.g. "ux_shared")
@@ -21,7 +21,33 @@ module ReactManifest
21
21
  bundles = ReactManifest.resolve_bundles(ctrl)
22
22
  return "".html_safe if bundles.empty?
23
23
 
24
- javascript_include_tag(*bundles, **html_options)
24
+ asset_names = bundles.map { |bundle| "#{bundle}.js" }
25
+ javascript_include_tag(*asset_names, extname: false, **html_options)
26
+ end
27
+
28
+ # react-rails integration:
29
+ # If a component-specific controller bundle can be inferred from the component
30
+ # symbol, include that bundle at the call site before delegating to react-rails.
31
+ # This avoids strict dependence on controller_path -> bundle naming alignment.
32
+ def react_component(*args, **kwargs, &block)
33
+ html = super
34
+
35
+ component_name = args.first
36
+ bundles = ReactManifest.resolve_bundles_for_component(component_name)
37
+ return html if bundles.empty?
38
+
39
+ emitted = (@_react_manifest_emitted_bundles ||= [])
40
+
41
+ new_tags = bundles.filter_map do |bundle|
42
+ next if emitted.include?(bundle)
43
+
44
+ emitted << bundle
45
+ javascript_include_tag("#{bundle}.js", extname: false)
46
+ end
47
+
48
+ return html if new_tags.empty?
49
+
50
+ safe_join(new_tags + [html])
25
51
  end
26
52
  end
27
53
  end
@@ -1,4 +1,5 @@
1
1
  require "fileutils"
2
+ require "set"
2
3
 
3
4
  require "react_manifest/version"
4
5
  require "react_manifest/configuration"
@@ -58,8 +59,122 @@ module ReactManifest
58
59
  bundles
59
60
  end
60
61
 
62
+ # Resolve a controller bundle from a React component symbol.
63
+ #
64
+ # This is primarily used to support react-rails `react_component` calls,
65
+ # where the requested component name is known and may not align 1:1 with
66
+ # controller_path-derived bundle names.
67
+ def resolve_bundle_for_component(component_name)
68
+ resolve_bundles_for_component(component_name).last
69
+ end
70
+
71
+ # Resolve all controller bundles needed for a React component symbol.
72
+ # Includes transitive controller-bundle dependencies inferred from
73
+ # component symbol usage across ux/app/* directories.
74
+ def resolve_bundles_for_component(component_name)
75
+ name = component_name.to_s
76
+ return [] if name.empty?
77
+
78
+ config = configuration
79
+ maps = component_maps(config)
80
+ root_bundle = maps[:symbol_to_bundle][name]
81
+ return [] unless root_bundle
82
+
83
+ ordered = []
84
+ visiting = Set.new
85
+ visited = Set.new
86
+
87
+ walk = lambda do |bundle_name|
88
+ return if visited.include?(bundle_name) || visiting.include?(bundle_name)
89
+
90
+ visiting << bundle_name
91
+ maps[:bundle_dependencies].fetch(bundle_name, Set.new).each { |dep| walk.call(dep) }
92
+ visiting.delete(bundle_name)
93
+
94
+ visited << bundle_name
95
+ ordered << bundle_name
96
+ end
97
+
98
+ walk.call(root_bundle)
99
+
100
+ ordered.filter_map { |bundle_name| resolve_bundle_reference(config, bundle_name) }
101
+ end
102
+
61
103
  private
62
104
 
105
+ def component_bundle_map(config)
106
+ component_maps(config)[:symbol_to_bundle]
107
+ end
108
+
109
+ def component_maps(config)
110
+ controller_dirs = TreeClassifier.new(config).classify.controller_dirs
111
+ symbol_to_bundle = {}
112
+ bundle_files = Hash.new { |h, k| h[k] = [] }
113
+ bundle_dependencies = Hash.new { |h, k| h[k] = Set.new }
114
+
115
+ controller_dirs.each do |ctrl|
116
+ bundle_name = ctrl[:bundle_name]
117
+ files = js_files_in_controller(ctrl[:path], config)
118
+ bundle_files[bundle_name].concat(files)
119
+
120
+ files.each do |file_path|
121
+ extract_defined_symbols(file_path).each do |symbol|
122
+ next unless symbol.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
123
+
124
+ # Keep first writer to ensure deterministic behavior if a symbol is duplicated.
125
+ symbol_to_bundle[symbol] ||= bundle_name
126
+ end
127
+ end
128
+ end
129
+
130
+ bundle_files.each do |bundle_name, files|
131
+ files.each do |file_path|
132
+ extract_used_component_symbols(file_path).each do |symbol|
133
+ dep_bundle = symbol_to_bundle[symbol]
134
+ next unless dep_bundle && dep_bundle != bundle_name
135
+
136
+ bundle_dependencies[bundle_name] << dep_bundle
137
+ end
138
+ end
139
+ end
140
+
141
+ {
142
+ symbol_to_bundle: symbol_to_bundle,
143
+ bundle_dependencies: bundle_dependencies
144
+ }
145
+ end
146
+
147
+ def js_files_in_controller(dir, config)
148
+ return [] unless Dir.exist?(dir)
149
+
150
+ Dir.glob(File.join(dir, "**", config.extensions_glob))
151
+ .reject { |f| File.directory?(f) }
152
+ .sort
153
+ end
154
+
155
+ def extract_defined_symbols(file_path)
156
+ content = File.read(file_path, encoding: "utf-8")
157
+ symbols = []
158
+ ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
159
+ content.scan(pattern) { |m| symbols << m[0] }
160
+ end
161
+ symbols.uniq
162
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
163
+ []
164
+ end
165
+
166
+ def extract_used_component_symbols(file_path)
167
+ content = File.read(file_path, encoding: "utf-8")
168
+ symbols = []
169
+
170
+ content.scan(ReactManifest::Scanner::JSX_ELEMENT_PATTERN) { |m| symbols << m[0] }
171
+ content.scan(ReactManifest::Scanner::REACT_CREATE_PATTERN) { |m| symbols << m[0] }
172
+
173
+ symbols.uniq
174
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
175
+ []
176
+ end
177
+
63
178
  def resolve_bundle_reference(config, bundle_name)
64
179
  manifest_path = File.join(config.abs_manifest_dir, "#{bundle_name}.js")
65
180
  return bundle_name if File.exist?(manifest_path)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react-manifest-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: railties
@@ -161,7 +160,6 @@ metadata:
161
160
  changelog_uri: https://github.com/olivernoonan/react-manifest-rails/blob/main/CHANGELOG.md
162
161
  bug_tracker_uri: https://github.com/olivernoonan/react-manifest-rails/issues
163
162
  rubygems_mfa_required: 'true'
164
- post_install_message:
165
163
  rdoc_options: []
166
164
  require_paths:
167
165
  - lib
@@ -176,8 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
174
  - !ruby/object:Gem::Version
177
175
  version: '0'
178
176
  requirements: []
179
- rubygems_version: 3.5.22
180
- signing_key:
177
+ rubygems_version: 3.6.9
181
178
  specification_version: 4
182
179
  summary: Zero-touch Sprockets manifest generation for react-rails apps
183
180
  test_files: []