react_on_rails 17.2.1.rc.0 → 17.2.1.rc.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: edfd94f0a53c71af11853ebd7d941792638456c4825ad4d81c5f9654a2aceab5
4
- data.tar.gz: 9f6795ffd2ff8fdb3af046f2bb5978bd1a773e860a67e4c6fd852ef582f29830
3
+ metadata.gz: 873030bfb660d497c78ad7f6b1d829afc2ea47f0e9af5eef17d32ecfcbb8eab0
4
+ data.tar.gz: 06fb62c87edb38282098cff52958084a52337589924caabe98e38ff9ee840180
5
5
  SHA512:
6
- metadata.gz: c093dc91758e7b2b7877a5ddcee62633bb69fc04ab85cc77157e9a4e2140eda3fa1b3da05954c59b17a7f855601d71a72fb596e8658a7c029804b854b90ee3e5
7
- data.tar.gz: beee84a4d4b7bcbd92eabfcee2595057683847c02e088420ad31d9e26639a3149ca1eba9c9c44629cb1c835102b5042218b6c125667aa86e4e70868b05e82e1f
6
+ metadata.gz: 07302dd91bb841ea7cd9e0f8ea66670c79bebb3bee67556a88836b6d37e8343a6718c419db6828f32981156ec7c7928cbaa1d20fe66ce527183b449329fc0083
7
+ data.tar.gz: 40a186a2d6db7331315ec50a85c3cf5a6a7e61fe1da14cecb60d6b4e6b05ce4c13955de07043c38e9d10a6c11fcca6ee090db812efd1a3581e0c6961860703ae
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (17.2.1.rc.0)
4
+ react_on_rails (17.2.1.rc.2)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -156,10 +156,15 @@ module ReactOnRails
156
156
  # this to later minors just because those releases are current on npm; React's RSC runtime and
157
157
  # bundler integration can change between minors. Move it together with RSC_PACKAGE_VERSION_PIN.
158
158
  #
159
+ # The floor is 19.3.1, stricter than the ^19.3.0 that react-on-rails-rsc 19.3.1 accepts: React's
160
+ # 2026-10-14 RSC advisories (GHSA-xqc2-xf4c-x3h3, GHSA-h3gv-c5rc-p453) affect
161
+ # react-server-dom-webpack 19.3.0, and the patched react-server-dom-webpack 19.3.1 peers on React
162
+ # ^19.3.1. A ~19.3.0 range would let a lockfile keep the vulnerable set.
163
+ #
159
164
  # This is intentionally distinct from RSC_PACKAGE_VERSION_PIN below, which pins
160
- # `react-on-rails-rsc`. Coordination note for #3609: Pro package metadata and generated apps
161
- # use the tested React range with the exact stable RSC package pin.
162
- RSC_REACT_VERSION_RANGE = "~19.3.0"
165
+ # `react-on-rails-rsc`. Coordination note for #3609: generated apps use this React range with
166
+ # the exact stable RSC package pin; the Pro packages' dev pins track the tested React release.
167
+ RSC_REACT_VERSION_RANGE = "~19.3.1"
163
168
  # Pinned to the stable 19.3.1 package, the current React 19.3 line. Apps generated by earlier
164
169
  # versions keep their own pins (19.2.x with React 19.2.7+, or 19.3.0 with React 19.2.8+), which
165
170
  # the React on Rails Pro 17 runtime check still supports.
@@ -42,6 +42,16 @@ module ReactOnRails
42
42
  RSC_SUPPORTED_REACT_MINOR = RSC_MINIMUM_REACT_VERSION_TUPLE.fetch(1)
43
43
  RSC_MINIMUM_REACT_PATCH = RSC_MINIMUM_REACT_VERSION_TUPLE.fetch(2)
44
44
  RSC_SUPPORTED_REACT_LINE = "#{RSC_SUPPORTED_REACT_MAJOR}.#{RSC_SUPPORTED_REACT_MINOR}.x".freeze
45
+ # Historical fact, not the generator floor: React's 2026-10-14 advisories patched
46
+ # react-server-dom-webpack at 19.3.1 on the 19.3 line (doctor's RSC_FLIGHT_PATCHED_VERSIONS).
47
+ # Never bump it with the floor; drop the advisory sentence once the supported line leaves 19.3.
48
+ RSC_ADVISORY_PATCHED_REACT_VERSION = "19.3.1"
49
+ RSC_ADVISORY_REACT_LINE = RSC_ADVISORY_PATCHED_REACT_VERSION.sub(/\.\d+\z/, ".x").freeze
50
+ # Keep in sync with doctor's report_vulnerable_rsc_flight_versions fallback.
51
+ RSC_NESTED_FLIGHT_OVERRIDE_HINT = <<~HINT.strip
52
+ If a react-server-dom-webpack copy nested under react-on-rails-rsc stays behind,
53
+ pin it with npm `overrides`, pnpm `pnpm.overrides`, or Yarn `resolutions`.
54
+ HINT
45
55
  MAX_LAYOUT_NAME_ATTEMPTS = 99
46
56
 
47
57
  # Main entry point for RSC setup.
@@ -89,30 +99,42 @@ module ReactOnRails
89
99
  GeneratorMessages.add_warning(<<~MSG.strip)
90
100
  ⚠️ RSC requires React #{RSC_SUPPORTED_REACT_LINE} (detected: #{react_version})
91
101
 
92
- This generator installs react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, which requires
93
- React #{RSC_SUPPORTED_REACT_LINE} with patch >= #{RSC_MINIMUM_REACT_VERSION}. The React on Rails Pro node renderer refuses
94
- to start when React and react-on-rails-rsc are on different lines.
102
+ This generator installs react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, which requires React #{RSC_SUPPORTED_REACT_LINE}.
103
+ The React on Rails Pro node renderer refuses to start when React and react-on-rails-rsc are on
104
+ different lines. New apps get React #{RSC_MINIMUM_REACT_VERSION} or later. React's 2026-10-14 security
105
+ advisories affect react-server-dom-webpack below #{RSC_ADVISORY_PATCHED_REACT_VERSION} on the #{RSC_ADVISORY_REACT_LINE} line.
95
106
 
96
- To install a compatible React version:
97
- #{manual_add_packages_command(["react@#{RSC_REACT_VERSION_RANGE}", "react-dom@#{RSC_REACT_VERSION_RANGE}"])}
107
+ To install a compatible set:
108
+ #{manual_add_packages_command(rsc_flight_upgrade_packages(RSC_REACT_VERSION_RANGE))}
98
109
 
99
- Run `bundle exec rake react_on_rails:doctor` afterwards to confirm the pairing.
110
+ #{RSC_NESTED_FLIGHT_OVERRIDE_HINT}
111
+
112
+ Then commit the refreshed lockfile and run `bundle exec rake react_on_rails:doctor` to confirm the pairing.
100
113
  MSG
101
114
  elsif patch < RSC_MINIMUM_REACT_PATCH
102
115
  GeneratorMessages.add_warning(<<~MSG.strip)
103
116
  ⚠️ React #{react_version} is below the recommended minimum for RSC.
104
117
 
105
- Please upgrade to at least React #{RSC_MINIMUM_REACT_VERSION}:
106
- #{manual_add_packages_command(["react@#{RSC_MINIMUM_REACT_VERSION}", "react-dom@#{RSC_MINIMUM_REACT_VERSION}"])}
118
+ React's 2026-10-14 security advisories affect react-server-dom-webpack below #{RSC_ADVISORY_PATCHED_REACT_VERSION} on the
119
+ #{RSC_ADVISORY_REACT_LINE} line, and each patched react-server-dom-webpack requires the matching React release.
120
+ Upgrade all three together to #{RSC_MINIMUM_REACT_VERSION} so a retained lockfile does not keep a vulnerable copy:
121
+ #{manual_add_packages_command(rsc_flight_upgrade_packages(RSC_MINIMUM_REACT_VERSION))}
122
+
123
+ #{RSC_NESTED_FLIGHT_OVERRIDE_HINT}
107
124
 
108
- react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN} is coordinated with
109
- React/React DOM #{RSC_MINIMUM_REACT_VERSION}+ for the React on Rails Pro 17 RSC runtime.
125
+ Then commit the refreshed lockfile and run `bundle exec rake react_on_rails:doctor` to confirm the pairing.
110
126
  MSG
111
127
  end
112
128
  end
113
129
 
114
130
  private
115
131
 
132
+ # react-server-dom-webpack arrives through react-on-rails-rsc, so naming it upgrades the copy a
133
+ # retained lockfile would otherwise keep; the patched release shares React's version number.
134
+ def rsc_flight_upgrade_packages(version)
135
+ %w[react react-dom react-server-dom-webpack].map { |package| "#{package}@#{version}" }
136
+ end
137
+
116
138
  def add_rsc_config_to_pro_initializer
117
139
  initializer_path = "config/initializers/react_on_rails_pro.rb"
118
140
  full_path = File.join(destination_root, initializer_path)
@@ -136,9 +136,18 @@ const configureServer = () => {
136
136
 
137
137
 
138
138
  // Don't hash the server bundle b/c would conflict with the client manifest
139
- // And no need for the MiniCssExtractPlugin
139
+ // And no need for the CSS extraction plugin (MiniCssExtractPlugin or CssExtractRspackPlugin)
140
+ // Rspack's plugins are matched by class because rspack-manifest-plugin names its
141
+ // class WebpackManifestPlugin (exported as RspackManifestPlugin).
142
+ const { RspackManifestPlugin } = config.assets_bundler === 'rspack'
143
+ ? require('rspack-manifest-plugin')
144
+ : {};
145
+ const isRspackClientOnlyPlugin = (plugin) =>
146
+ (RspackManifestPlugin && plugin instanceof RspackManifestPlugin) ||
147
+ (bundler.CssExtractRspackPlugin && plugin instanceof bundler.CssExtractRspackPlugin);
140
148
  serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter(
141
149
  (plugin) =>
150
+ !isRspackClientOnlyPlugin(plugin) &&
142
151
  plugin.constructor.name !== 'WebpackAssetsManifest' &&
143
152
  plugin.constructor.name !== 'MiniCssExtractPlugin' &&
144
153
  plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin',
@@ -21,6 +21,7 @@ require_relative "system_checker"
21
21
  require_relative "doctor_schema"
22
22
  require_relative "pro_migration"
23
23
  require_relative "node_renderer_procfile"
24
+ require_relative "version_checker/lockfile_resolution"
24
25
  require_relative "../generators/react_on_rails/js_dependency_manager"
25
26
 
26
27
  begin
@@ -5084,6 +5085,19 @@ module ReactOnRails
5084
5085
  min_patch.positive? ? "#{line} starting at #{RSC_SUPPORTED_PACKAGE_MAJOR}.#{minor}.#{min_patch}" : line
5085
5086
  end.join(" or ")
5086
5087
  RSC_PACKAGE_INSTALL_VERSION = ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN
5088
+ # React's 2026-10-14 RSC advisories (critical RCE GHSA-xqc2-xf4c-x3h3, DoS GHSA-h3gv-c5rc-p453)
5089
+ # affect react-server-dom-webpack 19.0.0 through 19.3.0. Each entry is the first patched release
5090
+ # on its minor line. This is a security floor, separate from the React pairing in
5091
+ # RSC_REACT_SUPPORT_RANGES, so it has no node-renderer counterpart to keep in sync.
5092
+ RSC_FLIGHT_PACKAGE_NAME = "react-server-dom-webpack"
5093
+ RSC_FLIGHT_ADVISORIES = "GHSA-xqc2-xf4c-x3h3 (critical remote code execution) and " \
5094
+ "GHSA-h3gv-c5rc-p453 (denial of service)"
5095
+ RSC_FLIGHT_PATCHED_VERSIONS = {
5096
+ [19, 0] => "19.0.9",
5097
+ [19, 1] => "19.1.10",
5098
+ [19, 2] => "19.2.9",
5099
+ [19, 3] => "19.3.1"
5100
+ }.freeze
5087
5101
  # Rejected prerelease lines with a known published stable successor: the 19.3.0-rc.4 pin that
5088
5102
  # React on Rails 17.1.0 generated, and the 19.3.1-rc.x soak that 17.2.0.rc.0 and rc.1 admitted.
5089
5103
  # Mirrors prereleaseLinesWithStableSuccessor in rscPeerSupport.ts.
@@ -5143,7 +5157,7 @@ module ReactOnRails
5143
5157
  check_rsc_bundler_config
5144
5158
  check_rsc_rspack_version
5145
5159
  check_rsc_rspack_lazy_compilation
5146
- check_rsc_react_version
5160
+ check_rsc_package_versions
5147
5161
  check_rsc_procfile_watcher
5148
5162
  check_rsc_client_manifest
5149
5163
  check_rsc_artifacts
@@ -5334,6 +5348,99 @@ module ReactOnRails
5334
5348
  check_legacy_rsc_react_version(react_version)
5335
5349
  end
5336
5350
 
5351
+ def check_rsc_package_versions
5352
+ check_rsc_react_version
5353
+ check_rsc_flight_advisories
5354
+ end
5355
+
5356
+ # Reads every react-server-dom-webpack copy the lockfile resolves, not node_modules, so the
5357
+ # warning matches what a fresh install or deploy would get from the committed lockfile.
5358
+ def check_rsc_flight_advisories
5359
+ package_root = resolved_package_root
5360
+ return if package_root_missing?(package_root)
5361
+
5362
+ versions = ReactOnRails::VersionChecker::LockfileResolution.all_versions(package_root, RSC_FLIGHT_PACKAGE_NAME)
5363
+ if versions.empty?
5364
+ report_unchecked_rsc_flight_advisories(package_root)
5365
+ return
5366
+ end
5367
+
5368
+ by_status = versions.group_by { |version| rsc_flight_version_status(version) }
5369
+ report_vulnerable_rsc_flight_versions(by_status[:vulnerable]) if by_status[:vulnerable]
5370
+ report_unjudged_rsc_flight_versions(by_status[:unknown]) if by_status[:unknown]
5371
+ return unless by_status.keys == [:patched]
5372
+
5373
+ verb = versions.size == 1 ? "is" : "are"
5374
+ checker.add_success("✅ #{RSC_FLIGHT_PACKAGE_NAME} #{versions.join(', ')} #{verb} not affected by " \
5375
+ "#{RSC_FLIGHT_ADVISORIES}")
5376
+ end
5377
+
5378
+ # Copies rsc_flight_version_status cannot judge (see there) get this instead of a success.
5379
+ def report_unjudged_rsc_flight_versions(versions)
5380
+ checker.add_warning(<<~MSG.strip)
5381
+ ⚠️ Could not tell whether #{RSC_FLIGHT_PACKAGE_NAME} #{versions.join(', ')} includes the fixes for React's RSC
5382
+ security advisories #{RSC_FLIGHT_ADVISORIES}.
5383
+
5384
+ The advisories cover 19.0.0 through 19.3.0, fixed in #{RSC_FLIGHT_PATCHED_VERSIONS.values.join(', ')}.
5385
+ Prefer a stable patched release, or confirm this build was published after 2026-10-14.
5386
+ MSG
5387
+ end
5388
+
5389
+ def report_vulnerable_rsc_flight_versions(vulnerable)
5390
+ # React on Rails Pro 17 RSC supports the 19.2 and 19.3 lines, so older lines move to 19.3.
5391
+ on_19_2_line = vulnerable.map { |version| npm_version_tuple(version)[1] == 2 }.uniq
5392
+ target_text = case on_19_2_line
5393
+ when [true] then RSC_FLIGHT_PATCHED_VERSIONS.fetch([19, 2])
5394
+ when [false] then RSC_FLIGHT_PATCHED_VERSIONS.fetch([19, 3])
5395
+ else "#{RSC_FLIGHT_PATCHED_VERSIONS.fetch([19, 3])} " \
5396
+ "(or #{RSC_FLIGHT_PATCHED_VERSIONS.fetch([19, 2])} for copies on the 19.2 line)"
5397
+ end
5398
+ checker.add_warning(<<~MSG.strip)
5399
+ ⚠️ Your lockfile resolves #{RSC_FLIGHT_PACKAGE_NAME} #{vulnerable.join(', ')}, which React's RSC security
5400
+ advisories #{RSC_FLIGHT_ADVISORIES} affect.
5401
+
5402
+ Fix: update react, react-dom, and #{RSC_FLIGHT_PACKAGE_NAME} together to #{target_text}
5403
+ (each patched #{RSC_FLIGHT_PACKAGE_NAME} requires the matching React release), then commit the
5404
+ refreshed lockfile. If a copy nested under #{RSC_PACKAGE_NAME} stays behind, pin it with npm
5405
+ `overrides`, pnpm `pnpm.overrides`, or Yarn `resolutions`.
5406
+ Confirm with `npm ls #{RSC_FLIGHT_PACKAGE_NAME}`, `pnpm why #{RSC_FLIGHT_PACKAGE_NAME}`,
5407
+ or `yarn why #{RSC_FLIGHT_PACKAGE_NAME}`.
5408
+ MSG
5409
+ end
5410
+
5411
+ # RSC always installs react-server-dom-webpack, so finding no copy means the check did not run:
5412
+ # the lockfile is missing, unreadable, or at a workspace root above the package root.
5413
+ def report_unchecked_rsc_flight_advisories(package_root)
5414
+ checker.add_warning(<<~MSG.strip)
5415
+ ⚠️ Could not check #{RSC_FLIGHT_PACKAGE_NAME} against React's RSC security advisories
5416
+ #{RSC_FLIGHT_ADVISORIES}.
5417
+
5418
+ No readable yarn.lock, package-lock.json, pnpm-lock.yaml, or bun.lock in #{package_root} lists it.
5419
+ If your lockfile lives at a workspace root, confirm every copy is #{RSC_FLIGHT_PATCHED_VERSIONS.values.join(', ')}
5420
+ or later on its line with `npm ls #{RSC_FLIGHT_PACKAGE_NAME}`, `pnpm why #{RSC_FLIGHT_PACKAGE_NAME}`,
5421
+ or `yarn why #{RSC_FLIGHT_PACKAGE_NAME}`.
5422
+ MSG
5423
+ end
5424
+
5425
+ # :vulnerable below its line's patched release, whatever the prerelease tag: even a canary built
5426
+ # after the fix is answered by moving to the stable patched release. :patched for a stable release
5427
+ # at or above it or on a later line. :unknown for what the advisories give no rule for: a
5428
+ # prerelease above the patched release, experimental builds, earlier lines, unparseable versions.
5429
+ def rsc_flight_version_status(version)
5430
+ return :unknown unless Gem::Version.correct?(version)
5431
+
5432
+ major, minor, = npm_version_tuple(version)
5433
+ patched = RSC_FLIGHT_PATCHED_VERSIONS[[major, minor]]
5434
+ stable = npm_prerelease(version).blank?
5435
+ if patched
5436
+ return :vulnerable if Gem::Version.new(version) < Gem::Version.new(patched)
5437
+
5438
+ return stable ? :patched : :unknown
5439
+ end
5440
+
5441
+ stable && ([major, minor] <=> RSC_FLIGHT_PATCHED_VERSIONS.keys.max).positive? ? :patched : :unknown
5442
+ end
5443
+
5337
5444
  def check_legacy_rsc_react_version(react_version)
5338
5445
  if supported_rsc_react_version?(react_version)
5339
5446
  checker.add_success("✅ React #{react_version} is compatible with RSC")
@@ -23,6 +23,7 @@ module ReactOnRails
23
23
  include ReactOnRails::FontHelper
24
24
 
25
25
  COMPONENT_HTML_KEY = "componentHtml"
26
+ CSP_NONCE_OVERRIDE_FIBER_KEY = :react_on_rails_csp_nonce_override
26
27
  IMMEDIATE_HYDRATION_WARNING_MUTEX = Mutex.new
27
28
 
28
29
  class << self
@@ -371,8 +372,6 @@ module ReactOnRails
371
372
  end
372
373
  end
373
374
 
374
- add_csp_nonce_to_context(result)
375
-
376
375
  if defined?(request) && request.present?
377
376
  # Check for encoding of the request's original_url and try to force-encoding the
378
377
  # URLs as UTF-8. This situation can occur in browsers that do not encode the
@@ -408,6 +407,10 @@ module ReactOnRails
408
407
  end
409
408
 
410
409
  context = @rails_context.merge(serverSide: server_side)
410
+ # Added per call rather than memoized: a component rendered for the Pro component
411
+ # cache sees a placeholder nonce (see csp_nonce), and that must not leak into the
412
+ # context later renders of the same request use.
413
+ add_csp_nonce_to_context(context)
411
414
  add_rsc_stream_observability_to_context(context)
412
415
  context
413
416
  end
@@ -722,10 +725,21 @@ module ReactOnRails
722
725
  )
723
726
  end
724
727
 
728
+ # Returns the CSP script nonce that framework-emitted scripts carry. React on Rails Pro
729
+ # sets CSP_NONCE_OVERRIDE_FIBER_KEY while it renders a component whose HTML it will
730
+ # cache, so every framework script in that HTML carries a random placeholder only the
731
+ # framework knows. Pro then swaps the placeholder for the serving request's nonce on
732
+ # every response, which leaves app markup carrying the request nonce unpromoted
733
+ # (issue #5267). Fiber storage scopes the override to that render and the async tasks
734
+ # it starts, so concurrent renders of other components keep the request nonce.
735
+ def csp_nonce
736
+ Fiber[CSP_NONCE_OVERRIDE_FIBER_KEY] || request_csp_nonce
737
+ end
738
+
725
739
  # Returns the CSP script nonce for the current request, or nil if CSP is not enabled.
726
740
  # Rails 5.2-6.0 use content_security_policy_nonce with no arguments.
727
741
  # Rails 6.1+ accept an optional directive argument.
728
- def csp_nonce
742
+ def request_csp_nonce
729
743
  return unless respond_to?(:content_security_policy_nonce)
730
744
 
731
745
  begin
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "17.2.1.rc.0"
4
+ VERSION = "17.2.1.rc.2"
5
5
  end
@@ -35,6 +35,20 @@ module ReactOnRails
35
35
  nil
36
36
  end
37
37
 
38
+ # Every version of +package_name+ that the lockfiles in +dir+ resolve, transitive copies
39
+ # included. .version answers "what did package.json's direct dependency resolve to"; this
40
+ # answers "which copies get installed", so a security check sees a vulnerable nested copy
41
+ # (react-server-dom-webpack arrives through react-on-rails-rsc, not package.json).
42
+ # Unreadable lockfiles contribute nothing. Returns a unique Array of version Strings.
43
+ def self.all_versions(dir, package_name)
44
+ ALL_VERSION_READERS.flat_map do |filename, reader|
45
+ path = File.join(dir, filename)
46
+ next [] unless File.exist?(path)
47
+
48
+ guard { reader.call(path, package_name) } || []
49
+ end.uniq
50
+ end
51
+
38
52
  # Yarn Berry (yarn 2+) yarn.lock content, dispatched from version_from_yarn_lock when the
39
53
  # __metadata marker is present. Returns a version String or nil.
40
54
  def self.berry_yarn_version(content, package_name)
@@ -77,6 +91,43 @@ module ReactOnRails
77
91
  end
78
92
  end
79
93
 
94
+ # Keys under packages/snapshots name every installed copy:
95
+ # 9.0: "name@1.2.3" or "name@1.2.3(peer@4.5.6)"; 6.0: "/name@1.2.3(...)"; 5.x: "/name/1.2.3_peer@4.5.6"
96
+ # 5.x and 6.0 from a non-default registry: "registry.example.com/name/1.2.3",
97
+ # "localhost+4873/name/1.2.3", or "registry.example.com/name@1.2.3"
98
+ # Tarball, git, and file: copies are returned as their whole key, so callers report them as unknown
99
+ # instead of skipping them or misreading a tarball name such as "19.3.1.tgz" as a version. 9.0 keys
100
+ # them as "name@https://...tgz(peer@4.5.6)"; 5.x and 6.0 key them by source ("github.com/org/repo/sha",
101
+ # "file:vendor/pkg.tgz") and record the package only in the entry's name.
102
+ PLAIN_VERSION = /\A\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/
103
+
104
+ def self.all_versions(path, package_name)
105
+ doc = project_document(path)
106
+ return [] unless doc
107
+
108
+ key_pattern = %r{\A/?(?:[^/@]+/)?#{Regexp.escape(package_name)}[@/](.+)\z}
109
+ versions = []
110
+ %w[packages snapshots].each do |section|
111
+ entries = doc[section]
112
+ next unless entries.is_a?(Hash)
113
+
114
+ entries.each do |key, entry|
115
+ version = copy_version(key.to_s, entry, key_pattern, package_name)
116
+ versions << version if version
117
+ end
118
+ end
119
+ versions.uniq
120
+ end
121
+
122
+ def self.copy_version(key, entry, key_pattern, package_name)
123
+ rest = key[key_pattern, 1]
124
+ resolution = key[0, key.index("(") || key.length]
125
+ return (resolution if entry.is_a?(Hash) && entry["name"] == package_name) unless rest
126
+
127
+ version = strip_peer_suffix(rest)
128
+ version&.match?(PLAIN_VERSION) ? version : resolution
129
+ end
130
+
80
131
  def self.dependencies(doc)
81
132
  return doc.dig("importers", ".", "dependencies") if doc.key?("importers")
82
133
 
@@ -119,6 +170,19 @@ module ReactOnRails
119
170
  resolution.rpartition("@").last
120
171
  end
121
172
 
173
+ # packages is keyed by install path ("name", "parent/name"); entry[0] is "name@version".
174
+ def self.all_versions(path, package_name)
175
+ packages = JSON.parse(jsonc_to_json(File.read(path)))["packages"]
176
+ return [] unless packages.is_a?(Hash)
177
+
178
+ packages.values.filter_map do |entry|
179
+ resolution = entry.is_a?(Array) ? entry[0] : nil
180
+ next unless resolution.is_a?(String) && resolution.start_with?("#{package_name}@")
181
+
182
+ resolution.delete_prefix("#{package_name}@")
183
+ end
184
+ end
185
+
122
186
  # One complete JSON string literal (escape-aware), a // line comment, a /* */ block
123
187
  # comment, and a trailing comma directly before } or ].
124
188
  STRING_LITERAL = /"(?:[^"\\]|\\.)*"/
@@ -157,14 +221,99 @@ module ReactOnRails
157
221
  # can merge several selectors into one key. Matching "#{name}@" cannot collide with
158
222
  # longer names ("react-on-rails-pro@..." does not start with "react-on-rails@").
159
223
  doc.each do |key, entry|
160
- next if key == "__metadata"
161
- next unless key.to_s.split(",").any? { |selector| selector.strip.start_with?("#{package_name}@") }
224
+ next unless entry_for?(key, package_name)
162
225
 
163
226
  return entry["version"]&.to_s if entry.is_a?(Hash)
164
227
  end
165
228
  nil
166
229
  end
230
+
231
+ def self.all_versions(content, package_name)
232
+ doc = YAML.safe_load(content, permitted_classes: [Date, Time])
233
+ return [] unless doc.is_a?(Hash)
234
+
235
+ # An alias ("flight@npm:react-server-dom-webpack@19.3.0") is keyed by the alias name, but
236
+ # its resolution still names the real package.
237
+ doc.filter_map do |key, entry|
238
+ next unless entry.is_a?(Hash)
239
+ next unless entry_for?(key, package_name) || entry["resolution"].to_s.start_with?("#{package_name}@")
240
+
241
+ entry["version"]&.to_s
242
+ end
243
+ end
244
+
245
+ def self.entry_for?(key, package_name)
246
+ return false if key == "__metadata"
247
+
248
+ key.to_s.split(",").any? { |selector| selector.strip.start_with?("#{package_name}@") }
249
+ end
250
+ end
251
+
252
+ # Yarn classic yarn.lock: one block per resolved copy, keyed by its selectors
253
+ # ("name@^1.0.0", "name@~1.0.2":) with an indented version "1.0.3" line. An npm alias
254
+ # selector ("flight@npm:name@19.3.0") names the real package after "npm:".
255
+ module ClassicYarnLockfile
256
+ def self.all_versions(path, package_name)
257
+ content = File.read(path)
258
+ return BerryYarnLockfile.all_versions(content, package_name) if content[0, 1024].include?("__metadata:")
259
+
260
+ selector = /\A"?(?:[^\s"]+@npm:)?#{Regexp.escape(package_name)}@/
261
+ versions = []
262
+ in_block = false
263
+ content.each_line do |line|
264
+ if line.match?(/\A\S/)
265
+ in_block = line.match?(selector)
266
+ elsif in_block && (match = line.match(/\A\s+version\s+"?([^"\s]+)"?/))
267
+ versions << match[1]
268
+ in_block = false
269
+ end
270
+ end
271
+ versions
272
+ end
167
273
  end
274
+
275
+ # package-lock.json v2/v3 keys every copy by install path ("node_modules/name",
276
+ # "node_modules/parent/node_modules/name"); an npm alias installs under the alias path and
277
+ # records the real package in "name". v1 nests copies under dependencies, and records an
278
+ # alias's real package in its version ("flight": { "version": "npm:name@1.2.3" }).
279
+ module NpmLockfile
280
+ def self.all_versions(path, package_name)
281
+ doc = JSON.parse(File.read(path))
282
+ return [] unless doc.is_a?(Hash)
283
+
284
+ install_path = %r{(?:\A|/)node_modules/#{Regexp.escape(package_name)}\z}
285
+ from_packages = (doc["packages"].is_a?(Hash) ? doc["packages"] : {}).filter_map do |key, entry|
286
+ next unless entry.is_a?(Hash)
287
+
288
+ entry["version"] if key.match?(install_path) || (key != "" && entry["name"] == package_name)
289
+ end
290
+ from_packages + v1_versions(doc["dependencies"], package_name)
291
+ end
292
+
293
+ def self.v1_versions(dependencies, package_name)
294
+ return [] unless dependencies.is_a?(Hash)
295
+
296
+ alias_prefix = "npm:#{package_name}@"
297
+ dependencies.flat_map do |name, entry|
298
+ next [] unless entry.is_a?(Hash)
299
+
300
+ version = entry["version"].to_s
301
+ own = if version.start_with?(alias_prefix) then [version.delete_prefix(alias_prefix)]
302
+ elsif name == package_name && !version.empty? then [version]
303
+ else
304
+ []
305
+ end
306
+ own + v1_versions(entry["dependencies"], package_name)
307
+ end
308
+ end
309
+ end
310
+
311
+ ALL_VERSION_READERS = {
312
+ "yarn.lock" => ClassicYarnLockfile.method(:all_versions),
313
+ "package-lock.json" => NpmLockfile.method(:all_versions),
314
+ "pnpm-lock.yaml" => PnpmLockfile.method(:all_versions),
315
+ "bun.lock" => BunLockfile.method(:all_versions)
316
+ }.freeze
168
317
  end
169
318
  end
170
319
  end
@@ -1,13 +1,20 @@
1
1
  module ReactOnRails
2
2
  class VersionChecker
3
3
  module LockfileResolution
4
+ ALL_VERSION_READERS: Hash[String, Method]
5
+
4
6
  def self.version: (String dir, String package_name) -> String?
7
+ def self.all_versions: (String dir, String package_name) -> Array[String]
5
8
  def self.berry_yarn_version: (String content, String package_name) -> String?
6
9
  def self.guard: [T] () { () -> T } -> T?
7
10
  def self.log_ignored: (Exception error) -> void
8
11
 
9
12
  module PnpmLockfile
13
+ PLAIN_VERSION: Regexp
14
+
10
15
  def self.version: (String path, String package_name) -> String?
16
+ def self.all_versions: (String path, String package_name) -> Array[String]
17
+ def self.copy_version: (String key, untyped entry, Regexp key_pattern, String package_name) -> String?
11
18
  def self.dependencies: (Hash[String, untyped] doc) -> Hash[String, untyped]?
12
19
  def self.project_document: (String path) -> Hash[String, untyped]?
13
20
  def self.strip_peer_suffix: (String? version) -> String?
@@ -20,11 +27,23 @@ module ReactOnRails
20
27
  TRAILING_COMMA: Regexp
21
28
 
22
29
  def self.version: (String path, String package_name) -> String?
30
+ def self.all_versions: (String path, String package_name) -> Array[String]
23
31
  def self.jsonc_to_json: (String content) -> String
24
32
  end
25
33
 
26
34
  module BerryYarnLockfile
27
35
  def self.version: (String content, String package_name) -> String?
36
+ def self.all_versions: (String content, String package_name) -> Array[String]
37
+ def self.entry_for?: (untyped key, String package_name) -> bool
38
+ end
39
+
40
+ module ClassicYarnLockfile
41
+ def self.all_versions: (String path, String package_name) -> Array[String]
42
+ end
43
+
44
+ module NpmLockfile
45
+ def self.all_versions: (String path, String package_name) -> Array[String]
46
+ def self.v1_versions: (untyped dependencies, String package_name) -> Array[String]
28
47
  end
29
48
  end
30
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 17.2.1.rc.0
4
+ version: 17.2.1.rc.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon