react_on_rails 16.4.0.rc.9 → 16.4.0.rc.10
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/base_generator.rb +312 -19
- data/lib/generators/react_on_rails/generator_messages.rb +18 -17
- data/lib/generators/react_on_rails/install_generator.rb +85 -9
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +7 -0
- data/lib/generators/react_on_rails/rsc_setup.rb +228 -1
- data/lib/generators/react_on_rails/templates/base/base/app/controllers/hello_world_controller.rb +1 -1
- data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/{hello_world.html.erb → react_on_rails_default.html.erb} +1 -1
- data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/{hello_server_controller.rb → hello_server_controller.rb.tt} +1 -1
- data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +1 -2
- data/lib/react_on_rails/configuration.rb +4 -4
- data/lib/react_on_rails/helper.rb +51 -38
- data/lib/react_on_rails/locales/base.rb +2 -2
- data/lib/react_on_rails/locales/to_js.rb +2 -2
- data/lib/react_on_rails/packer_utils.rb +4 -4
- data/lib/react_on_rails/pro_helper.rb +1 -1
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +1 -1
- data/lib/react_on_rails/system_checker.rb +1 -1
- data/lib/react_on_rails/test_helper.rb +1 -1
- data/lib/react_on_rails/utils.rb +1 -1
- data/lib/react_on_rails/version.rb +1 -1
- data/rakelib/update_changelog.rake +143 -25
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdd0253666ff913084d12f9f5b3f24b41f0f33ed418165f54a5d038cb567012d
|
|
4
|
+
data.tar.gz: 510bcdc628f66dd6c9ddfae7be24582fef28ea35c263440e86a640b419bff379
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa0ec02748d25524f70fa7fe73803fc25c8c73249a17b953517ab3b226fa2101efc0443b3bab201737711f91d4a4f4d9d280e29005c28b505ae3d2d6d4bda153
|
|
7
|
+
data.tar.gz: 365c80cbf379d64e9c74958e96334fe494d2f9770f8fa0f1b01eae03f89e11fd08590459bbe3a021f6a4e3ff697a7ad78bcfa9fd93ac0646ada6007f6d471b8a
|
data/Gemfile.lock
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "erb"
|
|
5
6
|
require_relative "generator_messages"
|
|
6
7
|
require_relative "generator_helper"
|
|
7
8
|
require_relative "js_dependency_manager"
|
|
@@ -47,6 +48,56 @@ module ReactOnRails
|
|
|
47
48
|
default: false,
|
|
48
49
|
hide: true
|
|
49
50
|
|
|
51
|
+
# Keep this map in sync with templates under:
|
|
52
|
+
# lib/generators/react_on_rails/templates/**/config/webpack/*.tt
|
|
53
|
+
MANAGED_WEBPACK_FILE_TEMPLATES = {
|
|
54
|
+
"clientWebpackConfig.js" => "base/base/config/webpack/clientWebpackConfig.js.tt",
|
|
55
|
+
"commonWebpackConfig.js" => "base/base/config/webpack/commonWebpackConfig.js.tt",
|
|
56
|
+
"test.js" => "base/base/config/webpack/test.js.tt",
|
|
57
|
+
"development.js" => "base/base/config/webpack/development.js.tt",
|
|
58
|
+
"production.js" => "base/base/config/webpack/production.js.tt",
|
|
59
|
+
"serverWebpackConfig.js" => "base/base/config/webpack/serverWebpackConfig.js.tt",
|
|
60
|
+
"rscWebpackConfig.js" => "rsc/base/config/webpack/rscWebpackConfig.js.tt",
|
|
61
|
+
"ServerClientOrBoth.js" => "base/base/config/webpack/ServerClientOrBoth.js.tt",
|
|
62
|
+
# Legacy filename generated by older React on Rails versions.
|
|
63
|
+
# We compare against the current options (including --pro/--rsc); if those
|
|
64
|
+
# don't match the original generation, cleanup intentionally preserves files.
|
|
65
|
+
"generateWebpackConfigs.js" => "base/base/config/webpack/ServerClientOrBoth.js.tt"
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
# Keep these helper delegates in sync with ERB calls in managed webpack
|
|
69
|
+
# templates. This is the intended template-facing API; missing delegates
|
|
70
|
+
# intentionally fail rendering and keep cleanup conservative by treating
|
|
71
|
+
# files as non-removable.
|
|
72
|
+
TemplateRenderContext = Struct.new(:generator, :config) do
|
|
73
|
+
def erb_binding
|
|
74
|
+
binding
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_documentation_reference(*args)
|
|
78
|
+
generator.__send__(:add_documentation_reference, *args)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def use_pro?
|
|
82
|
+
generator.__send__(:use_pro?)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def use_rsc?
|
|
86
|
+
generator.__send__(:use_rsc?)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def shakapacker_version_9_or_higher?
|
|
90
|
+
generator.__send__(:shakapacker_version_9_or_higher?)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
REMOVABLE_WEBPACK_FILES = (MANAGED_WEBPACK_FILE_TEMPLATES.keys +
|
|
95
|
+
%w[webpack.config.js]).freeze
|
|
96
|
+
DOCS_REFERENCE_MESSAGE = "// The source code including full typescript support is available at:"
|
|
97
|
+
TEMPLATE_RENDER_FAILED = Object.new.freeze # unique sentinel compared by identity via .equal?
|
|
98
|
+
private_constant :MANAGED_WEBPACK_FILE_TEMPLATES, :REMOVABLE_WEBPACK_FILES, :TemplateRenderContext,
|
|
99
|
+
:DOCS_REFERENCE_MESSAGE, :TEMPLATE_RENDER_FAILED
|
|
100
|
+
|
|
50
101
|
def add_hello_world_route
|
|
51
102
|
# RSC uses HelloServer instead of HelloWorld, but Redux still needs hello_world route
|
|
52
103
|
return if use_rsc? && !options.redux?
|
|
@@ -70,9 +121,9 @@ module ReactOnRails
|
|
|
70
121
|
.env.example
|
|
71
122
|
bin/shakapacker-precompile-hook]
|
|
72
123
|
|
|
73
|
-
#
|
|
74
|
-
# packs without requiring a hardcoded application.js pack entry.
|
|
75
|
-
base_files << "app/views/layouts/
|
|
124
|
+
# react_on_rails_default layout provides empty pack tags so React on Rails can
|
|
125
|
+
# inject generated packs without requiring a hardcoded application.js pack entry.
|
|
126
|
+
base_files << "app/views/layouts/react_on_rails_default.html.erb"
|
|
76
127
|
|
|
77
128
|
# HelloWorld controller only when not using RSC (RSC uses HelloServer)
|
|
78
129
|
# Exception: Redux still needs the HelloWorld controller even with RSC
|
|
@@ -104,6 +155,10 @@ module ReactOnRails
|
|
|
104
155
|
end
|
|
105
156
|
|
|
106
157
|
def copy_webpack_config
|
|
158
|
+
# Cleanup must run before writing new webpack/rspack configs so we only
|
|
159
|
+
# evaluate pre-existing stale entries, never files generated in this run.
|
|
160
|
+
cleanup_stale_webpack_config_dir_for_rspack
|
|
161
|
+
|
|
107
162
|
say "Adding #{using_rspack? ? 'Rspack' : 'Webpack'} config"
|
|
108
163
|
base_path = "base/base"
|
|
109
164
|
base_files = %w[babel.config.js
|
|
@@ -114,9 +169,7 @@ module ReactOnRails
|
|
|
114
169
|
config/webpack/production.js
|
|
115
170
|
config/webpack/serverWebpackConfig.js
|
|
116
171
|
config/webpack/ServerClientOrBoth.js]
|
|
117
|
-
config = {
|
|
118
|
-
message: "// The source code including full typescript support is available at:"
|
|
119
|
-
}
|
|
172
|
+
config = { message: DOCS_REFERENCE_MESSAGE }
|
|
120
173
|
base_files.each do |file|
|
|
121
174
|
template("#{base_path}/#{file}.tt", destination_config_path(file), config)
|
|
122
175
|
end
|
|
@@ -183,7 +236,7 @@ module ReactOnRails
|
|
|
183
236
|
add_configure_minitest_to_compile_assets(test_helper) if File.exist?(test_helper)
|
|
184
237
|
end
|
|
185
238
|
|
|
186
|
-
CONFIGURE_RSPEC_TO_COMPILE_ASSETS =
|
|
239
|
+
CONFIGURE_RSPEC_TO_COMPILE_ASSETS = <<~STR
|
|
187
240
|
RSpec.configure do |config|
|
|
188
241
|
# Ensure that if we are running js tests, we are using latest webpack assets
|
|
189
242
|
# This will use the defaults of :js and :server_rendering meta tags
|
|
@@ -193,7 +246,7 @@ module ReactOnRails
|
|
|
193
246
|
end
|
|
194
247
|
STR
|
|
195
248
|
|
|
196
|
-
CONFIGURE_MINITEST_TO_COMPILE_ASSETS =
|
|
249
|
+
CONFIGURE_MINITEST_TO_COMPILE_ASSETS = <<~STR
|
|
197
250
|
# Ensure that tests run against fresh webpack assets.
|
|
198
251
|
ActiveSupport::TestCase.setup do
|
|
199
252
|
ReactOnRails::TestHelper.ensure_assets_compiled
|
|
@@ -219,7 +272,7 @@ module ReactOnRails
|
|
|
219
272
|
existing_content = File.read(webpack_config_path)
|
|
220
273
|
|
|
221
274
|
# Check if it's the standard Shakapacker config that we can safely replace
|
|
222
|
-
if standard_shakapacker_config?(existing_content)
|
|
275
|
+
if standard_shakapacker_config?(existing_content, strip_comments: true)
|
|
223
276
|
# Remove the file first to avoid conflict prompt, then recreate it
|
|
224
277
|
remove_file(webpack_config_path, verbose: false)
|
|
225
278
|
# Show what we're doing
|
|
@@ -273,25 +326,265 @@ module ReactOnRails
|
|
|
273
326
|
end
|
|
274
327
|
end
|
|
275
328
|
|
|
276
|
-
def
|
|
329
|
+
def cleanup_stale_webpack_config_dir_for_rspack
|
|
330
|
+
return unless cleanup_stale_webpack_config_dir?
|
|
331
|
+
|
|
332
|
+
webpack_config_relative_dir = "config/webpack"
|
|
333
|
+
all_entries = stale_webpack_config_entries
|
|
334
|
+
unless all_entries
|
|
335
|
+
say_status :warning,
|
|
336
|
+
"Keeping #{webpack_config_relative_dir}; could not read directory entries " \
|
|
337
|
+
"(permission denied or path changed)",
|
|
338
|
+
:yellow
|
|
339
|
+
return
|
|
340
|
+
end
|
|
341
|
+
if all_entries.empty?
|
|
342
|
+
say_status :skip, "#{webpack_config_relative_dir} (empty directory, leaving as-is)", :yellow
|
|
343
|
+
return
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
return warn_dotfiles_only_webpack_dir(webpack_config_relative_dir, all_entries) if all_dotfiles?(all_entries)
|
|
347
|
+
|
|
348
|
+
# We only clean up known top-level files. Any directory or unknown entry is
|
|
349
|
+
# treated as non-removable so we don't recurse into user-managed content.
|
|
350
|
+
non_removable_entries = non_removable_webpack_entries(all_entries)
|
|
351
|
+
removable_entries = removable_webpack_entries(all_entries)
|
|
352
|
+
if non_removable_entries.empty?
|
|
353
|
+
remove_stale_webpack_dir(webpack_config_relative_dir)
|
|
354
|
+
return
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if all_dotfiles?(non_removable_entries)
|
|
358
|
+
handle_dotfile_only_non_removable_entries(
|
|
359
|
+
webpack_config_relative_dir,
|
|
360
|
+
removable_entries,
|
|
361
|
+
non_removable_entries
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
removed_entries = remove_stale_webpack_files(webpack_config_relative_dir, removable_entries)
|
|
367
|
+
warn_non_removable_webpack_entries(webpack_config_relative_dir, non_removable_entries, removed_entries)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def remove_stale_webpack_dir(webpack_config_relative_dir)
|
|
371
|
+
# Thor's remove_dir uses paths relative to destination_root.
|
|
372
|
+
# TOCTOU: a file created between the removability check and remove_dir
|
|
373
|
+
# would be deleted. Acceptable for this generator cleanup path.
|
|
374
|
+
remove_dir(webpack_config_relative_dir, verbose: false)
|
|
375
|
+
say_status :remove,
|
|
376
|
+
"#{webpack_config_relative_dir} (stale webpack configs after switching to --rspack)",
|
|
377
|
+
:green
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def handle_dotfile_only_non_removable_entries(
|
|
381
|
+
webpack_config_relative_dir,
|
|
382
|
+
removable_entries,
|
|
383
|
+
non_removable_entries
|
|
384
|
+
)
|
|
385
|
+
removed_entries = remove_stale_webpack_files(webpack_config_relative_dir, removable_entries)
|
|
386
|
+
warn_dotfiles_in_webpack_dir(webpack_config_relative_dir, non_removable_entries, removed_entries)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def cleanup_stale_webpack_config_dir?
|
|
390
|
+
using_rspack? && Dir.exist?(stale_webpack_config_dir)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def all_dotfiles?(entries)
|
|
394
|
+
entries.any? && entries.all? { |entry| entry.start_with?(".") }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def warn_dotfiles_only_webpack_dir(webpack_config_relative_dir, all_entries)
|
|
398
|
+
say_status :warning,
|
|
399
|
+
"Keeping #{webpack_config_relative_dir}; only dotfiles found: " \
|
|
400
|
+
"#{all_entries.join(', ')}",
|
|
401
|
+
:yellow
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def warn_non_removable_webpack_entries(webpack_config_relative_dir, non_removable_entries, removed_entries = [])
|
|
405
|
+
non_dotfile_entries = non_removable_entries.reject { |entry| entry.start_with?(".") }
|
|
406
|
+
dotfiles = non_removable_entries.select { |entry| entry.start_with?(".") }
|
|
407
|
+
non_removable_directories, non_removable_files = non_dotfile_entries.partition do |entry|
|
|
408
|
+
directory_entry?(entry)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
non_removable_sections = []
|
|
412
|
+
non_removable_sections << "files: #{non_removable_files.join(', ')}" if non_removable_files.any?
|
|
413
|
+
if non_removable_directories.any?
|
|
414
|
+
non_removable_sections << "directories: #{non_removable_directories.join(', ')}"
|
|
415
|
+
end
|
|
416
|
+
non_removable_sections << "dotfiles: #{dotfiles.join(', ')}" if dotfiles.any?
|
|
417
|
+
|
|
418
|
+
non_removable_sections_message = non_removable_sections.join("; ")
|
|
419
|
+
removed_entries_message = if removed_entries.any?
|
|
420
|
+
" Removed stale managed files: #{removed_entries.join(', ')}."
|
|
421
|
+
else
|
|
422
|
+
""
|
|
423
|
+
end
|
|
424
|
+
say_status :warning,
|
|
425
|
+
"Keeping #{webpack_config_relative_dir}; " \
|
|
426
|
+
"custom/non-removable entries detected: #{non_removable_sections_message}." \
|
|
427
|
+
"#{removed_entries_message}",
|
|
428
|
+
:yellow
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def directory_entry?(entry)
|
|
432
|
+
File.directory?(File.join(stale_webpack_config_dir, entry))
|
|
433
|
+
rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT
|
|
434
|
+
false
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def warn_dotfiles_in_webpack_dir(webpack_config_relative_dir, dotfiles, removed_entries)
|
|
438
|
+
removed_entries_message = if removed_entries.any?
|
|
439
|
+
" Removed stale managed files: #{removed_entries.join(', ')}."
|
|
440
|
+
else
|
|
441
|
+
""
|
|
442
|
+
end
|
|
443
|
+
say_status :warning,
|
|
444
|
+
"Keeping #{webpack_config_relative_dir}; dotfiles present: #{dotfiles.join(', ')}. " \
|
|
445
|
+
"Remove them manually if you want the directory cleaned up." \
|
|
446
|
+
"#{removed_entries_message}",
|
|
447
|
+
:yellow
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def non_removable_webpack_entries(all_entries)
|
|
451
|
+
all_entries.select do |entry|
|
|
452
|
+
entry.start_with?(".") || !removable_webpack_entry?(entry)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def removable_webpack_entries(all_entries)
|
|
457
|
+
all_entries.select { |entry| removable_webpack_entry?(entry) }
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def remove_stale_webpack_files(webpack_config_relative_dir, entries)
|
|
461
|
+
entries.each_with_object([]) do |entry, removed_entries|
|
|
462
|
+
relative_path = File.join(webpack_config_relative_dir, entry)
|
|
463
|
+
full_path = File.join(destination_root, relative_path)
|
|
464
|
+
remove_file(relative_path, verbose: false)
|
|
465
|
+
removed_entries << entry unless File.exist?(full_path)
|
|
466
|
+
rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::ENOTDIR
|
|
467
|
+
# If we cannot stat after removal attempt, conservatively report as not removed.
|
|
468
|
+
nil
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def removable_webpack_entry?(entry)
|
|
473
|
+
return false unless REMOVABLE_WEBPACK_FILES.include?(entry)
|
|
474
|
+
|
|
475
|
+
full_path = File.join(stale_webpack_config_dir, entry)
|
|
476
|
+
return false if File.symlink?(full_path)
|
|
477
|
+
return false unless File.file?(full_path)
|
|
478
|
+
|
|
479
|
+
content = safe_read_cleanup_file(full_path)
|
|
480
|
+
return false unless content
|
|
481
|
+
|
|
482
|
+
return removable_webpack_main_config?(content) if entry == "webpack.config.js"
|
|
483
|
+
|
|
484
|
+
removable_managed_webpack_entry?(entry, content)
|
|
485
|
+
rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::ENOTDIR
|
|
486
|
+
false
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def removable_webpack_main_config?(content)
|
|
490
|
+
webpack_template = rendered_template_for_cleanup("base/base/config/webpack/webpack.config.js.tt")
|
|
491
|
+
return standard_shakapacker_config?(content) if webpack_template.equal?(TEMPLATE_RENDER_FAILED)
|
|
492
|
+
|
|
493
|
+
# Cleanup is deliberately comment-sensitive (unlike copy_webpack_main_config)
|
|
494
|
+
# so comment-only edits keep the file as a potential customization.
|
|
495
|
+
standard_shakapacker_config?(content) || content_matches_template?(content, webpack_template)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def removable_managed_webpack_entry?(entry, content)
|
|
499
|
+
template_path = MANAGED_WEBPACK_FILE_TEMPLATES[entry]
|
|
500
|
+
return false unless template_path
|
|
501
|
+
|
|
502
|
+
rendered_template = rendered_template_for_cleanup(template_path)
|
|
503
|
+
return false if rendered_template.equal?(TEMPLATE_RENDER_FAILED)
|
|
504
|
+
|
|
505
|
+
content_matches_template?(content, rendered_template)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def rendered_template_for_cleanup(template_path)
|
|
509
|
+
@rendered_template_cache ||= {}
|
|
510
|
+
@rendered_template_cache[template_path] ||= begin
|
|
511
|
+
# Cleanup comparisons only need the injected documentation comment.
|
|
512
|
+
template_doc_config = { message: DOCS_REFERENCE_MESSAGE }
|
|
513
|
+
template_content = File.read(File.join(self.class.source_root, template_path))
|
|
514
|
+
# Render against current generator options. Any mismatch is treated as non-removable,
|
|
515
|
+
# which is intentional because cleanup should be conservative.
|
|
516
|
+
# Note: files originally generated with --pro or --rsc will not match when the
|
|
517
|
+
# current run omits those options; in that case, we preserve the directory.
|
|
518
|
+
# Templates rely on config[:message] plus a small helper subset exposed by
|
|
519
|
+
# TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?,
|
|
520
|
+
# shakapacker_version_9_or_higher?). Missing method delegates raise
|
|
521
|
+
# NoMethodError and are caught below, treating the file as non-removable.
|
|
522
|
+
# Missing config hash keys return nil silently, so any new config key
|
|
523
|
+
# required by templates must be added to template_doc_config above.
|
|
524
|
+
# Use TemplateRenderContext#erb_binding to avoid leaking method-local
|
|
525
|
+
# variables from rendered_template_for_cleanup into the ERB scope.
|
|
526
|
+
template_render_context = TemplateRenderContext.new(self, template_doc_config)
|
|
527
|
+
ERB.new(template_content, trim_mode: "-").result(template_render_context.erb_binding)
|
|
528
|
+
rescue StandardError => e
|
|
529
|
+
say_status :warning,
|
|
530
|
+
"Could not render template #{template_path} for cleanup check (#{e.class}: #{e.message}); " \
|
|
531
|
+
"treating as non-removable",
|
|
532
|
+
:yellow
|
|
533
|
+
# Rendering failures should never abort installation. Returning a sentinel
|
|
534
|
+
# guarantees a non-match so the file is treated as non-removable.
|
|
535
|
+
# We cache the sentinel intentionally (same options within one generator run)
|
|
536
|
+
# so repeated checks stay consistent and avoid noisy duplicate warnings.
|
|
537
|
+
TEMPLATE_RENDER_FAILED
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def stale_webpack_config_dir
|
|
542
|
+
File.join(destination_root, "config/webpack")
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def stale_webpack_config_entries
|
|
546
|
+
Dir.children(stale_webpack_config_dir).sort
|
|
547
|
+
rescue Errno::EACCES, Errno::ENOENT, Errno::ENOTDIR
|
|
548
|
+
# TOCTOU: directory may disappear between exist? check and read
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def safe_read_cleanup_file(path)
|
|
553
|
+
File.read(path)
|
|
554
|
+
rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::EISDIR, Errno::ENOTDIR
|
|
555
|
+
nil
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def standard_shakapacker_config?(content, strip_comments: false)
|
|
559
|
+
# Keep strip_comments false by default so comment-only edits are treated as
|
|
560
|
+
# potential customizations during cleanup. Callers can pass true when they
|
|
561
|
+
# need historical comment-insensitive replacement matching.
|
|
277
562
|
# Get the expected default config based on Shakapacker version
|
|
278
563
|
expected_configs = shakapacker_default_configs
|
|
279
564
|
|
|
280
565
|
# Check if the content matches any of the known default configurations
|
|
281
|
-
expected_configs.any? { |config| content_matches_template?(content, config) }
|
|
566
|
+
expected_configs.any? { |config| content_matches_template?(content, config, strip_comments: strip_comments) }
|
|
282
567
|
end
|
|
283
568
|
|
|
284
|
-
def content_matches_template?(content, template)
|
|
569
|
+
def content_matches_template?(content, template, strip_comments: false)
|
|
285
570
|
# Normalize whitespace and compare
|
|
286
|
-
normalize_config_content(content) ==
|
|
571
|
+
normalize_config_content(content, strip_comments: strip_comments) ==
|
|
572
|
+
normalize_config_content(template, strip_comments: strip_comments)
|
|
287
573
|
end
|
|
288
574
|
|
|
289
|
-
def normalize_config_content(content)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
575
|
+
def normalize_config_content(content, strip_comments: false)
|
|
576
|
+
normalized_content = content
|
|
577
|
+
if strip_comments
|
|
578
|
+
# Replacement detection historically removed inline // comments too.
|
|
579
|
+
# Note: this also strips `//` in string literals (for example URLs),
|
|
580
|
+
# which can yield false-negatives and prompt for replacement.
|
|
581
|
+
normalized_content = normalized_content.gsub(%r{//.*$}, "")
|
|
582
|
+
.gsub(%r{/\*.*?\*/}m, "")
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Normalize whitespace while preserving comments by default so added comments
|
|
586
|
+
# count as potential customizations and keep cleanup conservative.
|
|
587
|
+
normalized_content.gsub(/\s+/, " ").strip
|
|
295
588
|
end
|
|
296
589
|
|
|
297
590
|
def shakapacker_default_configs
|
|
@@ -47,11 +47,7 @@ module GeneratorMessages
|
|
|
47
47
|
package_manager = detect_package_manager
|
|
48
48
|
shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed: shakapacker_just_installed)
|
|
49
49
|
render_example = build_render_example(component_name: component_name, route: route, rsc: rsc)
|
|
50
|
-
render_label =
|
|
51
|
-
"• Streaming server rendering in app/views/#{route}/index.html.erb:"
|
|
52
|
-
else
|
|
53
|
-
"• Server-side rendering - Enabled with prerender option in app/views/#{route}/index.html.erb:"
|
|
54
|
-
end
|
|
50
|
+
render_label = build_render_label(route: route, rsc: rsc)
|
|
55
51
|
|
|
56
52
|
<<~MSG
|
|
57
53
|
|
|
@@ -90,6 +86,18 @@ module GeneratorMessages
|
|
|
90
86
|
MSG
|
|
91
87
|
end
|
|
92
88
|
|
|
89
|
+
# Uses relative lockfile paths resolved against Dir.pwd, so callers must invoke
|
|
90
|
+
# this while the current working directory is the target Rails app root.
|
|
91
|
+
def detect_package_manager
|
|
92
|
+
# Check for lock files to determine package manager
|
|
93
|
+
return "yarn" if File.exist?("yarn.lock")
|
|
94
|
+
return "pnpm" if File.exist?("pnpm-lock.yaml")
|
|
95
|
+
return "bun" if File.exist?("bun.lock") || File.exist?("bun.lockb")
|
|
96
|
+
|
|
97
|
+
# Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
|
|
98
|
+
"npm"
|
|
99
|
+
end
|
|
100
|
+
|
|
93
101
|
private
|
|
94
102
|
|
|
95
103
|
def build_render_example(component_name:, route:, rsc:)
|
|
@@ -100,6 +108,11 @@ module GeneratorMessages
|
|
|
100
108
|
end
|
|
101
109
|
end
|
|
102
110
|
|
|
111
|
+
def build_render_label(route:, rsc:)
|
|
112
|
+
prefix = rsc ? "Streaming server rendering" : "Server-side rendering - Enabled with prerender option"
|
|
113
|
+
"• #{prefix} in app/views/#{route}/index.html.erb:"
|
|
114
|
+
end
|
|
115
|
+
|
|
103
116
|
def build_process_manager_section
|
|
104
117
|
process_manager = detect_process_manager
|
|
105
118
|
if process_manager
|
|
@@ -196,17 +209,5 @@ module GeneratorMessages
|
|
|
196
209
|
# If version detection fails, don't show a warning to avoid noise
|
|
197
210
|
""
|
|
198
211
|
end
|
|
199
|
-
|
|
200
|
-
def detect_package_manager
|
|
201
|
-
# Check for lock files to determine package manager
|
|
202
|
-
if File.exist?("yarn.lock")
|
|
203
|
-
"yarn"
|
|
204
|
-
elsif File.exist?("pnpm-lock.yaml")
|
|
205
|
-
"pnpm"
|
|
206
|
-
else
|
|
207
|
-
# Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
|
|
208
|
-
"npm"
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
212
|
end
|
|
212
213
|
end
|
|
@@ -8,6 +8,8 @@ require_relative "generator_messages"
|
|
|
8
8
|
require_relative "js_dependency_manager"
|
|
9
9
|
require_relative "pro_setup"
|
|
10
10
|
require_relative "rsc_setup"
|
|
11
|
+
# Load-path require: git_utils lives under react_on_rails/lib, not relative to this generator directory.
|
|
12
|
+
require "react_on_rails/git_utils"
|
|
11
13
|
|
|
12
14
|
module ReactOnRails
|
|
13
15
|
module Generators
|
|
@@ -100,9 +102,7 @@ module ReactOnRails
|
|
|
100
102
|
if installation_prerequisites_met? || options.ignore_warnings?
|
|
101
103
|
invoke_generators
|
|
102
104
|
add_bin_scripts
|
|
103
|
-
|
|
104
|
-
# Redux generator handles its own messages
|
|
105
|
-
add_post_install_message unless options.redux?
|
|
105
|
+
add_post_install_message
|
|
106
106
|
else
|
|
107
107
|
error = <<~MSG.strip
|
|
108
108
|
🚫 React on Rails generator prerequisites not met!
|
|
@@ -148,6 +148,7 @@ module ReactOnRails
|
|
|
148
148
|
# - Without --rsc: Normal behavior (HelloWorld or HelloWorldApp based on --redux)
|
|
149
149
|
if options.redux?
|
|
150
150
|
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript?,
|
|
151
|
+
invoked_by_install: true,
|
|
151
152
|
force: options[:force], skip: options[:skip],
|
|
152
153
|
pretend: options[:pretend] }
|
|
153
154
|
elsif !use_rsc?
|
|
@@ -242,6 +243,7 @@ module ReactOnRails
|
|
|
242
243
|
end
|
|
243
244
|
|
|
244
245
|
def ensure_shakapacker_installed
|
|
246
|
+
@shakapacker_setup_incomplete = false
|
|
245
247
|
return if shakapacker_configured?
|
|
246
248
|
|
|
247
249
|
if options[:pretend]
|
|
@@ -250,14 +252,19 @@ module ReactOnRails
|
|
|
250
252
|
end
|
|
251
253
|
|
|
252
254
|
print_shakapacker_setup_banner
|
|
253
|
-
ensure_shakapacker_in_gemfile
|
|
255
|
+
gemfile_ok = ensure_shakapacker_in_gemfile
|
|
256
|
+
@shakapacker_setup_incomplete = true unless gemfile_ok
|
|
254
257
|
|
|
255
258
|
# NOTE: File.exist?/File.read use Dir.pwd (not destination_root) because
|
|
256
259
|
# Rails generators always run from the destination root. This is consistent
|
|
257
260
|
# with other relative-path file checks in this generator (e.g. shakapacker_configured?).
|
|
258
261
|
yml_content_before = File.exist?(SHAKAPACKER_YML_PATH) ? File.read(SHAKAPACKER_YML_PATH) : nil
|
|
259
262
|
|
|
260
|
-
|
|
263
|
+
if install_shakapacker
|
|
264
|
+
finalize_shakapacker_setup(yml_content_before)
|
|
265
|
+
else
|
|
266
|
+
@shakapacker_setup_incomplete = true
|
|
267
|
+
end
|
|
261
268
|
end
|
|
262
269
|
|
|
263
270
|
# Checks whether "shakapacker" is explicitly declared in this project's Gemfile.
|
|
@@ -297,6 +304,11 @@ module ReactOnRails
|
|
|
297
304
|
end
|
|
298
305
|
|
|
299
306
|
def add_post_install_message
|
|
307
|
+
if shakapacker_setup_incomplete?
|
|
308
|
+
GeneratorMessages.add_warning(incomplete_installation_message)
|
|
309
|
+
return
|
|
310
|
+
end
|
|
311
|
+
|
|
300
312
|
# Determine what route and component will be created by the generator
|
|
301
313
|
if use_rsc? && !options.redux?
|
|
302
314
|
# RSC without Redux: HelloServer replaces HelloWorld
|
|
@@ -315,6 +327,67 @@ module ReactOnRails
|
|
|
315
327
|
))
|
|
316
328
|
end
|
|
317
329
|
|
|
330
|
+
def shakapacker_setup_incomplete?
|
|
331
|
+
# Strict comparison keeps nil (unset) distinct from true.
|
|
332
|
+
@shakapacker_setup_incomplete == true
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def recovery_install_command
|
|
336
|
+
flags = []
|
|
337
|
+
flags << "--redux" if options.redux?
|
|
338
|
+
flags << "--typescript" if options.typescript?
|
|
339
|
+
flags << "--rspack" if options.rspack?
|
|
340
|
+
|
|
341
|
+
if use_rsc?
|
|
342
|
+
flags << "--rsc"
|
|
343
|
+
elsif options.pro?
|
|
344
|
+
flags << "--pro"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
["rails generate react_on_rails:install", *flags].join(" ")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def recovery_working_tree_lines
|
|
351
|
+
[
|
|
352
|
+
"If this run created or changed files, clean up your working tree before rerunning",
|
|
353
|
+
"(commit, stash, or discard the partial changes), or re-run with --ignore-warnings",
|
|
354
|
+
"if you intentionally want to continue on a dirty tree."
|
|
355
|
+
]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def recovery_working_tree_note
|
|
359
|
+
"#{recovery_working_tree_lines.join("\n")}\n"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def recovery_working_tree_step(step_number)
|
|
363
|
+
first_line, *remaining_lines = recovery_working_tree_lines
|
|
364
|
+
(["#{step_number}. #{first_line}"] + remaining_lines.map { |line| " #{line}" }).join("\n")
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def incomplete_installation_message
|
|
368
|
+
package_install_step = "#{GeneratorMessages.detect_package_manager} install"
|
|
369
|
+
|
|
370
|
+
<<~MSG
|
|
371
|
+
|
|
372
|
+
⚠️ React on Rails installation is incomplete.
|
|
373
|
+
─────────────────────────────────────────────────────────────────────────
|
|
374
|
+
Shakapacker setup failed, so this app is not ready to run yet.
|
|
375
|
+
Avoid running ./bin/dev until Shakapacker is installed successfully.
|
|
376
|
+
Note: Some generator files may have been partially created during this run.
|
|
377
|
+
|
|
378
|
+
Next steps:
|
|
379
|
+
1. #{Rainbow('bundle install').cyan}
|
|
380
|
+
2. #{Rainbow('bundle exec rails shakapacker:install').cyan}
|
|
381
|
+
3. #{Rainbow(package_install_step).cyan}
|
|
382
|
+
#{recovery_working_tree_step(4)}
|
|
383
|
+
5. Re-run #{Rainbow(recovery_install_command).cyan}
|
|
384
|
+
(add #{Rainbow('--force').cyan} to overwrite files if needed)
|
|
385
|
+
|
|
386
|
+
Troubleshooting:
|
|
387
|
+
• https://github.com/shakacode/shakapacker/blob/main/docs/installation.md
|
|
388
|
+
MSG
|
|
389
|
+
end
|
|
390
|
+
|
|
318
391
|
def shakapacker_loaded_in_process?(gem_name)
|
|
319
392
|
Gem.loaded_specs.key?(gem_name)
|
|
320
393
|
end
|
|
@@ -366,15 +439,16 @@ module ReactOnRails
|
|
|
366
439
|
end
|
|
367
440
|
|
|
368
441
|
def ensure_shakapacker_in_gemfile
|
|
369
|
-
return if shakapacker_in_gemfile?
|
|
442
|
+
return true if shakapacker_in_gemfile?
|
|
370
443
|
|
|
371
444
|
say "📝 Adding Shakapacker to Gemfile...", :yellow
|
|
372
445
|
# Use with_unbundled_env to prevent inheriting BUNDLE_GEMFILE from parent process
|
|
373
446
|
# See: https://github.com/shakacode/react_on_rails/issues/2287
|
|
374
447
|
success = Bundler.with_unbundled_env { system("bundle add shakapacker --strict") }
|
|
375
|
-
return if success
|
|
448
|
+
return true if success
|
|
376
449
|
|
|
377
450
|
handle_shakapacker_gemfile_error
|
|
451
|
+
false
|
|
378
452
|
end
|
|
379
453
|
|
|
380
454
|
def install_shakapacker
|
|
@@ -443,7 +517,8 @@ module ReactOnRails
|
|
|
443
517
|
Please try manually:
|
|
444
518
|
bundle add shakapacker --strict
|
|
445
519
|
|
|
446
|
-
|
|
520
|
+
#{recovery_working_tree_note}
|
|
521
|
+
Then re-run: #{recovery_install_command}
|
|
447
522
|
MSG
|
|
448
523
|
GeneratorMessages.add_error(error)
|
|
449
524
|
raise Thor::Error, error unless options.ignore_warnings?
|
|
@@ -464,7 +539,8 @@ module ReactOnRails
|
|
|
464
539
|
2. Run: bundle install
|
|
465
540
|
3. Try manually: bundle exec rails shakapacker:install
|
|
466
541
|
4. Check for error output above
|
|
467
|
-
5
|
|
542
|
+
#{recovery_working_tree_step(5)}
|
|
543
|
+
6. Re-run: #{recovery_install_command}
|
|
468
544
|
|
|
469
545
|
Need help? Visit: https://github.com/shakacode/shakapacker/blob/main/docs/installation.md
|
|
470
546
|
MSG
|
|
@@ -18,6 +18,11 @@ module ReactOnRails
|
|
|
18
18
|
desc: "Generate TypeScript files",
|
|
19
19
|
aliases: "-T"
|
|
20
20
|
|
|
21
|
+
class_option :invoked_by_install,
|
|
22
|
+
type: :boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
hide: true
|
|
25
|
+
|
|
21
26
|
def create_redux_directories
|
|
22
27
|
# Create auto-bundling directory structure for Redux
|
|
23
28
|
empty_directory("app/javascript/src/HelloWorldApp/ror_components")
|
|
@@ -90,6 +95,8 @@ module ReactOnRails
|
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
def add_redux_specific_messages
|
|
98
|
+
return if options.invoked_by_install?
|
|
99
|
+
|
|
93
100
|
# Append Redux-specific post-install instructions
|
|
94
101
|
GeneratorMessages.add_info(
|
|
95
102
|
GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp", route: "hello_world")
|