react-manifest-rails 0.1.0 → 0.2.1
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/README.md +110 -1
- data/lib/react_manifest/application_analyzer.rb +33 -33
- data/lib/react_manifest/application_migrator.rb +10 -6
- data/lib/react_manifest/configuration.rb +22 -0
- data/lib/react_manifest/dependency_map.rb +21 -5
- data/lib/react_manifest/generator.rb +40 -34
- data/lib/react_manifest/railtie.rb +2 -2
- data/lib/react_manifest/reporter.rb +5 -5
- data/lib/react_manifest/scanner.rb +62 -46
- data/lib/react_manifest/tree_classifier.rb +17 -4
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest/watcher.rb +3 -3
- data/lib/react_manifest.rb +1 -4
- data/tasks/react_manifest.rake +3 -3
- metadata +44 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ba6746290fd4cd708ac90748ef648429b162b84a7d63a36ddb74ae79f12d5a
|
|
4
|
+
data.tar.gz: 5d88bc95ea7f7500183bd5850936366414f39b1c8d85b153c1247b1640160eca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e581629322110b25163179454d47ad2c507a8df3481125561d33b67fa5158d5e85d8cbfb67bf970af2c94e38f84b9c163e7034fd8b28a8b6e22ece8edd25840
|
|
7
|
+
data.tar.gz: e8b77df28997e14466d541458f051ca77aadbb0d0adff7dc3160fbbd8dd2c4180d5eb5b5924f0b7586f92f16bb161e86247d3f244593f17b123e5d997836ec6a
|
data/README.md
CHANGED
|
@@ -228,9 +228,118 @@ rails react_manifest:report
|
|
|
228
228
|
- Consider splitting large bundles into smaller, more focused ones
|
|
229
229
|
- Adjust `config.size_threshold_kb` in your initializer if needed
|
|
230
230
|
|
|
231
|
+
## TypeScript / Custom Extensions
|
|
232
|
+
|
|
233
|
+
By default the gem scans `*.js` and `*.jsx` files. To add TypeScript support:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
ReactManifest.configure do |config|
|
|
237
|
+
config.extensions = %w[js jsx ts tsx]
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
This affects file scanning, manifest generation, and the development file watcher.
|
|
242
|
+
|
|
243
|
+
## File Watching (`listen` gem)
|
|
244
|
+
|
|
245
|
+
The development file watcher requires the `listen` gem, which is **not** a hard dependency. If `listen` is absent the watcher is silently disabled and you must regenerate manifests manually.
|
|
246
|
+
|
|
247
|
+
To enable watching, add `listen` to the development group in your app's `Gemfile`:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
gem "listen", "~> 3.0", group: :development
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> **Note:** Changes to `config/initializers/react_manifest.rb` are not picked up by the watcher — you must restart the server after editing the initializer.
|
|
254
|
+
|
|
255
|
+
## Migrating an Existing `application.js`
|
|
256
|
+
|
|
257
|
+
If you are adding this gem to an app that already has a monolithic `application.js`, use the built-in migration tools:
|
|
258
|
+
|
|
259
|
+
**Step 1 — Analyse what's there:**
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
rails react_manifest:analyze
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
This classifies each `//= require` line as `:vendor` (keep), `:ux_code` (remove), or `:unknown` (review manually). No files are modified.
|
|
266
|
+
|
|
267
|
+
**Step 2 — Preview changes:**
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
REACT_MANIFEST_DRY_RUN=1 rails react_manifest:migrate_application
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Step 3 — Apply changes:**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
rails react_manifest:migrate_application
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A `.bak` backup is created next to each modified file before any write.
|
|
280
|
+
|
|
281
|
+
**Step 4 — Generate bundles and verify:**
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
rails react_manifest:generate
|
|
285
|
+
rails assets:precompile # or just start the dev server
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Rolling back:** Simply restore the `.bak` file and remove the generated `ux_*.js` manifests.
|
|
289
|
+
|
|
290
|
+
## Troubleshooting
|
|
291
|
+
|
|
292
|
+
### Bundle not being generated
|
|
293
|
+
- Verify components live under `ux/app/<controller>/` (not directly under `ux/`)
|
|
294
|
+
- Run `rails react_manifest:report` to see detected bundles
|
|
295
|
+
- Check Rails logs for file watcher errors
|
|
296
|
+
|
|
297
|
+
### Components not loading
|
|
298
|
+
- Confirm `react_bundle_tag` is present in your layout
|
|
299
|
+
- Check that the bundle name matches your controller path
|
|
300
|
+
- Run `rails react_manifest:generate` to force regeneration
|
|
301
|
+
|
|
302
|
+
### Bundle appears more than once in HTML output
|
|
303
|
+
- A bundle name in `config.always_include` that is also the controller bundle will be deduplicated automatically. Check whether both the shared bundle name and a specific bundle are listed in `always_include`.
|
|
304
|
+
|
|
305
|
+
### Watcher stops responding
|
|
306
|
+
- This is usually resolved by restarting the development server
|
|
307
|
+
- Ensure the `listen` gem is installed (see above)
|
|
308
|
+
|
|
309
|
+
### Generator crashes on first run
|
|
310
|
+
- Check that the process has write permission to `config.output_dir`
|
|
311
|
+
- Verify `config.ux_root` exists; the generator is a no-op if the directory is missing
|
|
312
|
+
|
|
313
|
+
### Size warnings
|
|
314
|
+
- The gem warns when bundles exceed `config.size_threshold_kb` (default 500 KB)
|
|
315
|
+
- Consider splitting large controller directories or moving shared code into the shared bundle
|
|
316
|
+
- Adjust the threshold: `config.size_threshold_kb = 1000`
|
|
317
|
+
|
|
318
|
+
## Performance
|
|
319
|
+
|
|
320
|
+
| Scenario | Typical time |
|
|
321
|
+
|----------|-------------|
|
|
322
|
+
| Initial generation (small project, ~20 controllers) | < 1 s |
|
|
323
|
+
| Initial generation (large project, ~100 controllers) | 2–5 s |
|
|
324
|
+
| Watcher debounce (regeneration after file change) | ~300 ms |
|
|
325
|
+
| Memory overhead (symbol index, large project) | < 10 MB |
|
|
326
|
+
|
|
327
|
+
Scanning is purely regex-based — no Node.js or transpilation step required.
|
|
328
|
+
|
|
329
|
+
## Compatibility
|
|
330
|
+
|
|
331
|
+
| Dependency | Supported versions |
|
|
332
|
+
|------------|-------------------|
|
|
333
|
+
| Ruby | 3.2, 3.3 |
|
|
334
|
+
| Rails / Railties | 6.1 – 8.x |
|
|
335
|
+
| Sprockets | 3.x, 4.x |
|
|
336
|
+
| listen (optional) | ~> 3.0 |
|
|
337
|
+
|
|
338
|
+
> **Pre-1.0 notice:** The public API (configuration keys, rake task names, view helper signature) may change in minor versions until 1.0 is released. Pin to a patch version if stability is critical: `gem "react-manifest-rails", "~> 0.1.0"`.
|
|
339
|
+
|
|
231
340
|
## Requirements
|
|
232
341
|
|
|
233
|
-
- Ruby >= 2
|
|
342
|
+
- Ruby >= 3.2
|
|
234
343
|
- Rails >= 6.1
|
|
235
344
|
- Sprockets
|
|
236
345
|
- react-rails
|
|
@@ -6,26 +6,34 @@ module ReactManifest
|
|
|
6
6
|
#
|
|
7
7
|
# Produces a human-readable report without writing anything.
|
|
8
8
|
class ApplicationAnalyzer
|
|
9
|
-
DIRECTIVE_PATTERN =
|
|
9
|
+
DIRECTIVE_PATTERN = %r{^\s*//=\s+(require(?:_tree|_directory)?)\s+(.+)$}
|
|
10
10
|
|
|
11
11
|
# Libs we recognise as vendor (case-insensitive partial match on the require path)
|
|
12
12
|
VENDOR_HINTS = %w[
|
|
13
13
|
react react-dom react_dom reactdom
|
|
14
14
|
mui material-ui
|
|
15
|
-
redux redux-thunk
|
|
15
|
+
redux redux-thunk redux-saga redux-toolkit
|
|
16
16
|
axios lodash underscore
|
|
17
17
|
jquery backbone handlebars
|
|
18
18
|
turbo stimulus
|
|
19
19
|
vendor
|
|
20
|
+
bootstrap tailwind tailwindcss
|
|
21
|
+
moment date-fns dayjs
|
|
22
|
+
formik react-hook-form
|
|
23
|
+
recharts chartjs chart.js
|
|
24
|
+
framer-motion
|
|
25
|
+
i18next
|
|
26
|
+
classnames clsx
|
|
27
|
+
uuid
|
|
20
28
|
].freeze
|
|
21
29
|
|
|
22
30
|
ClassifiedDirective = Struct.new(:original_line, :directive, :path, :classification, :note, keyword_init: true)
|
|
23
31
|
|
|
24
32
|
Result = Struct.new(:file, :directives, keyword_init: true) do
|
|
25
|
-
def vendor_lines
|
|
26
|
-
def ux_code_lines
|
|
27
|
-
def unknown_lines
|
|
28
|
-
def clean
|
|
33
|
+
def vendor_lines = directives.select { |d| d.classification == :vendor }
|
|
34
|
+
def ux_code_lines = directives.select { |d| d.classification == :ux_code }
|
|
35
|
+
def unknown_lines = directives.select { |d| d.classification == :unknown }
|
|
36
|
+
def clean? = ux_code_lines.empty? && unknown_lines.empty?
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def initialize(config = ReactManifest.configuration)
|
|
@@ -47,17 +55,14 @@ module ReactManifest
|
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
results.each do |result|
|
|
50
|
-
rel = result.file.sub(Rails.root
|
|
58
|
+
rel = result.file.sub("#{Rails.root}/", "")
|
|
51
59
|
status = result.clean? ? "✓ already clean" : "⚠ needs migration"
|
|
52
60
|
puts "\n#{rel} [#{status}]"
|
|
53
61
|
puts "-" * 60
|
|
54
62
|
|
|
63
|
+
icon_map = { vendor: " ✓ KEEP ", ux_code: " ✗ REMOVE ", unknown: " ? REVIEW " }
|
|
55
64
|
result.directives.each do |d|
|
|
56
|
-
icon =
|
|
57
|
-
when :vendor then " ✓ KEEP "
|
|
58
|
-
when :ux_code then " ✗ REMOVE "
|
|
59
|
-
when :unknown then " ? REVIEW "
|
|
60
|
-
end
|
|
65
|
+
icon = icon_map[d.classification]
|
|
61
66
|
puts "#{icon} #{d.original_line.strip}"
|
|
62
67
|
puts " → #{d.note}" if d.note
|
|
63
68
|
end
|
|
@@ -86,26 +91,26 @@ module ReactManifest
|
|
|
86
91
|
unless match
|
|
87
92
|
# Non-directive lines (comments, blank) — pass through as :vendor (keep)
|
|
88
93
|
directives << ClassifiedDirective.new(
|
|
89
|
-
original_line:
|
|
90
|
-
directive:
|
|
91
|
-
path:
|
|
94
|
+
original_line: raw,
|
|
95
|
+
directive: nil,
|
|
96
|
+
path: nil,
|
|
92
97
|
classification: :passthrough,
|
|
93
|
-
note:
|
|
98
|
+
note: nil
|
|
94
99
|
)
|
|
95
100
|
next
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
directive = match[1]
|
|
103
|
+
directive = match[1] # require, require_tree, require_directory
|
|
99
104
|
path = match[2].strip
|
|
100
105
|
|
|
101
106
|
classification, note = classify_directive(directive, path)
|
|
102
107
|
|
|
103
108
|
directives << ClassifiedDirective.new(
|
|
104
|
-
original_line:
|
|
105
|
-
directive:
|
|
106
|
-
path:
|
|
109
|
+
original_line: raw,
|
|
110
|
+
directive: directive,
|
|
111
|
+
path: path,
|
|
107
112
|
classification: classification,
|
|
108
|
-
note:
|
|
113
|
+
note: note
|
|
109
114
|
)
|
|
110
115
|
end
|
|
111
116
|
|
|
@@ -115,27 +120,22 @@ module ReactManifest
|
|
|
115
120
|
def classify_directive(directive, path)
|
|
116
121
|
# require_tree is almost always too greedy
|
|
117
122
|
if directive.include?("tree") || directive.include?("directory")
|
|
118
|
-
if path_is_ux?(path)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
+
return [:ux_code, "require_tree over ux/ — will be replaced by ux_*.js bundles"] if path_is_ux?(path)
|
|
124
|
+
|
|
125
|
+
return [:unknown, "require_tree/require_directory — review manually: #{path}"]
|
|
126
|
+
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
# Explicit require
|
|
126
|
-
if path_is_ux?(path)
|
|
127
|
-
return [:ux_code, "ux/ code — will be served by ux_*.js bundles"]
|
|
128
|
-
end
|
|
130
|
+
return [:ux_code, "ux/ code — will be served by ux_*.js bundles"] if path_is_ux?(path)
|
|
129
131
|
|
|
130
|
-
if path_is_vendor?(path)
|
|
131
|
-
return [:vendor, nil]
|
|
132
|
-
end
|
|
132
|
+
return [:vendor, nil] if path_is_vendor?(path)
|
|
133
133
|
|
|
134
134
|
[:unknown, "Could not auto-classify — review manually"]
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def path_is_ux?(path)
|
|
138
|
-
ux_prefix = @config.ux_root.split("/").last
|
|
138
|
+
ux_prefix = @config.ux_root.split("/").last # e.g. "ux"
|
|
139
139
|
path.include?(ux_prefix) ||
|
|
140
140
|
path.start_with?("./ux") ||
|
|
141
141
|
path.start_with?("ux/")
|
|
@@ -8,7 +8,7 @@ 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
|
-
MANAGED_COMMENT = <<~JS
|
|
11
|
+
MANAGED_COMMENT = <<~JS.freeze
|
|
12
12
|
// Vendor libraries — loaded on every page.
|
|
13
13
|
// React app code is now served per-controller via react_bundle_tag.
|
|
14
14
|
// Managed by react-manifest-rails — do not add require_tree.
|
|
@@ -54,8 +54,9 @@ module ReactManifest
|
|
|
54
54
|
bak_path = "#{file}.bak"
|
|
55
55
|
begin
|
|
56
56
|
FileUtils.cp(file, bak_path)
|
|
57
|
+
File.chmod(0o600, bak_path)
|
|
57
58
|
$stdout.puts "[ReactManifest] Backup: #{short(bak_path)}"
|
|
58
|
-
rescue => e
|
|
59
|
+
rescue StandardError => e
|
|
59
60
|
$stdout.puts "[ReactManifest] ERROR: Could not create backup of #{short(file)}: #{e.message}"
|
|
60
61
|
$stdout.puts "[ReactManifest] Migration aborted for #{short(file)} — original file unchanged."
|
|
61
62
|
return { file: file, status: :backup_failed, error: e.message }
|
|
@@ -69,8 +70,11 @@ module ReactManifest
|
|
|
69
70
|
|
|
70
71
|
def build_new_content(result)
|
|
71
72
|
kept_lines = result.directives
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
.select do |d|
|
|
74
|
+
%i[vendor
|
|
75
|
+
passthrough].include?(d.classification)
|
|
76
|
+
end
|
|
77
|
+
.map(&:original_line)
|
|
74
78
|
|
|
75
79
|
# Remove leading blank lines from kept_lines
|
|
76
80
|
kept_lines.shift while kept_lines.first&.strip&.empty?
|
|
@@ -78,7 +82,7 @@ module ReactManifest
|
|
|
78
82
|
lines = []
|
|
79
83
|
lines << MANAGED_COMMENT
|
|
80
84
|
lines += kept_lines
|
|
81
|
-
lines << ""
|
|
85
|
+
lines << "" # trailing newline
|
|
82
86
|
|
|
83
87
|
lines.join("\n")
|
|
84
88
|
end
|
|
@@ -95,7 +99,7 @@ module ReactManifest
|
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def short(path)
|
|
98
|
-
path.to_s.sub(Rails.root
|
|
102
|
+
path.to_s.sub("#{Rails.root}/", "")
|
|
99
103
|
end
|
|
100
104
|
end
|
|
101
105
|
end
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
module ReactManifest
|
|
2
|
+
# Holds all configuration for the gem. Obtain via {ReactManifest.configure}.
|
|
3
|
+
#
|
|
4
|
+
# @example
|
|
5
|
+
# ReactManifest.configure do |c|
|
|
6
|
+
# c.ux_root = "app/assets/javascripts/ux"
|
|
7
|
+
# c.extensions = %w[js jsx ts tsx]
|
|
8
|
+
# c.size_threshold_kb = 1000
|
|
9
|
+
# end
|
|
2
10
|
class Configuration
|
|
3
11
|
# Root of the ux/ tree to scan (relative to Rails.root)
|
|
4
12
|
attr_accessor :ux_root
|
|
@@ -24,6 +32,9 @@ module ReactManifest
|
|
|
24
32
|
# Warn if a bundle exceeds this size in KB (0 = disabled)
|
|
25
33
|
attr_accessor :size_threshold_kb
|
|
26
34
|
|
|
35
|
+
# File extensions to scan (default: js and jsx; add "ts", "tsx" for TypeScript)
|
|
36
|
+
attr_accessor :extensions
|
|
37
|
+
|
|
27
38
|
# Print what would change, write nothing
|
|
28
39
|
attr_accessor :dry_run
|
|
29
40
|
|
|
@@ -39,6 +50,7 @@ module ReactManifest
|
|
|
39
50
|
@ignore = []
|
|
40
51
|
@exclude_paths = %w[react react_dev vendor]
|
|
41
52
|
@size_threshold_kb = 500
|
|
53
|
+
@extensions = %w[js jsx]
|
|
42
54
|
@dry_run = false
|
|
43
55
|
@verbose = false
|
|
44
56
|
end
|
|
@@ -51,6 +63,16 @@ module ReactManifest
|
|
|
51
63
|
!!@verbose
|
|
52
64
|
end
|
|
53
65
|
|
|
66
|
+
# Glob fragment used by Dir.glob, e.g. "*.{js,jsx}" or "*.{js,jsx,ts,tsx}"
|
|
67
|
+
def extensions_glob
|
|
68
|
+
"*.{#{extensions.join(',')}}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Regexp used by the file watcher to filter events, e.g. /\.(js|jsx)$/
|
|
72
|
+
def extensions_pattern
|
|
73
|
+
Regexp.new("\\.(#{extensions.map { |e| Regexp.escape(e) }.join('|')})$")
|
|
74
|
+
end
|
|
75
|
+
|
|
54
76
|
# Absolute path helpers (requires Rails.root to be set)
|
|
55
77
|
def abs_ux_root
|
|
56
78
|
Rails.root.join(ux_root).to_s
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module ReactManifest
|
|
2
|
-
# Wraps
|
|
3
|
-
#
|
|
2
|
+
# Wraps {Scanner} results into a queryable dependency map.
|
|
3
|
+
#
|
|
4
|
+
# Used by +react_manifest:analyze+ rake task and {Reporter} for diagnostics.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# result = ReactManifest::Scanner.new.scan(classifier.classify)
|
|
8
|
+
# map = ReactManifest::DependencyMap.new(result)
|
|
9
|
+
# map.shared_files_for("users") # => ["ux/components/...", ...]
|
|
10
|
+
# map.controllers_using("ux/lib/api_helpers") # => ["users", "admin"]
|
|
4
11
|
class DependencyMap
|
|
5
12
|
attr_reader :symbol_index, :controller_usages, :warnings
|
|
6
13
|
|
|
@@ -8,6 +15,12 @@ module ReactManifest
|
|
|
8
15
|
@symbol_index = scan_result.symbol_index
|
|
9
16
|
@controller_usages = scan_result.controller_usages
|
|
10
17
|
@warnings = scan_result.warnings
|
|
18
|
+
|
|
19
|
+
# Inverted index: shared_file → [controllers] for O(1) lookup
|
|
20
|
+
@controllers_by_file = {}
|
|
21
|
+
@controller_usages.each do |ctrl, files|
|
|
22
|
+
files.each { |f| (@controllers_by_file[f] ||= []) << ctrl }
|
|
23
|
+
end
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
# All shared files used by the given controller
|
|
@@ -15,9 +28,9 @@ module ReactManifest
|
|
|
15
28
|
@controller_usages.fetch(controller_name, [])
|
|
16
29
|
end
|
|
17
30
|
|
|
18
|
-
# Which controllers use a given shared file
|
|
31
|
+
# Which controllers use a given shared file (O(1) via inverted index)
|
|
19
32
|
def controllers_using(shared_file)
|
|
20
|
-
@
|
|
33
|
+
@controllers_by_file.fetch(shared_file, [])
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
# Symbols defined in shared dirs
|
|
@@ -31,7 +44,10 @@ module ReactManifest
|
|
|
31
44
|
|
|
32
45
|
puts "Shared Symbol Index (#{@symbol_index.size} symbols):"
|
|
33
46
|
@symbol_index.each do |sym, file|
|
|
34
|
-
|
|
47
|
+
# Strip non-printable/control characters to prevent terminal manipulation
|
|
48
|
+
safe_sym = sym.gsub(/[^\x20-\x7E]/, "?")
|
|
49
|
+
safe_file = file.gsub(/[^\x20-\x7E]/, "?")
|
|
50
|
+
puts " #{safe_sym.ljust(40)} #{safe_file}"
|
|
35
51
|
end
|
|
36
52
|
|
|
37
53
|
puts "\nPer-Controller Usage:"
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
require "digest"
|
|
2
|
-
require "set"
|
|
3
2
|
require "time"
|
|
4
3
|
require "tmpdir"
|
|
5
4
|
|
|
6
5
|
module ReactManifest
|
|
7
6
|
# Generates all ux_*.js Sprockets manifest files.
|
|
8
7
|
#
|
|
8
|
+
# Instantiate with a {Configuration} and call {#run!}:
|
|
9
|
+
#
|
|
10
|
+
# ReactManifest::Generator.new(ReactManifest.configuration).run!
|
|
11
|
+
#
|
|
12
|
+
# Returns an array of result hashes:
|
|
13
|
+
# [{path: "/abs/path/ux_shared.js", status: :written}, ...]
|
|
14
|
+
#
|
|
15
|
+
# Possible +status+ values: +:written+, +:unchanged+, +:skipped_pinned+, +:dry_run+.
|
|
16
|
+
#
|
|
9
17
|
# Generates:
|
|
10
18
|
# ux_shared.js — requires all files from shared dirs (components/, hooks/, lib/, etc.)
|
|
11
19
|
# ux_<ctrl>.js — one per controller subdir, requires ux_shared + controller files
|
|
@@ -16,7 +24,7 @@ module ReactManifest
|
|
|
16
24
|
#
|
|
17
25
|
# Never touches application.js, application_dev.js, or files in exclude_paths.
|
|
18
26
|
class Generator
|
|
19
|
-
HEADER = <<~JS
|
|
27
|
+
HEADER = <<~JS.freeze
|
|
20
28
|
// AUTO-GENERATED — DO NOT EDIT
|
|
21
29
|
// react-manifest-rails %<version>s | %<timestamp>s
|
|
22
30
|
// Run `rails react_manifest:generate` to regenerate.
|
|
@@ -28,17 +36,20 @@ module ReactManifest
|
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
# Run full generation. Returns array of {path:, status:} hashes.
|
|
39
|
+
#
|
|
40
|
+
# All manifest content is built first (no filesystem writes), then written
|
|
41
|
+
# in a second pass so that a failure midway does not leave some bundles
|
|
42
|
+
# written and others stale/missing.
|
|
31
43
|
def run!
|
|
32
|
-
results = []
|
|
33
44
|
classification = @classifier.classify
|
|
34
45
|
|
|
35
|
-
# 1
|
|
36
|
-
|
|
46
|
+
# Phase 1: build all content in memory — no I/O.
|
|
47
|
+
manifests = []
|
|
48
|
+
manifests << build_shared(classification.shared_dirs)
|
|
49
|
+
classification.controller_dirs.each { |ctrl| manifests << build_controller(ctrl) }
|
|
37
50
|
|
|
38
|
-
# 2
|
|
39
|
-
|
|
40
|
-
results << generate_controller(ctrl)
|
|
41
|
-
end
|
|
51
|
+
# Phase 2: write — each write is atomic (tmp + rename).
|
|
52
|
+
results = manifests.map { |m| write_manifest(m[:filename], m[:content]) }
|
|
42
53
|
|
|
43
54
|
print_summary(results) if @config.verbose?
|
|
44
55
|
results
|
|
@@ -48,7 +59,7 @@ module ReactManifest
|
|
|
48
59
|
|
|
49
60
|
# ------------------------------------------------------------------ shared
|
|
50
61
|
|
|
51
|
-
def
|
|
62
|
+
def build_shared(shared_dirs)
|
|
52
63
|
lines = header_lines
|
|
53
64
|
any_files = false
|
|
54
65
|
|
|
@@ -60,17 +71,14 @@ module ReactManifest
|
|
|
60
71
|
files.each { |f| lines << "//= require #{relative_require_path(f)}" }
|
|
61
72
|
end
|
|
62
73
|
|
|
63
|
-
unless any_files
|
|
64
|
-
lines << "// (no shared files found)"
|
|
65
|
-
end
|
|
74
|
+
lines << "// (no shared files found)" unless any_files
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
write_manifest("#{bundle_name}.js", lines.join("\n") + "\n")
|
|
76
|
+
{ filename: "#{@config.shared_bundle}.js", content: "#{lines.join("\n")}\n" }
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
# --------------------------------------------------------------- controller
|
|
72
80
|
|
|
73
|
-
def
|
|
81
|
+
def build_controller(ctrl)
|
|
74
82
|
lines = header_lines
|
|
75
83
|
lines << "//= require #{@config.shared_bundle}"
|
|
76
84
|
lines << ""
|
|
@@ -82,7 +90,7 @@ module ReactManifest
|
|
|
82
90
|
files.each { |f| lines << "//= require #{relative_require_path(f)}" }
|
|
83
91
|
end
|
|
84
92
|
|
|
85
|
-
|
|
93
|
+
{ filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
# --------------------------------------------------------------- write
|
|
@@ -92,17 +100,13 @@ module ReactManifest
|
|
|
92
100
|
|
|
93
101
|
# Safety: never touch files not bearing our AUTO-GENERATED header
|
|
94
102
|
# (unless they don't exist yet)
|
|
95
|
-
if File.exist?(dest) && !auto_generated?(dest)
|
|
96
|
-
return { path: dest, status: :skipped_pinned }
|
|
97
|
-
end
|
|
103
|
+
return { path: dest, status: :skipped_pinned } if File.exist?(dest) && !auto_generated?(dest)
|
|
98
104
|
|
|
99
105
|
new_digest = Digest::SHA256.hexdigest(content)
|
|
100
106
|
|
|
101
107
|
if File.exist?(dest)
|
|
102
108
|
existing_digest = Digest::SHA256.hexdigest(File.read(dest, encoding: "utf-8"))
|
|
103
|
-
if existing_digest == new_digest
|
|
104
|
-
return { path: dest, status: :unchanged }
|
|
105
|
-
end
|
|
109
|
+
return { path: dest, status: :unchanged } if existing_digest == new_digest
|
|
106
110
|
end
|
|
107
111
|
|
|
108
112
|
if @config.dry_run?
|
|
@@ -119,8 +123,8 @@ module ReactManifest
|
|
|
119
123
|
begin
|
|
120
124
|
File.write(tmp, content, encoding: "utf-8")
|
|
121
125
|
File.rename(tmp, dest)
|
|
122
|
-
rescue => e
|
|
123
|
-
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
FileUtils.rm_f(tmp)
|
|
124
128
|
raise e
|
|
125
129
|
end
|
|
126
130
|
|
|
@@ -131,7 +135,7 @@ module ReactManifest
|
|
|
131
135
|
|
|
132
136
|
def header_lines
|
|
133
137
|
[
|
|
134
|
-
HEADER
|
|
138
|
+
format(HEADER, version: ReactManifest::VERSION, timestamp: Time.now.utc.iso8601),
|
|
135
139
|
""
|
|
136
140
|
].flatten
|
|
137
141
|
end
|
|
@@ -139,11 +143,11 @@ module ReactManifest
|
|
|
139
143
|
def js_files_in(dir)
|
|
140
144
|
return [] unless Dir.exist?(dir)
|
|
141
145
|
|
|
142
|
-
files = Dir.glob(File.join(dir, "**",
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
files = Dir.glob(File.join(dir, "**", @config.extensions_glob))
|
|
147
|
+
.reject { |f| File.directory?(f) }
|
|
148
|
+
.reject { |f| auto_generated?(f) }
|
|
149
|
+
.reject { |f| excluded_path?(f) }
|
|
150
|
+
.sort
|
|
147
151
|
|
|
148
152
|
# Deduplicate by logical require path: if both foo.js and foo.jsx exist,
|
|
149
153
|
# keep foo.js (sorted first) to avoid duplicate //= require directives
|
|
@@ -152,6 +156,7 @@ module ReactManifest
|
|
|
152
156
|
files.each_with_object([]) do |f, uniq|
|
|
153
157
|
logical = relative_require_path(f)
|
|
154
158
|
next if seen.include?(logical)
|
|
159
|
+
|
|
155
160
|
seen << logical
|
|
156
161
|
uniq << f
|
|
157
162
|
end
|
|
@@ -174,11 +179,12 @@ module ReactManifest
|
|
|
174
179
|
end
|
|
175
180
|
|
|
176
181
|
def auto_generated?(path)
|
|
177
|
-
|
|
178
|
-
#
|
|
179
|
-
# without incorrectly treating them as user-pinned.
|
|
182
|
+
# Avoid TOCTOU: don't check existence separately — just attempt the read
|
|
183
|
+
# and treat a missing/unreadable file as not auto-generated.
|
|
180
184
|
first_two = File.foreach(path).first(2).join
|
|
181
185
|
first_two.include?("AUTO-GENERATED")
|
|
186
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
187
|
+
false
|
|
182
188
|
end
|
|
183
189
|
|
|
184
190
|
def print_diff(dest, new_content)
|
|
@@ -12,7 +12,7 @@ module ReactManifest
|
|
|
12
12
|
if Rails.env.development? && !ReactManifest::Watcher.running?
|
|
13
13
|
begin
|
|
14
14
|
ReactManifest::Watcher.start(ReactManifest.configuration)
|
|
15
|
-
rescue => e
|
|
15
|
+
rescue StandardError => e
|
|
16
16
|
Rails.logger.warn "[ReactManifest] Could not start file watcher: #{e.message}"
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -36,7 +36,7 @@ module ReactManifest
|
|
|
36
36
|
# (which is subject to parallel task ordering under rake -j).
|
|
37
37
|
# ----------------------------------------------------------------
|
|
38
38
|
rake_tasks do
|
|
39
|
-
load File.expand_path("
|
|
39
|
+
load File.expand_path("../../tasks/react_manifest.rake", __dir__)
|
|
40
40
|
|
|
41
41
|
if Rake::Task.task_defined?("assets:precompile")
|
|
42
42
|
Rake::Task["assets:precompile"].enhance(["react_manifest:generate"])
|
|
@@ -63,8 +63,8 @@ module ReactManifest
|
|
|
63
63
|
gzip_kb = File.exist?(gz_path) ? (File.size(gz_path) / 1024.0).round(1) : nil
|
|
64
64
|
|
|
65
65
|
bundles << {
|
|
66
|
-
name:
|
|
67
|
-
raw_kb:
|
|
66
|
+
name: logical_path,
|
|
67
|
+
raw_kb: raw_kb,
|
|
68
68
|
gzip_kb: gzip_kb
|
|
69
69
|
}
|
|
70
70
|
end
|
|
@@ -75,13 +75,13 @@ module ReactManifest
|
|
|
75
75
|
def print_table(bundles)
|
|
76
76
|
gzip_available = bundles.any? { |b| b[:gzip_kb] }
|
|
77
77
|
|
|
78
|
-
puts "\n#{
|
|
78
|
+
puts "\n#{'Bundle'.ljust(35)} #{'Raw (KB)'.rjust(10)}#{" #{'Gzip (KB)'.rjust(10)}" if gzip_available}"
|
|
79
79
|
puts "-" * (gzip_available ? 62 : 48)
|
|
80
80
|
|
|
81
81
|
bundles.each do |b|
|
|
82
|
-
over_threshold = @config.size_threshold_kb
|
|
82
|
+
over_threshold = @config.size_threshold_kb.positive? && b[:raw_kb] > @config.size_threshold_kb
|
|
83
83
|
flag = over_threshold ? " ⚠ exceeds #{@config.size_threshold_kb}KB threshold" : ""
|
|
84
|
-
gzip_col = gzip_available ? " #{(b[:gzip_kb] ||
|
|
84
|
+
gzip_col = gzip_available ? " #{(b[:gzip_kb] || 'n/a').to_s.rjust(10)}" : ""
|
|
85
85
|
|
|
86
86
|
puts "#{b[:name].ljust(35)} #{b[:raw_kb].to_s.rjust(10)}#{gzip_col}#{flag}"
|
|
87
87
|
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
require "set"
|
|
2
|
-
|
|
3
1
|
module ReactManifest
|
|
4
|
-
# Scans JSX/
|
|
2
|
+
# Scans JS/JSX (and optionally TS/TSX) files using regex — no AST, no Node.js required.
|
|
3
|
+
#
|
|
4
|
+
# Returns a {Result} containing:
|
|
5
|
+
# - +symbol_index+ — map of exported symbol name → shared require path
|
|
6
|
+
# - +controller_usages+ — map of controller name → sorted array of referenced shared files
|
|
7
|
+
# - +warnings+ — non-fatal issues found during scanning
|
|
5
8
|
#
|
|
6
9
|
# Phase 1 — builds a symbol index from shared dirs:
|
|
7
10
|
# "PrimaryButton" => "ux/components/buttons/primary_button"
|
|
@@ -13,23 +16,34 @@ module ReactManifest
|
|
|
13
16
|
#
|
|
14
17
|
# Phase 3 — emits non-fatal warnings.
|
|
15
18
|
class Scanner
|
|
16
|
-
# Patterns to detect symbol definitions (
|
|
19
|
+
# Patterns to detect symbol definitions (CommonJS and ES module style)
|
|
17
20
|
DEFINITION_PATTERNS = [
|
|
18
|
-
|
|
19
|
-
/
|
|
20
|
-
/
|
|
21
|
-
/
|
|
22
|
-
/
|
|
23
|
-
/
|
|
24
|
-
|
|
21
|
+
# CommonJS / variable-assignment style
|
|
22
|
+
/(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/, # const FooBar =
|
|
23
|
+
/function\s+([A-Z][A-Za-z0-9_]*)\s*\(/, # function FooBar(
|
|
24
|
+
/class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/, # class FooBar
|
|
25
|
+
/(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/, # const useFoo = (hooks)
|
|
26
|
+
/function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/, # function useFoo(
|
|
27
|
+
/(?:const|let|var)\s+([a-z][A-Za-z0-9_]{2,})\s*=\s*(?:function|\()/, # const formatDate = function/arrow
|
|
28
|
+
/^function\s+([a-z][A-Za-z0-9_]{2,})\s*\(/, # function formatDate( at line start
|
|
29
|
+
|
|
30
|
+
# ES module style (export default / named exports)
|
|
31
|
+
/^export\s+default\s+(?:function|class)\s+([A-Z][A-Za-z0-9_]*)/, # export default function Foo
|
|
32
|
+
/^export\s+default\s+(?:function|class)\s+(use[A-Z][A-Za-z0-9_]*)/, # export default function useFoo
|
|
33
|
+
/^export\s+(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/, # export const Foo =
|
|
34
|
+
/^export\s+(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/, # export const useFoo =
|
|
35
|
+
/^export\s+(?:const|let|var)\s+([a-z][A-Za-z0-9_]{2,})\s*=\s*(?:function|\()/, # export const formatDate =
|
|
36
|
+
/^export\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(/, # export function Foo(
|
|
37
|
+
/^export\s+function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/, # export function useFoo(
|
|
38
|
+
/^export\s+class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/ # export class Foo
|
|
25
39
|
].freeze
|
|
26
40
|
|
|
27
41
|
# Patterns to detect usage in controller files
|
|
28
|
-
JSX_ELEMENT_PATTERN =
|
|
29
|
-
REACT_CREATE_PATTERN = /React\.createElement\(\s*([A-Z][A-Za-z0-9_]*)[\s,)]
|
|
30
|
-
HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(
|
|
42
|
+
JSX_ELEMENT_PATTERN = %r{<([A-Z][A-Za-z0-9_]*)[\s/>]}
|
|
43
|
+
REACT_CREATE_PATTERN = /React\.createElement\(\s*([A-Z][A-Za-z0-9_]*)[\s,)]/
|
|
44
|
+
HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(/
|
|
31
45
|
# Lib calls matched against known lib symbols to reduce false positives
|
|
32
|
-
LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(
|
|
46
|
+
LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
|
|
33
47
|
|
|
34
48
|
# Common JS built-ins to exclude from lib-call matching
|
|
35
49
|
JS_BUILTINS = %w[
|
|
@@ -66,9 +80,7 @@ module ReactManifest
|
|
|
66
80
|
end
|
|
67
81
|
end
|
|
68
82
|
|
|
69
|
-
if @config.verbose?
|
|
70
|
-
$stdout.puts "[ReactManifest] Shared symbol index: #{symbol_index.size} symbols indexed"
|
|
71
|
-
end
|
|
83
|
+
$stdout.puts "[ReactManifest] Shared symbol index: #{symbol_index.size} symbols indexed" if @config.verbose?
|
|
72
84
|
|
|
73
85
|
# Phase 2: scan controller dirs for usage
|
|
74
86
|
controller_usages = {}
|
|
@@ -77,45 +89,44 @@ module ReactManifest
|
|
|
77
89
|
files = js_files_in(ctrl[:path])
|
|
78
90
|
used = Set.new
|
|
79
91
|
|
|
80
|
-
if files.empty? && @config.verbose?
|
|
81
|
-
warnings << "Controller dir '#{ctrl[:name]}' has no JS/JSX files"
|
|
82
|
-
end
|
|
92
|
+
warnings << "Controller dir '#{ctrl[:name]}' has no JS/JSX files" if files.empty? && @config.verbose?
|
|
83
93
|
|
|
84
94
|
files.each do |file_path|
|
|
85
95
|
validate_naming(file_path, ctrl[:name], warnings)
|
|
86
|
-
|
|
96
|
+
begin
|
|
97
|
+
content = File.read(file_path, encoding: "utf-8")
|
|
98
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
99
|
+
warnings << "Skipping #{file_path}: #{e.message}"
|
|
100
|
+
next
|
|
101
|
+
rescue Encoding::InvalidByteSequenceError
|
|
102
|
+
warnings << "Skipping #{file_path}: not valid UTF-8"
|
|
103
|
+
next
|
|
104
|
+
end
|
|
87
105
|
|
|
88
106
|
# JSX element usage: <PrimaryButton (JSX tag syntax)
|
|
89
107
|
content.scan(JSX_ELEMENT_PATTERN) do |match|
|
|
90
108
|
sym = match[0]
|
|
91
|
-
if symbol_index.key?(sym)
|
|
92
|
-
used << symbol_index[sym]
|
|
93
|
-
end
|
|
109
|
+
used << symbol_index[sym] if symbol_index.key?(sym)
|
|
94
110
|
end
|
|
95
111
|
|
|
96
112
|
# React.createElement(PrimaryButton, ...) (non-JSX style)
|
|
97
113
|
content.scan(REACT_CREATE_PATTERN) do |match|
|
|
98
114
|
sym = match[0]
|
|
99
|
-
if symbol_index.key?(sym)
|
|
100
|
-
used << symbol_index[sym]
|
|
101
|
-
end
|
|
115
|
+
used << symbol_index[sym] if symbol_index.key?(sym)
|
|
102
116
|
end
|
|
103
117
|
|
|
104
118
|
# Hook calls: useFetch(
|
|
105
119
|
content.scan(HOOK_CALL_PATTERN) do |match|
|
|
106
120
|
sym = match[0]
|
|
107
|
-
if symbol_index.key?(sym)
|
|
108
|
-
used << symbol_index[sym]
|
|
109
|
-
end
|
|
121
|
+
used << symbol_index[sym] if symbol_index.key?(sym)
|
|
110
122
|
end
|
|
111
123
|
|
|
112
124
|
# Lib function calls: formatDate( — filtered against lib symbol index
|
|
113
125
|
content.scan(LIB_CALL_PATTERN) do |match|
|
|
114
126
|
sym = match[0]
|
|
115
127
|
next if JS_BUILTINS.include?(sym)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
end
|
|
128
|
+
|
|
129
|
+
used << symbol_index[sym] if symbol_index.key?(sym)
|
|
119
130
|
end
|
|
120
131
|
end
|
|
121
132
|
|
|
@@ -126,9 +137,9 @@ module ReactManifest
|
|
|
126
137
|
emit_fanout_warnings(controller_usages, warnings)
|
|
127
138
|
|
|
128
139
|
Result.new(
|
|
129
|
-
symbol_index:
|
|
130
|
-
controller_usages:
|
|
131
|
-
warnings:
|
|
140
|
+
symbol_index: symbol_index,
|
|
141
|
+
controller_usages: controller_usages,
|
|
142
|
+
warnings: warnings
|
|
132
143
|
)
|
|
133
144
|
end
|
|
134
145
|
|
|
@@ -136,10 +147,11 @@ module ReactManifest
|
|
|
136
147
|
|
|
137
148
|
def js_files_in(dir)
|
|
138
149
|
return [] unless Dir.exist?(dir)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
|
|
151
|
+
Dir.glob(File.join(dir, "**", @config.extensions_glob))
|
|
152
|
+
.reject { |f| File.directory?(f) }
|
|
153
|
+
.reject { |f| excluded_path?(f) }
|
|
154
|
+
.sort
|
|
143
155
|
end
|
|
144
156
|
|
|
145
157
|
# Returns true if the file path contains a segment matching any exclude_path.
|
|
@@ -149,7 +161,11 @@ module ReactManifest
|
|
|
149
161
|
end
|
|
150
162
|
|
|
151
163
|
def extract_definitions(file_path)
|
|
152
|
-
|
|
164
|
+
begin
|
|
165
|
+
content = File.read(file_path, encoding: "utf-8")
|
|
166
|
+
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
167
|
+
return []
|
|
168
|
+
end
|
|
153
169
|
symbols = []
|
|
154
170
|
DEFINITION_PATTERNS.each do |pattern|
|
|
155
171
|
content.scan(pattern) { |m| symbols << m[0] }
|
|
@@ -168,10 +184,10 @@ module ReactManifest
|
|
|
168
184
|
def validate_naming(file_path, ctrl_name, warnings)
|
|
169
185
|
basename = File.basename(file_path, ".*").sub(/\.js$/, "")
|
|
170
186
|
# Expected: <controller>_index, <controller>_show, <controller>_form, etc.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
return if basename.start_with?("#{ctrl_name}_") || basename == ctrl_name
|
|
188
|
+
|
|
189
|
+
warnings << "File '#{File.basename(file_path)}' in '#{ctrl_name}' does not follow " \
|
|
190
|
+
"'#{ctrl_name}_<action>.js.jsx' naming convention"
|
|
175
191
|
end
|
|
176
192
|
|
|
177
193
|
def emit_fanout_warnings(controller_usages, warnings)
|
|
@@ -24,18 +24,18 @@ module ReactManifest
|
|
|
24
24
|
begin
|
|
25
25
|
Dir.children(@config.abs_ux_root).sort.each do |entry|
|
|
26
26
|
full_path = File.join(@config.abs_ux_root, entry)
|
|
27
|
-
next unless
|
|
27
|
+
next unless real_directory?(full_path)
|
|
28
28
|
|
|
29
29
|
if entry == @config.app_dir
|
|
30
30
|
begin
|
|
31
31
|
Dir.children(full_path).sort.each do |ctrl_entry|
|
|
32
32
|
ctrl_path = File.join(full_path, ctrl_entry)
|
|
33
|
-
next unless
|
|
33
|
+
next unless real_directory?(ctrl_path)
|
|
34
34
|
next if @config.ignore.include?(ctrl_entry)
|
|
35
35
|
|
|
36
36
|
controller_dirs << {
|
|
37
|
-
name:
|
|
38
|
-
path:
|
|
37
|
+
name: ctrl_entry,
|
|
38
|
+
path: ctrl_path,
|
|
39
39
|
bundle_name: "ux_#{ctrl_entry}"
|
|
40
40
|
}
|
|
41
41
|
end
|
|
@@ -57,10 +57,23 @@ module ReactManifest
|
|
|
57
57
|
Result.new(controller_dirs: controller_dirs, shared_dirs: shared_dirs)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Returns true for both plain directories and symlinks that point to directories.
|
|
63
|
+
# File.directory? returns false for symlinks on some systems.
|
|
64
|
+
def real_directory?(path)
|
|
65
|
+
File.directory?(File.realpath(path))
|
|
66
|
+
rescue Errno::ENOENT, Errno::ELOOP
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
public
|
|
71
|
+
|
|
60
72
|
# Watch ux_root recursively so newly added controller directories
|
|
61
73
|
# are automatically detected without restarting the development server.
|
|
62
74
|
def watched_dirs
|
|
63
75
|
return [] unless Dir.exist?(@config.abs_ux_root)
|
|
76
|
+
|
|
64
77
|
dirs = [@config.abs_ux_root]
|
|
65
78
|
dirs << @config.abs_app_dir if Dir.exist?(@config.abs_app_dir)
|
|
66
79
|
dirs
|
|
@@ -27,11 +27,11 @@ module ReactManifest
|
|
|
27
27
|
return
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
log "Watching #{root.sub(Rails.root
|
|
30
|
+
log "Watching #{root.sub("#{Rails.root}/", '')} for changes..."
|
|
31
31
|
|
|
32
32
|
@listener = Listen.to(
|
|
33
33
|
root,
|
|
34
|
-
only:
|
|
34
|
+
only: config.extensions_pattern,
|
|
35
35
|
latency: DEBOUNCE_SECONDS
|
|
36
36
|
) do |modified, added, removed|
|
|
37
37
|
changed = (modified + added + removed).map { |f| File.basename(f) }
|
|
@@ -56,7 +56,7 @@ module ReactManifest
|
|
|
56
56
|
def regenerate!(config)
|
|
57
57
|
Generator.new(config).run!
|
|
58
58
|
log "Manifests regenerated"
|
|
59
|
-
rescue => e
|
|
59
|
+
rescue StandardError => e
|
|
60
60
|
log "Error during regeneration: #{e.message}"
|
|
61
61
|
log e.backtrace.first(5).join("\n") if config.verbose?
|
|
62
62
|
end
|
data/lib/react_manifest.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require "set"
|
|
2
1
|
require "fileutils"
|
|
3
2
|
|
|
4
3
|
require "react_manifest/version"
|
|
@@ -35,9 +34,7 @@ module ReactManifest
|
|
|
35
34
|
bundles = []
|
|
36
35
|
|
|
37
36
|
# 1. Shared bundle always first
|
|
38
|
-
if bundle_exists?(output, config.shared_bundle)
|
|
39
|
-
bundles << config.shared_bundle
|
|
40
|
-
end
|
|
37
|
+
bundles << config.shared_bundle if bundle_exists?(output, config.shared_bundle)
|
|
41
38
|
|
|
42
39
|
# 2. always_include bundles (e.g. ux_main)
|
|
43
40
|
config.always_include.each do |b|
|
data/tasks/react_manifest.rake
CHANGED
|
@@ -15,7 +15,7 @@ namespace :react_manifest do
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Print any scanner warnings
|
|
18
|
-
results
|
|
18
|
+
results # warnings are printed inline by scanner via $stdout in verbose mode
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
desc "Print the JSX dependency map and warnings without writing any files"
|
|
@@ -65,10 +65,10 @@ namespace :react_manifest do
|
|
|
65
65
|
first_line = File.foreach(file).first.to_s
|
|
66
66
|
if first_line.include?("AUTO-GENERATED")
|
|
67
67
|
File.delete(file)
|
|
68
|
-
puts "[ReactManifest] Removed: #{file.sub(Rails.root
|
|
68
|
+
puts "[ReactManifest] Removed: #{file.sub("#{Rails.root}/", '')}"
|
|
69
69
|
removed += 1
|
|
70
70
|
else
|
|
71
|
-
puts "[ReactManifest] Skipped (not auto-generated): #{file.sub(Rails.root
|
|
71
|
+
puts "[ReactManifest] Skipped (not auto-generated): #{file.sub("#{Rails.root}/", '')}"
|
|
72
72
|
skipped += 1
|
|
73
73
|
end
|
|
74
74
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react-manifest-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -16,70 +16,82 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '7.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
29
|
+
version: '7.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
34
|
+
name: rails
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
30
36
|
requirements:
|
|
31
|
-
- - "
|
|
37
|
+
- - ">="
|
|
32
38
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
-
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '9'
|
|
43
|
+
type: :development
|
|
35
44
|
prerelease: false
|
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
46
|
requirements:
|
|
38
|
-
- - "
|
|
47
|
+
- - ">="
|
|
39
48
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
49
|
+
version: '7.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '9'
|
|
41
53
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
54
|
+
name: rake
|
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
|
44
56
|
requirements:
|
|
45
|
-
- - "
|
|
57
|
+
- - ">="
|
|
46
58
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
59
|
+
version: '0'
|
|
48
60
|
type: :development
|
|
49
61
|
prerelease: false
|
|
50
62
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
63
|
requirements:
|
|
52
|
-
- - "
|
|
64
|
+
- - ">="
|
|
53
65
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
66
|
+
version: '0'
|
|
55
67
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: rspec
|
|
68
|
+
name: rspec
|
|
57
69
|
requirement: !ruby/object:Gem::Requirement
|
|
58
70
|
requirements:
|
|
59
71
|
- - "~>"
|
|
60
72
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
73
|
+
version: '3.12'
|
|
62
74
|
type: :development
|
|
63
75
|
prerelease: false
|
|
64
76
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
77
|
requirements:
|
|
66
78
|
- - "~>"
|
|
67
79
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
80
|
+
version: '3.12'
|
|
69
81
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: rails
|
|
82
|
+
name: rspec-rails
|
|
71
83
|
requirement: !ruby/object:Gem::Requirement
|
|
72
84
|
requirements:
|
|
73
|
-
- - "
|
|
85
|
+
- - "~>"
|
|
74
86
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '6.
|
|
87
|
+
version: '6.0'
|
|
76
88
|
type: :development
|
|
77
89
|
prerelease: false
|
|
78
90
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
91
|
requirements:
|
|
80
|
-
- - "
|
|
92
|
+
- - "~>"
|
|
81
93
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '6.
|
|
94
|
+
version: '6.0'
|
|
83
95
|
- !ruby/object:Gem::Dependency
|
|
84
96
|
name: sprockets-rails
|
|
85
97
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,19 +107,19 @@ dependencies:
|
|
|
95
107
|
- !ruby/object:Gem::Version
|
|
96
108
|
version: '0'
|
|
97
109
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
110
|
+
name: listen
|
|
99
111
|
requirement: !ruby/object:Gem::Requirement
|
|
100
112
|
requirements:
|
|
101
|
-
- - "
|
|
113
|
+
- - "~>"
|
|
102
114
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0'
|
|
115
|
+
version: '3.0'
|
|
104
116
|
type: :development
|
|
105
117
|
prerelease: false
|
|
106
118
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
119
|
requirements:
|
|
108
|
-
- - "
|
|
120
|
+
- - "~>"
|
|
109
121
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0'
|
|
122
|
+
version: '3.0'
|
|
111
123
|
description: |
|
|
112
124
|
react-manifest-rails automatically generates per-controller Sprockets manifest
|
|
113
125
|
files for Rails applications using react-rails + Sprockets. It eliminates the
|
|
@@ -144,6 +156,8 @@ metadata:
|
|
|
144
156
|
homepage_uri: https://github.com/olivernoonan/react-manifest-rails
|
|
145
157
|
source_code_uri: https://github.com/olivernoonan/react-manifest-rails
|
|
146
158
|
changelog_uri: https://github.com/olivernoonan/react-manifest-rails/blob/main/CHANGELOG.md
|
|
159
|
+
bug_tracker_uri: https://github.com/olivernoonan/react-manifest-rails/issues
|
|
160
|
+
rubygems_mfa_required: 'true'
|
|
147
161
|
post_install_message:
|
|
148
162
|
rdoc_options: []
|
|
149
163
|
require_paths:
|
|
@@ -152,14 +166,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
152
166
|
requirements:
|
|
153
167
|
- - ">="
|
|
154
168
|
- !ruby/object:Gem::Version
|
|
155
|
-
version: 2.
|
|
169
|
+
version: 3.2.0
|
|
156
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
171
|
requirements:
|
|
158
172
|
- - ">="
|
|
159
173
|
- !ruby/object:Gem::Version
|
|
160
174
|
version: '0'
|
|
161
175
|
requirements: []
|
|
162
|
-
rubygems_version: 3.
|
|
176
|
+
rubygems_version: 3.5.22
|
|
163
177
|
signing_key:
|
|
164
178
|
specification_version: 4
|
|
165
179
|
summary: Zero-touch Sprockets manifest generation for react-rails apps
|