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.
@@ -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,2 @@
1
+ require "react_manifest"
2
+ require "react_manifest/railtie" if defined?(Rails)
@@ -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: []