react-manifest-rails 0.2.10 → 0.2.14

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: 9c85309337765f3f24fae46e4cf3895e11e4a3a59b5cbfb96fa90f296f3ba48e
4
+ data.tar.gz: b0f320a6ebf2a4210a4fac89d04f5609bd3d261a458f2db26fb92dd814b45465
5
5
  SHA512:
6
- metadata.gz: f9501224f4fe519573393612a126f6bedaeea60ece7a44c6adce4e6c143f90db6ec948075ac228b93e1f7ca944195a5645e621d932ec3d1ce7c4890f208a46a4
7
- data.tar.gz: e68251a96c1beb886969b2bc479babddd2fedf471d110f861bec99410c3dbbd4a72bf3961bd4088abe887857acdb72f9bd4b84de541c8d108d3f36adcb49c19c
6
+ metadata.gz: 85edbe32bf32f739097e60779a34874d1c792db5ea40ed41e671e8b0552718d4549dfddb9b120d74bcce42a15bbc598ed0384c7e609cb6f0d4044fb80900098c
7
+ data.tar.gz: d2728828f1fd930cd702aa35fb4ab5ab88b4219dff1fe996fa5cdb8491888b15bb25b0fd1d44c84aa9c0707cd80b87d3d3c2554b15f0723fd979bf484db4b3f1
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
  # ----------------------------------------------------------------
@@ -41,6 +41,9 @@ module ReactManifest
41
41
  # Patterns to detect usage in controller files
42
42
  JSX_ELEMENT_PATTERN = %r{<([A-Z][A-Za-z0-9_]*)[\s/>]}
43
43
  REACT_CREATE_PATTERN = /React\.createElement\(\s*([A-Z][A-Za-z0-9_]*)[\s,)]/
44
+ JSX_PROP_COMPONENT_PATTERN = /[A-Za-z_][A-Za-z0-9_]*\s*=\s*\{\s*([A-Z][A-Za-z0-9_]*)\s*\}/
45
+ OBJECT_COMPONENT_PATTERN = /:\s*([A-Z][A-Za-z0-9_]*)\b/
46
+ ARRAY_COMPONENT_LIST_PATTERN = /\[\s*([A-Z][A-Za-z0-9_]*(?:\s*,\s*[A-Z][A-Za-z0-9_]*)*\s*,?)\s*\]/
44
47
  HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(/
45
48
  # Lib calls matched against known lib symbols to reduce false positives
46
49
  LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
@@ -93,41 +96,10 @@ module ReactManifest
93
96
 
94
97
  files.each do |file_path|
95
98
  validate_naming(file_path, ctrl[:name], warnings)
96
- begin
97
- content = File.read(file_path, encoding: "utf-8")
98
- rescue Errno::ENOENT, Errno::EACCES => e
99
- warnings << "Skipping #{file_path}: #{e.message}"
100
- next
101
- rescue Encoding::InvalidByteSequenceError
102
- warnings << "Skipping #{file_path}: not valid UTF-8"
103
- next
104
- end
105
-
106
- # JSX element usage: <PrimaryButton (JSX tag syntax)
107
- content.scan(JSX_ELEMENT_PATTERN) do |match|
108
- sym = match[0]
109
- used << symbol_index[sym] if symbol_index.key?(sym)
110
- end
111
-
112
- # React.createElement(PrimaryButton, ...) (non-JSX style)
113
- content.scan(REACT_CREATE_PATTERN) do |match|
114
- sym = match[0]
115
- used << symbol_index[sym] if symbol_index.key?(sym)
116
- end
117
-
118
- # Hook calls: useFetch(
119
- content.scan(HOOK_CALL_PATTERN) do |match|
120
- sym = match[0]
121
- used << symbol_index[sym] if symbol_index.key?(sym)
122
- end
99
+ content = read_controller_file(file_path, warnings)
100
+ next unless content
123
101
 
124
- # Lib function calls: formatDate( — filtered against lib symbol index
125
- content.scan(LIB_CALL_PATTERN) do |match|
126
- sym = match[0]
127
- next if JS_BUILTINS.include?(sym)
128
-
129
- used << symbol_index[sym] if symbol_index.key?(sym)
130
- end
102
+ used.merge(extract_used_shared_paths(content, symbol_index))
131
103
  end
132
104
 
133
105
  controller_usages[ctrl[:name]] = used.to_a.sort
@@ -177,7 +149,7 @@ module ReactManifest
177
149
  # Build relative to output_dir (configurable) rather than a hardcoded path.
178
150
  base = @config.abs_output_dir + File::SEPARATOR
179
151
  rel = abs_path.sub(base, "")
180
- # Strip Sprockets-understood extensions: .js.jsx → "", .jsx "", .js → ""
152
+ # Strip Sprockets-understood extensions: .js.jsx/.jsx/.js -> logical path.
181
153
  rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
182
154
  end
183
155
 
@@ -204,5 +176,56 @@ module ReactManifest
204
176
  end
205
177
  end
206
178
  end
179
+
180
+ def read_controller_file(file_path, warnings)
181
+ File.read(file_path, encoding: "utf-8")
182
+ rescue Errno::ENOENT, Errno::EACCES => e
183
+ warnings << "Skipping #{file_path}: #{e.message}"
184
+ nil
185
+ rescue Encoding::InvalidByteSequenceError
186
+ warnings << "Skipping #{file_path}: not valid UTF-8"
187
+ nil
188
+ end
189
+
190
+ def extract_used_shared_paths(content, symbol_index)
191
+ used = Set.new
192
+
193
+ scan_component_usage(content, JSX_ELEMENT_PATTERN, symbol_index, used)
194
+ scan_component_usage(content, REACT_CREATE_PATTERN, symbol_index, used)
195
+ scan_component_usage(content, JSX_PROP_COMPONENT_PATTERN, symbol_index, used)
196
+ scan_component_usage(content, OBJECT_COMPONENT_PATTERN, symbol_index, used)
197
+ scan_array_component_usage(content, symbol_index, used)
198
+ scan_component_usage(content, HOOK_CALL_PATTERN, symbol_index, used)
199
+
200
+ content.scan(LIB_CALL_PATTERN) do |match|
201
+ sym = match[0]
202
+ next if JS_BUILTINS.include?(sym)
203
+ next unless symbol_index.key?(sym)
204
+
205
+ used << symbol_index[sym]
206
+ end
207
+
208
+ used
209
+ end
210
+
211
+ def scan_component_usage(content, pattern, symbol_index, used)
212
+ content.scan(pattern) do |match|
213
+ sym = match[0]
214
+ next unless symbol_index.key?(sym)
215
+
216
+ used << symbol_index[sym]
217
+ end
218
+ end
219
+
220
+ def scan_array_component_usage(content, symbol_index, used)
221
+ content.scan(ARRAY_COMPONENT_LIST_PATTERN) do |match|
222
+ list = match[0]
223
+ list.split(/\s*,\s*/).each do |sym|
224
+ next unless symbol_index.key?(sym)
225
+
226
+ used << symbol_index[sym]
227
+ end
228
+ end
229
+ end
207
230
  end
208
231
  end
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.10".freeze
2
+ VERSION = "0.2.14".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,127 @@ 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
+ content.scan(ReactManifest::Scanner::JSX_PROP_COMPONENT_PATTERN) { |m| symbols << m[0] }
173
+ content.scan(ReactManifest::Scanner::OBJECT_COMPONENT_PATTERN) { |m| symbols << m[0] }
174
+ content.scan(ReactManifest::Scanner::ARRAY_COMPONENT_LIST_PATTERN) do |m|
175
+ m[0].split(/\s*,\s*/).each { |sym| symbols << sym }
176
+ end
177
+
178
+ symbols.uniq
179
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
180
+ []
181
+ end
182
+
63
183
  def resolve_bundle_reference(config, bundle_name)
64
184
  manifest_path = File.join(config.abs_manifest_dir, "#{bundle_name}.js")
65
185
  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.14
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: []