react-manifest-rails 0.2.22 → 0.2.24
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 +13 -0
- data/lib/react_manifest/generator.rb +104 -34
- data/lib/react_manifest/scanner.rb +22 -39
- 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: 786d53e91fe8a65339def86bc62a94179762364cf13d1abcc8427ea9585f0c38
|
|
4
|
+
data.tar.gz: 90c58cd92bc06eb2911deaf829aca60ed8533e6dc58e535759e9f6d60eefdcd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30c7f0633646f06cfc497a617c76dbbb50f74e84104fd590f3054bff5c228a42e08aa6683dfdc96702656a52cec57533917b2438d4bb39ede667ef6c0686f219
|
|
7
|
+
data.tar.gz: 0ea044f3d55bb64be2ce9a725cc90cf9b33da8f343fde043707c098995ce6e88840e609947867203efe739623ce5dba3a32e4d444dfb9b3f8b252b56b991adbb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.24] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Controller manifests now inline files from bundles listed in `always_include` (for example `ux_main`), so runtime symbol availability no longer depends on cross-bundle script execution order in production.
|
|
14
|
+
- Scanner analysis no longer emits warnings for ux/app file naming convention mismatches, reducing noise for apps that intentionally use custom filename patterns.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Updated `Gemfile.lock` to keep lockfile state aligned with the released codebase.
|
|
18
|
+
|
|
19
|
+
## [0.2.23] - 2026-04-21
|
|
20
|
+
|
|
10
21
|
### Fixed
|
|
11
22
|
- `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
23
|
- 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
24
|
- View helper bundle deduplication now canonicalizes bundle names (e.g. `ux_shared` vs `ux_manifests/ux_shared`) to avoid re-emitting equivalent script tags.
|
|
25
|
+
- Controller manifests now include scanner-detected shared dependencies after `ux_shared` removal, including transitive shared dependencies needed by shared components (for example `DataTable` -> `SortHeader`).
|
|
26
|
+
- Shared `ux/lib` utility files are now included in controller manifests, restoring runtime availability for global helper functions such as `formatDate` and `formatCurrency`.
|
|
14
27
|
|
|
15
28
|
## [0.2.10] - 2026-04-16
|
|
16
29
|
|
|
@@ -42,12 +42,12 @@ 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,
|
|
47
|
+
scan_result)
|
|
46
48
|
|
|
47
49
|
# Phase 1: build all content in memory — no I/O.
|
|
48
|
-
manifests =
|
|
49
|
-
manifests << build_shared(classification.shared_dirs)
|
|
50
|
-
classification.controller_dirs.each { |ctrl| manifests << build_controller(ctrl, controller_context) }
|
|
50
|
+
manifests = classification.controller_dirs.map { |ctrl| build_controller(ctrl, controller_context) }
|
|
51
51
|
|
|
52
52
|
migrate_legacy_manifests!
|
|
53
53
|
|
|
@@ -62,33 +62,19 @@ module ReactManifest
|
|
|
62
62
|
|
|
63
63
|
# ------------------------------------------------------------------ shared
|
|
64
64
|
|
|
65
|
-
def build_shared(shared_dirs)
|
|
66
|
-
lines = header_lines
|
|
67
|
-
any_files = false
|
|
68
|
-
|
|
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
|
-
|
|
82
65
|
# --------------------------------------------------------------- controller
|
|
83
66
|
|
|
84
67
|
def build_controller(ctrl, controller_context)
|
|
85
68
|
lines = header_lines
|
|
69
|
+
always_include_reqs = controller_context[:always_include_requires].fetch(ctrl[:bundle_name], [])
|
|
86
70
|
dep_requires = controller_dependency_requires(ctrl[:bundle_name], controller_context)
|
|
71
|
+
lib_reqs = controller_context[:shared_lib_requires]
|
|
72
|
+
shared_reqs = controller_context[:shared_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
87
73
|
ext_reqs = controller_context[:external_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
88
74
|
|
|
89
75
|
files = js_files_in(ctrl[:path])
|
|
90
76
|
own_requires = files.map { |f| relative_require_path(f) }
|
|
91
|
-
all_requires = (dep_requires + ext_reqs + own_requires).uniq
|
|
77
|
+
all_requires = (always_include_reqs + dep_requires + lib_reqs + shared_reqs + ext_reqs + own_requires).uniq
|
|
92
78
|
|
|
93
79
|
if all_requires.empty?
|
|
94
80
|
lines << "// (no JSX files found in #{ctrl[:name]}/)"
|
|
@@ -99,16 +85,26 @@ module ReactManifest
|
|
|
99
85
|
{ filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
|
|
100
86
|
end
|
|
101
87
|
|
|
102
|
-
|
|
88
|
+
# rubocop:disable Metrics/AbcSize
|
|
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)
|
|
109
99
|
|
|
110
|
-
# Index controller-defined symbols for cross-app detection
|
|
111
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
|
+
|
|
107
|
+
# Index controller-defined symbols for cross-app detection
|
|
112
108
|
bundle_name = ctrl[:bundle_name]
|
|
113
109
|
files = js_files_in(ctrl[:path])
|
|
114
110
|
bundle_files[bundle_name] = files
|
|
@@ -127,10 +123,6 @@ module ReactManifest
|
|
|
127
123
|
abs_root = abs_external_root(root_path)
|
|
128
124
|
external_js_files_in(abs_root).each do |file_path|
|
|
129
125
|
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
126
|
|
|
135
127
|
warn_on_external_controller_references(file_path, symbol_to_bundle)
|
|
136
128
|
|
|
@@ -142,12 +134,6 @@ module ReactManifest
|
|
|
142
134
|
|
|
143
135
|
# Explicit external_providers win over scanned roots on symbol conflicts
|
|
144
136
|
@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
|
-
|
|
151
137
|
external_symbol_to_require[sym] = req_path
|
|
152
138
|
end
|
|
153
139
|
|
|
@@ -164,12 +150,18 @@ module ReactManifest
|
|
|
164
150
|
end
|
|
165
151
|
end
|
|
166
152
|
|
|
153
|
+
always_include_requires = build_always_include_requires(bundle_files, dependencies)
|
|
154
|
+
|
|
167
155
|
{
|
|
168
156
|
bundle_files: bundle_files,
|
|
169
157
|
dependencies: dependencies,
|
|
158
|
+
always_include_requires: always_include_requires,
|
|
159
|
+
shared_lib_requires: shared_lib_requires,
|
|
160
|
+
shared_requires: shared_requires,
|
|
170
161
|
external_requires: external_requires
|
|
171
162
|
}
|
|
172
163
|
end
|
|
164
|
+
# rubocop:enable Metrics/AbcSize
|
|
173
165
|
|
|
174
166
|
def controller_dependency_requires(bundle_name, controller_context)
|
|
175
167
|
deps = transitive_dependencies(bundle_name, controller_context[:dependencies])
|
|
@@ -199,6 +191,32 @@ module ReactManifest
|
|
|
199
191
|
ordered
|
|
200
192
|
end
|
|
201
193
|
|
|
194
|
+
def build_always_include_requires(bundle_files, dependencies)
|
|
195
|
+
bundles = @config.always_include.map(&:to_s).reject(&:empty?).uniq
|
|
196
|
+
return Hash.new { |h, k| h[k] = [] } if bundles.empty?
|
|
197
|
+
|
|
198
|
+
requires_by_bundle = Hash.new { |h, k| h[k] = [] }
|
|
199
|
+
|
|
200
|
+
bundle_files.each_key do |bundle_name|
|
|
201
|
+
requires = Set.new
|
|
202
|
+
|
|
203
|
+
bundles.each do |always_bundle|
|
|
204
|
+
next if always_bundle == bundle_name
|
|
205
|
+
|
|
206
|
+
transitive = [always_bundle] + transitive_dependencies(always_bundle, dependencies)
|
|
207
|
+
transitive.each do |dep_bundle|
|
|
208
|
+
bundle_files.fetch(dep_bundle, []).each do |abs_path|
|
|
209
|
+
requires << relative_require_path(abs_path)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
requires_by_bundle[bundle_name] = requires.to_a.sort
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
requires_by_bundle
|
|
218
|
+
end
|
|
219
|
+
|
|
202
220
|
# --------------------------------------------------------------- write
|
|
203
221
|
|
|
204
222
|
def write_manifest(filename, content)
|
|
@@ -370,6 +388,58 @@ module ReactManifest
|
|
|
370
388
|
end
|
|
371
389
|
end
|
|
372
390
|
|
|
391
|
+
def shared_lib_require_paths(shared_dirs)
|
|
392
|
+
shared_dirs.each_with_object([]) do |shared_dir, paths|
|
|
393
|
+
next unless File.basename(shared_dir[:path]) == "lib"
|
|
394
|
+
|
|
395
|
+
js_files_in(shared_dir[:path]).each do |file_path|
|
|
396
|
+
paths << normalize_require_path(relative_require_path(file_path))
|
|
397
|
+
end
|
|
398
|
+
end.sort.uniq
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def build_shared_dependency_map(shared_dirs, shared_require_paths, scan_result)
|
|
402
|
+
dependency_map = Hash.new { |h, k| h[k] = Set.new }
|
|
403
|
+
|
|
404
|
+
shared_symbol_index = scan_result.symbol_index.each_with_object({}) do |(sym, req_path), index|
|
|
405
|
+
normalized = normalize_require_path(req_path)
|
|
406
|
+
next unless shared_require_paths.include?(normalized)
|
|
407
|
+
|
|
408
|
+
index[sym] = normalized
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
shared_dirs.each do |shared_dir|
|
|
412
|
+
js_files_in(shared_dir[:path]).each do |file_path|
|
|
413
|
+
from_req = normalize_require_path(relative_require_path(file_path))
|
|
414
|
+
extract_used_component_symbols(file_path).each do |sym|
|
|
415
|
+
to_req = shared_symbol_index[sym]
|
|
416
|
+
next if to_req.nil? || to_req == from_req
|
|
417
|
+
|
|
418
|
+
dependency_map[from_req] << to_req
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
dependency_map
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def expand_shared_requires(initial_requires, dependency_map)
|
|
427
|
+
expanded = Set.new(initial_requires)
|
|
428
|
+
queue = initial_requires.to_a
|
|
429
|
+
|
|
430
|
+
until queue.empty?
|
|
431
|
+
req = queue.shift
|
|
432
|
+
dependency_map.fetch(req, Set.new).each do |dep_req|
|
|
433
|
+
next if expanded.include?(dep_req)
|
|
434
|
+
|
|
435
|
+
expanded << dep_req
|
|
436
|
+
queue << dep_req
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
expanded
|
|
441
|
+
end
|
|
442
|
+
|
|
373
443
|
def normalize_require_path(path)
|
|
374
444
|
path.to_s.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
|
|
375
445
|
end
|
|
@@ -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
26
|
/function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/, # function useFoo(
|
|
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
|
|
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.
|
|
@@ -66,7 +62,7 @@ module ReactManifest
|
|
|
66
62
|
|
|
67
63
|
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
68
64
|
def scan(classification)
|
|
69
|
-
warnings =
|
|
65
|
+
warnings = Set.new
|
|
70
66
|
symbol_index = {}
|
|
71
67
|
external_file_paths = {} # file_path => relative_require_path for external_roots files
|
|
72
68
|
|
|
@@ -79,7 +75,7 @@ module ReactManifest
|
|
|
79
75
|
symbols = extract_definitions(file_path)
|
|
80
76
|
symbols.each do |sym|
|
|
81
77
|
if symbol_index.key?(sym)
|
|
82
|
-
warnings
|
|
78
|
+
warnings.add("Duplicate symbol '#{sym}' in #{relative} (already from #{symbol_index[sym]})")
|
|
83
79
|
else
|
|
84
80
|
symbol_index[sym] = relative
|
|
85
81
|
end
|
|
@@ -131,10 +127,9 @@ module ReactManifest
|
|
|
131
127
|
files = js_files_in(ctrl[:path])
|
|
132
128
|
used = Set.new
|
|
133
129
|
|
|
134
|
-
warnings
|
|
130
|
+
warnings.add("Controller dir '#{ctrl[:name]}' has no JS/JSX files") if files.empty? && @config.verbose?
|
|
135
131
|
|
|
136
132
|
files.each do |file_path|
|
|
137
|
-
validate_naming(file_path, ctrl[:name], warnings)
|
|
138
133
|
content = read_controller_file(file_path, warnings)
|
|
139
134
|
next unless content
|
|
140
135
|
|
|
@@ -150,7 +145,7 @@ module ReactManifest
|
|
|
150
145
|
Result.new(
|
|
151
146
|
symbol_index: symbol_index,
|
|
152
147
|
controller_usages: controller_usages,
|
|
153
|
-
warnings: warnings,
|
|
148
|
+
warnings: warnings.to_a,
|
|
154
149
|
shared_violations: shared_violations,
|
|
155
150
|
external_violations: external_violations
|
|
156
151
|
)
|
|
@@ -195,15 +190,6 @@ module ReactManifest
|
|
|
195
190
|
rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
|
|
196
191
|
end
|
|
197
192
|
|
|
198
|
-
def validate_naming(file_path, ctrl_name, warnings)
|
|
199
|
-
basename = File.basename(file_path, ".*").sub(/\.js$/, "")
|
|
200
|
-
# Expected: <controller>_index, <controller>_show, <controller>_form, etc.
|
|
201
|
-
return if basename.start_with?("#{ctrl_name}_") || basename == ctrl_name
|
|
202
|
-
|
|
203
|
-
warnings << "File '#{File.basename(file_path)}' in '#{ctrl_name}' does not follow " \
|
|
204
|
-
"'#{ctrl_name}_<action>.js.jsx' naming convention"
|
|
205
|
-
end
|
|
206
|
-
|
|
207
193
|
def detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
|
|
208
194
|
violations = []
|
|
209
195
|
shared_file_paths.each do |file_path, relative|
|
|
@@ -225,9 +211,9 @@ module ReactManifest
|
|
|
225
211
|
info = controller_symbol_index[sym]
|
|
226
212
|
violations << { shared_file: relative, symbol: sym,
|
|
227
213
|
controller: info[:controller], app_file: info[:file] }
|
|
228
|
-
warnings
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
warnings.add("Shared file '#{relative}' uses app-dir symbol '#{sym}' " \
|
|
215
|
+
"(from ux/app/#{info[:controller]}). " \
|
|
216
|
+
"Move '#{sym}' to a shared dir or the shared file will be incomplete.")
|
|
231
217
|
end
|
|
232
218
|
end
|
|
233
219
|
end
|
|
@@ -255,9 +241,9 @@ module ReactManifest
|
|
|
255
241
|
info = controller_symbol_index[sym]
|
|
256
242
|
violations << { external_file: relative, symbol: sym,
|
|
257
243
|
controller: info[:controller], app_file: info[:file] }
|
|
258
|
-
warnings
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
warnings.add("External file '#{relative}' uses app-dir symbol '#{sym}' " \
|
|
245
|
+
"(from ux/app/#{info[:controller]}). " \
|
|
246
|
+
"Move '#{sym}' into a shared ux dir to avoid duplicate runtime declarations.")
|
|
261
247
|
end
|
|
262
248
|
end
|
|
263
249
|
end
|
|
@@ -272,20 +258,17 @@ module ReactManifest
|
|
|
272
258
|
end
|
|
273
259
|
|
|
274
260
|
fanout.each do |file, count|
|
|
275
|
-
if count > 3
|
|
276
|
-
warnings << "High fan-out: '#{file}' is used by #{count} controllers " \
|
|
277
|
-
"(consider ensuring it's in the shared bundle)"
|
|
278
|
-
end
|
|
261
|
+
warnings.add("High fan-out: '#{file}' is used by #{count} controllers") if count > 3
|
|
279
262
|
end
|
|
280
263
|
end
|
|
281
264
|
|
|
282
265
|
def read_controller_file(file_path, warnings)
|
|
283
266
|
File.read(file_path, encoding: "utf-8")
|
|
284
267
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
285
|
-
warnings
|
|
268
|
+
warnings.add("Skipping #{file_path}: #{e.message}")
|
|
286
269
|
nil
|
|
287
270
|
rescue Encoding::InvalidByteSequenceError
|
|
288
|
-
warnings
|
|
271
|
+
warnings.add("Skipping #{file_path}: not valid UTF-8")
|
|
289
272
|
nil
|
|
290
273
|
end
|
|
291
274
|
|
|
@@ -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
|
+
@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"
|