react_on_rails 16.6.0 → 16.7.0.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.development_dependencies +2 -2
- data/Gemfile.lock +2 -14
- data/Rakefile +0 -6
- data/Steepfile +4 -0
- data/lib/generators/react_on_rails/base_generator.rb +4 -4
- data/lib/generators/react_on_rails/demo_page_config.rb +3 -3
- data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
- data/lib/generators/react_on_rails/generator_helper.rb +6 -65
- data/lib/generators/react_on_rails/generator_messages/ci_section.rb +42 -0
- data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +194 -0
- data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +61 -0
- data/lib/generators/react_on_rails/generator_messages.rb +22 -79
- data/lib/generators/react_on_rails/install_generator.rb +243 -28
- data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -4
- data/lib/generators/react_on_rails/pro/USAGE +1 -1
- data/lib/generators/react_on_rails/pro_generator.rb +206 -183
- data/lib/generators/react_on_rails/pro_setup.rb +102 -26
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +3 -2
- data/lib/generators/react_on_rails/templates/base/base/.env.example +25 -0
- data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +86 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -3
- data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +6 -5
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
- data/lib/generators/react_on_rails/templates/pro/base/{client → renderer}/node-renderer.js +1 -0
- data/lib/react_on_rails/config_path_resolver.rb +101 -4
- data/lib/react_on_rails/configuration.rb +22 -0
- data/lib/react_on_rails/dev/file_manager.rb +135 -8
- data/lib/react_on_rails/dev/port_selector.rb +259 -7
- data/lib/react_on_rails/dev/process_manager.rb +29 -2
- data/lib/react_on_rails/dev/server_manager.rb +607 -39
- data/lib/react_on_rails/doctor.rb +513 -45
- data/lib/react_on_rails/helper.rb +3 -11
- data/lib/react_on_rails/js_code_builder.rb +66 -0
- data/lib/react_on_rails/length_prefixed_parser.rb +142 -0
- data/lib/react_on_rails/packs_generator.rb +65 -12
- data/lib/react_on_rails/pro_migration.rb +175 -0
- data/lib/react_on_rails/render_request.rb +74 -0
- data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +29 -0
- data/lib/react_on_rails/rendering_strategy.rb +44 -0
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +33 -22
- data/lib/react_on_rails/system_checker.rb +44 -23
- data/lib/react_on_rails/utils.rb +5 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails.rb +3 -0
- data/rakelib/run_rspec.rake +0 -5
- data/rakelib/shakapacker_examples.rake +66 -23
- data/react_on_rails.gemspec +18 -8
- data/sig/react_on_rails/js_code_builder.rbs +11 -0
- data/sig/react_on_rails/render_request.rbs +28 -0
- data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +11 -0
- data/sig/react_on_rails/rendering_strategy.rbs +7 -0
- data/sig/react_on_rails.rbs +6 -0
- metadata +31 -10
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require "rainbow"
|
|
4
4
|
|
|
5
|
+
require_relative "generator_messages/package_manager_detection"
|
|
6
|
+
require_relative "generator_messages/ci_section"
|
|
7
|
+
require_relative "generator_messages/shakapacker_status_section"
|
|
8
|
+
|
|
5
9
|
module GeneratorMessages
|
|
6
10
|
PRO_UPGRADE_HINT = "\n\n 💎 For RSC, streaming SSR, and 10-100x faster SSR, try React on Rails Pro:" \
|
|
7
11
|
"\n #{Rainbow('https://reactonrails.com/docs/pro/upgrading-to-pro/').cyan.underline}".freeze
|
|
8
|
-
|
|
12
|
+
# Package manager constants and detection helpers live in PackageManagerDetection,
|
|
13
|
+
# re-exported here for backwards compatibility (external callers use ::SUPPORTED_PACKAGE_MANAGERS).
|
|
14
|
+
SUPPORTED_PACKAGE_MANAGERS = PackageManagerDetection::SUPPORTED_PACKAGE_MANAGERS
|
|
9
15
|
|
|
10
|
-
# rubocop:disable Metrics/ClassLength
|
|
11
16
|
class << self
|
|
17
|
+
include PackageManagerDetection
|
|
18
|
+
include CiSection
|
|
19
|
+
include ShakapackerStatusSection
|
|
20
|
+
|
|
12
21
|
def output
|
|
13
22
|
@output ||= []
|
|
14
23
|
end
|
|
@@ -46,11 +55,14 @@ module GeneratorMessages
|
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def helpful_message_after_installation(component_name: "HelloWorld", route: "hello_world", pro: false,
|
|
49
|
-
rsc: false, shakapacker_just_installed: false, landing_page: false
|
|
58
|
+
rsc: false, shakapacker_just_installed: false, landing_page: false,
|
|
59
|
+
ci_workflow_generated: false, app_root: Dir.pwd)
|
|
50
60
|
process_manager_section = build_process_manager_section
|
|
51
|
-
testing_section = build_testing_section
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
testing_section = build_testing_section(app_root: app_root)
|
|
62
|
+
ci_section = build_ci_section(app_root: app_root, ci_workflow_generated: ci_workflow_generated)
|
|
63
|
+
package_manager = detect_package_manager(app_root: app_root)
|
|
64
|
+
shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed: shakapacker_just_installed,
|
|
65
|
+
app_root: app_root)
|
|
54
66
|
render_example = build_render_example(component_name: component_name, route: route, rsc: rsc)
|
|
55
67
|
render_label = build_render_label(route: route, rsc: rsc)
|
|
56
68
|
normalized_route = route.to_s.sub(%r{\A/+}, "")
|
|
@@ -99,33 +111,10 @@ module GeneratorMessages
|
|
|
99
111
|
• Documentation: #{Rainbow('https://reactonrails.com/docs/').cyan.underline}
|
|
100
112
|
• Webpack customization: #{Rainbow('https://github.com/shakacode/shakapacker#webpack-configuration').cyan.underline}
|
|
101
113
|
|
|
102
|
-
💡 TIP: Run 'bin/dev help' for development server options and troubleshooting#{testing_section}#{pro_hint}
|
|
114
|
+
💡 TIP: Run 'bin/dev help' for development server options and troubleshooting#{testing_section}#{ci_section}#{pro_hint}
|
|
103
115
|
MSG
|
|
104
116
|
end
|
|
105
117
|
|
|
106
|
-
# Uses relative lockfile paths resolved against Dir.pwd, so callers must invoke
|
|
107
|
-
# this while the current working directory is the target Rails app root.
|
|
108
|
-
def detect_package_manager
|
|
109
|
-
env_package_manager = ENV.fetch("REACT_ON_RAILS_PACKAGE_MANAGER", nil)&.strip&.downcase
|
|
110
|
-
return env_package_manager if supported_package_manager?(env_package_manager)
|
|
111
|
-
|
|
112
|
-
# Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
|
|
113
|
-
detect_package_manager_from_lockfiles || "npm"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def detect_package_manager_from_lockfiles
|
|
117
|
-
return "yarn" if File.exist?("yarn.lock")
|
|
118
|
-
return "pnpm" if File.exist?("pnpm-lock.yaml")
|
|
119
|
-
return "bun" if File.exist?("bun.lock") || File.exist?("bun.lockb")
|
|
120
|
-
return "npm" if File.exist?("package-lock.json")
|
|
121
|
-
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def supported_package_manager?(package_manager)
|
|
126
|
-
SUPPORTED_PACKAGE_MANAGERS.include?(package_manager)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
118
|
private
|
|
130
119
|
|
|
131
120
|
def build_render_example(component_name:, route:, rsc:)
|
|
@@ -160,8 +149,9 @@ module GeneratorMessages
|
|
|
160
149
|
end
|
|
161
150
|
end
|
|
162
151
|
|
|
163
|
-
def build_testing_section
|
|
164
|
-
return "" if File.exist?("spec/rails_helper.rb") ||
|
|
152
|
+
def build_testing_section(app_root: Dir.pwd)
|
|
153
|
+
return "" if File.exist?(File.join(app_root, "spec/rails_helper.rb")) ||
|
|
154
|
+
File.exist?(File.join(app_root, "spec/spec_helper.rb"))
|
|
165
155
|
|
|
166
156
|
<<~TESTING
|
|
167
157
|
|
|
@@ -182,52 +172,5 @@ module GeneratorMessages
|
|
|
182
172
|
"foreman"
|
|
183
173
|
end
|
|
184
174
|
end
|
|
185
|
-
|
|
186
|
-
def build_shakapacker_status_section(shakapacker_just_installed: false)
|
|
187
|
-
version_warning = check_shakapacker_version_warning
|
|
188
|
-
if shakapacker_just_installed
|
|
189
|
-
base = <<~SHAKAPACKER
|
|
190
|
-
|
|
191
|
-
📦 SHAKAPACKER SETUP:
|
|
192
|
-
─────────────────────────────────────────────────────────────────────────
|
|
193
|
-
#{Rainbow('✓ Added to Gemfile automatically').green}
|
|
194
|
-
#{Rainbow('✓ Installer ran successfully').green}
|
|
195
|
-
#{Rainbow('✓ Webpack integration configured').green}
|
|
196
|
-
SHAKAPACKER
|
|
197
|
-
base.chomp + version_warning
|
|
198
|
-
elsif File.exist?("bin/shakapacker") && File.exist?("bin/shakapacker-dev-server")
|
|
199
|
-
"\n📦 #{Rainbow('Shakapacker already configured ✓').green}#{version_warning}"
|
|
200
|
-
else
|
|
201
|
-
"\n📦 #{Rainbow('Shakapacker setup may be incomplete').yellow}#{version_warning}"
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def check_shakapacker_version_warning
|
|
206
|
-
return "" unless File.exist?("Gemfile.lock")
|
|
207
|
-
|
|
208
|
-
shakapacker_match = File.read("Gemfile.lock").match(/shakapacker \((\d+\.\d+\.\d+)\)/)
|
|
209
|
-
return "" unless shakapacker_match
|
|
210
|
-
|
|
211
|
-
version = shakapacker_match[1]
|
|
212
|
-
if version.split(".").first.to_i < 8
|
|
213
|
-
<<~WARNING
|
|
214
|
-
|
|
215
|
-
⚠️ #{Rainbow('IMPORTANT: Upgrade Recommended').yellow.bold}
|
|
216
|
-
─────────────────────────────────────────────────────────────────────────
|
|
217
|
-
You are using Shakapacker #{version}. React on Rails v15+ works best with
|
|
218
|
-
Shakapacker 8.0+ for optimal Hot Module Replacement and build performance.
|
|
219
|
-
|
|
220
|
-
To upgrade: #{Rainbow('bundle update shakapacker').cyan}
|
|
221
|
-
|
|
222
|
-
Learn more: #{Rainbow('https://github.com/shakacode/shakapacker').cyan.underline}
|
|
223
|
-
WARNING
|
|
224
|
-
else
|
|
225
|
-
""
|
|
226
|
-
end
|
|
227
|
-
rescue StandardError
|
|
228
|
-
# If version detection fails, don't show a warning to avoid noise
|
|
229
|
-
""
|
|
230
|
-
end
|
|
231
175
|
end
|
|
232
|
-
# rubocop:enable Metrics/ClassLength
|
|
233
176
|
end
|
|
@@ -57,21 +57,13 @@ module ReactOnRails
|
|
|
57
57
|
class_option :pro,
|
|
58
58
|
type: :boolean,
|
|
59
59
|
default: false,
|
|
60
|
-
desc: "Install React on Rails Pro with Node Renderer. "
|
|
61
|
-
"Combined with --rsc, uses --rsc-pro mode. Default: false"
|
|
60
|
+
desc: "Install React on Rails Pro with Node Renderer. Default: false"
|
|
62
61
|
|
|
63
62
|
# --rsc
|
|
64
63
|
class_option :rsc,
|
|
65
64
|
type: :boolean,
|
|
66
65
|
default: false,
|
|
67
|
-
desc: "Install React Server Components support (includes Pro). "
|
|
68
|
-
"Combined with --pro, uses --rsc-pro mode. Default: false"
|
|
69
|
-
|
|
70
|
-
# --rsc-pro
|
|
71
|
-
class_option :rsc_pro,
|
|
72
|
-
type: :boolean,
|
|
73
|
-
default: false,
|
|
74
|
-
desc: "Install first-class Pro RSC mode with matched Pro/RSC defaults. Default: false"
|
|
66
|
+
desc: "Install React Server Components support (includes Pro). Default: false"
|
|
75
67
|
|
|
76
68
|
# Hidden option: allows tests (and advanced users) to signal that Shakapacker
|
|
77
69
|
# was just installed, triggering force-overwrite of shakapacker.yml with RoR's template.
|
|
@@ -142,6 +134,17 @@ module ReactOnRails
|
|
|
142
134
|
SH
|
|
143
135
|
].map { |template| template.gsub("\r\n", "\n").strip }.freeze
|
|
144
136
|
|
|
137
|
+
# Exact fallback used when the scaffolded CI workflow has to supply a pnpm
|
|
138
|
+
# version because `pnpm/action-setup` requires one unless package.json declares
|
|
139
|
+
# `packageManager`. Match the repo's own packageManager version so generated
|
|
140
|
+
# CI defaults to the pnpm major this codebase tests with. Track the exact release
|
|
141
|
+
# used for this fallback at https://github.com/pnpm/pnpm/releases/tag/v9.14.2;
|
|
142
|
+
# update this URL with the constant when bumping. Users who need exact
|
|
143
|
+
# reproducibility should commit `packageManager` to their package.json instead.
|
|
144
|
+
# renovate: datasource=github-releases depName=pnpm/pnpm extractVersion=^v(?<version>.+)$
|
|
145
|
+
CI_PNPM_FALLBACK_VERSION = "9.14.2"
|
|
146
|
+
private_constant :CI_PNPM_FALLBACK_VERSION
|
|
147
|
+
|
|
145
148
|
# Main generator entry point
|
|
146
149
|
#
|
|
147
150
|
# Sets up React on Rails in a Rails application by:
|
|
@@ -163,6 +166,8 @@ module ReactOnRails
|
|
|
163
166
|
|
|
164
167
|
if installation_prerequisites_met? || options.ignore_warnings?
|
|
165
168
|
invoke_generators
|
|
169
|
+
add_package_json_scripts
|
|
170
|
+
add_ci_workflow
|
|
166
171
|
add_bin_scripts
|
|
167
172
|
add_post_install_message
|
|
168
173
|
else
|
|
@@ -251,6 +256,172 @@ module ReactOnRails
|
|
|
251
256
|
setup_js_dependencies
|
|
252
257
|
end
|
|
253
258
|
|
|
259
|
+
def add_ci_workflow
|
|
260
|
+
return if options[:pretend]
|
|
261
|
+
|
|
262
|
+
ci_path = ".github/workflows/ci.yml"
|
|
263
|
+
# Generators may run non-interactively (CI, scripts), so we never want Thor's
|
|
264
|
+
# `template` to prompt on conflict. Treat any existing workflow as "skip" by
|
|
265
|
+
# default; users who want to overwrite must pass --force explicitly. --skip
|
|
266
|
+
# falls into the same path because the desired outcome is identical.
|
|
267
|
+
if File.exist?(File.join(destination_root, ci_path)) && !options[:force]
|
|
268
|
+
say_status :skip, "#{ci_path} already exists (pass --force to overwrite)", :yellow
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
package_json = GeneratorMessages.read_package_json(destination_root)
|
|
273
|
+
package_manager = GeneratorMessages.detect_package_manager(
|
|
274
|
+
app_root: destination_root,
|
|
275
|
+
package_json: package_json
|
|
276
|
+
)
|
|
277
|
+
# Scope the lockfile check to the detected manager: a generic "any lockfile exists" check
|
|
278
|
+
# would emit `cache: "pnpm"` in CI when only `yarn.lock` is on disk, breaking setup-node.
|
|
279
|
+
has_lockfile = GeneratorMessages.lockfile_for_manager?(package_manager, app_root: destination_root)
|
|
280
|
+
# `pnpm/action-setup@v4` requires an explicit `version:` unless package.json declares
|
|
281
|
+
# `packageManager: pnpm@...`. Only ask the question for pnpm projects — other managers
|
|
282
|
+
# never read this flag — and require a pnpm-specific declaration so an env-override to
|
|
283
|
+
# pnpm while package.json declares a different manager still gets the version pin.
|
|
284
|
+
pnpm_version_declared = package_manager == "pnpm" &&
|
|
285
|
+
GeneratorMessages.package_manager_declared?(
|
|
286
|
+
app_root: destination_root,
|
|
287
|
+
manager: "pnpm",
|
|
288
|
+
package_json: package_json
|
|
289
|
+
)
|
|
290
|
+
has_active_record = File.exist?(File.join(destination_root, "config/database.yml"))
|
|
291
|
+
has_rspec = File.exist?(File.join(destination_root, "spec/rails_helper.rb")) ||
|
|
292
|
+
File.exist?(File.join(destination_root, "spec/spec_helper.rb"))
|
|
293
|
+
template("templates/base/base/.github/workflows/ci.yml.tt", ci_path,
|
|
294
|
+
{ package_manager: package_manager, has_lockfile: has_lockfile,
|
|
295
|
+
pnpm_version_declared: pnpm_version_declared,
|
|
296
|
+
pnpm_fallback_version: CI_PNPM_FALLBACK_VERSION,
|
|
297
|
+
has_active_record: has_active_record, has_rspec: has_rspec })
|
|
298
|
+
@ci_workflow_generated = true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# NODE_ENV=production ensures Shakapacker emits a minified production bundle;
|
|
302
|
+
# without it the default is "development" which produces an unminified dev bundle
|
|
303
|
+
# and is almost never what `npm run build` is expected to do.
|
|
304
|
+
DEFAULT_PACKAGE_JSON_SCRIPTS = {
|
|
305
|
+
"build" => "NODE_ENV=production bin/shakapacker",
|
|
306
|
+
"build:test" => "RAILS_ENV=test NODE_ENV=test bin/shakapacker"
|
|
307
|
+
}.freeze
|
|
308
|
+
private_constant :DEFAULT_PACKAGE_JSON_SCRIPTS
|
|
309
|
+
|
|
310
|
+
def add_package_json_scripts
|
|
311
|
+
return if options[:pretend]
|
|
312
|
+
|
|
313
|
+
package_json_path = File.join(destination_root, "package.json")
|
|
314
|
+
return unless File.exist?(package_json_path)
|
|
315
|
+
|
|
316
|
+
original_text = File.read(package_json_path)
|
|
317
|
+
existing_scripts = JSON.parse(original_text)["scripts"] || {}
|
|
318
|
+
scripts_to_add = DEFAULT_PACKAGE_JSON_SCRIPTS.reject { |key, _| existing_scripts.key?(key) }
|
|
319
|
+
|
|
320
|
+
if scripts_to_add.empty?
|
|
321
|
+
say_status :skip, "build scripts already present in package.json", :yellow
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
updated_text = inject_scripts_into_package_json(original_text, scripts_to_add, existing_scripts)
|
|
326
|
+
File.write(package_json_path, updated_text)
|
|
327
|
+
say_status :append, "📝 Added build scripts (#{scripts_to_add.keys.join(', ')}) to package.json", :yellow
|
|
328
|
+
rescue JSON::ParserError => e
|
|
329
|
+
GeneratorMessages.add_warning("⚠️ Could not parse package.json to add scripts: #{e.message}")
|
|
330
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
|
331
|
+
GeneratorMessages.add_warning("⚠️ Failed to add build scripts to package.json: #{e.message}")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Inserts new entries into the existing "scripts" object without rewriting the rest of
|
|
335
|
+
# package.json, so Prettier-formatted files only see the added lines in the diff.
|
|
336
|
+
# Falls back to a structured rewrite when the "scripts" key is absent or when the
|
|
337
|
+
# scripts object can't be located unambiguously (e.g. malformed JSON).
|
|
338
|
+
#
|
|
339
|
+
# Relies on the JSON invariant that `"scripts": {` cannot appear unescaped inside a
|
|
340
|
+
# preceding string value — in valid JSON the `"` characters are escaped as `\"`, so
|
|
341
|
+
# the regex can never falsely match a substring nested in a string literal.
|
|
342
|
+
def inject_scripts_into_package_json(original_text, scripts_to_add, existing_scripts)
|
|
343
|
+
opener = original_text.match(/"scripts"\s*:\s*\{/m)
|
|
344
|
+
return rewrite_package_json_with_scripts(original_text, scripts_to_add, existing_scripts) unless opener
|
|
345
|
+
|
|
346
|
+
inner_start = opener.end(0)
|
|
347
|
+
inner_end = find_matching_brace(original_text, inner_start)
|
|
348
|
+
return rewrite_package_json_with_scripts(original_text, scripts_to_add, existing_scripts) unless inner_end
|
|
349
|
+
|
|
350
|
+
inner = original_text[inner_start...inner_end]
|
|
351
|
+
# Detect the indent of the "scripts" key wherever it appears (any object position),
|
|
352
|
+
# not only when it's the first key. Defaults to two spaces so the closing `}` of the
|
|
353
|
+
# rebuilt scripts block lines up under "scripts" instead of being emitted at column 0.
|
|
354
|
+
object_indent = original_text[/\n([ \t]*)"scripts"/, 1] || " "
|
|
355
|
+
entry_indent = inner[/\n([ \t]+)"/, 1] || "#{object_indent} "
|
|
356
|
+
new_entries = scripts_to_add.map { |key, value| %(#{entry_indent}#{key.to_json}: #{value.to_json}) }
|
|
357
|
+
|
|
358
|
+
rebuilt_inner =
|
|
359
|
+
if existing_scripts.any?
|
|
360
|
+
trimmed = inner.sub(/\s*\z/, "")
|
|
361
|
+
separator = trimmed.end_with?(",") ? "" : ","
|
|
362
|
+
"#{trimmed}#{separator}\n#{new_entries.join(",\n")}\n#{object_indent}"
|
|
363
|
+
else
|
|
364
|
+
"\n#{new_entries.join(",\n")}\n#{object_indent}"
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
"#{original_text[0...opener.begin(0)]}\"scripts\": {#{rebuilt_inner}}#{original_text[(inner_end + 1)..]}"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Returns the index of the `}` that closes the `{` whose body starts at `start`,
|
|
371
|
+
# or nil if the object is unterminated. Tracks brace depth while stepping through
|
|
372
|
+
# JSON string literals so `}` characters inside script values (e.g.
|
|
373
|
+
# "lint": "eslint '{src,test}/**/*.js'") do not match a non-matching brace.
|
|
374
|
+
def find_matching_brace(text, start)
|
|
375
|
+
depth = 1
|
|
376
|
+
i = start
|
|
377
|
+
while i < text.length
|
|
378
|
+
case text[i]
|
|
379
|
+
when '"'
|
|
380
|
+
i = skip_json_string(text, i)
|
|
381
|
+
return nil unless i
|
|
382
|
+
when "{"
|
|
383
|
+
depth += 1
|
|
384
|
+
i += 1
|
|
385
|
+
when "}"
|
|
386
|
+
depth -= 1
|
|
387
|
+
return i if depth.zero?
|
|
388
|
+
|
|
389
|
+
i += 1
|
|
390
|
+
else
|
|
391
|
+
i += 1
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Given an index pointing at the opening `"` of a JSON string, returns the index
|
|
398
|
+
# just past the closing `"`. Honours `\"` and `\\` escapes. Returns nil if the
|
|
399
|
+
# string is unterminated.
|
|
400
|
+
def skip_json_string(text, start)
|
|
401
|
+
i = start + 1
|
|
402
|
+
while i < text.length
|
|
403
|
+
case text[i]
|
|
404
|
+
when "\\"
|
|
405
|
+
i += 2
|
|
406
|
+
when '"'
|
|
407
|
+
return i + 1
|
|
408
|
+
else
|
|
409
|
+
i += 1
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Used only when the "scripts" key is missing entirely or the regex can't locate it.
|
|
416
|
+
# This path does reformat the whole file, but it's rare — a Rails package.json with
|
|
417
|
+
# no scripts key at all is unusual.
|
|
418
|
+
def rewrite_package_json_with_scripts(original_text, scripts_to_add, existing_scripts)
|
|
419
|
+
content = JSON.parse(original_text)
|
|
420
|
+
content["scripts"] = existing_scripts.merge(scripts_to_add)
|
|
421
|
+
indent = original_text[/\A\{\n(\s+)/, 1] || " "
|
|
422
|
+
"#{JSON.pretty_generate(content, indent: indent)}\n"
|
|
423
|
+
end
|
|
424
|
+
|
|
254
425
|
def ensure_jsx_in_js_compatibility
|
|
255
426
|
return if options[:pretend]
|
|
256
427
|
return unless using_swc?
|
|
@@ -273,6 +444,8 @@ module ReactOnRails
|
|
|
273
444
|
# js(.coffee) are not checked by this method, but instead produce warning messages
|
|
274
445
|
# and allow the build to continue
|
|
275
446
|
def installation_prerequisites_met?
|
|
447
|
+
warn_if_unsupported_env_package_manager
|
|
448
|
+
|
|
276
449
|
# Non-blocking: warn about dirty worktree but don't prevent installation.
|
|
277
450
|
# A clean tree makes the generator diff easier to review, but blocking would
|
|
278
451
|
# be too strict for a generator that creates many new files.
|
|
@@ -298,10 +471,21 @@ module ReactOnRails
|
|
|
298
471
|
!(missing_node? || missing_package_manager? || (!has_worktree_issues && missing_pro_gem?))
|
|
299
472
|
end
|
|
300
473
|
|
|
301
|
-
def
|
|
302
|
-
|
|
474
|
+
def warn_if_unsupported_env_package_manager
|
|
475
|
+
env_value = ENV.fetch("REACT_ON_RAILS_PACKAGE_MANAGER", nil)&.strip
|
|
476
|
+
return if env_value.nil? || env_value.empty?
|
|
477
|
+
return if GeneratorMessages.supported_package_manager?(env_value.downcase)
|
|
303
478
|
|
|
304
|
-
|
|
479
|
+
supported = GeneratorMessages::SUPPORTED_PACKAGE_MANAGERS.join(", ")
|
|
480
|
+
GeneratorMessages.add_warning(<<~MSG.strip)
|
|
481
|
+
⚠️ REACT_ON_RAILS_PACKAGE_MANAGER='#{env_value}' is not a supported package manager.
|
|
482
|
+
Supported values: #{supported}.
|
|
483
|
+
Falling through to package.json / lockfile / npm-default detection.
|
|
484
|
+
MSG
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def missing_node?
|
|
488
|
+
unless ReactOnRails::Utils.command_available?("node")
|
|
305
489
|
error = <<~MSG.strip
|
|
306
490
|
🚫 Node.js is required but not found on your system.
|
|
307
491
|
|
|
@@ -409,7 +593,8 @@ module ReactOnRails
|
|
|
409
593
|
return
|
|
410
594
|
end
|
|
411
595
|
|
|
412
|
-
# Make these and only these files executable
|
|
596
|
+
# Make these and only these files executable. Use destination_root so
|
|
597
|
+
# chmod remains correct even if an earlier generator step changed Dir.pwd.
|
|
413
598
|
files_to_become_executable = bin_scripts_to_chmod(template_bin_path)
|
|
414
599
|
File.chmod(0o755, *files_to_become_executable)
|
|
415
600
|
end
|
|
@@ -444,7 +629,7 @@ module ReactOnRails
|
|
|
444
629
|
def bin_scripts_to_chmod(template_bin_path)
|
|
445
630
|
files = Dir.children(template_bin_path).reject { |filename| filename == "dev" }
|
|
446
631
|
files << "dev" unless preserve_existing_bin_dev?
|
|
447
|
-
files.map { |filename| "bin/#{filename}" }
|
|
632
|
+
files.map { |filename| File.join(destination_root, "bin/#{filename}") }
|
|
448
633
|
end
|
|
449
634
|
|
|
450
635
|
def default_bin_dev_route
|
|
@@ -487,9 +672,11 @@ module ReactOnRails
|
|
|
487
672
|
pro: use_pro?,
|
|
488
673
|
rsc: use_rsc?,
|
|
489
674
|
shakapacker_just_installed: shakapacker_just_installed?,
|
|
490
|
-
landing_page: options.new_app? && new_app_root_route_available
|
|
675
|
+
landing_page: options.new_app? && new_app_root_route_available?,
|
|
676
|
+
ci_workflow_generated: @ci_workflow_generated == true,
|
|
677
|
+
app_root: destination_root
|
|
491
678
|
))
|
|
492
|
-
GeneratorMessages.add_info(
|
|
679
|
+
GeneratorMessages.add_info(rsc_verification_message) if use_rsc?
|
|
493
680
|
end
|
|
494
681
|
|
|
495
682
|
def shakapacker_setup_incomplete?
|
|
@@ -503,9 +690,7 @@ module ReactOnRails
|
|
|
503
690
|
flags << "--typescript" if options.typescript?
|
|
504
691
|
flags << "--rspack" if options.rspack?
|
|
505
692
|
|
|
506
|
-
if
|
|
507
|
-
flags << "--rsc-pro"
|
|
508
|
-
elsif options.rsc?
|
|
693
|
+
if options.rsc?
|
|
509
694
|
flags << "--rsc"
|
|
510
695
|
elsif options.pro?
|
|
511
696
|
flags << "--pro"
|
|
@@ -514,7 +699,7 @@ module ReactOnRails
|
|
|
514
699
|
["rails generate react_on_rails:install", *flags].join(" ")
|
|
515
700
|
end
|
|
516
701
|
|
|
517
|
-
def
|
|
702
|
+
def rsc_verification_message
|
|
518
703
|
<<~MSG
|
|
519
704
|
|
|
520
705
|
🔎 RSC Pro Verification:
|
|
@@ -543,7 +728,7 @@ module ReactOnRails
|
|
|
543
728
|
end
|
|
544
729
|
|
|
545
730
|
def incomplete_installation_message
|
|
546
|
-
package_install_step = "#{GeneratorMessages.detect_package_manager} install"
|
|
731
|
+
package_install_step = "#{GeneratorMessages.detect_package_manager(app_root: destination_root)} install"
|
|
547
732
|
|
|
548
733
|
<<~MSG
|
|
549
734
|
|
|
@@ -589,8 +774,7 @@ module ReactOnRails
|
|
|
589
774
|
end
|
|
590
775
|
|
|
591
776
|
def cli_exists?(command)
|
|
592
|
-
|
|
593
|
-
system(which_command, command, out: File::NULL, err: File::NULL)
|
|
777
|
+
ReactOnRails::Utils.command_available?(command)
|
|
594
778
|
end
|
|
595
779
|
|
|
596
780
|
def normalize_bin_dev_content(content)
|
|
@@ -801,13 +985,19 @@ module ReactOnRails
|
|
|
801
985
|
end
|
|
802
986
|
|
|
803
987
|
def missing_package_manager?
|
|
804
|
-
|
|
805
|
-
|
|
988
|
+
selected, source = GeneratorMessages.detect_package_manager_with_source(app_root: destination_root)
|
|
989
|
+
return false if GeneratorMessages.package_manager_executable_available?(selected)
|
|
990
|
+
|
|
991
|
+
available_package_managers = GeneratorMessages::SUPPORTED_PACKAGE_MANAGERS.select do |pm|
|
|
992
|
+
pm != selected && GeneratorMessages.package_manager_executable_available?(pm)
|
|
993
|
+
end
|
|
806
994
|
|
|
807
|
-
if
|
|
995
|
+
if available_package_managers.empty?
|
|
808
996
|
error = <<~MSG.strip
|
|
809
997
|
🚫 No JavaScript package manager found on your system.
|
|
810
998
|
|
|
999
|
+
#{package_manager_source_description(selected, source)}
|
|
1000
|
+
|
|
811
1001
|
React on Rails requires a JavaScript package manager to install dependencies.
|
|
812
1002
|
Please install one of the following:
|
|
813
1003
|
|
|
@@ -822,7 +1012,32 @@ module ReactOnRails
|
|
|
822
1012
|
return true
|
|
823
1013
|
end
|
|
824
1014
|
|
|
825
|
-
|
|
1015
|
+
action_separator = %i[default env].include?(source) ? " or " : ", update the source above, or "
|
|
1016
|
+
error = <<~MSG.strip
|
|
1017
|
+
🚫 JavaScript package manager '#{selected}' was selected, but the command was not found.
|
|
1018
|
+
|
|
1019
|
+
#{package_manager_source_description(selected, source)}
|
|
1020
|
+
Install '#{selected}'#{action_separator}set REACT_ON_RAILS_PACKAGE_MANAGER
|
|
1021
|
+
to one of the available package managers: #{available_package_managers.join(', ')}.
|
|
1022
|
+
MSG
|
|
1023
|
+
GeneratorMessages.add_error(error)
|
|
1024
|
+
true
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def package_manager_source_description(selected, source)
|
|
1028
|
+
case source
|
|
1029
|
+
when :env
|
|
1030
|
+
"Selected via the REACT_ON_RAILS_PACKAGE_MANAGER environment variable."
|
|
1031
|
+
when :package_json
|
|
1032
|
+
"Selected via the `packageManager` field in package.json."
|
|
1033
|
+
when :lockfile
|
|
1034
|
+
lockfile = GeneratorMessages.lockfile_filename_for(selected, app_root: destination_root)
|
|
1035
|
+
lockfile ? "Selected via the #{lockfile} lockfile on disk." : "Selected via a lockfile on disk."
|
|
1036
|
+
when :default
|
|
1037
|
+
"Selected via the npm default fallback (no env var, packageManager field, or lockfile detected)."
|
|
1038
|
+
else
|
|
1039
|
+
raise ArgumentError, "Unknown package manager source: #{source.inspect}"
|
|
1040
|
+
end
|
|
826
1041
|
end
|
|
827
1042
|
|
|
828
1043
|
def jsx_in_js_files_present?
|
|
@@ -91,16 +91,19 @@ module ReactOnRails
|
|
|
91
91
|
].freeze
|
|
92
92
|
|
|
93
93
|
# Rspack core dependencies (only installed when --rspack flag is used)
|
|
94
|
+
# @rspack/core uses ^2.0.0-0 (with -0 prerelease suffix) to include RC/beta prereleases
|
|
95
|
+
# of 2.0.0 until the stable 2.0.0 release lands.
|
|
94
96
|
RSPACK_DEPENDENCIES = %w[
|
|
95
|
-
@rspack/core@^
|
|
97
|
+
@rspack/core@^2.0.0-0
|
|
96
98
|
rspack-manifest-plugin@^5.0.0
|
|
97
99
|
].freeze
|
|
98
100
|
|
|
99
101
|
# Rspack development dependencies for hot reloading
|
|
100
102
|
# react-refresh is pre-1.0, so left bare (see pinning note above).
|
|
103
|
+
# @rspack/cli uses ^2.0.0-0 to match @rspack/core's prerelease range.
|
|
101
104
|
RSPACK_DEV_DEPENDENCIES = %w[
|
|
102
|
-
@rspack/cli@^
|
|
103
|
-
@rspack/plugin-react-refresh@^
|
|
105
|
+
@rspack/cli@^2.0.0-0
|
|
106
|
+
@rspack/plugin-react-refresh@^2.0.0
|
|
104
107
|
react-refresh
|
|
105
108
|
].freeze
|
|
106
109
|
|
|
@@ -557,7 +560,7 @@ module ReactOnRails
|
|
|
557
560
|
end
|
|
558
561
|
|
|
559
562
|
def fallback_package_manager
|
|
560
|
-
package_manager = GeneratorMessages.detect_package_manager
|
|
563
|
+
package_manager = GeneratorMessages.detect_package_manager(app_root: destination_root)
|
|
561
564
|
return package_manager if GeneratorMessages.supported_package_manager?(package_manager)
|
|
562
565
|
|
|
563
566
|
"npm"
|