react-manifest-rails 0.2.27 → 0.2.29
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 +5 -0
- data/lib/react_manifest/application_analyzer.rb +11 -9
- data/lib/react_manifest/application_migrator.rb +19 -10
- data/lib/react_manifest/configuration.rb +9 -0
- data/lib/react_manifest/dependency_map.rb +12 -10
- data/lib/react_manifest/generator.rb +44 -120
- data/lib/react_manifest/layout_patcher.rb +14 -5
- data/lib/react_manifest/logging.rb +31 -0
- data/lib/react_manifest/path_utils.rb +11 -0
- data/lib/react_manifest/railtie.rb +9 -30
- data/lib/react_manifest/reporter.rb +12 -10
- data/lib/react_manifest/scanner.rb +98 -130
- data/lib/react_manifest/sprockets_manifest_patcher.rb +5 -3
- data/lib/react_manifest/symbol_extractor.rb +69 -0
- data/lib/react_manifest/tree_classifier.rb +6 -4
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest/watcher.rb +65 -19
- data/lib/react_manifest.rb +16 -22
- data/tasks/react_manifest.rake +3 -20
- metadata +30 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8688e621b55d1a5b9db497668b4942fc97c8f4aad5f7778c7a7fc4a412990d04
|
|
4
|
+
data.tar.gz: c46872893e465f9684c68688a8f8ca02d361afd7bfa8421eb1b69b0180f8c3d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76a4b4aed865531470d641be4bb0d6608d30410d66463aad17e71b37a73acafe2fb4a825c5f3f89c0bc5e6a91d7016396699572d575950dca71a242378cbc6f3
|
|
7
|
+
data.tar.gz: 2bbd77ad7db859306d6cfdb6a75a476bfca219b1e22cf0b3aa54bd12f3c42d8a441c35a1e07dbb4e5a0c3c198b968683a15f814fd64bf59a55a91b1ca3d2b9d1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.28] - 2026-05-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Boot generation now runs after app initializers (`config/initializers/`) have loaded, so the generator always uses the fully-configured `ux_root` and related settings. Previously the initializer ran during the Railtie phase (before `config/initializers/`), causing it to silently generate against default paths and produce unchanged manifests.
|
|
14
|
+
|
|
10
15
|
## [0.2.27] - 2026-05-11
|
|
11
16
|
|
|
12
17
|
### Fixed
|
|
@@ -6,6 +6,8 @@ module ReactManifest
|
|
|
6
6
|
#
|
|
7
7
|
# Produces a human-readable report without writing anything.
|
|
8
8
|
class ApplicationAnalyzer
|
|
9
|
+
include ReactManifest::Logging
|
|
10
|
+
|
|
9
11
|
DIRECTIVE_PATTERN = %r{^\s*//=\s+(require(?:_tree|_directory)?)\s+(.+)$}
|
|
10
12
|
|
|
11
13
|
# Libs we recognise as vendor (case-insensitive partial match on the require path)
|
|
@@ -47,30 +49,30 @@ module ReactManifest
|
|
|
47
49
|
|
|
48
50
|
# Pretty-print the analysis report to stdout
|
|
49
51
|
def print_report(results = analyze)
|
|
50
|
-
|
|
52
|
+
log_info "\n=== ReactManifest: Application Manifest Analysis ===\n"
|
|
51
53
|
|
|
52
54
|
if results.empty?
|
|
53
|
-
|
|
55
|
+
log_info "No application*.js files found in #{@config.abs_output_dir}"
|
|
54
56
|
return
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
results.each do |result|
|
|
58
60
|
rel = result.file.sub("#{Rails.root}/", "")
|
|
59
61
|
status = result.clean? ? "✓ already clean" : "⚠ needs migration"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
log_info "\n#{rel} [#{status}]"
|
|
63
|
+
log_info "-" * 60
|
|
62
64
|
|
|
63
65
|
icon_map = { vendor: " ✓ KEEP ", ux_code: " ✗ REMOVE ", unknown: " ? REVIEW " }
|
|
64
66
|
result.directives.each do |d|
|
|
65
67
|
icon = icon_map[d.classification]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
log_info "#{icon} #{d.original_line.strip}"
|
|
69
|
+
log_info " → #{d.note}" if d.note
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
log_info "\n"
|
|
74
|
+
log_info "Run `rails react_manifest:migrate_application` to apply changes."
|
|
75
|
+
log_info "Use `--dry-run` (or config.dry_run = true) to preview first.\n\n"
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
private
|
|
@@ -8,6 +8,8 @@ module ReactManifest
|
|
|
8
8
|
# - Never removes :vendor or :passthrough lines
|
|
9
9
|
# - Adds a managed-by comment at the top
|
|
10
10
|
class ApplicationMigrator
|
|
11
|
+
include ReactManifest::Logging
|
|
12
|
+
|
|
11
13
|
MANAGED_COMMENT = <<~JS.freeze
|
|
12
14
|
// Non-UX libraries — loaded on every page.
|
|
13
15
|
// React app code is now served per-controller via react_bundle_tag.
|
|
@@ -30,13 +32,13 @@ module ReactManifest
|
|
|
30
32
|
results = @analyzer.analyze
|
|
31
33
|
|
|
32
34
|
if results.empty?
|
|
33
|
-
|
|
35
|
+
log_info "No application*.js files found to migrate."
|
|
34
36
|
return []
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
results.map do |result|
|
|
38
40
|
if result.clean?
|
|
39
|
-
|
|
41
|
+
log_info "#{short(result.file)} — already clean, skipping."
|
|
40
42
|
{ file: result.file, status: :already_clean }
|
|
41
43
|
else
|
|
42
44
|
rewrite(result)
|
|
@@ -51,7 +53,7 @@ module ReactManifest
|
|
|
51
53
|
new_content = build_new_content(result)
|
|
52
54
|
|
|
53
55
|
if @config.dry_run?
|
|
54
|
-
|
|
56
|
+
log_info "DRY-RUN: #{short(file)}"
|
|
55
57
|
print_diff(file, new_content)
|
|
56
58
|
return { file: file, status: :dry_run }
|
|
57
59
|
end
|
|
@@ -61,15 +63,22 @@ module ReactManifest
|
|
|
61
63
|
begin
|
|
62
64
|
FileUtils.cp(file, bak_path)
|
|
63
65
|
File.chmod(0o600, bak_path)
|
|
64
|
-
|
|
66
|
+
log_info "Backup: #{short(bak_path)}"
|
|
65
67
|
rescue StandardError => e
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
log_warn "ERROR: Could not create backup of #{short(file)}: #{e.message}"
|
|
69
|
+
log_warn "Migration aborted for #{short(file)} — original file unchanged."
|
|
68
70
|
return { file: file, status: :backup_failed, error: e.message }
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
tmp = "#{file}.tmp.#{Process.pid}"
|
|
74
|
+
begin
|
|
75
|
+
File.write(tmp, new_content, encoding: "utf-8")
|
|
76
|
+
File.rename(tmp, file)
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
FileUtils.rm_f(tmp)
|
|
79
|
+
raise e
|
|
80
|
+
end
|
|
81
|
+
log_info "Migrated: #{short(file)}"
|
|
73
82
|
|
|
74
83
|
{ file: file, status: :migrated, backup: bak_path }
|
|
75
84
|
end
|
|
@@ -120,8 +129,8 @@ module ReactManifest
|
|
|
120
129
|
removed = old_lines - new_lines
|
|
121
130
|
added = new_lines - old_lines
|
|
122
131
|
|
|
123
|
-
removed.each { |l|
|
|
124
|
-
added.each { |l|
|
|
132
|
+
removed.each { |l| log_info " - #{l}" }
|
|
133
|
+
added.each { |l| log_info " + #{l}" }
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
def short(path)
|
|
@@ -135,8 +135,17 @@ module ReactManifest
|
|
|
135
135
|
File.join(abs_output_dir, subdir)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
def excluded_path?(abs_path)
|
|
139
|
+
parts = abs_path.split(File::SEPARATOR)
|
|
140
|
+
exclude_paths.any? { |ep| parts.include?(ep) }
|
|
141
|
+
end
|
|
142
|
+
|
|
138
143
|
def normalized_manifest_subdir
|
|
139
144
|
manifest_subdir.to_s.gsub(%r{\A/+|/+\z}, "")
|
|
140
145
|
end
|
|
146
|
+
|
|
147
|
+
def cache_key
|
|
148
|
+
[ux_root, app_dir, extensions, always_include, exclude_paths, external_providers].hash
|
|
149
|
+
end
|
|
141
150
|
end
|
|
142
151
|
end
|
|
@@ -9,6 +9,8 @@ module ReactManifest
|
|
|
9
9
|
# map.shared_files_for("users") # => ["ux/components/...", ...]
|
|
10
10
|
# map.controllers_using("ux/lib/api_helpers") # => ["users", "admin"]
|
|
11
11
|
class DependencyMap
|
|
12
|
+
include ReactManifest::Logging
|
|
13
|
+
|
|
12
14
|
attr_reader :symbol_index, :controller_usages, :warnings
|
|
13
15
|
|
|
14
16
|
def initialize(scan_result)
|
|
@@ -40,29 +42,29 @@ module ReactManifest
|
|
|
40
42
|
|
|
41
43
|
# Pretty-print for the analyze rake task
|
|
42
44
|
def print_report
|
|
43
|
-
|
|
45
|
+
log_info "\n=== ReactManifest Dependency Analysis ===\n\n"
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
log_info "Shared Symbol Index (#{@symbol_index.size} symbols):"
|
|
46
48
|
@symbol_index.each do |sym, file|
|
|
47
49
|
# Strip non-printable/control characters to prevent terminal manipulation
|
|
48
50
|
safe_sym = sym.gsub(/[^\x20-\x7E]/, "?")
|
|
49
51
|
safe_file = file.gsub(/[^\x20-\x7E]/, "?")
|
|
50
|
-
|
|
52
|
+
log_info " #{safe_sym.ljust(40)} #{safe_file}"
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
log_info "\nPer-Controller Usage:"
|
|
54
56
|
@controller_usages.each do |ctrl, files|
|
|
55
|
-
|
|
56
|
-
files.each { |f|
|
|
57
|
-
|
|
57
|
+
log_info "\n [#{ctrl}] (#{files.size} shared references)"
|
|
58
|
+
files.each { |f| log_info " #{f}" }
|
|
59
|
+
log_info " (none)" if files.empty?
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
unless @warnings.empty?
|
|
61
|
-
|
|
62
|
-
@warnings.each { |w|
|
|
63
|
+
log_info "\nWarnings (#{@warnings.size}):"
|
|
64
|
+
@warnings.each { |w| log_warn " ⚠ #{w}" }
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
log_info "\n"
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
require "tmpdir"
|
|
3
|
+
require_relative "path_utils"
|
|
3
4
|
|
|
4
5
|
module ReactManifest
|
|
5
6
|
# Generates all ux_*.js Sprockets manifest files.
|
|
@@ -24,6 +25,9 @@ module ReactManifest
|
|
|
24
25
|
# Never touches application.js, application_dev.js, or files in exclude_paths.
|
|
25
26
|
# rubocop:disable Metrics/ClassLength
|
|
26
27
|
class Generator
|
|
28
|
+
include PathUtils
|
|
29
|
+
include ReactManifest::Logging
|
|
30
|
+
|
|
27
31
|
HEADER = <<~JS.freeze
|
|
28
32
|
// AUTO-GENERATED — DO NOT EDIT
|
|
29
33
|
// react-manifest-rails %<version>s
|
|
@@ -42,9 +46,7 @@ module ReactManifest
|
|
|
42
46
|
# written and others stale/missing.
|
|
43
47
|
def run!
|
|
44
48
|
classification = @classifier.classify
|
|
45
|
-
|
|
46
|
-
controller_context = build_controller_context(classification.controller_dirs, classification.shared_dirs,
|
|
47
|
-
scan_result)
|
|
49
|
+
controller_context = build_controller_context(classification.controller_dirs)
|
|
48
50
|
|
|
49
51
|
# Phase 1: build all content in memory — no I/O.
|
|
50
52
|
shared_manifest = build_shared(classification.shared_dirs)
|
|
@@ -61,6 +63,28 @@ module ReactManifest
|
|
|
61
63
|
results
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# Remove all AUTO-GENERATED ux_*.js manifests. Silently skips files that
|
|
67
|
+
# disappear between the directory scan and the read (TOCTOU-safe).
|
|
68
|
+
# Returns { removed: N, skipped: N }.
|
|
69
|
+
def clean!
|
|
70
|
+
targets = [@config.abs_manifest_dir, @config.abs_output_dir].uniq
|
|
71
|
+
removed = 0
|
|
72
|
+
skipped = 0
|
|
73
|
+
|
|
74
|
+
targets.each do |dir|
|
|
75
|
+
Dir.glob(File.join(dir, "ux_*.js")).each do |file|
|
|
76
|
+
if auto_generated?(file)
|
|
77
|
+
File.delete(file)
|
|
78
|
+
removed += 1
|
|
79
|
+
else
|
|
80
|
+
skipped += 1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
{ removed: removed, skipped: skipped }
|
|
86
|
+
end
|
|
87
|
+
|
|
64
88
|
private
|
|
65
89
|
|
|
66
90
|
# ------------------------------------------------------------------ shared
|
|
@@ -88,8 +112,6 @@ module ReactManifest
|
|
|
88
112
|
lines = header_lines
|
|
89
113
|
always_include_reqs = controller_context[:always_include_requires].fetch(ctrl[:bundle_name], [])
|
|
90
114
|
dep_requires = controller_dependency_requires(ctrl[:bundle_name], controller_context)
|
|
91
|
-
controller_context[:shared_lib_requires]
|
|
92
|
-
controller_context[:shared_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
93
115
|
ext_reqs = controller_context[:external_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
|
|
94
116
|
|
|
95
117
|
files = js_files_in(ctrl[:path])
|
|
@@ -105,25 +127,14 @@ module ReactManifest
|
|
|
105
127
|
{ filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
|
|
106
128
|
end
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
def build_controller_context(controller_dirs, shared_dirs, scan_result)
|
|
130
|
+
def build_controller_context(controller_dirs)
|
|
110
131
|
bundle_files = {}
|
|
111
132
|
symbol_to_bundle = {}
|
|
112
133
|
external_symbol_to_require = {}
|
|
113
134
|
dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
114
135
|
external_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
115
|
-
shared_require_paths = shared_require_path_set(shared_dirs)
|
|
116
|
-
shared_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
117
|
-
shared_dependency_map = build_shared_dependency_map(shared_dirs, shared_require_paths, scan_result)
|
|
118
|
-
shared_lib_requires = shared_lib_require_paths(shared_dirs)
|
|
119
136
|
|
|
120
137
|
controller_dirs.each do |ctrl|
|
|
121
|
-
scan_result.controller_usages.fetch(ctrl[:name], []).each do |req_path|
|
|
122
|
-
shared_requires[ctrl[:bundle_name]] << req_path
|
|
123
|
-
end
|
|
124
|
-
shared_requires[ctrl[:bundle_name]] = expand_shared_requires(shared_requires[ctrl[:bundle_name]],
|
|
125
|
-
shared_dependency_map)
|
|
126
|
-
|
|
127
138
|
# Index controller-defined symbols for cross-app detection
|
|
128
139
|
bundle_name = ctrl[:bundle_name]
|
|
129
140
|
files = js_files_in(ctrl[:path])
|
|
@@ -176,12 +187,9 @@ module ReactManifest
|
|
|
176
187
|
bundle_files: bundle_files,
|
|
177
188
|
dependencies: dependencies,
|
|
178
189
|
always_include_requires: always_include_requires,
|
|
179
|
-
shared_lib_requires: shared_lib_requires,
|
|
180
|
-
shared_requires: shared_requires,
|
|
181
190
|
external_requires: external_requires
|
|
182
191
|
}
|
|
183
192
|
end
|
|
184
|
-
# rubocop:enable Metrics/AbcSize
|
|
185
193
|
|
|
186
194
|
def controller_dependency_requires(bundle_name, controller_context)
|
|
187
195
|
deps = transitive_dependencies(bundle_name, controller_context[:dependencies])
|
|
@@ -254,7 +262,7 @@ module ReactManifest
|
|
|
254
262
|
end
|
|
255
263
|
|
|
256
264
|
if @config.dry_run?
|
|
257
|
-
|
|
265
|
+
log_info "DRY-RUN: would write #{dest}"
|
|
258
266
|
print_diff(dest, content)
|
|
259
267
|
return { path: dest, status: :dry_run }
|
|
260
268
|
end
|
|
@@ -286,7 +294,7 @@ module ReactManifest
|
|
|
286
294
|
if @config.dry_run?
|
|
287
295
|
legacy_files.each do |legacy|
|
|
288
296
|
target = File.join(manifest_dir, File.basename(legacy))
|
|
289
|
-
|
|
297
|
+
log_info "DRY-RUN: would move #{legacy} -> #{target}"
|
|
290
298
|
end
|
|
291
299
|
return
|
|
292
300
|
end
|
|
@@ -320,7 +328,7 @@ module ReactManifest
|
|
|
320
328
|
files = Dir.glob(File.join(dir, "**", @config.extensions_glob))
|
|
321
329
|
.reject { |f| File.directory?(f) }
|
|
322
330
|
.reject { |f| auto_generated?(f) }
|
|
323
|
-
.reject { |f| excluded_path?(f) }
|
|
331
|
+
.reject { |f| @config.excluded_path?(f) }
|
|
324
332
|
.sort
|
|
325
333
|
|
|
326
334
|
# Deduplicate by logical require path: if both foo.js and foo.jsx exist,
|
|
@@ -336,20 +344,11 @@ module ReactManifest
|
|
|
336
344
|
end
|
|
337
345
|
end
|
|
338
346
|
|
|
339
|
-
# Returns true if the file path contains a component matching any exclude_path.
|
|
340
|
-
# exclude_paths are matched against individual path segments, so "vendor" matches
|
|
341
|
-
# ux/vendor/foo.js but not ux/vendor_custom/foo.js.
|
|
342
|
-
def excluded_path?(abs_path)
|
|
343
|
-
parts = abs_path.split(File::SEPARATOR)
|
|
344
|
-
@config.exclude_paths.any? { |ep| parts.include?(ep) }
|
|
345
|
-
end
|
|
346
|
-
|
|
347
347
|
def relative_require_path(abs_path)
|
|
348
348
|
# Build relative to output_dir (configurable) rather than a hardcoded path.
|
|
349
349
|
base = @config.abs_output_dir + File::SEPARATOR
|
|
350
350
|
rel = abs_path.sub(base, "")
|
|
351
|
-
|
|
352
|
-
rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
|
|
351
|
+
strip_asset_extension(rel)
|
|
353
352
|
end
|
|
354
353
|
|
|
355
354
|
def extract_defined_symbols(file_path)
|
|
@@ -365,22 +364,7 @@ module ReactManifest
|
|
|
365
364
|
|
|
366
365
|
def extract_used_component_symbols(file_path)
|
|
367
366
|
content = File.read(file_path, encoding: "utf-8")
|
|
368
|
-
|
|
369
|
-
# Collect locally-defined symbols to avoid self-reference false positives
|
|
370
|
-
local_syms = Set.new
|
|
371
|
-
ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
|
|
372
|
-
content.scan(pattern) { |m| local_syms << m[0] }
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
symbols = []
|
|
376
|
-
content.scan(ReactManifest::Scanner::PASCAL_TOKEN_PATTERN) do |m|
|
|
377
|
-
symbols << m[0] unless local_syms.include?(m[0])
|
|
378
|
-
end
|
|
379
|
-
content.scan(ReactManifest::Scanner::HOOK_TOKEN_PATTERN) do |m|
|
|
380
|
-
symbols << m[0] unless local_syms.include?(m[0])
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
symbols.uniq
|
|
367
|
+
SymbolExtractor.extract_usages(content)
|
|
384
368
|
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
385
369
|
[]
|
|
386
370
|
end
|
|
@@ -390,7 +374,7 @@ module ReactManifest
|
|
|
390
374
|
|
|
391
375
|
Dir.glob(File.join(dir, "**", @config.extensions_glob))
|
|
392
376
|
.reject { |f| File.directory?(f) }
|
|
393
|
-
.reject { |f| excluded_path?(f) }
|
|
377
|
+
.reject { |f| @config.excluded_path?(f) }
|
|
394
378
|
.sort
|
|
395
379
|
end
|
|
396
380
|
|
|
@@ -400,68 +384,8 @@ module ReactManifest
|
|
|
400
384
|
Rails.root.join(path).to_s
|
|
401
385
|
end
|
|
402
386
|
|
|
403
|
-
def shared_require_path_set(shared_dirs)
|
|
404
|
-
shared_dirs.each_with_object(Set.new) do |shared_dir, paths|
|
|
405
|
-
js_files_in(shared_dir[:path]).each do |file_path|
|
|
406
|
-
paths << normalize_require_path(relative_require_path(file_path))
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def shared_lib_require_paths(shared_dirs)
|
|
412
|
-
shared_dirs.each_with_object([]) do |shared_dir, paths|
|
|
413
|
-
next unless File.basename(shared_dir[:path]) == "lib"
|
|
414
|
-
|
|
415
|
-
js_files_in(shared_dir[:path]).each do |file_path|
|
|
416
|
-
paths << normalize_require_path(relative_require_path(file_path))
|
|
417
|
-
end
|
|
418
|
-
end.sort.uniq
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def build_shared_dependency_map(shared_dirs, shared_require_paths, scan_result)
|
|
422
|
-
dependency_map = Hash.new { |h, k| h[k] = Set.new }
|
|
423
|
-
|
|
424
|
-
shared_symbol_index = scan_result.symbol_index.each_with_object({}) do |(sym, req_path), index|
|
|
425
|
-
normalized = normalize_require_path(req_path)
|
|
426
|
-
next unless shared_require_paths.include?(normalized)
|
|
427
|
-
|
|
428
|
-
index[sym] = normalized
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
shared_dirs.each do |shared_dir|
|
|
432
|
-
js_files_in(shared_dir[:path]).each do |file_path|
|
|
433
|
-
from_req = normalize_require_path(relative_require_path(file_path))
|
|
434
|
-
extract_used_component_symbols(file_path).each do |sym|
|
|
435
|
-
to_req = shared_symbol_index[sym]
|
|
436
|
-
next if to_req.nil? || to_req == from_req
|
|
437
|
-
|
|
438
|
-
dependency_map[from_req] << to_req
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
dependency_map
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def expand_shared_requires(initial_requires, dependency_map)
|
|
447
|
-
expanded = Set.new(initial_requires)
|
|
448
|
-
queue = initial_requires.to_a
|
|
449
|
-
|
|
450
|
-
until queue.empty?
|
|
451
|
-
req = queue.shift
|
|
452
|
-
dependency_map.fetch(req, Set.new).each do |dep_req|
|
|
453
|
-
next if expanded.include?(dep_req)
|
|
454
|
-
|
|
455
|
-
expanded << dep_req
|
|
456
|
-
queue << dep_req
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
expanded
|
|
461
|
-
end
|
|
462
|
-
|
|
463
387
|
def normalize_require_path(path)
|
|
464
|
-
path
|
|
388
|
+
strip_asset_extension(path)
|
|
465
389
|
end
|
|
466
390
|
|
|
467
391
|
def warn_on_external_controller_references(file_path, symbol_to_bundle)
|
|
@@ -469,9 +393,9 @@ module ReactManifest
|
|
|
469
393
|
dep_bundle = symbol_to_bundle[sym]
|
|
470
394
|
next unless dep_bundle
|
|
471
395
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
396
|
+
log_warn "External file '#{relative_require_path(file_path)}' references " \
|
|
397
|
+
"controller-only symbol '#{sym}' (#{dep_bundle}). " \
|
|
398
|
+
"Move '#{sym}' to a shared ux dir to avoid duplicate runtime declarations."
|
|
475
399
|
end
|
|
476
400
|
end
|
|
477
401
|
|
|
@@ -492,18 +416,18 @@ module ReactManifest
|
|
|
492
416
|
removed = old_lines - new_lines
|
|
493
417
|
added = new_lines - old_lines
|
|
494
418
|
|
|
495
|
-
removed.each { |l|
|
|
496
|
-
added.each { |l|
|
|
419
|
+
removed.each { |l| log_info " - #{l.chomp}" }
|
|
420
|
+
added.each { |l| log_info " + #{l.chomp}" }
|
|
497
421
|
else
|
|
498
|
-
new_content.each_line { |l|
|
|
422
|
+
new_content.each_line { |l| log_info " + #{l.chomp}" }
|
|
499
423
|
end
|
|
500
424
|
end
|
|
501
425
|
|
|
502
426
|
def print_summary(results)
|
|
503
427
|
counts = results.group_by { |r| r[:status] }.transform_values(&:count)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
428
|
+
log_info "Generated: #{counts[:written] || 0} written, " \
|
|
429
|
+
"#{counts[:unchanged] || 0} unchanged, " \
|
|
430
|
+
"#{counts[:skipped_pinned] || 0} skipped (not auto-generated)"
|
|
507
431
|
end
|
|
508
432
|
end
|
|
509
433
|
# rubocop:enable Metrics/ClassLength
|
|
@@ -9,6 +9,8 @@ module ReactManifest
|
|
|
9
9
|
# Usage:
|
|
10
10
|
# ReactManifest::LayoutPatcher.new(config).patch!
|
|
11
11
|
class LayoutPatcher
|
|
12
|
+
include ReactManifest::Logging
|
|
13
|
+
|
|
12
14
|
LAYOUTS_GLOB = "app/views/layouts/*.html.{erb,haml,slim}".freeze
|
|
13
15
|
BUNDLE_TAG_ERB = "<%= react_bundle_tag %>\n".freeze
|
|
14
16
|
BUNDLE_TAG_HAML = "= react_bundle_tag\n".freeze
|
|
@@ -23,7 +25,7 @@ module ReactManifest
|
|
|
23
25
|
def patch!
|
|
24
26
|
layouts = find_layouts
|
|
25
27
|
if layouts.empty?
|
|
26
|
-
|
|
28
|
+
log_info "No layout files found in #{layouts_dir}"
|
|
27
29
|
return []
|
|
28
30
|
end
|
|
29
31
|
layouts.map { |f| patch_file(f) }
|
|
@@ -58,13 +60,20 @@ module ReactManifest
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
if @config.dry_run?
|
|
61
|
-
|
|
63
|
+
log_info "DRY-RUN: would patch #{short(path)}"
|
|
62
64
|
print_diff(content, new_content)
|
|
63
65
|
return Result.new(file: path, status: :dry_run, detail: nil)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
tmp = "#{path}.tmp.#{Process.pid}"
|
|
69
|
+
begin
|
|
70
|
+
File.write(tmp, new_content, encoding: "utf-8")
|
|
71
|
+
File.rename(tmp, path)
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
FileUtils.rm_f(tmp)
|
|
74
|
+
raise e
|
|
75
|
+
end
|
|
76
|
+
log_info "Patched layout: #{short(path)}"
|
|
68
77
|
Result.new(file: path, status: :patched, detail: nil)
|
|
69
78
|
end
|
|
70
79
|
|
|
@@ -115,7 +124,7 @@ module ReactManifest
|
|
|
115
124
|
def print_diff(old_content, new_content)
|
|
116
125
|
old_lines = old_content.lines.map(&:chomp)
|
|
117
126
|
new_lines = new_content.lines.map(&:chomp)
|
|
118
|
-
(new_lines - old_lines).each { |l|
|
|
127
|
+
(new_lines - old_lines).each { |l| log_info " + #{l}" }
|
|
119
128
|
end
|
|
120
129
|
end
|
|
121
130
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ReactManifest
|
|
2
|
+
module Logging
|
|
3
|
+
def log_debug(message)
|
|
4
|
+
full = "[ReactManifest] #{message}"
|
|
5
|
+
Rails.logger.debug(full)
|
|
6
|
+
$stdout.puts(full) if stdout_logging_needed?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def log_info(message)
|
|
10
|
+
full = "[ReactManifest] #{message}"
|
|
11
|
+
Rails.logger.info(full)
|
|
12
|
+
$stdout.puts(full) if stdout_logging_needed?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def log_warn(message)
|
|
16
|
+
full = "[ReactManifest] #{message}"
|
|
17
|
+
Rails.logger.warn(full)
|
|
18
|
+
$stdout.puts(full) if stdout_logging_needed?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def stdout_logging_needed?
|
|
24
|
+
ReactManifest.configuration.stdout_logging? && !rails_console?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rails_console?
|
|
28
|
+
defined?(Rails::Console)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ReactManifest
|
|
2
|
+
module PathUtils
|
|
3
|
+
# Matches compound and single Sprockets-understood asset extensions.
|
|
4
|
+
# Order is significant: compound forms must precede their singles.
|
|
5
|
+
STRIPPABLE_EXTENSIONS = /\.(ts\.tsx|js\.jsx|tsx|ts|jsx|js)$/
|
|
6
|
+
|
|
7
|
+
def strip_asset_extension(path)
|
|
8
|
+
path.to_s.sub(STRIPPABLE_EXTENSIONS, "")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -3,28 +3,23 @@ require "rails/railtie"
|
|
|
3
3
|
module ReactManifest
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
5
|
# ----------------------------------------------------------------
|
|
6
|
-
# In development,
|
|
7
|
-
#
|
|
6
|
+
# In development, always regenerate manifests on boot so that files
|
|
7
|
+
# added between restarts (e.g. via git merge) are picked up immediately.
|
|
8
|
+
# The generator is idempotent — it skips writes when content is unchanged.
|
|
8
9
|
# ----------------------------------------------------------------
|
|
9
|
-
initializer "react_manifest.ensure_manifests" do
|
|
10
|
+
initializer "react_manifest.ensure_manifests", after: :load_config_initializers do
|
|
10
11
|
next unless Rails.env.development?
|
|
11
12
|
|
|
12
13
|
config = ReactManifest.configuration
|
|
13
|
-
# Private class method: call via send from the initializer instance context.
|
|
14
|
-
missing = self.class.send(:missing_manifest_bundles, config)
|
|
15
|
-
next if missing.empty?
|
|
16
|
-
|
|
17
|
-
message = "[ReactManifest] Missing manifests on boot: #{missing.join(', ')}. Generating now..."
|
|
18
|
-
Rails.logger&.info(message)
|
|
19
|
-
$stdout.puts(message) if config.stdout_logging?
|
|
20
14
|
|
|
21
15
|
begin
|
|
22
16
|
results = ReactManifest::Generator.new(config).run!
|
|
23
17
|
written = results.count { |r| r[:status] == :written }
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
if written.positive?
|
|
19
|
+
done = "[ReactManifest] Boot generation complete: #{written} written"
|
|
20
|
+
Rails.logger&.info(done)
|
|
21
|
+
$stdout.puts(done) if config.stdout_logging?
|
|
22
|
+
end
|
|
28
23
|
rescue StandardError => e
|
|
29
24
|
error = "[ReactManifest] Could not generate manifests on boot: #{e.message}"
|
|
30
25
|
Rails.logger&.warn(error)
|
|
@@ -99,21 +94,5 @@ module ReactManifest
|
|
|
99
94
|
Rake::Task["assets:precompile"].enhance(["react_manifest:generate"])
|
|
100
95
|
end
|
|
101
96
|
end
|
|
102
|
-
|
|
103
|
-
class << self
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def missing_manifest_bundles(config)
|
|
107
|
-
expected_manifest_bundles(config).reject do |bundle_name|
|
|
108
|
-
File.exist?(File.join(config.abs_manifest_dir, "#{bundle_name}.js")) ||
|
|
109
|
-
File.exist?(File.join(config.abs_output_dir, "#{bundle_name}.js"))
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def expected_manifest_bundles(config)
|
|
114
|
-
classification = ReactManifest::TreeClassifier.new(config).classify
|
|
115
|
-
([config.shared_bundle] + classification.controller_dirs.map { |ctrl| ctrl[:bundle_name] }).uniq
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
97
|
end
|
|
119
98
|
end
|