react-manifest-rails 0.2.10 → 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: 22469e4a0a5114411640201194bae46bdb549f365a7bb7064eeb95bfd4ddb24e
4
- data.tar.gz: 6b85f5dec1cf64f9132fd7cf1267f66d021a00f3058400b1644a7ac8f04bb534
3
+ metadata.gz: 78103a797d9951bb67d7e9cd4fcbb0fbf5a6589cd5995f52f745de079b3d6824
4
+ data.tar.gz: 229244d4844f0accdc1c23538f0a5ec8965a35e5246a62abc91dfae3ce16676d
5
5
  SHA512:
6
- metadata.gz: f9501224f4fe519573393612a126f6bedaeea60ece7a44c6adce4e6c143f90db6ec948075ac228b93e1f7ca944195a5645e621d932ec3d1ce7c4890f208a46a4
7
- data.tar.gz: e68251a96c1beb886969b2bc479babddd2fedf471d110f861bec99410c3dbbd4a72bf3961bd4088abe887857acdb72f9bd4b84de541c8d108d3f36adcb49c19c
6
+ metadata.gz: 43ba51a2daca72844e5d6606fa4e6c15dda1cef4b42582ab2db4d2a49037675157814a20030e41f60d9bc6c395dd2681f548f0244884b733104f37b66996c8a2
7
+ data.tar.gz: 01a71eda1dc257b0e0c151e34ac227323a2349c02c8d7709edd619975ba9051c9f759f89b440e317e6575d1e37edfba0c9da20ed2f306d57b587ddd239162ca1
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,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.10".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.10
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-16 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: []