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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: daa4f99f9713669a2de0a7b58659c117b757f91f55674609633c6d8485f0b2d1
|
|
4
|
+
data.tar.gz: 89f475046a66dcfcf3e21413a7067110d2e9afe36c6e2e4436327afe0b89c68c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2aa4e2dba93c13c2a0de444d56991c9c1819aef7c47108a35a3e88e11a363e39ebe3c0aa96958dd24bc27d9a5a233f5a9e881a0d5fefdb608946f26206532709
|
|
7
|
+
data.tar.gz: 4ae3e039a9913c8829858123b74477aff6adc9f13b6febd2ef500796a3a71d77da29a934ecddc31573b3141de495a84def5aed0f84c23545035d0ed5aaa75821
|
data/.rubocop.yml
CHANGED
|
@@ -35,6 +35,7 @@ Metrics/ClassLength:
|
|
|
35
35
|
Exclude:
|
|
36
36
|
- 'lib/generators/react_on_rails/base_generator.rb' # Generator complexity justified
|
|
37
37
|
- 'lib/react_on_rails/dev/server_manager.rb' # Dev tool with comprehensive help system
|
|
38
|
+
- 'lib/react_on_rails/dev/port_selector.rb' # Base-port mode plus dual-stack probing keep this above the 150-line threshold
|
|
38
39
|
|
|
39
40
|
Metrics/MethodLength:
|
|
40
41
|
Exclude:
|
|
@@ -31,7 +31,7 @@ group :development, :test do
|
|
|
31
31
|
gem "listen"
|
|
32
32
|
gem "debug"
|
|
33
33
|
gem "pry"
|
|
34
|
-
gem "pry-byebug"
|
|
34
|
+
gem "pry-byebug", require: false
|
|
35
35
|
gem "pry-doc"
|
|
36
36
|
gem "pry-rails"
|
|
37
37
|
gem "pry-rescue"
|
|
@@ -48,7 +48,7 @@ end
|
|
|
48
48
|
group :test do
|
|
49
49
|
gem "capybara", "~> 3.40"
|
|
50
50
|
gem "capybara-screenshot"
|
|
51
|
-
gem "
|
|
51
|
+
gem "simplecov", "~> 0.16.1", require: false
|
|
52
52
|
gem "cypress-on-rails", "~> 1.19"
|
|
53
53
|
gem "equivalent-xml"
|
|
54
54
|
gem "generator_spec"
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
react_on_rails (16.
|
|
4
|
+
react_on_rails (16.7.0.rc.0)
|
|
5
5
|
addressable
|
|
6
6
|
connection_pool
|
|
7
7
|
execjs (~> 2.5)
|
|
@@ -119,12 +119,6 @@ GEM
|
|
|
119
119
|
coderay (1.1.3)
|
|
120
120
|
concurrent-ruby (1.3.6)
|
|
121
121
|
connection_pool (3.0.2)
|
|
122
|
-
coveralls (0.8.23)
|
|
123
|
-
json (>= 1.8, < 3)
|
|
124
|
-
simplecov (~> 0.16.1)
|
|
125
|
-
term-ansicolor (~> 1.3)
|
|
126
|
-
thor (>= 0.19.4, < 2.0)
|
|
127
|
-
tins (~> 1.6)
|
|
128
122
|
crass (1.0.6)
|
|
129
123
|
csv (3.3.5)
|
|
130
124
|
cypress-on-rails (1.20.0)
|
|
@@ -401,17 +395,11 @@ GEM
|
|
|
401
395
|
uri (>= 0.12.0)
|
|
402
396
|
stringio (3.2.0)
|
|
403
397
|
strscan (3.1.0)
|
|
404
|
-
sync (0.5.0)
|
|
405
|
-
term-ansicolor (1.8.0)
|
|
406
|
-
tins (~> 1.0)
|
|
407
398
|
terminal-table (3.0.2)
|
|
408
399
|
unicode-display_width (>= 1.1.1, < 3)
|
|
409
400
|
thor (1.5.0)
|
|
410
401
|
tilt (2.3.0)
|
|
411
402
|
timeout (0.6.0)
|
|
412
|
-
tins (1.33.0)
|
|
413
|
-
bigdecimal
|
|
414
|
-
sync
|
|
415
403
|
tsort (0.2.0)
|
|
416
404
|
turbo-rails (2.0.20)
|
|
417
405
|
actionpack (>= 7.1.0)
|
|
@@ -448,7 +436,6 @@ DEPENDENCIES
|
|
|
448
436
|
bootsnap
|
|
449
437
|
capybara (~> 3.40)
|
|
450
438
|
capybara-screenshot
|
|
451
|
-
coveralls
|
|
452
439
|
cypress-on-rails (~> 1.19)
|
|
453
440
|
debug
|
|
454
441
|
equivalent-xml
|
|
@@ -481,6 +468,7 @@ DEPENDENCIES
|
|
|
481
468
|
sdoc
|
|
482
469
|
selenium-webdriver (= 4.9.0)
|
|
483
470
|
shakapacker (= 9.6.1)
|
|
471
|
+
simplecov (~> 0.16.1)
|
|
484
472
|
spring (~> 4.0)
|
|
485
473
|
sprockets (~> 4.0)
|
|
486
474
|
sqlite3 (~> 1.6)
|
data/Rakefile
CHANGED
|
@@ -6,12 +6,6 @@
|
|
|
6
6
|
tasks = %w[lint run_rspec]
|
|
7
7
|
prepare_for_ci = %w[node_package dummy_apps]
|
|
8
8
|
|
|
9
|
-
if ENV["USE_COVERALLS"] == "TRUE"
|
|
10
|
-
require "coveralls/rake/task"
|
|
11
|
-
Coveralls::RakeTask.new
|
|
12
|
-
tasks << "coveralls:push"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
9
|
desc "Run all tests and linting"
|
|
16
10
|
task default: tasks
|
|
17
11
|
|
data/Steepfile
CHANGED
|
@@ -36,7 +36,11 @@ target :lib do
|
|
|
36
36
|
check "lib/react_on_rails/dev/service_checker.rb"
|
|
37
37
|
check "lib/react_on_rails/git_utils.rb"
|
|
38
38
|
check "lib/react_on_rails/helper.rb"
|
|
39
|
+
check "lib/react_on_rails/js_code_builder.rb"
|
|
39
40
|
check "lib/react_on_rails/packer_utils.rb"
|
|
41
|
+
check "lib/react_on_rails/render_request.rb"
|
|
42
|
+
check "lib/react_on_rails/rendering_strategy.rb"
|
|
43
|
+
check "lib/react_on_rails/rendering_strategy/exec_js_strategy.rb"
|
|
40
44
|
check "lib/react_on_rails/server_rendering_pool.rb"
|
|
41
45
|
check "lib/react_on_rails/test_helper.rb"
|
|
42
46
|
check "lib/react_on_rails/utils.rb"
|
|
@@ -418,7 +418,7 @@ module ReactOnRails
|
|
|
418
418
|
|
|
419
419
|
if use_pro?
|
|
420
420
|
hints << {
|
|
421
|
-
path: "
|
|
421
|
+
path: "renderer/node-renderer.js",
|
|
422
422
|
description: "Node renderer entrypoint used for Pro SSR and RSC."
|
|
423
423
|
}
|
|
424
424
|
end
|
|
@@ -445,7 +445,7 @@ module ReactOnRails
|
|
|
445
445
|
},
|
|
446
446
|
{
|
|
447
447
|
title: "Marketplace RSC demo",
|
|
448
|
-
url: "https://github.com/shakacode/react-
|
|
448
|
+
url: "https://github.com/shakacode/react-on-rails-demo-marketplace-rsc",
|
|
449
449
|
description: "Study a larger app comparing traditional SSR, client boundaries, and streamed RSC."
|
|
450
450
|
}
|
|
451
451
|
]
|
|
@@ -524,7 +524,7 @@ module ReactOnRails
|
|
|
524
524
|
},
|
|
525
525
|
{
|
|
526
526
|
label: "Marketplace demo",
|
|
527
|
-
url: "https://github.com/shakacode/react-
|
|
527
|
+
url: "https://github.com/shakacode/react-on-rails-demo-marketplace-rsc"
|
|
528
528
|
}
|
|
529
529
|
]
|
|
530
530
|
|
|
@@ -666,7 +666,7 @@ module ReactOnRails
|
|
|
666
666
|
end
|
|
667
667
|
|
|
668
668
|
def home_page_pro_note_for_oss_app
|
|
669
|
-
"
|
|
669
|
+
"Review the Pro docs and upgrade guide first, then enable it with the appropriate license when you're ready."
|
|
670
670
|
end
|
|
671
671
|
|
|
672
672
|
def preferred_rspec_helper_file
|
|
@@ -145,7 +145,7 @@ module ReactOnRails
|
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
label: "Marketplace RSC demo",
|
|
148
|
-
url: "https://github.com/shakacode/react-
|
|
148
|
+
url: "https://github.com/shakacode/react-on-rails-demo-marketplace-rsc"
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
end
|
|
@@ -182,7 +182,7 @@ module ReactOnRails
|
|
|
182
182
|
description: "Rails view that calls stream_react_component."
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
|
-
path: "
|
|
185
|
+
path: "renderer/node-renderer.js",
|
|
186
186
|
description: "Node renderer entrypoint used by the Pro SSR and RSC stack."
|
|
187
187
|
}
|
|
188
188
|
]
|
|
@@ -216,7 +216,7 @@ module ReactOnRails
|
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
label: "Marketplace RSC demo",
|
|
219
|
-
url: "https://github.com/shakacode/react-
|
|
219
|
+
url: "https://github.com/shakacode/react-on-rails-demo-marketplace-rsc"
|
|
220
220
|
}
|
|
221
221
|
]
|
|
222
222
|
end
|
|
@@ -41,7 +41,7 @@ module ReactOnRails
|
|
|
41
41
|
gem("rspec-rails", group: :test)
|
|
42
42
|
# NOTE: chromedriver-helper was deprecated in 2019. Modern selenium-webdriver (4.x)
|
|
43
43
|
# and GitHub Actions have built-in driver management, so no driver helper is needed.
|
|
44
|
-
gem("
|
|
44
|
+
gem("simplecov", require: false, group: :test)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def replace_prerender_if_server_rendering
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
-
# rubocop:disable Metrics/ModuleLength
|
|
6
5
|
module GeneratorHelper
|
|
7
6
|
def package_json
|
|
8
7
|
# Lazy load package_json gem only when actually needed for dependency management
|
|
@@ -43,17 +42,6 @@ module GeneratorHelper
|
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
# Takes a relative path from the destination root, such as `.gitignore` or `app/assets/javascripts/application.js`
|
|
47
|
-
def dest_file_exists?(file)
|
|
48
|
-
dest_file = File.join(destination_root, file)
|
|
49
|
-
File.exist?(dest_file) ? dest_file : nil
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def dest_dir_exists?(dir)
|
|
53
|
-
dest_dir = File.join(destination_root, dir)
|
|
54
|
-
Dir.exist?(dest_dir) ? dest_dir : nil
|
|
55
|
-
end
|
|
56
|
-
|
|
57
45
|
# Detect whether config/routes.rb defines any non-commented root route.
|
|
58
46
|
#
|
|
59
47
|
# @param routes_path [String] absolute path to routes.rb
|
|
@@ -66,45 +54,6 @@ module GeneratorHelper
|
|
|
66
54
|
end
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
def setup_file_error(file, data)
|
|
70
|
-
<<~MSG
|
|
71
|
-
#{file} was not found.
|
|
72
|
-
Please add the following content to your #{file} file:
|
|
73
|
-
#{data}
|
|
74
|
-
MSG
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def empty_directory_with_keep_file(destination, config = {})
|
|
78
|
-
empty_directory(destination, config)
|
|
79
|
-
keep_file(destination)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def keep_file(destination)
|
|
83
|
-
create_file("#{destination}/.keep") unless options[:skip_keeps]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# As opposed to Rails::Generators::Testing.create_link, which creates a link pointing to
|
|
87
|
-
# source_root, this symlinks a file in destination_root to a file also in
|
|
88
|
-
# destination_root.
|
|
89
|
-
def symlink_dest_file_to_dest_file(target, link)
|
|
90
|
-
target_pathname = Pathname.new(File.join(destination_root, target))
|
|
91
|
-
link_pathname = Pathname.new(File.join(destination_root, link))
|
|
92
|
-
|
|
93
|
-
link_directory = link_pathname.dirname
|
|
94
|
-
link_basename = link_pathname.basename
|
|
95
|
-
target_relative_path = target_pathname.relative_path_from(link_directory)
|
|
96
|
-
|
|
97
|
-
`cd #{link_directory} && ln -s #{target_relative_path} #{link_basename}`
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def copy_file_and_missing_parent_directories(source_file, destination_file = nil)
|
|
101
|
-
destination_file ||= source_file
|
|
102
|
-
destination_path = Pathname.new(destination_file)
|
|
103
|
-
parent_directories = destination_path.dirname
|
|
104
|
-
empty_directory(parent_directories) unless dest_dir_exists?(parent_directories)
|
|
105
|
-
copy_file source_file, destination_file
|
|
106
|
-
end
|
|
107
|
-
|
|
108
57
|
def add_documentation_reference(message, source)
|
|
109
58
|
"#{message} \n#{source}"
|
|
110
59
|
end
|
|
@@ -148,32 +97,25 @@ module GeneratorHelper
|
|
|
148
97
|
@pro_gem_installed = Gem.loaded_specs.key?("react_on_rails_pro") || gem_in_lockfile?("react_on_rails_pro")
|
|
149
98
|
end
|
|
150
99
|
|
|
100
|
+
# TODO: CQS smell: mark_pro_gem_installed! makes pro_gem_installed? return true before install. See #3303.
|
|
151
101
|
def mark_pro_gem_installed!
|
|
152
102
|
@pro_gem_installed = true
|
|
153
103
|
end
|
|
154
104
|
|
|
155
|
-
# Check if first-class RSC Pro mode should be enabled.
|
|
156
|
-
# Returns true when --rsc-pro is set, or when users explicitly pass both --rsc and --pro.
|
|
157
|
-
#
|
|
158
|
-
# @return [Boolean] true if RSC Pro mode semantics should be applied
|
|
159
|
-
def use_rsc_pro_mode?
|
|
160
|
-
options[:rsc_pro] || (options[:rsc] && options[:pro])
|
|
161
|
-
end
|
|
162
|
-
|
|
163
105
|
# Check if Pro features should be enabled.
|
|
164
|
-
# Returns true if --pro
|
|
106
|
+
# Returns true if --pro or --rsc is set (RSC implies Pro).
|
|
165
107
|
#
|
|
166
108
|
# @return [Boolean] true if Pro setup should be included
|
|
167
109
|
def use_pro?
|
|
168
|
-
options[:pro] || options[:rsc]
|
|
110
|
+
options[:pro] || options[:rsc]
|
|
169
111
|
end
|
|
170
112
|
|
|
171
|
-
# Check if RSC (React Server Components) should be enabled
|
|
172
|
-
# Returns true if --rsc
|
|
113
|
+
# Check if RSC (React Server Components) should be enabled.
|
|
114
|
+
# Returns true if --rsc is set.
|
|
173
115
|
#
|
|
174
116
|
# @return [Boolean] true if RSC setup should be included
|
|
175
117
|
def use_rsc?
|
|
176
|
-
options[:rsc]
|
|
118
|
+
options[:rsc]
|
|
177
119
|
end
|
|
178
120
|
|
|
179
121
|
# Determine if the project is using rspack as the bundler.
|
|
@@ -378,4 +320,3 @@ module GeneratorHelper
|
|
|
378
320
|
config.dig("default", "assets_bundler") == "rspack"
|
|
379
321
|
end
|
|
380
322
|
end
|
|
381
|
-
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rainbow"
|
|
4
|
+
|
|
5
|
+
module GeneratorMessages
|
|
6
|
+
module CiSection
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_ci_section(app_root: Dir.pwd, ci_workflow_generated: false)
|
|
10
|
+
return "" unless ci_workflow_generated || File.exist?(File.join(app_root, ".github/workflows/ci.yml"))
|
|
11
|
+
|
|
12
|
+
# Read package.json once and reuse for both package-manager detection and the
|
|
13
|
+
# build:test script presence check to avoid a second I/O pass.
|
|
14
|
+
package_json = read_package_json(app_root)
|
|
15
|
+
package_manager = detect_package_manager(app_root: app_root, package_json: package_json)
|
|
16
|
+
ci_status = if ci_workflow_generated
|
|
17
|
+
"A GitHub Actions workflow has been generated at .github/workflows/ci.yml."
|
|
18
|
+
else
|
|
19
|
+
"A GitHub Actions workflow is available at .github/workflows/ci.yml."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
build_test_hint = if package_json&.dig("scripts", "build:test")
|
|
23
|
+
"\n\nOr use the generated package.json script:\n" \
|
|
24
|
+
"#{Rainbow("#{package_manager} run build:test").cyan}"
|
|
25
|
+
else
|
|
26
|
+
""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
<<~CI
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
🔄 CI / BUILD ORDERING:
|
|
33
|
+
─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
JavaScript bundles must be built before running Rails tests.
|
|
35
|
+
#{ci_status}
|
|
36
|
+
|
|
37
|
+
To build bundles manually before tests:
|
|
38
|
+
#{Rainbow('RAILS_ENV=test NODE_ENV=test bin/shakapacker').cyan}#{build_test_hint}
|
|
39
|
+
CI
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "react_on_rails/utils"
|
|
5
|
+
|
|
6
|
+
module GeneratorMessages
|
|
7
|
+
# Package-manager detection helpers used by the install generator and the
|
|
8
|
+
# post-install message. Split out of GeneratorMessages to keep that class
|
|
9
|
+
# under Metrics/ClassLength and to group related logic together.
|
|
10
|
+
module PackageManagerDetection
|
|
11
|
+
SUPPORTED_PACKAGE_MANAGERS = %w[npm pnpm yarn bun].freeze
|
|
12
|
+
PACKAGE_JSON_UNSET = Object.new.freeze
|
|
13
|
+
private_constant :PACKAGE_JSON_UNSET
|
|
14
|
+
|
|
15
|
+
# Hash insertion order is the detection priority used by
|
|
16
|
+
# detect_package_manager_from_lockfiles (yarn → pnpm → bun → npm).
|
|
17
|
+
LOCKFILE_CANDIDATES_BY_MANAGER = {
|
|
18
|
+
"yarn" => ["yarn.lock"],
|
|
19
|
+
"pnpm" => ["pnpm-lock.yaml"],
|
|
20
|
+
"bun" => ["bun.lock", "bun.lockb"],
|
|
21
|
+
"npm" => ["package-lock.json"]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Detects the package manager in priority order:
|
|
25
|
+
# 1. REACT_ON_RAILS_PACKAGE_MANAGER env variable
|
|
26
|
+
# 2. packageManager field in package.json (Corepack standard)
|
|
27
|
+
# 3. Lockfile on disk
|
|
28
|
+
# 4. Falls back to "npm" (Shakapacker 8.x default)
|
|
29
|
+
#
|
|
30
|
+
# Pass app_root: to resolve paths against a specific directory
|
|
31
|
+
# (e.g. destination_root in generators) instead of Dir.pwd.
|
|
32
|
+
# Omit `package_json:` (the default) to read package.json from disk.
|
|
33
|
+
# Pass package_json: <parsed_hash> to reuse an already-parsed package.json and
|
|
34
|
+
# avoid a re-read (callers that also inspect scripts/deps should parse once and
|
|
35
|
+
# pass the hash).
|
|
36
|
+
# Pass package_json: nil when the caller already attempted to read package.json and
|
|
37
|
+
# wants detection to fall through directly to lockfile heuristics.
|
|
38
|
+
def detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
|
|
39
|
+
detect_package_manager_with_source(
|
|
40
|
+
app_root: app_root,
|
|
41
|
+
package_json: package_json
|
|
42
|
+
).first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# source is one of :env, :package_json, :lockfile, :default — used to
|
|
46
|
+
# name the originating source when surfacing detection errors.
|
|
47
|
+
#
|
|
48
|
+
# See `detect_package_manager` for the `package_json:` three-way semantics
|
|
49
|
+
# (omitted = read from disk, nil = caller cached absent, Hash = pre-parsed).
|
|
50
|
+
def detect_package_manager_with_source(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
|
|
51
|
+
env_package_manager = ENV.fetch("REACT_ON_RAILS_PACKAGE_MANAGER", nil)&.strip&.downcase
|
|
52
|
+
return [env_package_manager, :env] if supported_package_manager?(env_package_manager)
|
|
53
|
+
|
|
54
|
+
content = package_json_content(
|
|
55
|
+
app_root: app_root,
|
|
56
|
+
package_json: package_json
|
|
57
|
+
)
|
|
58
|
+
pm_from_json = content ? package_manager_name_from_content(content) : nil
|
|
59
|
+
return [pm_from_json, :package_json] if pm_from_json
|
|
60
|
+
|
|
61
|
+
pm_from_lockfile = detect_package_manager_from_lockfiles(app_root: app_root)
|
|
62
|
+
return [pm_from_lockfile, :lockfile] if pm_from_lockfile
|
|
63
|
+
|
|
64
|
+
["npm", :default]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def lockfile_filename_for(package_manager, app_root: Dir.pwd)
|
|
68
|
+
LOCKFILE_CANDIDATES_BY_MANAGER[package_manager]&.find do |name|
|
|
69
|
+
File.exist?(File.join(app_root, name))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns true when package.json declares a top-level `packageManager` field with an
|
|
74
|
+
# npm-style version/range/tag (e.g. `"pnpm@9.0.0"`, `"pnpm@^10.0.0"`, or
|
|
75
|
+
# `"pnpm@latest"`) for the requested `manager`. The CI scaffold treats these as
|
|
76
|
+
# declared so it does not inject a conflicting fallback `version:`. Projects that
|
|
77
|
+
# need reproducible Corepack behavior should prefer an exact version, optionally
|
|
78
|
+
# with a hash (e.g. `"pnpm@9.0.0+sha256.abc"`). A bare name without `@<version>`
|
|
79
|
+
# returns false because `pnpm/action-setup` has no version to resolve from it.
|
|
80
|
+
# Used by the CI scaffold to decide whether `pnpm/action-setup` needs an explicit
|
|
81
|
+
# `version:` key; exact SemVer validation belongs only where a caller needs to
|
|
82
|
+
# extract a reproducible version pin.
|
|
83
|
+
# Pass package_json: <parsed_hash> to reuse an already-parsed package.json and
|
|
84
|
+
# package_json: nil to preserve a cached missing/unreadable read.
|
|
85
|
+
def package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
|
|
86
|
+
content = package_json_content(
|
|
87
|
+
app_root: app_root,
|
|
88
|
+
package_json: package_json
|
|
89
|
+
)
|
|
90
|
+
return false unless content
|
|
91
|
+
|
|
92
|
+
declared = versioned_package_manager_name_from_content(content)
|
|
93
|
+
return false if declared.nil?
|
|
94
|
+
|
|
95
|
+
declared == manager.to_s.downcase
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Used by the CI scaffold so `cache:` / `<pm> install` never reference a lockfile
|
|
99
|
+
# that's not on disk (e.g. `packageManager: pnpm` without `pnpm-lock.yaml`, which
|
|
100
|
+
# breaks `actions/setup-node`'s cache step).
|
|
101
|
+
def lockfile_for_manager?(package_manager, app_root: Dir.pwd)
|
|
102
|
+
!lockfile_filename_for(package_manager, app_root: app_root).nil?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def detect_package_manager_from_lockfiles(app_root: Dir.pwd)
|
|
106
|
+
LOCKFILE_CANDIDATES_BY_MANAGER.keys.find do |pm|
|
|
107
|
+
lockfile_for_manager?(pm, app_root: app_root)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def supported_package_manager?(package_manager)
|
|
112
|
+
SUPPORTED_PACKAGE_MANAGERS.include?(package_manager)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def package_manager_executable_available?(package_manager)
|
|
116
|
+
return false unless supported_package_manager?(package_manager)
|
|
117
|
+
|
|
118
|
+
ReactOnRails::Utils.command_available?(package_manager)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Parses package.json once and returns the hash, or nil if the file is missing
|
|
122
|
+
# or unreadable. Generator code can reuse the same parsed hash across setup,
|
|
123
|
+
# template, and message paths.
|
|
124
|
+
#
|
|
125
|
+
# Intentionally public: install_generator and other generator callers read
|
|
126
|
+
# package.json once and pass the result to detect_package_manager /
|
|
127
|
+
# package_manager_declared? to avoid repeated disk reads.
|
|
128
|
+
# When this returns nil, pass package_json: nil to those helpers to preserve
|
|
129
|
+
# that cached missing/unreadable state.
|
|
130
|
+
#
|
|
131
|
+
# @api public
|
|
132
|
+
def read_package_json(app_root)
|
|
133
|
+
package_json_path = File.join(app_root, "package.json")
|
|
134
|
+
return nil unless File.exist?(package_json_path)
|
|
135
|
+
|
|
136
|
+
JSON.parse(File.read(package_json_path))
|
|
137
|
+
rescue JSON::ParserError, Errno::EACCES, Errno::ENOENT
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Pipeline internals — external callers should go through `detect_package_manager`
|
|
144
|
+
# (which accepts `package_json:` for the read-once case). Reachable from sibling
|
|
145
|
+
# sub-modules (e.g. CiSection) via `include` without a receiver; tests use `send`.
|
|
146
|
+
|
|
147
|
+
def detect_package_manager_from_package_json(app_root: Dir.pwd)
|
|
148
|
+
content = read_package_json(app_root)
|
|
149
|
+
content ? package_manager_name_from_content(content) : nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def package_json_content(app_root:, package_json:)
|
|
153
|
+
return read_package_json(app_root) if package_json.equal?(PACKAGE_JSON_UNSET)
|
|
154
|
+
|
|
155
|
+
# nil means the caller cached that package.json was absent/unreadable.
|
|
156
|
+
package_json
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def package_manager_name_from_content(content)
|
|
160
|
+
raw_declared = raw_package_manager_field(content)
|
|
161
|
+
return nil if raw_declared.nil?
|
|
162
|
+
|
|
163
|
+
name = raw_declared.split("@", 2).first&.strip&.downcase
|
|
164
|
+
supported_package_manager?(name) ? name : nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Sibling of `package_manager_name_from_content` for places that need a resolvable
|
|
168
|
+
# spec, not just a manager name. Range or tag specs such as `"pnpm@^10.0.0"` and
|
|
169
|
+
# `"pnpm@latest"` are non-standard for reproducible Corepack usage, but this check
|
|
170
|
+
# treats them as declared to avoid injecting a conflicting fallback version.
|
|
171
|
+
def versioned_package_manager_name_from_content(content)
|
|
172
|
+
declared = raw_package_manager_field(content)
|
|
173
|
+
return nil if declared.nil?
|
|
174
|
+
|
|
175
|
+
match = declared.match(/\A([^@\s]+)@(?:\S+)\z/)
|
|
176
|
+
return nil unless match
|
|
177
|
+
|
|
178
|
+
name = match[1].downcase
|
|
179
|
+
supported_package_manager?(name) ? name : nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Single source of truth for reading and normalizing the raw `packageManager`
|
|
183
|
+
# string. Acceptance rules differ between callers (lenient name extraction vs.
|
|
184
|
+
# strict version-required regex), but field-handling concerns (type check,
|
|
185
|
+
# whitespace trim) belong in one place.
|
|
186
|
+
def raw_package_manager_field(content)
|
|
187
|
+
raw = content["packageManager"]
|
|
188
|
+
return nil unless raw.is_a?(String)
|
|
189
|
+
|
|
190
|
+
stripped = raw.strip
|
|
191
|
+
stripped.empty? ? nil : stripped
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rainbow"
|
|
4
|
+
|
|
5
|
+
module GeneratorMessages
|
|
6
|
+
module ShakapackerStatusSection
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_shakapacker_status_section(shakapacker_just_installed: false, app_root: Dir.pwd)
|
|
10
|
+
version_warning = check_shakapacker_version_warning(app_root: app_root)
|
|
11
|
+
if shakapacker_just_installed
|
|
12
|
+
base = <<~SHAKAPACKER
|
|
13
|
+
|
|
14
|
+
📦 SHAKAPACKER SETUP:
|
|
15
|
+
─────────────────────────────────────────────────────────────────────────
|
|
16
|
+
#{Rainbow('✓ Added to Gemfile automatically').green}
|
|
17
|
+
#{Rainbow('✓ Installer ran successfully').green}
|
|
18
|
+
#{Rainbow('✓ Webpack integration configured').green}
|
|
19
|
+
SHAKAPACKER
|
|
20
|
+
base.chomp + version_warning
|
|
21
|
+
elsif shakapacker_binstubs_present?(app_root)
|
|
22
|
+
"\n📦 #{Rainbow('Shakapacker already configured ✓').green}#{version_warning}"
|
|
23
|
+
else
|
|
24
|
+
"\n📦 #{Rainbow('Shakapacker setup may be incomplete').yellow}#{version_warning}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shakapacker_binstubs_present?(app_root)
|
|
29
|
+
File.exist?(File.join(app_root, "bin/shakapacker")) &&
|
|
30
|
+
File.exist?(File.join(app_root, "bin/shakapacker-dev-server"))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_shakapacker_version_warning(app_root: Dir.pwd)
|
|
34
|
+
gemfile_lock = File.join(app_root, "Gemfile.lock")
|
|
35
|
+
return "" unless File.exist?(gemfile_lock)
|
|
36
|
+
|
|
37
|
+
shakapacker_match = File.read(gemfile_lock).match(/shakapacker \((\d+\.\d+\.\d+)\)/)
|
|
38
|
+
return "" unless shakapacker_match
|
|
39
|
+
|
|
40
|
+
version = shakapacker_match[1]
|
|
41
|
+
if version.split(".").first.to_i < 8
|
|
42
|
+
<<~WARNING
|
|
43
|
+
|
|
44
|
+
⚠️ #{Rainbow('IMPORTANT: Upgrade Recommended').yellow.bold}
|
|
45
|
+
─────────────────────────────────────────────────────────────────────────
|
|
46
|
+
You are using Shakapacker #{version}. React on Rails v15+ works best with
|
|
47
|
+
Shakapacker 8.0+ for optimal Hot Module Replacement and build performance.
|
|
48
|
+
|
|
49
|
+
To upgrade: #{Rainbow('bundle update shakapacker').cyan}
|
|
50
|
+
|
|
51
|
+
Learn more: #{Rainbow('https://github.com/shakacode/shakapacker').cyan.underline}
|
|
52
|
+
WARNING
|
|
53
|
+
else
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
rescue StandardError
|
|
57
|
+
# If version detection fails, don't show a warning to avoid noise
|
|
58
|
+
""
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|