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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c76e23ad95cdf498e89dd0d63862c8d39b0acdf956df8dbf271a6157871e10f4
4
+ data.tar.gz: 68bb10eb5f7900798f4775db3399e9252879bdeb53e786b9d29e016e9e45600b
5
+ SHA512:
6
+ metadata.gz: fdf7e42fb7ea57bfa28d818f53d387ac1dc6fbeb8015bf34a563fd095f1cd70edd966013a36996fe864f56d2fc3c21c97030b136f6b547d317a2555998484198
7
+ data.tar.gz: 36055fab58f773760266ad9e706a52dfe71e333e0dbd3e61db234e65095cb54709b82b031071ca78576db54ed9afb042ab72a232496c24bb5defd058f6169f63
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-04-13
9
+
10
+ ### Added
11
+ - Initial release of react-manifest-rails gem
12
+ - Zero-touch Sprockets manifest generation for react-rails applications
13
+ - Automatic per-controller bundle generation (`ux_*.js` manifests)
14
+ - File watcher for development that regenerates bundles on file changes
15
+ - Smart `react_bundle_tag` view helper for automatic bundle selection
16
+ - Intelligent bundle resolution for namespaced controllers
17
+ - Automatic shared bundle generation for common dependencies
18
+ - Configuration system with sensible defaults
19
+ - Integration with Rails' `assets:precompile` for production deployments
20
+ - Comprehensive README with setup instructions and usage examples
21
+ - Bundle size warnings to catch oversized bundles
22
+ - Support for Rails 6.1+ and Ruby 2.6+
data/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # react-manifest-rails
2
+
3
+ **Zero-touch Sprockets manifest generation for react-rails apps**
4
+
5
+ A Rails gem that automatically generates lean, controller-specific JavaScript bundles for applications using `react-rails` and Sprockets, eliminating the need for monolithic `application.js` files.
6
+
7
+ ## Features
8
+
9
+ - **Automatic bundle generation**: Creates per-controller `ux_*.js` manifests on-demand
10
+ - **Smart file watching**: Automatically regenerates bundles when files change in development
11
+ - **Intelligent bundle resolution**: The `react_bundle_tag` view helper automatically selects the correct bundles for each controller
12
+ - **Namespace-aware**: Handles nested controllers and namespaces gracefully (e.g., `admin/users` → `ux_admin_users`)
13
+ - **Shared bundles**: Automatically extracts shared dependencies into a `ux_shared` bundle
14
+ - **Always-include bundles**: Configure bundles that should be loaded on every page
15
+ - **Production-ready**: Integrates seamlessly with `assets:precompile` for CI/production deployments
16
+ - **Zero configuration**: Works out-of-the-box with sensible defaults for standard Rails projects
17
+
18
+ ## Advantages
19
+
20
+ ### 1. Smaller Bundle Sizes
21
+ Instead of a single monolithic `application.js` that loads all React components for the entire app, each controller gets its own lean bundle containing only the components it needs. This means:
22
+ - **Faster page loads**: Users only download what they need
23
+ - **Better caching**: Components unchanged on their page won't bust the cache
24
+ - **Reduced bandwidth**: Particularly beneficial for mobile users
25
+
26
+ ### 2. Simplified Development
27
+ - **Automatic regeneration**: Changes to component files are automatically bundled—no manual build steps
28
+ - **No configuration needed**: Works with standard Rails directory structures immediately
29
+ - **Easy integration**: Drop the gem in and it works with your existing `react-rails` setup
30
+
31
+ ### 3. Production Reliability
32
+ - **CI/CD friendly**: Integrates with `assets:precompile` for automated deployments
33
+ - **Deterministic builds**: Consistent bundle generation across environments
34
+ - **Safety checks**: Warns about oversized bundles to catch potential issues
35
+
36
+ ### 4. Developer Experience
37
+ - **Per-controller organization**: Bundle structure mirrors your Rails controller layout
38
+ - **Smart bundle selection**: View helper automatically picks the right bundles—no manual tag management
39
+ - **Clear visibility**: Built-in reporter shows exactly what's in each bundle
40
+
41
+ ## Installation
42
+
43
+ Add the gem to your `Gemfile`:
44
+
45
+ ```ruby
46
+ gem 'react-manifest-rails'
47
+ ```
48
+
49
+ Then run:
50
+
51
+ ```bash
52
+ bundle install
53
+ ```
54
+
55
+ ## Setup
56
+
57
+ ### 1. Create your UX directory structure
58
+
59
+ By default, the gem expects your React components to live in `app/assets/javascripts/ux/`. You should organize it like this:
60
+
61
+ ```
62
+ app/assets/javascripts/ux/
63
+ ├── app/ # Per-controller components (becomes ux_*.js bundles)
64
+ │ ├── users/
65
+ │ │ ├── UserCard.jsx
66
+ │ │ └── UserList.jsx
67
+ │ ├── products/
68
+ │ │ └── ProductGrid.jsx
69
+ │ └── dashboard/
70
+ │ └── Dashboard.jsx
71
+ ├── shared/ # Shared utilities (becomes ux_shared)
72
+ │ ├── api.js
73
+ │ └── utils.js
74
+ └── components/ # Global components (becomes ux_shared)
75
+ └── Navigation.jsx
76
+ ```
77
+
78
+ ### 2. Configure the gem (Optional)
79
+
80
+ Create an initializer at `config/initializers/react_manifest.rb`:
81
+
82
+ ```ruby
83
+ ReactManifest.configure do |config|
84
+ # Where your UX root lives (default: "app/assets/javascripts/ux")
85
+ config.ux_root = "app/assets/javascripts/ux"
86
+
87
+ # Subdir within ux_root containing per-controller components (default: "app")
88
+ config.app_dir = "app"
89
+
90
+ # Where generated manifests are written (default: "app/assets/javascripts")
91
+ config.output_dir = "app/assets/javascripts"
92
+
93
+ # Name of the shared bundle (default: "ux_shared")
94
+ config.shared_bundle = "ux_shared"
95
+
96
+ # Bundles to always include on every page (default: [])
97
+ # config.always_include = ["ux_main"]
98
+
99
+ # Directories within app_dir to ignore (default: [])
100
+ # config.ignore = ["internal_tools"]
101
+
102
+ # Warn if any bundle exceeds this size in KB (default: 500, 0 to disable)
103
+ config.size_threshold_kb = 500
104
+ end
105
+ ```
106
+
107
+ ### 3. Use the view helper
108
+
109
+ In your layout template (`app/views/layouts/application.html.erb` or similar):
110
+
111
+ ```erb
112
+ <head>
113
+ <%= javascript_include_tag "application" %>
114
+ <%= react_bundle_tag defer: true %>
115
+ </head>
116
+ ```
117
+
118
+ The `react_bundle_tag` helper automatically:
119
+ 1. Includes the shared bundle (`ux_shared`)
120
+ 2. Includes any bundles in `config.always_include`
121
+ 3. Includes the controller-specific bundle (e.g., `ux_users` for UsersController)
122
+ 4. Returns an empty string gracefully if there's no matching bundle
123
+
124
+ ## How It Works
125
+
126
+ ### Bundle Generation
127
+
128
+ The gem scans your `app/assets/javascripts/ux/` directory and generates Sprockets manifests:
129
+
130
+ - **Controller-specific bundles**: Each directory under `ux/app/` becomes a bundle
131
+ - `ux/app/users/` → `ux_users.js`
132
+ - `ux/app/admin/reports/` → `ux_admin_reports.js`
133
+
134
+ - **Shared bundle**: Everything outside `ux/app/` (e.g., `shared/`, `components/`) automatically goes into `ux_shared.js`
135
+
136
+ - **Dependency tracking**: The gem analyzes dependencies to prevent duplication across bundles
137
+
138
+ ### Development
139
+
140
+ In development, a file watcher automatically:
141
+ - Detects changes to component files
142
+ - Regenerates affected bundles
143
+ - Updates manifests in real-time
144
+
145
+ ### Production
146
+
147
+ The gem integrates with Rails' `assets:precompile` task:
148
+
149
+ ```bash
150
+ rails assets:precompile
151
+ ```
152
+
153
+ This ensures all bundles are generated before deployment.
154
+
155
+ ## View Helper Usage
156
+
157
+ ### Basic usage
158
+
159
+ ```erb
160
+ <%= react_bundle_tag %>
161
+ ```
162
+
163
+ ### With HTML options
164
+
165
+ ```erb
166
+ <%= react_bundle_tag defer: true, async: true %>
167
+ ```
168
+
169
+ ### Automatic resolution
170
+
171
+ The helper automatically resolves bundles based on `controller_path`:
172
+
173
+ | Controller | Resolved Bundles |
174
+ |-----------|------------------|
175
+ | UsersController | ux_shared + ux_users |
176
+ | Admin::ReportsController | ux_shared + ux_admin_reports (or ux_admin if not found) |
177
+ | Pages::LandingController | ux_shared + ux_pages_landing (or ux_pages, or ux_landing) |
178
+
179
+ ## Rake Tasks
180
+
181
+ ### Generate bundles (one-time)
182
+
183
+ ```bash
184
+ rails react_manifest:generate
185
+ ```
186
+
187
+ ### Watch for changes (development)
188
+
189
+ ```bash
190
+ rails react_manifest:watch
191
+ ```
192
+
193
+ ### Show bundle contents
194
+
195
+ ```bash
196
+ rails react_manifest:report
197
+ ```
198
+
199
+ ## Configuration Reference
200
+
201
+ | Option | Default | Description |
202
+ |--------|---------|-------------|
203
+ | `ux_root` | `"app/assets/javascripts/ux"` | Root directory of your React components |
204
+ | `app_dir` | `"app"` | Subdirectory of `ux_root` for per-controller components |
205
+ | `output_dir` | `"app/assets/javascripts"` | Where generated manifests are written |
206
+ | `shared_bundle` | `"ux_shared"` | Name of the shared bundle |
207
+ | `always_include` | `[]` | Array of bundle names to load on every page |
208
+ | `ignore` | `[]` | Directories to skip during scanning |
209
+ | `exclude_paths` | `["react", "react_dev", "vendor"]` | Top-level directories to exclude |
210
+ | `size_threshold_kb` | `500` | Warn if a bundle exceeds this size (0 = disabled) |
211
+ | `dry_run` | `false` | Print what would change without writing files |
212
+ | `verbose` | `false` | Enable extra logging |
213
+
214
+ ## Troubleshooting
215
+
216
+ ### Bundle not being generated
217
+ - Check that your components are in the correct directory structure (`app/assets/javascripts/ux/app/...`)
218
+ - Run `rails react_manifest:report` to see what bundles were detected
219
+ - Check Rails logs for any file watcher errors
220
+
221
+ ### Components not loading
222
+ - Verify `react_bundle_tag` is in your layout
223
+ - Check that the bundle name matches your controller path
224
+ - Make sure components are in the `ux/` directory, not elsewhere
225
+
226
+ ### Size warnings
227
+ - The gem warns when bundles exceed 500KB
228
+ - Consider splitting large bundles into smaller, more focused ones
229
+ - Adjust `config.size_threshold_kb` in your initializer if needed
230
+
231
+ ## Requirements
232
+
233
+ - Ruby >= 2.6.0
234
+ - Rails >= 6.1
235
+ - Sprockets
236
+ - react-rails
237
+
238
+ ## License
239
+
240
+ MIT License. See LICENSE for details.
241
+
242
+ ## Contributing
243
+
244
+ Bug reports and pull requests are welcome on GitHub at https://github.com/olivernoonan/react-manifest-rails.
@@ -0,0 +1,150 @@
1
+ module ReactManifest
2
+ # Analyzes existing application*.js files to classify each directive as:
3
+ # :vendor — vendor lib require (keep)
4
+ # :ux_code — UX/app code require (remove — will be served by ux_*.js bundles)
5
+ # :unknown — needs manual review
6
+ #
7
+ # Produces a human-readable report without writing anything.
8
+ class ApplicationAnalyzer
9
+ DIRECTIVE_PATTERN = /^\s*\/\/=\s+(require(?:_tree|_directory)?)\s+(.+)$/.freeze
10
+
11
+ # Libs we recognise as vendor (case-insensitive partial match on the require path)
12
+ VENDOR_HINTS = %w[
13
+ react react-dom react_dom reactdom
14
+ mui material-ui
15
+ redux redux-thunk
16
+ axios lodash underscore
17
+ jquery backbone handlebars
18
+ turbo stimulus
19
+ vendor
20
+ ].freeze
21
+
22
+ ClassifiedDirective = Struct.new(:original_line, :directive, :path, :classification, :note, keyword_init: true)
23
+
24
+ Result = Struct.new(:file, :directives, keyword_init: true) do
25
+ def vendor_lines; directives.select { |d| d.classification == :vendor }; end
26
+ def ux_code_lines; directives.select { |d| d.classification == :ux_code }; end
27
+ def unknown_lines; directives.select { |d| d.classification == :unknown }; end
28
+ def clean?; ux_code_lines.empty? && unknown_lines.empty?; end
29
+ end
30
+
31
+ def initialize(config = ReactManifest.configuration)
32
+ @config = config
33
+ end
34
+
35
+ # Returns array of Result objects, one per application*.js found
36
+ def analyze
37
+ application_files.map { |f| analyze_file(f) }
38
+ end
39
+
40
+ # Pretty-print the analysis report to stdout
41
+ def print_report(results = analyze)
42
+ puts "\n=== ReactManifest: Application Manifest Analysis ===\n"
43
+
44
+ if results.empty?
45
+ puts "No application*.js files found in #{@config.abs_output_dir}"
46
+ return
47
+ end
48
+
49
+ results.each do |result|
50
+ rel = result.file.sub(Rails.root.to_s + "/", "")
51
+ status = result.clean? ? "✓ already clean" : "⚠ needs migration"
52
+ puts "\n#{rel} [#{status}]"
53
+ puts "-" * 60
54
+
55
+ result.directives.each do |d|
56
+ icon = case d.classification
57
+ when :vendor then " ✓ KEEP "
58
+ when :ux_code then " ✗ REMOVE "
59
+ when :unknown then " ? REVIEW "
60
+ end
61
+ puts "#{icon} #{d.original_line.strip}"
62
+ puts " → #{d.note}" if d.note
63
+ end
64
+ end
65
+
66
+ puts "\n"
67
+ puts "Run `rails react_manifest:migrate_application` to apply changes."
68
+ puts "Use `--dry-run` (or config.dry_run = true) to preview first.\n\n"
69
+ end
70
+
71
+ private
72
+
73
+ def application_files
74
+ Dir.glob(File.join(@config.abs_output_dir, "application*.js"))
75
+ .reject { |f| f.end_with?(".bak") }
76
+ .sort
77
+ end
78
+
79
+ def analyze_file(file_path)
80
+ directives = []
81
+
82
+ File.foreach(file_path, encoding: "utf-8") do |line|
83
+ raw = line.chomp
84
+ match = raw.match(DIRECTIVE_PATTERN)
85
+
86
+ unless match
87
+ # Non-directive lines (comments, blank) — pass through as :vendor (keep)
88
+ directives << ClassifiedDirective.new(
89
+ original_line: raw,
90
+ directive: nil,
91
+ path: nil,
92
+ classification: :passthrough,
93
+ note: nil
94
+ )
95
+ next
96
+ end
97
+
98
+ directive = match[1] # require, require_tree, require_directory
99
+ path = match[2].strip
100
+
101
+ classification, note = classify_directive(directive, path)
102
+
103
+ directives << ClassifiedDirective.new(
104
+ original_line: raw,
105
+ directive: directive,
106
+ path: path,
107
+ classification: classification,
108
+ note: note
109
+ )
110
+ end
111
+
112
+ Result.new(file: file_path, directives: directives)
113
+ end
114
+
115
+ def classify_directive(directive, path)
116
+ # require_tree is almost always too greedy
117
+ if directive.include?("tree") || directive.include?("directory")
118
+ if path_is_ux?(path)
119
+ return [:ux_code, "require_tree over ux/ — will be replaced by ux_*.js bundles"]
120
+ else
121
+ return [:unknown, "require_tree/require_directory — review manually: #{path}"]
122
+ end
123
+ end
124
+
125
+ # Explicit require
126
+ if path_is_ux?(path)
127
+ return [:ux_code, "ux/ code — will be served by ux_*.js bundles"]
128
+ end
129
+
130
+ if path_is_vendor?(path)
131
+ return [:vendor, nil]
132
+ end
133
+
134
+ [:unknown, "Could not auto-classify — review manually"]
135
+ end
136
+
137
+ def path_is_ux?(path)
138
+ ux_prefix = @config.ux_root.split("/").last # e.g. "ux"
139
+ path.include?(ux_prefix) ||
140
+ path.start_with?("./ux") ||
141
+ path.start_with?("ux/")
142
+ end
143
+
144
+ def path_is_vendor?(path)
145
+ normalised = path.downcase
146
+ VENDOR_HINTS.any? { |hint| normalised.include?(hint) } ||
147
+ @config.exclude_paths.any? { |ep| normalised.include?(ep.downcase) }
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,101 @@
1
+ module ReactManifest
2
+ # Rewrites application*.js files to remove UX/app code requires,
3
+ # keeping only vendor lib requires.
4
+ #
5
+ # Safety:
6
+ # - Creates a .bak backup before any write; aborts if backup fails
7
+ # - Dry-run mode: prints what would change, writes nothing
8
+ # - Never removes :vendor or :passthrough lines
9
+ # - Adds a managed-by comment at the top
10
+ class ApplicationMigrator
11
+ MANAGED_COMMENT = <<~JS
12
+ // Vendor libraries — loaded on every page.
13
+ // React app code is now served per-controller via react_bundle_tag.
14
+ // Managed by react-manifest-rails — do not add require_tree.
15
+ JS
16
+
17
+ def initialize(config = ReactManifest.configuration)
18
+ @config = config
19
+ @analyzer = ApplicationAnalyzer.new(config)
20
+ end
21
+
22
+ # Migrate all application*.js files. Returns array of {file:, status:} hashes.
23
+ def migrate!
24
+ results = @analyzer.analyze
25
+
26
+ if results.empty?
27
+ $stdout.puts "[ReactManifest] No application*.js files found to migrate."
28
+ return []
29
+ end
30
+
31
+ results.map do |result|
32
+ if result.clean?
33
+ $stdout.puts "[ReactManifest] #{short(result.file)} — already clean, skipping."
34
+ { file: result.file, status: :already_clean }
35
+ else
36
+ rewrite(result)
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def rewrite(result)
44
+ file = result.file
45
+ new_content = build_new_content(result)
46
+
47
+ if @config.dry_run?
48
+ $stdout.puts "\n[ReactManifest] DRY-RUN: #{short(file)}"
49
+ print_diff(file, new_content)
50
+ return { file: file, status: :dry_run }
51
+ end
52
+
53
+ # Backup first — abort if backup cannot be created to avoid data loss.
54
+ bak_path = "#{file}.bak"
55
+ begin
56
+ FileUtils.cp(file, bak_path)
57
+ $stdout.puts "[ReactManifest] Backup: #{short(bak_path)}"
58
+ rescue => e
59
+ $stdout.puts "[ReactManifest] ERROR: Could not create backup of #{short(file)}: #{e.message}"
60
+ $stdout.puts "[ReactManifest] Migration aborted for #{short(file)} — original file unchanged."
61
+ return { file: file, status: :backup_failed, error: e.message }
62
+ end
63
+
64
+ File.write(file, new_content, encoding: "utf-8")
65
+ $stdout.puts "[ReactManifest] Migrated: #{short(file)}"
66
+
67
+ { file: file, status: :migrated, backup: bak_path }
68
+ end
69
+
70
+ def build_new_content(result)
71
+ kept_lines = result.directives
72
+ .select { |d| %i[vendor passthrough].include?(d.classification) }
73
+ .map(&:original_line)
74
+
75
+ # Remove leading blank lines from kept_lines
76
+ kept_lines.shift while kept_lines.first&.strip&.empty?
77
+
78
+ lines = []
79
+ lines << MANAGED_COMMENT
80
+ lines += kept_lines
81
+ lines << "" # trailing newline
82
+
83
+ lines.join("\n")
84
+ end
85
+
86
+ def print_diff(file, new_content)
87
+ old_lines = File.readlines(file, encoding: "utf-8").map(&:chomp)
88
+ new_lines = new_content.lines.map(&:chomp)
89
+
90
+ removed = old_lines - new_lines
91
+ added = new_lines - old_lines
92
+
93
+ removed.each { |l| $stdout.puts " - #{l}" }
94
+ added.each { |l| $stdout.puts " + #{l}" }
95
+ end
96
+
97
+ def short(path)
98
+ path.to_s.sub(Rails.root.to_s + "/", "")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,67 @@
1
+ module ReactManifest
2
+ class Configuration
3
+ # Root of the ux/ tree to scan (relative to Rails.root)
4
+ attr_accessor :ux_root
5
+
6
+ # Subdir within ux_root that contains per-controller JSX
7
+ attr_accessor :app_dir
8
+
9
+ # Where generated ux_*.js manifests are written (relative to Rails.root)
10
+ attr_accessor :output_dir
11
+
12
+ # Bundle name for auto-generated shared bundle (all non-app/ dirs)
13
+ attr_accessor :shared_bundle
14
+
15
+ # Bundles always prepended by react_bundle_tag (e.g. ["ux_main"])
16
+ attr_accessor :always_include
17
+
18
+ # Directories under app_dir to completely ignore
19
+ attr_accessor :ignore
20
+
21
+ # Top-level dirs under output_dir to never scan (vendor libs, etc.)
22
+ attr_accessor :exclude_paths
23
+
24
+ # Warn if a bundle exceeds this size in KB (0 = disabled)
25
+ attr_accessor :size_threshold_kb
26
+
27
+ # Print what would change, write nothing
28
+ attr_accessor :dry_run
29
+
30
+ # Extra logging
31
+ attr_accessor :verbose
32
+
33
+ def initialize
34
+ @ux_root = "app/assets/javascripts/ux"
35
+ @app_dir = "app"
36
+ @output_dir = "app/assets/javascripts"
37
+ @shared_bundle = "ux_shared"
38
+ @always_include = []
39
+ @ignore = []
40
+ @exclude_paths = %w[react react_dev vendor]
41
+ @size_threshold_kb = 500
42
+ @dry_run = false
43
+ @verbose = false
44
+ end
45
+
46
+ def dry_run?
47
+ !!@dry_run
48
+ end
49
+
50
+ def verbose?
51
+ !!@verbose
52
+ end
53
+
54
+ # Absolute path helpers (requires Rails.root to be set)
55
+ def abs_ux_root
56
+ Rails.root.join(ux_root).to_s
57
+ end
58
+
59
+ def abs_app_dir
60
+ File.join(abs_ux_root, app_dir)
61
+ end
62
+
63
+ def abs_output_dir
64
+ Rails.root.join(output_dir).to_s
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ module ReactManifest
2
+ # Wraps scanner results into a queryable dependency map.
3
+ # Used by the analyze rake task and reporter for diagnostics.
4
+ class DependencyMap
5
+ attr_reader :symbol_index, :controller_usages, :warnings
6
+
7
+ def initialize(scan_result)
8
+ @symbol_index = scan_result.symbol_index
9
+ @controller_usages = scan_result.controller_usages
10
+ @warnings = scan_result.warnings
11
+ end
12
+
13
+ # All shared files used by the given controller
14
+ def shared_files_for(controller_name)
15
+ @controller_usages.fetch(controller_name, [])
16
+ end
17
+
18
+ # Which controllers use a given shared file
19
+ def controllers_using(shared_file)
20
+ @controller_usages.select { |_, files| files.include?(shared_file) }.keys
21
+ end
22
+
23
+ # Symbols defined in shared dirs
24
+ def all_symbols
25
+ @symbol_index.keys
26
+ end
27
+
28
+ # Pretty-print for the analyze rake task
29
+ def print_report
30
+ puts "\n=== ReactManifest Dependency Analysis ===\n\n"
31
+
32
+ puts "Shared Symbol Index (#{@symbol_index.size} symbols):"
33
+ @symbol_index.each do |sym, file|
34
+ puts " #{sym.ljust(40)} #{file}"
35
+ end
36
+
37
+ puts "\nPer-Controller Usage:"
38
+ @controller_usages.each do |ctrl, files|
39
+ puts "\n [#{ctrl}] (#{files.size} shared references)"
40
+ files.each { |f| puts " #{f}" }
41
+ puts " (none)" if files.empty?
42
+ end
43
+
44
+ unless @warnings.empty?
45
+ puts "\nWarnings (#{@warnings.size}):"
46
+ @warnings.each { |w| puts " ⚠ #{w}" }
47
+ end
48
+
49
+ puts "\n"
50
+ end
51
+ end
52
+ end