react-manifest-rails 0.1.0
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 +7 -0
- data/CHANGELOG.md +22 -0
- data/README.md +244 -0
- data/lib/react_manifest/application_analyzer.rb +150 -0
- data/lib/react_manifest/application_migrator.rb +101 -0
- data/lib/react_manifest/configuration.rb +67 -0
- data/lib/react_manifest/dependency_map.rb +52 -0
- data/lib/react_manifest/generator.rb +206 -0
- data/lib/react_manifest/railtie.rb +46 -0
- data/lib/react_manifest/reporter.rb +96 -0
- data/lib/react_manifest/scanner.rb +192 -0
- data/lib/react_manifest/tree_classifier.rb +69 -0
- data/lib/react_manifest/version.rb +3 -0
- data/lib/react_manifest/view_helpers.rb +27 -0
- data/lib/react_manifest/watcher.rb +74 -0
- data/lib/react_manifest.rb +83 -0
- data/lib/react_manifest_rails.rb +2 -0
- data/tasks/react_manifest.rake +89 -0
- metadata +166 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module ReactManifest
|
|
2
|
+
# Watches the ux/ directory tree for file changes and triggers
|
|
3
|
+
# manifest regeneration automatically.
|
|
4
|
+
#
|
|
5
|
+
# Uses the `listen` gem (soft dependency — degrades gracefully if not available).
|
|
6
|
+
# Auto-started in development via the Railtie initializer.
|
|
7
|
+
#
|
|
8
|
+
# Watches ux_root recursively so newly added controller directories are
|
|
9
|
+
# picked up without a server restart.
|
|
10
|
+
module Watcher
|
|
11
|
+
DEBOUNCE_SECONDS = 0.3
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def start(config = ReactManifest.configuration)
|
|
15
|
+
begin
|
|
16
|
+
require "listen"
|
|
17
|
+
rescue LoadError
|
|
18
|
+
log "listen gem not available — file watching disabled. " \
|
|
19
|
+
"Add `gem 'listen'` to the development group in your Gemfile."
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
root = config.abs_ux_root
|
|
24
|
+
|
|
25
|
+
unless Dir.exist?(root)
|
|
26
|
+
log "ux_root does not exist (#{root}) — file watching disabled until directory is created."
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
log "Watching #{root.sub(Rails.root.to_s + '/', '')} for changes..."
|
|
31
|
+
|
|
32
|
+
@listener = Listen.to(
|
|
33
|
+
root,
|
|
34
|
+
only: /\.(js|jsx)$/,
|
|
35
|
+
latency: DEBOUNCE_SECONDS
|
|
36
|
+
) do |modified, added, removed|
|
|
37
|
+
changed = (modified + added + removed).map { |f| File.basename(f) }
|
|
38
|
+
log "File change detected: #{changed.join(', ')}"
|
|
39
|
+
regenerate!(config)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@listener.start
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
@listener&.stop
|
|
47
|
+
@listener = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def running?
|
|
51
|
+
!@listener.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def regenerate!(config)
|
|
57
|
+
Generator.new(config).run!
|
|
58
|
+
log "Manifests regenerated"
|
|
59
|
+
rescue => e
|
|
60
|
+
log "Error during regeneration: #{e.message}"
|
|
61
|
+
log e.backtrace.first(5).join("\n") if config.verbose?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log(message)
|
|
65
|
+
msg = "[ReactManifest] #{message}"
|
|
66
|
+
if defined?(Rails) && Rails.logger
|
|
67
|
+
Rails.logger.info(msg)
|
|
68
|
+
else
|
|
69
|
+
$stdout.puts msg
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
require "react_manifest/version"
|
|
5
|
+
require "react_manifest/configuration"
|
|
6
|
+
require "react_manifest/tree_classifier"
|
|
7
|
+
require "react_manifest/scanner"
|
|
8
|
+
require "react_manifest/dependency_map"
|
|
9
|
+
require "react_manifest/generator"
|
|
10
|
+
require "react_manifest/application_analyzer"
|
|
11
|
+
require "react_manifest/application_migrator"
|
|
12
|
+
require "react_manifest/watcher"
|
|
13
|
+
require "react_manifest/reporter"
|
|
14
|
+
require "react_manifest/view_helpers"
|
|
15
|
+
|
|
16
|
+
module ReactManifest
|
|
17
|
+
class << self
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset!
|
|
27
|
+
@configuration = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the ordered list of bundle logical names for a given controller.
|
|
31
|
+
# Used by the react_bundle_tag view helper.
|
|
32
|
+
def resolve_bundles(ctrl_name)
|
|
33
|
+
config = configuration
|
|
34
|
+
output = config.abs_output_dir
|
|
35
|
+
bundles = []
|
|
36
|
+
|
|
37
|
+
# 1. Shared bundle always first
|
|
38
|
+
if bundle_exists?(output, config.shared_bundle)
|
|
39
|
+
bundles << config.shared_bundle
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 2. always_include bundles (e.g. ux_main)
|
|
43
|
+
config.always_include.each do |b|
|
|
44
|
+
bundles << b if bundle_exists?(output, b) && !bundles.include?(b)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 3. Controller-specific bundle
|
|
48
|
+
# Try fully-namespaced first: admin/users → ux_admin_users
|
|
49
|
+
# Then drop segments: ux_admin
|
|
50
|
+
controller_candidates(ctrl_name).each do |candidate|
|
|
51
|
+
if bundle_exists?(output, candidate) && !bundles.include?(candidate)
|
|
52
|
+
bundles << candidate
|
|
53
|
+
break
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
bundles
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def bundle_exists?(output_dir, bundle_name)
|
|
63
|
+
File.exist?(File.join(output_dir, "#{bundle_name}.js"))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def controller_candidates(ctrl_name)
|
|
67
|
+
# "admin/reports/summary" → ["ux_admin_reports_summary", "ux_admin_reports", "ux_admin", "ux_summary"]
|
|
68
|
+
parts = ctrl_name.to_s.split("/")
|
|
69
|
+
candidates = []
|
|
70
|
+
|
|
71
|
+
# Longest match first (most specific)
|
|
72
|
+
parts.length.downto(1) do |len|
|
|
73
|
+
candidates << "ux_#{parts.first(len).join('_')}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Also try just the last segment (common Rails pattern)
|
|
77
|
+
last = parts.last
|
|
78
|
+
candidates << "ux_#{last}" unless candidates.include?("ux_#{last}")
|
|
79
|
+
|
|
80
|
+
candidates.uniq
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
namespace :react_manifest do
|
|
2
|
+
desc "Generate all ux_*.js Sprockets manifests from the ux/ directory tree"
|
|
3
|
+
task generate: :environment do
|
|
4
|
+
results = ReactManifest::Generator.new.run!
|
|
5
|
+
|
|
6
|
+
written = results.count { |r| r[:status] == :written }
|
|
7
|
+
unchanged = results.count { |r| r[:status] == :unchanged }
|
|
8
|
+
skipped = results.count { |r| r[:status] == :skipped_pinned }
|
|
9
|
+
dry = results.count { |r| r[:status] == :dry_run }
|
|
10
|
+
|
|
11
|
+
if ReactManifest.configuration.dry_run?
|
|
12
|
+
puts "[ReactManifest] DRY-RUN complete: #{dry} manifest(s) would be written"
|
|
13
|
+
else
|
|
14
|
+
puts "[ReactManifest] Done: #{written} written, #{unchanged} unchanged, #{skipped} skipped"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Print any scanner warnings
|
|
18
|
+
results # warnings are printed inline by scanner via $stdout in verbose mode
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Print the JSX dependency map and warnings without writing any files"
|
|
22
|
+
task analyze: :environment do
|
|
23
|
+
config = ReactManifest.configuration
|
|
24
|
+
classifier = ReactManifest::TreeClassifier.new(config)
|
|
25
|
+
scanner = ReactManifest::Scanner.new(config)
|
|
26
|
+
|
|
27
|
+
classification = classifier.classify
|
|
28
|
+
scan_result = scanner.scan(classification)
|
|
29
|
+
dep_map = ReactManifest::DependencyMap.new(scan_result)
|
|
30
|
+
dep_map.print_report
|
|
31
|
+
|
|
32
|
+
unless scan_result.warnings.empty?
|
|
33
|
+
puts "Warnings (#{scan_result.warnings.size}):"
|
|
34
|
+
scan_result.warnings.each { |w| puts " ⚠ #{w}" }
|
|
35
|
+
puts
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "Analyze application*.js files — show what migrate_application would change"
|
|
40
|
+
task analyze_application: :environment do
|
|
41
|
+
analyzer = ReactManifest::ApplicationAnalyzer.new
|
|
42
|
+
results = analyzer.analyze
|
|
43
|
+
analyzer.print_report(results)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Rewrite application*.js files to remove UX code (creates .bak backups)"
|
|
47
|
+
task migrate_application: :environment do
|
|
48
|
+
migrator = ReactManifest::ApplicationMigrator.new
|
|
49
|
+
migrator.migrate!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc "Print per-bundle raw and gzip sizes from compiled public/assets/"
|
|
53
|
+
task report: :environment do
|
|
54
|
+
ReactManifest::Reporter.new.report
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Remove all AUTO-GENERATED ux_*.js manifests"
|
|
58
|
+
task clean: :environment do
|
|
59
|
+
config = ReactManifest.configuration
|
|
60
|
+
output = config.abs_output_dir
|
|
61
|
+
removed = 0
|
|
62
|
+
skipped = 0
|
|
63
|
+
|
|
64
|
+
Dir.glob(File.join(output, "ux_*.js")).each do |file|
|
|
65
|
+
first_line = File.foreach(file).first.to_s
|
|
66
|
+
if first_line.include?("AUTO-GENERATED")
|
|
67
|
+
File.delete(file)
|
|
68
|
+
puts "[ReactManifest] Removed: #{file.sub(Rails.root.to_s + '/', '')}"
|
|
69
|
+
removed += 1
|
|
70
|
+
else
|
|
71
|
+
puts "[ReactManifest] Skipped (not auto-generated): #{file.sub(Rails.root.to_s + '/', '')}"
|
|
72
|
+
skipped += 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "[ReactManifest] Clean complete: #{removed} removed, #{skipped} skipped"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "Start the file watcher in the foreground (for debugging)"
|
|
80
|
+
task watch: :environment do
|
|
81
|
+
puts "[ReactManifest] Starting file watcher (Ctrl-C to stop)..."
|
|
82
|
+
ReactManifest::Watcher.start(ReactManifest.configuration)
|
|
83
|
+
# Block the process
|
|
84
|
+
sleep
|
|
85
|
+
rescue Interrupt
|
|
86
|
+
ReactManifest::Watcher.stop
|
|
87
|
+
puts "\n[ReactManifest] Watcher stopped."
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: react-manifest-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Oliver Noonan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: listen
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec-rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rails
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '6.1'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '6.1'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sprockets-rails
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
description: |
|
|
112
|
+
react-manifest-rails automatically generates per-controller Sprockets manifest
|
|
113
|
+
files for Rails applications using react-rails + Sprockets. It eliminates the
|
|
114
|
+
monolithic application.js by creating lean, controller-specific ux_*.js bundles,
|
|
115
|
+
watches for file changes in development, and provides a smart react_bundle_tag
|
|
116
|
+
view helper that auto-selects the correct bundle per controller.
|
|
117
|
+
email:
|
|
118
|
+
- ''
|
|
119
|
+
executables: []
|
|
120
|
+
extensions: []
|
|
121
|
+
extra_rdoc_files: []
|
|
122
|
+
files:
|
|
123
|
+
- CHANGELOG.md
|
|
124
|
+
- README.md
|
|
125
|
+
- lib/react_manifest.rb
|
|
126
|
+
- lib/react_manifest/application_analyzer.rb
|
|
127
|
+
- lib/react_manifest/application_migrator.rb
|
|
128
|
+
- lib/react_manifest/configuration.rb
|
|
129
|
+
- lib/react_manifest/dependency_map.rb
|
|
130
|
+
- lib/react_manifest/generator.rb
|
|
131
|
+
- lib/react_manifest/railtie.rb
|
|
132
|
+
- lib/react_manifest/reporter.rb
|
|
133
|
+
- lib/react_manifest/scanner.rb
|
|
134
|
+
- lib/react_manifest/tree_classifier.rb
|
|
135
|
+
- lib/react_manifest/version.rb
|
|
136
|
+
- lib/react_manifest/view_helpers.rb
|
|
137
|
+
- lib/react_manifest/watcher.rb
|
|
138
|
+
- lib/react_manifest_rails.rb
|
|
139
|
+
- tasks/react_manifest.rake
|
|
140
|
+
homepage: https://github.com/olivernoonan/react-manifest-rails
|
|
141
|
+
licenses:
|
|
142
|
+
- MIT
|
|
143
|
+
metadata:
|
|
144
|
+
homepage_uri: https://github.com/olivernoonan/react-manifest-rails
|
|
145
|
+
source_code_uri: https://github.com/olivernoonan/react-manifest-rails
|
|
146
|
+
changelog_uri: https://github.com/olivernoonan/react-manifest-rails/blob/main/CHANGELOG.md
|
|
147
|
+
post_install_message:
|
|
148
|
+
rdoc_options: []
|
|
149
|
+
require_paths:
|
|
150
|
+
- lib
|
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: 2.6.0
|
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '0'
|
|
161
|
+
requirements: []
|
|
162
|
+
rubygems_version: 3.4.19
|
|
163
|
+
signing_key:
|
|
164
|
+
specification_version: 4
|
|
165
|
+
summary: Zero-touch Sprockets manifest generation for react-rails apps
|
|
166
|
+
test_files: []
|