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 +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +32 -1
- data/lib/react_manifest/generator.rb +39 -2
- data/lib/react_manifest/scanner.rb +38 -3
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest/view_helpers.rb +13 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c87f68d7ed8248c19732ce3b946a1207ed85907a92bd39ab5a2eda665f5b001e
|
|
4
|
+
data.tar.gz: 7ee42c73f25048a31d8ca713d0cdfc23b344eb99ec68d1f72c58b5cfda3b635d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`:
|
|
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,
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|