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
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
|