react-manifest-rails 0.2.21 → 0.2.23
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 +4 -0
- data/README.md +12 -0
- data/lib/react_manifest/generator.rb +84 -29
- data/lib/react_manifest/scanner.rb +59 -29
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest/view_helpers.rb +35 -9
- data/tasks/react_manifest.rake +0 -6
- 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: 907e5e7bb8b3e6a8554009c4e8a87a39379d8bd09c4455fd9c02b082a014c18b
|
|
4
|
+
data.tar.gz: 11d4e6ca8833973107937eee1b82881f620f3b2e74442ab0883521ea5803d58b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 950abc3b34f1ffe0e8eb6fcd4ae7798469b60746531d0bb0171de0975881cb2572f35182e07785255fcf932ce14ccaac369a1238e2d5e2fe3b8689d323861d06
|
|
7
|
+
data.tar.gz: dc6fcbfb087a8aa9f95ff109147ee0345b2aa32fc1a0281118ffb60d12313735d659ecb55200832eb940bf6def8675ab711e64208fcc0ec17514e71e0ea57f42
|
data/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.23] - 2026-04-21
|
|
11
|
+
|
|
10
12
|
### Fixed
|
|
11
13
|
- `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
14
|
- 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
15
|
- View helper bundle deduplication now canonicalizes bundle names (e.g. `ux_shared` vs `ux_manifests/ux_shared`) to avoid re-emitting equivalent script tags.
|
|
16
|
+
- Controller manifests now include scanner-detected shared dependencies after `ux_shared` removal, including transitive shared dependencies needed by shared components (for example `DataTable` -> `SortHeader`).
|
|
17
|
+
- Shared `ux/lib` utility files are now included in controller manifests, restoring runtime availability for global helper functions such as `formatDate` and `formatCurrency`.
|
|
14
18
|
|
|
15
19
|
## [0.2.10] - 2026-04-16
|
|
16
20
|
|
data/README.md
CHANGED
|
@@ -235,6 +235,18 @@ If you see errors like `UserSignInForm is not defined` (often from `eval` inside
|
|
|
235
235
|
|
|
236
236
|
Using `defer: true` can cause `react_component` inline scripts to run before your `ux_*.js` bundles are executed.
|
|
237
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
|
+
|
|
238
250
|
## Compatibility
|
|
239
251
|
|
|
240
252
|
- Ruby: 3.2+
|
|
@@ -42,11 +42,11 @@ module ReactManifest
|
|
|
42
42
|
# written and others stale/missing.
|
|
43
43
|
def run!
|
|
44
44
|
classification = @classifier.classify
|
|
45
|
-
|
|
45
|
+
scan_result = Scanner.new(@config).scan(classification)
|
|
46
|
+
controller_context = build_controller_context(classification.controller_dirs, classification.shared_dirs, scan_result)
|
|
46
47
|
|
|
47
48
|
# Phase 1: build all content in memory — no I/O.
|
|
48
49
|
manifests = []
|
|
49
|
-
manifests << build_shared(classification.shared_dirs)
|
|
50
50
|
classification.controller_dirs.each { |ctrl| manifests << build_controller(ctrl, controller_context) }
|
|
51
51
|
|
|
52
52
|
migrate_legacy_manifests!
|
|
@@ -62,33 +62,20 @@ module ReactManifest
|
|
|
62
62
|
|
|
63
63
|
# ------------------------------------------------------------------ shared
|
|
64
64
|
|
|
65
|
-
def build_shared(shared_dirs)
|
|
66
|
-
lines = header_lines
|
|
67
|
-
any_files = false
|
|
68
65
|
|
|
69
|
-
shared_dirs.each do |shared_dir|
|
|
70
|
-
files = js_files_in(shared_dir[:path])
|
|
71
|
-
next if files.empty?
|
|
72
|
-
|
|
73
|
-
any_files = true
|
|
74
|
-
files.each { |f| lines << "//= require #{relative_require_path(f)}" }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
lines << "// (no shared files found)" unless any_files
|
|
78
|
-
|
|
79
|
-
{ filename: "#{@config.shared_bundle}.js", content: "#{lines.join("\n")}\n" }
|
|
80
|
-
end
|
|
81
66
|
|
|
82
67
|
# --------------------------------------------------------------- controller
|
|
83
68
|
|
|
84
69
|
def build_controller(ctrl, controller_context)
|
|
85
70
|
lines = header_lines
|
|
86
71
|
dep_requires = controller_dependency_requires(ctrl[:bundle_name], controller_context)
|
|
72
|
+
lib_reqs = controller_context[:shared_lib_requires]
|
|
73
|
+
shared_reqs = controller_context[:shared_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
87
74
|
ext_reqs = controller_context[:external_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
88
75
|
|
|
89
76
|
files = js_files_in(ctrl[:path])
|
|
90
77
|
own_requires = files.map { |f| relative_require_path(f) }
|
|
91
|
-
all_requires = (dep_requires + ext_reqs + own_requires).uniq
|
|
78
|
+
all_requires = (dep_requires + lib_reqs + shared_reqs + ext_reqs + own_requires).uniq
|
|
92
79
|
|
|
93
80
|
if all_requires.empty?
|
|
94
81
|
lines << "// (no JSX files found in #{ctrl[:name]}/)"
|
|
@@ -99,13 +86,24 @@ module ReactManifest
|
|
|
99
86
|
{ filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
|
|
100
87
|
end
|
|
101
88
|
|
|
102
|
-
def build_controller_context(controller_dirs, shared_dirs)
|
|
89
|
+
def build_controller_context(controller_dirs, shared_dirs, scan_result)
|
|
103
90
|
bundle_files = {}
|
|
104
91
|
symbol_to_bundle = {}
|
|
105
92
|
external_symbol_to_require = {}
|
|
106
93
|
dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
107
94
|
external_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
108
95
|
shared_require_paths = shared_require_path_set(shared_dirs)
|
|
96
|
+
shared_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
97
|
+
shared_dependency_map = build_shared_dependency_map(shared_dirs, shared_require_paths, scan_result)
|
|
98
|
+
shared_lib_requires = shared_lib_require_paths(shared_dirs)
|
|
99
|
+
|
|
100
|
+
controller_dirs.each do |ctrl|
|
|
101
|
+
scan_result.controller_usages.fetch(ctrl[:name], []).each do |req_path|
|
|
102
|
+
shared_requires[ctrl[:bundle_name]] << req_path
|
|
103
|
+
end
|
|
104
|
+
shared_requires[ctrl[:bundle_name]] = expand_shared_requires(shared_requires[ctrl[:bundle_name]],
|
|
105
|
+
shared_dependency_map)
|
|
106
|
+
end
|
|
109
107
|
|
|
110
108
|
# Index controller-defined symbols for cross-app detection
|
|
111
109
|
controller_dirs.each do |ctrl|
|
|
@@ -127,10 +125,8 @@ module ReactManifest
|
|
|
127
125
|
abs_root = abs_external_root(root_path)
|
|
128
126
|
external_js_files_in(abs_root).each do |file_path|
|
|
129
127
|
req_path = relative_require_path(file_path)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
next
|
|
133
|
-
end
|
|
128
|
+
|
|
129
|
+
warn_on_external_controller_references(file_path, symbol_to_bundle)
|
|
134
130
|
|
|
135
131
|
extract_defined_symbols(file_path).each do |sym|
|
|
136
132
|
external_symbol_to_require[sym] ||= req_path
|
|
@@ -140,12 +136,6 @@ module ReactManifest
|
|
|
140
136
|
|
|
141
137
|
# Explicit external_providers win over scanned roots on symbol conflicts
|
|
142
138
|
@config.external_providers.each do |sym, req_path|
|
|
143
|
-
if shared_require_paths.include?(normalize_require_path(req_path))
|
|
144
|
-
warn "[ReactManifest] Skipping external provider '#{sym}' because it is already " \
|
|
145
|
-
"provided by shared bundle: #{req_path}"
|
|
146
|
-
next
|
|
147
|
-
end
|
|
148
|
-
|
|
149
139
|
external_symbol_to_require[sym] = req_path
|
|
150
140
|
end
|
|
151
141
|
|
|
@@ -165,6 +155,8 @@ module ReactManifest
|
|
|
165
155
|
{
|
|
166
156
|
bundle_files: bundle_files,
|
|
167
157
|
dependencies: dependencies,
|
|
158
|
+
shared_lib_requires: shared_lib_requires,
|
|
159
|
+
shared_requires: shared_requires,
|
|
168
160
|
external_requires: external_requires
|
|
169
161
|
}
|
|
170
162
|
end
|
|
@@ -368,10 +360,73 @@ module ReactManifest
|
|
|
368
360
|
end
|
|
369
361
|
end
|
|
370
362
|
|
|
363
|
+
def shared_lib_require_paths(shared_dirs)
|
|
364
|
+
shared_dirs.each_with_object([]) do |shared_dir, paths|
|
|
365
|
+
next unless File.basename(shared_dir[:path]) == "lib"
|
|
366
|
+
|
|
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.sort.uniq
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def build_shared_dependency_map(shared_dirs, shared_require_paths, scan_result)
|
|
374
|
+
dependency_map = Hash.new { |h, k| h[k] = Set.new }
|
|
375
|
+
|
|
376
|
+
shared_symbol_index = scan_result.symbol_index.each_with_object({}) do |(sym, req_path), index|
|
|
377
|
+
normalized = normalize_require_path(req_path)
|
|
378
|
+
next unless shared_require_paths.include?(normalized)
|
|
379
|
+
|
|
380
|
+
index[sym] = normalized
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
shared_dirs.each do |shared_dir|
|
|
384
|
+
js_files_in(shared_dir[:path]).each do |file_path|
|
|
385
|
+
from_req = normalize_require_path(relative_require_path(file_path))
|
|
386
|
+
extract_used_component_symbols(file_path).each do |sym|
|
|
387
|
+
to_req = shared_symbol_index[sym]
|
|
388
|
+
next if to_req.nil? || to_req == from_req
|
|
389
|
+
|
|
390
|
+
dependency_map[from_req] << to_req
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
dependency_map
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def expand_shared_requires(initial_requires, dependency_map)
|
|
399
|
+
expanded = Set.new(initial_requires)
|
|
400
|
+
queue = initial_requires.to_a
|
|
401
|
+
|
|
402
|
+
until queue.empty?
|
|
403
|
+
req = queue.shift
|
|
404
|
+
dependency_map.fetch(req, Set.new).each do |dep_req|
|
|
405
|
+
next if expanded.include?(dep_req)
|
|
406
|
+
|
|
407
|
+
expanded << dep_req
|
|
408
|
+
queue << dep_req
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
expanded
|
|
413
|
+
end
|
|
414
|
+
|
|
371
415
|
def normalize_require_path(path)
|
|
372
416
|
path.to_s.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
|
|
373
417
|
end
|
|
374
418
|
|
|
419
|
+
def warn_on_external_controller_references(file_path, symbol_to_bundle)
|
|
420
|
+
extract_used_component_symbols(file_path).each do |sym|
|
|
421
|
+
dep_bundle = symbol_to_bundle[sym]
|
|
422
|
+
next unless dep_bundle
|
|
423
|
+
|
|
424
|
+
warn "[ReactManifest] External file '#{relative_require_path(file_path)}' references " \
|
|
425
|
+
"controller-only symbol '#{sym}' (#{dep_bundle}). " \
|
|
426
|
+
"Move '#{sym}' to a shared ux dir to avoid duplicate runtime declarations."
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
375
430
|
def auto_generated?(path)
|
|
376
431
|
# Avoid TOCTOU: don't check existence separately — just attempt the read
|
|
377
432
|
# and treat a missing/unreadable file as not auto-generated.
|
|
@@ -9,7 +9,6 @@ module ReactManifest
|
|
|
9
9
|
# Phase 1 — builds a symbol index from shared dirs:
|
|
10
10
|
# "PrimaryButton" => "ux/components/buttons/primary_button"
|
|
11
11
|
# "useFetch" => "ux/hooks/use_fetch"
|
|
12
|
-
# "formatDate" => "ux/lib/format_date"
|
|
13
12
|
#
|
|
14
13
|
# Phase 2 — scans controller files for usage of those symbols
|
|
15
14
|
# and produces per-controller lists of referenced shared files.
|
|
@@ -21,22 +20,19 @@ module ReactManifest
|
|
|
21
20
|
DEFINITION_PATTERNS = [
|
|
22
21
|
# CommonJS / variable-assignment style
|
|
23
22
|
/(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/, # const FooBar =
|
|
24
|
-
/function\s+([A-Z][A-Za-z0-9_]*)\s*\(/,
|
|
25
|
-
/class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/,
|
|
23
|
+
/function\s+([A-Z][A-Za-z0-9_]*)\s*\(/, # function FooBar(
|
|
24
|
+
/class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/, # class FooBar
|
|
26
25
|
/(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/, # const useFoo = (hooks)
|
|
27
|
-
/function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/,
|
|
28
|
-
/(?:const|let|var)\s+([a-z][A-Za-z0-9_]{2,})\s*=\s*(?:function|\()/, # const formatDate = function/arrow
|
|
29
|
-
/^function\s+([a-z][A-Za-z0-9_]{2,})\s*\(/, # function formatDate( at line start
|
|
26
|
+
/function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/, # function useFoo(
|
|
30
27
|
|
|
31
28
|
# ES module style (export default / named exports)
|
|
32
|
-
/^export\s+default\s+(?:function|class)\s+([A-Z][A-Za-z0-9_]*)/,
|
|
33
|
-
/^export\s+default\s+(?:function|class)\s+(use[A-Z][A-Za-z0-9_]*)/,
|
|
34
|
-
/^export\s+(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/,
|
|
35
|
-
/^export\s+(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/,
|
|
36
|
-
/^export\s+
|
|
37
|
-
/^export\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(/,
|
|
38
|
-
/^export\s+
|
|
39
|
-
/^export\s+class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/ # export class Foo
|
|
29
|
+
/^export\s+default\s+(?:function|class)\s+([A-Z][A-Za-z0-9_]*)/,
|
|
30
|
+
/^export\s+default\s+(?:function|class)\s+(use[A-Z][A-Za-z0-9_]*)/,
|
|
31
|
+
/^export\s+(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/,
|
|
32
|
+
/^export\s+(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/,
|
|
33
|
+
/^export\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(/,
|
|
34
|
+
/^export\s+function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/,
|
|
35
|
+
/^export\s+class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/
|
|
40
36
|
].freeze
|
|
41
37
|
|
|
42
38
|
# Patterns to detect usage in controller files.
|
|
@@ -57,7 +53,8 @@ module ReactManifest
|
|
|
57
53
|
Object Array String Number Boolean Symbol Map Set WeakMap
|
|
58
54
|
].freeze
|
|
59
55
|
|
|
60
|
-
Result = Struct.new(:symbol_index, :controller_usages, :warnings, :shared_violations,
|
|
56
|
+
Result = Struct.new(:symbol_index, :controller_usages, :warnings, :shared_violations,
|
|
57
|
+
:external_violations, keyword_init: true)
|
|
61
58
|
|
|
62
59
|
def initialize(config = ReactManifest.configuration)
|
|
63
60
|
@config = config
|
|
@@ -65,8 +62,9 @@ module ReactManifest
|
|
|
65
62
|
|
|
66
63
|
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
67
64
|
def scan(classification)
|
|
68
|
-
warnings =
|
|
65
|
+
warnings = Set.new
|
|
69
66
|
symbol_index = {}
|
|
67
|
+
external_file_paths = {} # file_path => relative_require_path for external_roots files
|
|
70
68
|
|
|
71
69
|
# Phase 1a: index symbols from shared dirs
|
|
72
70
|
shared_file_paths = {} # file_path => relative_require_path for all shared files
|
|
@@ -77,7 +75,7 @@ module ReactManifest
|
|
|
77
75
|
symbols = extract_definitions(file_path)
|
|
78
76
|
symbols.each do |sym|
|
|
79
77
|
if symbol_index.key?(sym)
|
|
80
|
-
warnings
|
|
78
|
+
warnings.add("Duplicate symbol '#{sym}' in #{relative} (already from #{symbol_index[sym]})")
|
|
81
79
|
else
|
|
82
80
|
symbol_index[sym] = relative
|
|
83
81
|
end
|
|
@@ -90,7 +88,8 @@ module ReactManifest
|
|
|
90
88
|
abs_root = abs_external_root(root_path)
|
|
91
89
|
js_files_in(abs_root).each do |file_path|
|
|
92
90
|
relative = relative_require_path(file_path)
|
|
93
|
-
|
|
91
|
+
external_file_paths[file_path] = relative
|
|
92
|
+
symbols = extract_definitions(file_path)
|
|
94
93
|
symbols.each do |sym|
|
|
95
94
|
symbol_index[sym] ||= relative
|
|
96
95
|
end
|
|
@@ -119,6 +118,7 @@ module ReactManifest
|
|
|
119
118
|
|
|
120
119
|
# Phase 1e: detect shared files that use app-dir (controller) symbols
|
|
121
120
|
shared_violations = detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
|
|
121
|
+
external_violations = detect_external_root_violations(external_file_paths, controller_symbol_index, warnings)
|
|
122
122
|
|
|
123
123
|
# Phase 2: scan controller dirs for usage
|
|
124
124
|
controller_usages = {}
|
|
@@ -127,7 +127,7 @@ module ReactManifest
|
|
|
127
127
|
files = js_files_in(ctrl[:path])
|
|
128
128
|
used = Set.new
|
|
129
129
|
|
|
130
|
-
warnings
|
|
130
|
+
warnings.add("Controller dir '#{ctrl[:name]}' has no JS/JSX files") if files.empty? && @config.verbose?
|
|
131
131
|
|
|
132
132
|
files.each do |file_path|
|
|
133
133
|
validate_naming(file_path, ctrl[:name], warnings)
|
|
@@ -146,8 +146,9 @@ module ReactManifest
|
|
|
146
146
|
Result.new(
|
|
147
147
|
symbol_index: symbol_index,
|
|
148
148
|
controller_usages: controller_usages,
|
|
149
|
-
warnings: warnings,
|
|
150
|
-
shared_violations: shared_violations
|
|
149
|
+
warnings: warnings.to_a,
|
|
150
|
+
shared_violations: shared_violations,
|
|
151
|
+
external_violations: external_violations
|
|
151
152
|
)
|
|
152
153
|
end
|
|
153
154
|
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
@@ -195,8 +196,8 @@ module ReactManifest
|
|
|
195
196
|
# Expected: <controller>_index, <controller>_show, <controller>_form, etc.
|
|
196
197
|
return if basename.start_with?("#{ctrl_name}_") || basename == ctrl_name
|
|
197
198
|
|
|
198
|
-
warnings
|
|
199
|
-
"'#{ctrl_name}_<action>.js.jsx' naming convention"
|
|
199
|
+
warnings.add("File '#{File.basename(file_path)}' in '#{ctrl_name}' does not follow " \
|
|
200
|
+
"'#{ctrl_name}_<action>.js.jsx' naming convention")
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
def detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
|
|
@@ -220,9 +221,39 @@ module ReactManifest
|
|
|
220
221
|
info = controller_symbol_index[sym]
|
|
221
222
|
violations << { shared_file: relative, symbol: sym,
|
|
222
223
|
controller: info[:controller], app_file: info[:file] }
|
|
223
|
-
warnings
|
|
224
|
+
warnings.add("Shared file '#{relative}' uses app-dir symbol '#{sym}' " \
|
|
225
|
+
"(from ux/app/#{info[:controller]}). " \
|
|
226
|
+
"Move '#{sym}' to a shared dir or the shared file will be incomplete.")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
violations
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def detect_external_root_violations(external_file_paths, controller_symbol_index, warnings)
|
|
234
|
+
violations = []
|
|
235
|
+
external_file_paths.each do |file_path, relative|
|
|
236
|
+
content = begin
|
|
237
|
+
File.read(file_path, encoding: "utf-8")
|
|
238
|
+
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
239
|
+
next
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
local_syms = Set.new
|
|
243
|
+
DEFINITION_PATTERNS.each { |p| content.scan(p) { |m| local_syms << m[0] } }
|
|
244
|
+
|
|
245
|
+
[PASCAL_TOKEN_PATTERN, HOOK_TOKEN_PATTERN].each do |pattern|
|
|
246
|
+
content.scan(pattern) do |match|
|
|
247
|
+
sym = match[0]
|
|
248
|
+
next if local_syms.include?(sym)
|
|
249
|
+
next unless controller_symbol_index.key?(sym)
|
|
250
|
+
|
|
251
|
+
info = controller_symbol_index[sym]
|
|
252
|
+
violations << { external_file: relative, symbol: sym,
|
|
253
|
+
controller: info[:controller], app_file: info[:file] }
|
|
254
|
+
warnings.add("External file '#{relative}' uses app-dir symbol '#{sym}' " \
|
|
224
255
|
"(from ux/app/#{info[:controller]}). " \
|
|
225
|
-
"Move '#{sym}'
|
|
256
|
+
"Move '#{sym}' into a shared ux dir to avoid duplicate runtime declarations.")
|
|
226
257
|
end
|
|
227
258
|
end
|
|
228
259
|
end
|
|
@@ -238,8 +269,7 @@ module ReactManifest
|
|
|
238
269
|
|
|
239
270
|
fanout.each do |file, count|
|
|
240
271
|
if count > 3
|
|
241
|
-
warnings
|
|
242
|
-
"(consider ensuring it's in the shared bundle)"
|
|
272
|
+
warnings.add("High fan-out: '#{file}' is used by #{count} controllers")
|
|
243
273
|
end
|
|
244
274
|
end
|
|
245
275
|
end
|
|
@@ -247,10 +277,10 @@ module ReactManifest
|
|
|
247
277
|
def read_controller_file(file_path, warnings)
|
|
248
278
|
File.read(file_path, encoding: "utf-8")
|
|
249
279
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
250
|
-
warnings
|
|
280
|
+
warnings.add("Skipping #{file_path}: #{e.message}")
|
|
251
281
|
nil
|
|
252
282
|
rescue Encoding::InvalidByteSequenceError
|
|
253
|
-
warnings
|
|
283
|
+
warnings.add("Skipping #{file_path}: not valid UTF-8")
|
|
254
284
|
nil
|
|
255
285
|
end
|
|
256
286
|
|
|
@@ -5,11 +5,10 @@ module ReactManifest
|
|
|
5
5
|
# <%= react_bundle_tag %>
|
|
6
6
|
#
|
|
7
7
|
# Resolves which ux_*.js bundles to include based on controller_path:
|
|
8
|
-
# 1.
|
|
9
|
-
# 2.
|
|
10
|
-
# 3.
|
|
11
|
-
# 4.
|
|
12
|
-
# 5. Returns "" for pure ERB/HAML pages with no matching bundle, or when
|
|
8
|
+
# 1. Appends config.always_include (e.g. ["ux_main"])
|
|
9
|
+
# 2. Appends "ux_<controller_path>" if that bundle file exists
|
|
10
|
+
# 3. For namespaced controllers (admin/users): checks ux_admin_users, then ux_admin
|
|
11
|
+
# 4. Returns "" for pure ERB/HAML pages with no matching bundle, or when
|
|
13
12
|
# called outside a controller context (mailers, engines, etc.)
|
|
14
13
|
module ViewHelpers
|
|
15
14
|
def react_bundle_tag(**html_options)
|
|
@@ -22,10 +21,14 @@ module ReactManifest
|
|
|
22
21
|
return "".html_safe if bundles.empty?
|
|
23
22
|
|
|
24
23
|
# Record emitted bundles so react_component doesn't re-emit them.
|
|
25
|
-
emitted =
|
|
26
|
-
bundles.
|
|
24
|
+
emitted = emitted_bundles
|
|
25
|
+
fresh_bundles = bundles.reject { |b| emitted_bundle?(emitted, b) }
|
|
26
|
+
return "".html_safe if fresh_bundles.empty?
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
fresh_bundles.each { |b| emitted << b }
|
|
29
|
+
mark_bundle_tag_rendered
|
|
30
|
+
|
|
31
|
+
asset_names = fresh_bundles.map { |bundle| "#{bundle}.js" }
|
|
29
32
|
javascript_include_tag(*asset_names, extname: false, **html_options)
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -35,12 +38,13 @@ module ReactManifest
|
|
|
35
38
|
# This avoids strict dependence on controller_path -> bundle naming alignment.
|
|
36
39
|
def react_component(*args, **kwargs, &block)
|
|
37
40
|
html = super
|
|
41
|
+
return html if bundle_tag_rendered?
|
|
38
42
|
|
|
39
43
|
component_name = args.first
|
|
40
44
|
bundles = ReactManifest.resolve_bundles_for_component_direct(component_name)
|
|
41
45
|
return html if bundles.empty?
|
|
42
46
|
|
|
43
|
-
emitted =
|
|
47
|
+
emitted = emitted_bundles
|
|
44
48
|
|
|
45
49
|
new_tags = bundles.filter_map do |bundle|
|
|
46
50
|
next if emitted_bundle?(emitted, bundle)
|
|
@@ -64,5 +68,27 @@ module ReactManifest
|
|
|
64
68
|
def canonical_bundle_name(bundle)
|
|
65
69
|
bundle.to_s.split("/").last
|
|
66
70
|
end
|
|
71
|
+
|
|
72
|
+
def emitted_bundles
|
|
73
|
+
# ActionView can instantiate multiple helper contexts during one request.
|
|
74
|
+
# Store emitted bundles in request env so layout + template helpers dedupe.
|
|
75
|
+
if respond_to?(:request, true) && request
|
|
76
|
+
request.env["react_manifest.emitted_bundles"] ||= []
|
|
77
|
+
else
|
|
78
|
+
@_react_manifest_emitted_bundles ||= []
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def mark_bundle_tag_rendered
|
|
83
|
+
return unless respond_to?(:request, true) && request
|
|
84
|
+
|
|
85
|
+
request.env["react_manifest.bundle_tag_rendered"] = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def bundle_tag_rendered?
|
|
89
|
+
return false unless respond_to?(:request, true) && request
|
|
90
|
+
|
|
91
|
+
request.env["react_manifest.bundle_tag_rendered"] == true
|
|
92
|
+
end
|
|
67
93
|
end
|
|
68
94
|
end
|
data/tasks/react_manifest.rake
CHANGED
|
@@ -130,12 +130,6 @@ namespace :react_manifest do
|
|
|
130
130
|
scan_result = scanner.scan(classification)
|
|
131
131
|
dep_map = ReactManifest::DependencyMap.new(scan_result)
|
|
132
132
|
dep_map.print_report
|
|
133
|
-
|
|
134
|
-
unless scan_result.warnings.empty?
|
|
135
|
-
puts "Warnings (#{scan_result.warnings.size}):"
|
|
136
|
-
scan_result.warnings.each { |w| puts " ⚠ #{w}" }
|
|
137
|
-
puts
|
|
138
|
-
end
|
|
139
133
|
end
|
|
140
134
|
|
|
141
135
|
desc "Analyze application*.js files — show what migrate_application would change"
|