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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/generators/react_on_rails/js_dependency_manager.rb +8 -3
- data/lib/generators/react_on_rails/rsc_setup.rb +32 -10
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +10 -1
- data/lib/react_on_rails/doctor.rb +108 -1
- data/lib/react_on_rails/helper.rb +17 -3
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker/lockfile_resolution.rb +151 -2
- data/sig/react_on_rails/version_checker/lockfile_resolution.rbs +19 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 873030bfb660d497c78ad7f6b1d829afc2ea47f0e9af5eef17d32ecfcbb8eab0
|
|
4
|
+
data.tar.gz: 06fb62c87edb38282098cff52958084a52337589924caabe98e38ff9ee840180
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07302dd91bb841ea7cd9e0f8ea66670c79bebb3bee67556a88836b6d37e8343a6718c419db6828f32981156ec7c7928cbaa1d20fe66ce527183b449329fc0083
|
|
7
|
+
data.tar.gz: 40a186a2d6db7331315ec50a85c3cf5a6a7e61fe1da14cecb60d6b4e6b05ce4c13955de07043c38e9d10a6c11fcca6ee090db812efd1a3581e0c6961860703ae
|
data/Gemfile.lock
CHANGED
|
@@ -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:
|
|
161
|
-
#
|
|
162
|
-
RSC_REACT_VERSION_RANGE = "~19.3.
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
97
|
-
#{manual_add_packages_command(
|
|
107
|
+
To install a compatible set:
|
|
108
|
+
#{manual_add_packages_command(rsc_flight_upgrade_packages(RSC_REACT_VERSION_RANGE))}
|
|
98
109
|
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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)
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt
CHANGED
|
@@ -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
|
-
|
|
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
|
|
742
|
+
def request_csp_nonce
|
|
729
743
|
return unless respond_to?(:content_security_policy_nonce)
|
|
730
744
|
|
|
731
745
|
begin
|
|
@@ -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
|
|
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
|