react-manifest-rails 0.2.20 → 0.2.22

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: 573c8f1d4441a029cdc192ceef3e3e140702e55a17fc367e195243752f3970d6
4
- data.tar.gz: d02437b64aeee8fb45879b9b5d1f740e4eea85b42023ac8fadb84c3c6c491e97
3
+ metadata.gz: c87f68d7ed8248c19732ce3b946a1207ed85907a92bd39ab5a2eda665f5b001e
4
+ data.tar.gz: 7ee42c73f25048a31d8ca713d0cdfc23b344eb99ec68d1f72c58b5cfda3b635d
5
5
  SHA512:
6
- metadata.gz: f4aaf9bc43e9cc067c47790eb98061a0af8ad58efaa61c7c9a663749ca1d3e5cb464881987ca9ee31e015c820578780857cf5b15c9fae2a5d2b7bd0e0ce3038c
7
- data.tar.gz: 34273febe66a2821de77756baad5be6e3193234746db4f0ded7ba80d180ba1ce3d0d7a7c01e67456060ae743d5cab2723914ec5e163f1eaefb885f3504177569
6
+ metadata.gz: d71e1c8b255678a3b1a72df766134b80dadd8bf7553d4380e240d3a8b526dff5e6f6ef848574b93c86f6778feef896cc04b945a0f020d03d0bccf034acfe7d74
7
+ data.tar.gz: 874538d0cd06e8b8a121d9343a3846649b6612cd3ad4bb8c0c28a7c1589c1e05bf2f48b80ae0fe6a8af478d07a3d4309d9a5bc692536f4cdebeeed90b89ca74c
data/CHANGELOG.md CHANGED
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Fixed
11
11
  - `react_component` now emits only `ux_shared` plus the direct owning bundle for the requested component symbol, instead of also emitting transitive controller manifests as separate script tags. This prevents unnecessary network requests for additional `ux_*.js` manifests while preserving dependency loading through generated manifest `require` directives.
12
+ - Generator now skips `external_roots` and `external_providers` entries that resolve to files already included in `ux_shared`, preventing duplicate runtime declarations from overlapping shared/external includes.
13
+ - View helper bundle deduplication now canonicalizes bundle names (e.g. `ux_shared` vs `ux_manifests/ux_shared`) to avoid re-emitting equivalent script tags.
12
14
 
13
15
  ## [0.2.10] - 2026-04-16
14
16
 
data/README.md CHANGED
@@ -84,7 +84,9 @@ In development:
84
84
  Generation is **directory-based** — deterministic and conservative by design.
85
85
 
86
86
  - `ux_shared.js`: every file from directories outside `ux/app/` (i.e. `components/`, `hooks/`, `lib/`, etc.)
87
- - `ux_<controller>.js`: `ux_shared` + every file under `ux/app/<controller>/`
87
+ - `ux_<controller>.js`: every file under `ux/app/<controller>/`, plus transitive controller dependencies inferred from component usage across `ux/app/*`
88
+
89
+ `ux_shared.js` is loaded as a separate script by `react_bundle_tag` and `react_component` helpers. It is intentionally not inlined into every `ux_<controller>.js` manifest to avoid duplicating shared code across controller bundles.
88
90
 
89
91
  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
92
 
@@ -141,6 +143,7 @@ end
141
143
  - **`exclude_paths`**: excludes files whose path contains any listed segment. Not based on `application.js`.
142
144
  - **`dry_run`**: also honoured by `DRY_RUN=1` environment variable at runtime.
143
145
  - **`extensions`**: add `ts` and `tsx` to enable TypeScript source detection.
146
+ - **`external_roots` / `external_providers`**: do not point these at files already inside shared `ux/` dirs (`components/`, `hooks/`, `lib/`, etc.). The generator now skips those overlaps to prevent duplicate declarations in browser runtime.
144
147
 
145
148
  ## Commands
146
149
 
@@ -168,6 +171,22 @@ bundle exec rails react_manifest:watch
168
171
  bundle exec rails react_manifest:clean
169
172
  ```
170
173
 
174
+ ## Releasing
175
+
176
+ The publish workflow runs on tag pushes (`v*`), not on branch pushes.
177
+
178
+ 1. Bump `ReactManifest::VERSION` in `lib/react_manifest/version.rb`.
179
+ 2. Run `bundle install` to refresh the local path spec version in `Gemfile.lock`.
180
+ 3. Commit and push to `main`/`master`.
181
+ 4. Create and push a matching annotated tag:
182
+
183
+ ```bash
184
+ git tag -a vX.Y.Z -m "Release vX.Y.Z"
185
+ git push origin vX.Y.Z
186
+ ```
187
+
188
+ This tag push triggers `.github/workflows/release.yml`, which runs tests, creates a GitHub Release, and publishes to RubyGems.
189
+
171
190
  ## Troubleshooting
172
191
 
173
192
  ### `AssetNotPrecompiledError` for `ux_*.js`
@@ -216,6 +235,18 @@ If you see errors like `UserSignInForm is not defined` (often from `eval` inside
216
235
 
217
236
  Using `defer: true` can cause `react_component` inline scripts to run before your `ux_*.js` bundles are executed.
218
237
 
238
+ ### `Identifier '<Name>' has already been declared`
239
+
240
+ This usually means the same component/function is being loaded from two separate bundles.
241
+
242
+ Common cause: a file outside `ux/` (for example `app/assets/javascripts/components/navbar/*` loaded by `application.js`) references a symbol defined in `ux/app/<controller>/...`, while that controller bundle is also loaded via `react_bundle_tag`.
243
+
244
+ Recommended fix:
245
+ - Move globally reused symbols out of `ux/app/*` into a shared dir (`ux/components`, `ux/hooks`, `ux/lib`) so they are emitted once via `ux_shared.js`.
246
+ - Keep controller-specific symbols in `ux/app/*` and avoid importing/using them from non-ux global assets.
247
+
248
+ If you use `external_roots`, `react_manifest:analyze` and generation warnings will now flag this pattern explicitly.
249
+
219
250
  ## Compatibility
220
251
 
221
252
  - Ruby: 3.2+
@@ -42,7 +42,7 @@ module ReactManifest
42
42
  # written and others stale/missing.
43
43
  def run!
44
44
  classification = @classifier.classify
45
- controller_context = build_controller_context(classification.controller_dirs)
45
+ controller_context = build_controller_context(classification.controller_dirs, classification.shared_dirs)
46
46
 
47
47
  # Phase 1: build all content in memory — no I/O.
48
48
  manifests = []
@@ -99,12 +99,13 @@ module ReactManifest
99
99
  { filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
100
100
  end
101
101
 
102
- def build_controller_context(controller_dirs)
102
+ def build_controller_context(controller_dirs, shared_dirs)
103
103
  bundle_files = {}
104
104
  symbol_to_bundle = {}
105
105
  external_symbol_to_require = {}
106
106
  dependencies = Hash.new { |h, k| h[k] = Set.new }
107
107
  external_requires = Hash.new { |h, k| h[k] = Set.new }
108
+ shared_require_paths = shared_require_path_set(shared_dirs)
108
109
 
109
110
  # Index controller-defined symbols for cross-app detection
110
111
  controller_dirs.each do |ctrl|
@@ -126,6 +127,13 @@ module ReactManifest
126
127
  abs_root = abs_external_root(root_path)
127
128
  external_js_files_in(abs_root).each do |file_path|
128
129
  req_path = relative_require_path(file_path)
130
+ if shared_require_paths.include?(normalize_require_path(req_path))
131
+ warn "[ReactManifest] Skipping external_roots file already provided by shared bundle: #{req_path}"
132
+ next
133
+ end
134
+
135
+ warn_on_external_controller_references(file_path, symbol_to_bundle)
136
+
129
137
  extract_defined_symbols(file_path).each do |sym|
130
138
  external_symbol_to_require[sym] ||= req_path
131
139
  end
@@ -134,6 +142,12 @@ module ReactManifest
134
142
 
135
143
  # Explicit external_providers win over scanned roots on symbol conflicts
136
144
  @config.external_providers.each do |sym, req_path|
145
+ if shared_require_paths.include?(normalize_require_path(req_path))
146
+ warn "[ReactManifest] Skipping external provider '#{sym}' because it is already " \
147
+ "provided by shared bundle: #{req_path}"
148
+ next
149
+ end
150
+
137
151
  external_symbol_to_require[sym] = req_path
138
152
  end
139
153
 
@@ -348,6 +362,29 @@ module ReactManifest
348
362
  Rails.root.join(path).to_s
349
363
  end
350
364
 
365
+ def shared_require_path_set(shared_dirs)
366
+ shared_dirs.each_with_object(Set.new) do |shared_dir, paths|
367
+ js_files_in(shared_dir[:path]).each do |file_path|
368
+ paths << normalize_require_path(relative_require_path(file_path))
369
+ end
370
+ end
371
+ end
372
+
373
+ def normalize_require_path(path)
374
+ path.to_s.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
375
+ end
376
+
377
+ def warn_on_external_controller_references(file_path, symbol_to_bundle)
378
+ extract_used_component_symbols(file_path).each do |sym|
379
+ dep_bundle = symbol_to_bundle[sym]
380
+ next unless dep_bundle
381
+
382
+ warn "[ReactManifest] External file '#{relative_require_path(file_path)}' references " \
383
+ "controller-only symbol '#{sym}' (#{dep_bundle}). " \
384
+ "Move '#{sym}' to a shared ux dir to avoid duplicate runtime declarations."
385
+ end
386
+ end
387
+
351
388
  def auto_generated?(path)
352
389
  # Avoid TOCTOU: don't check existence separately — just attempt the read
353
390
  # and treat a missing/unreadable file as not auto-generated.
@@ -57,7 +57,8 @@ module ReactManifest
57
57
  Object Array String Number Boolean Symbol Map Set WeakMap
58
58
  ].freeze
59
59
 
60
- Result = Struct.new(:symbol_index, :controller_usages, :warnings, :shared_violations, keyword_init: true)
60
+ Result = Struct.new(:symbol_index, :controller_usages, :warnings, :shared_violations,
61
+ :external_violations, keyword_init: true)
61
62
 
62
63
  def initialize(config = ReactManifest.configuration)
63
64
  @config = config
@@ -67,6 +68,7 @@ module ReactManifest
67
68
  def scan(classification)
68
69
  warnings = []
69
70
  symbol_index = {}
71
+ external_file_paths = {} # file_path => relative_require_path for external_roots files
70
72
 
71
73
  # Phase 1a: index symbols from shared dirs
72
74
  shared_file_paths = {} # file_path => relative_require_path for all shared files
@@ -90,7 +92,8 @@ module ReactManifest
90
92
  abs_root = abs_external_root(root_path)
91
93
  js_files_in(abs_root).each do |file_path|
92
94
  relative = relative_require_path(file_path)
93
- symbols = extract_definitions(file_path)
95
+ external_file_paths[file_path] = relative
96
+ symbols = extract_definitions(file_path)
94
97
  symbols.each do |sym|
95
98
  symbol_index[sym] ||= relative
96
99
  end
@@ -119,6 +122,7 @@ module ReactManifest
119
122
 
120
123
  # Phase 1e: detect shared files that use app-dir (controller) symbols
121
124
  shared_violations = detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
125
+ external_violations = detect_external_root_violations(external_file_paths, controller_symbol_index, warnings)
122
126
 
123
127
  # Phase 2: scan controller dirs for usage
124
128
  controller_usages = {}
@@ -147,7 +151,8 @@ module ReactManifest
147
151
  symbol_index: symbol_index,
148
152
  controller_usages: controller_usages,
149
153
  warnings: warnings,
150
- shared_violations: shared_violations
154
+ shared_violations: shared_violations,
155
+ external_violations: external_violations
151
156
  )
152
157
  end
153
158
  # rubocop:enable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
@@ -229,6 +234,36 @@ module ReactManifest
229
234
  violations
230
235
  end
231
236
 
237
+ def detect_external_root_violations(external_file_paths, controller_symbol_index, warnings)
238
+ violations = []
239
+ external_file_paths.each do |file_path, relative|
240
+ content = begin
241
+ File.read(file_path, encoding: "utf-8")
242
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
243
+ next
244
+ end
245
+
246
+ local_syms = Set.new
247
+ DEFINITION_PATTERNS.each { |p| content.scan(p) { |m| local_syms << m[0] } }
248
+
249
+ [PASCAL_TOKEN_PATTERN, HOOK_TOKEN_PATTERN].each do |pattern|
250
+ content.scan(pattern) do |match|
251
+ sym = match[0]
252
+ next if local_syms.include?(sym)
253
+ next unless controller_symbol_index.key?(sym)
254
+
255
+ info = controller_symbol_index[sym]
256
+ violations << { external_file: relative, symbol: sym,
257
+ controller: info[:controller], app_file: info[:file] }
258
+ warnings << "External file '#{relative}' uses app-dir symbol '#{sym}' " \
259
+ "(from ux/app/#{info[:controller]}). " \
260
+ "Move '#{sym}' into a shared ux dir to avoid duplicate runtime declarations."
261
+ end
262
+ end
263
+ end
264
+ violations
265
+ end
266
+
232
267
  # Count how many controllers use each shared file
233
268
  def emit_fanout_warnings(controller_usages, warnings)
234
269
  fanout = Hash.new(0)
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.20".freeze
2
+ VERSION = "0.2.22".freeze
3
3
  end
@@ -23,7 +23,7 @@ module ReactManifest
23
23
 
24
24
  # Record emitted bundles so react_component doesn't re-emit them.
25
25
  emitted = (@_react_manifest_emitted_bundles ||= [])
26
- bundles.each { |b| emitted << b unless emitted.include?(b) }
26
+ bundles.each { |b| emitted << b unless emitted_bundle?(emitted, b) }
27
27
 
28
28
  asset_names = bundles.map { |bundle| "#{bundle}.js" }
29
29
  javascript_include_tag(*asset_names, extname: false, **html_options)
@@ -43,7 +43,7 @@ module ReactManifest
43
43
  emitted = (@_react_manifest_emitted_bundles ||= [])
44
44
 
45
45
  new_tags = bundles.filter_map do |bundle|
46
- next if emitted.include?(bundle)
46
+ next if emitted_bundle?(emitted, bundle)
47
47
 
48
48
  emitted << bundle
49
49
  javascript_include_tag("#{bundle}.js", extname: false)
@@ -53,5 +53,16 @@ module ReactManifest
53
53
 
54
54
  safe_join(new_tags + [html])
55
55
  end
56
+
57
+ private
58
+
59
+ def emitted_bundle?(emitted, bundle)
60
+ canonical = canonical_bundle_name(bundle)
61
+ emitted.any? { |existing| canonical_bundle_name(existing) == canonical }
62
+ end
63
+
64
+ def canonical_bundle_name(bundle)
65
+ bundle.to_s.split("/").last
66
+ end
56
67
  end
57
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react-manifest-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.20
4
+ version: 0.2.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan