react-manifest-rails 0.1.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c76e23ad95cdf498e89dd0d63862c8d39b0acdf956df8dbf271a6157871e10f4
4
- data.tar.gz: 68bb10eb5f7900798f4775db3399e9252879bdeb53e786b9d29e016e9e45600b
3
+ metadata.gz: 36adf78621e6d6b409a6242cccfe34fc7ab916357771c81683c17ef58c80b9a7
4
+ data.tar.gz: 4a4f18f2c4b3749376bb2c3adae8c9be6a881f9df04411a49c903c01d917282f
5
5
  SHA512:
6
- metadata.gz: fdf7e42fb7ea57bfa28d818f53d387ac1dc6fbeb8015bf34a563fd095f1cd70edd966013a36996fe864f56d2fc3c21c97030b136f6b547d317a2555998484198
7
- data.tar.gz: 36055fab58f773760266ad9e706a52dfe71e333e0dbd3e61db234e65095cb54709b82b031071ca78576db54ed9afb042ab72a232496c24bb5defd058f6169f63
6
+ metadata.gz: 665b92d9ba95e189fbc7791ad3a620df282df1ac9d865ccdd481618f41d47f53d86cb013881f5beac312115b73a7f862933145980ba6b4d9cda872ec3cd3e938
7
+ data.tar.gz: 2be3a86848583a94dcf49cd0b1f637b20d9f88ed8f2ca5abafe357f50ae51589d74c14164024a46e4c0ef6b9f969baa0e76017ba79518b4d98d1ff8a72716e59
data/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] - 2026-04-15
9
+
10
+ ### Changed
11
+ - Lowered required Ruby version from >= 3.2.0 to >= 3.0.0
12
+
8
13
  ## [0.1.0] - 2026-04-13
9
14
 
10
15
  ### Added
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.6.0
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 = /^\s*\/\/=\s+(require(?:_tree|_directory)?)\s+(.+)$/.freeze
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; 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
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.to_s + "/", "")
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 = case d.classification
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: raw,
90
- directive: nil,
91
- path: nil,
94
+ original_line: raw,
95
+ directive: nil,
96
+ path: nil,
92
97
  classification: :passthrough,
93
- note: nil
98
+ note: nil
94
99
  )
95
100
  next
96
101
  end
97
102
 
98
- directive = match[1] # require, require_tree, require_directory
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: raw,
105
- directive: directive,
106
- path: path,
109
+ original_line: raw,
110
+ directive: directive,
111
+ path: path,
107
112
  classification: classification,
108
- note: 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
- 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
+ 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 # e.g. "ux"
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
- .select { |d| %i[vendor passthrough].include?(d.classification) }
73
- .map(&:original_line)
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 << "" # trailing newline
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.to_s + "/", "")
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 scanner results into a queryable dependency map.
3
- # Used by the analyze rake task and reporter for diagnostics.
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
- @controller_usages.select { |_, files| files.include?(shared_file) }.keys
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
- puts " #{sym.ljust(40)} #{file}"
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. Generate ux_shared.js
36
- results << generate_shared(classification.shared_dirs)
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. Generate one ux_<controller>.js per controller dir
39
- classification.controller_dirs.each do |ctrl|
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 generate_shared(shared_dirs)
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
- bundle_name = @config.shared_bundle
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 generate_controller(ctrl)
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
- write_manifest("#{ctrl[:bundle_name]}.js", lines.join("\n") + "\n")
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
- File.unlink(tmp) if File.exist?(tmp)
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 % { version: ReactManifest::VERSION, timestamp: Time.now.utc.iso8601 },
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, "**", "*.{js,jsx}"))
143
- .reject { |f| File.directory?(f) }
144
- .reject { |f| auto_generated?(f) }
145
- .reject { |f| excluded_path?(f) }
146
- .sort
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
- return false unless File.exist?(path)
178
- # Read up to first 2 lines handles empty files (first.to_s returns "")
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("../../../tasks/react_manifest.rake", __FILE__)
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: logical_path,
67
- raw_kb: 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#{"Bundle".ljust(35)} #{"Raw (KB)".rjust(10)}#{gzip_available ? " #{"Gzip (KB)".rjust(10)}" : ""}"
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 > 0 && b[:raw_kb] > @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] || "n/a").to_s.rjust(10)}" : ""
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/JS files using regex (no AST, no Node.js required).
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 (no import/export in this codebase)
19
+ # Patterns to detect symbol definitions (CommonJS and ES module style)
17
20
  DEFINITION_PATTERNS = [
18
- /(?:const|let|var)\s+([A-Z][A-Za-z0-9_]*)\s*=/, # const FooBar =
19
- /function\s+([A-Z][A-Za-z0-9_]*)\s*\(/, # function FooBar(
20
- /class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/, # class FooBar
21
- /(?:const|let|var)\s+(use[A-Z][A-Za-z0-9_]*)\s*=/, # const useFoo = (hooks)
22
- /function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/, # function useFoo(
23
- /(?:const|let|var)\s+([a-z][A-Za-z0-9_]{2,})\s*=\s*(?:function|\()/, # const formatDate = function/arrow
24
- /^function\s+([a-z][A-Za-z0-9_]{2,})\s*\(/, # function formatDate( at line start
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 = /<([A-Z][A-Za-z0-9_]*)[\s\/>]/.freeze
29
- REACT_CREATE_PATTERN = /React\.createElement\(\s*([A-Z][A-Za-z0-9_]*)[\s,)]/.freeze
30
- HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(/.freeze
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*\(/.freeze
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
- content = File.read(file_path, encoding: "utf-8")
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
- if symbol_index.key?(sym)
117
- used << symbol_index[sym]
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: symbol_index,
130
- controller_usages: controller_usages,
131
- warnings: 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
- Dir.glob(File.join(dir, "**", "*.{js,js.jsx}"))
140
- .reject { |f| File.directory?(f) }
141
- .reject { |f| excluded_path?(f) }
142
- .sort
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
- content = File.read(file_path, encoding: "utf-8")
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
- unless basename.start_with?("#{ctrl_name}_") || basename == ctrl_name
172
- warnings << "File '#{File.basename(file_path)}' in '#{ctrl_name}' does not follow " \
173
- "'#{ctrl_name}_<action>.js.jsx' naming convention"
174
- end
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 File.directory?(full_path)
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 File.directory?(ctrl_path)
33
+ next unless real_directory?(ctrl_path)
34
34
  next if @config.ignore.include?(ctrl_entry)
35
35
 
36
36
  controller_dirs << {
37
- name: ctrl_entry,
38
- path: ctrl_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
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.2".freeze
3
3
  end
@@ -27,11 +27,11 @@ module ReactManifest
27
27
  return
28
28
  end
29
29
 
30
- log "Watching #{root.sub(Rails.root.to_s + '/', '')} for changes..."
30
+ log "Watching #{root.sub("#{Rails.root}/", '')} for changes..."
31
31
 
32
32
  @listener = Listen.to(
33
33
  root,
34
- only: /\.(js|jsx)$/,
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
@@ -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|
@@ -15,7 +15,7 @@ namespace :react_manifest do
15
15
  end
16
16
 
17
17
  # Print any scanner warnings
18
- results # warnings are printed inline by scanner via $stdout in verbose mode
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.to_s + '/', '')}"
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.to_s + '/', '')}"
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.0
4
+ version: 0.2.2
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-13 00:00:00.000000000 Z
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: '6.1'
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: '6.1'
29
+ version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
27
33
  - !ruby/object:Gem::Dependency
28
- name: listen
34
+ name: rails
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - "~>"
37
+ - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: '3.0'
34
- type: :runtime
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: '3.0'
49
+ version: '7.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9'
41
53
  - !ruby/object:Gem::Dependency
42
- name: rspec
54
+ name: rake
43
55
  requirement: !ruby/object:Gem::Requirement
44
56
  requirements:
45
- - - "~>"
57
+ - - ">="
46
58
  - !ruby/object:Gem::Version
47
- version: '3.12'
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: '3.12'
66
+ version: '0'
55
67
  - !ruby/object:Gem::Dependency
56
- name: rspec-rails
68
+ name: rspec
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
71
  - - "~>"
60
72
  - !ruby/object:Gem::Version
61
- version: '6.0'
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: '6.0'
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.1'
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.1'
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: rake
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.6.0
169
+ version: 3.0.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.4.19
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