react_on_rails 17.0.1 → 17.1.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/Gemfile.lock +1 -1
- data/docs/agent/README.md +26 -0
- data/docs/agent/doctor-fix-loop.md +25 -0
- data/docs/agent/install-and-upgrade.md +162 -0
- data/docs/agent/rsc-adoption.md +31 -0
- data/docs/agent/streaming-debug.md +57 -0
- data/lib/generators/react_on_rails/base_generator.rb +4 -4
- data/lib/generators/react_on_rails/generator_helper.rb +12 -1
- data/lib/generators/react_on_rails/install_generator.rb +50 -5
- data/lib/generators/react_on_rails/js_dependency_manager.rb +1 -1
- data/lib/generators/react_on_rails/pro_generator.rb +5 -3
- data/lib/generators/react_on_rails/pro_setup.rb +51 -19
- data/lib/generators/react_on_rails/rsc_generator.rb +18 -1
- data/lib/generators/react_on_rails/rsc_setup/client_references.rb +63 -25
- data/lib/generators/react_on_rails/rsc_setup.rb +141 -39
- data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +31 -3
- data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev-static-assets → Procfile.dev-static-assets.tt} +1 -1
- data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev → Procfile.dev.tt} +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +18 -42
- data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb +215 -0
- data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md +71 -0
- data/lib/react_on_rails/agent_guardrails.rb +223 -0
- data/lib/react_on_rails/dev/service_checker.rb +1 -1
- data/lib/react_on_rails/doctor.rb +880 -48
- data/lib/react_on_rails/doctor_schema.rb +192 -0
- data/lib/react_on_rails/engine.rb +2 -2
- data/lib/react_on_rails/helper.rb +17 -10
- data/lib/react_on_rails/length_prefixed_parser.rb +22 -14
- data/lib/react_on_rails/lenient_json.rb +0 -0
- data/lib/react_on_rails/prerender_error.rb +38 -8
- data/lib/react_on_rails/react_component/render_options.rb +1 -1
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +168 -15
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/tasks/agent_guardrails.rake +21 -0
- data/rakelib/run_rspec.rake +14 -0
- data/sig/react_on_rails/prerender_error.rbs +2 -1
- data/skills/doctor-fix-loop/SKILL.md +26 -0
- data/skills/install-and-upgrade/SKILL.md +24 -0
- data/skills/rsc-adoption/SKILL.md +28 -0
- data/skills/streaming-debug/SKILL.md +53 -0
- metadata +18 -4
- data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch +0 -26
|
@@ -594,14 +594,10 @@ module ReactOnRails
|
|
|
594
594
|
# those configs manually.
|
|
595
595
|
return true if sections.empty?
|
|
596
596
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
scoped_rsc_client_references_defined?(content)
|
|
601
|
-
else
|
|
602
|
-
rsc_plugin_body_has_top_level_key?(body, "clientReferences")
|
|
597
|
+
generated_rsc_client_references_defined?(content) &&
|
|
598
|
+
sections.all? do |section|
|
|
599
|
+
rsc_plugin_body_has_top_level_scoped_client_references?(section.fetch(:body))
|
|
603
600
|
end
|
|
604
|
-
end
|
|
605
601
|
end
|
|
606
602
|
|
|
607
603
|
# Counts active `new RSCWebpackPlugin(...)` calls whose first argument is present but
|
|
@@ -1229,13 +1225,13 @@ module ReactOnRails
|
|
|
1229
1225
|
# the same comment-, string-, and brace-aware semantics as the rewrite path.
|
|
1230
1226
|
def rsc_plugin_body_has_top_level_scoped_client_references?(body)
|
|
1231
1227
|
[
|
|
1232
|
-
/\bclientReferences\
|
|
1233
|
-
/(['"`])clientReferences\1
|
|
1234
|
-
].any? do |pattern|
|
|
1228
|
+
[/\bclientReferences\b/, false],
|
|
1229
|
+
[/(['"`])clientReferences\1/, true]
|
|
1230
|
+
].any? do |pattern, quoted|
|
|
1235
1231
|
search_from = 0
|
|
1236
1232
|
|
|
1237
1233
|
while (match = pattern.match(body, search_from))
|
|
1238
|
-
return true if
|
|
1234
|
+
return true if top_level_scoped_client_references_match?(body, match, quoted:)
|
|
1239
1235
|
|
|
1240
1236
|
search_from = match.end(0)
|
|
1241
1237
|
end
|
|
@@ -1244,6 +1240,20 @@ module ReactOnRails
|
|
|
1244
1240
|
false
|
|
1245
1241
|
end
|
|
1246
1242
|
|
|
1243
|
+
def top_level_scoped_client_references_match?(body, match, quoted:)
|
|
1244
|
+
return false unless js_top_level_position?(body, match.begin(0))
|
|
1245
|
+
return false unless rsc_plugin_body_key_position?(body, match.begin(0), match.end(0), quoted:)
|
|
1246
|
+
|
|
1247
|
+
colon_index = first_js_token_index(body, match.end(0))
|
|
1248
|
+
return false unless colon_index && body[colon_index] == ":"
|
|
1249
|
+
|
|
1250
|
+
value_index = first_js_token_index(body, colon_index + 1)
|
|
1251
|
+
identifier = "rscClientReferences"
|
|
1252
|
+
return false unless value_index && body[value_index, identifier.length] == identifier
|
|
1253
|
+
|
|
1254
|
+
!body[value_index + identifier.length]&.match?(/[A-Za-z0-9_$]/)
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1247
1257
|
def splice_client_references_into_rsc_plugin_body(body, is_server_match)
|
|
1248
1258
|
return splice_client_references_at_close_brace(body) if body.include?("\n")
|
|
1249
1259
|
# Other options follow `isServer:` on the same line — append after the last option so
|
|
@@ -1516,20 +1526,48 @@ module ReactOnRails
|
|
|
1516
1526
|
end
|
|
1517
1527
|
|
|
1518
1528
|
def generated_rsc_client_references_defined?(content)
|
|
1519
|
-
#
|
|
1520
|
-
#
|
|
1521
|
-
#
|
|
1522
|
-
#
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1529
|
+
# A fallback directory object plus a binding named `rscClientReferences` is not enough:
|
|
1530
|
+
# legacy configs can wire that fallback directly into the plugin and bypass graph-derived
|
|
1531
|
+
# manifest selection. Verify the generated IIFE shape and its essential manifest path in
|
|
1532
|
+
# executable code while allowing formatter-controlled whitespace to vary.
|
|
1533
|
+
iife_body = generated_rsc_client_references_iife_body(content)
|
|
1534
|
+
return false unless iife_body
|
|
1535
|
+
|
|
1536
|
+
scoped_object_literal_defined?(content, "fallbackRscClientReferences") &&
|
|
1537
|
+
js_code_matches?(
|
|
1538
|
+
iife_body,
|
|
1539
|
+
%r{\bdefaultRefsJson\s*=\s*resolve\(\s*['"]ssr-generated/rsc-client-references\.json['"]\s*,?\s*\)}
|
|
1540
|
+
) &&
|
|
1541
|
+
js_code_matches?(iife_body, /\breadManifestReferences\s*=\s*\(/) &&
|
|
1542
|
+
js_code_matches?(iife_body, /\breturn\s+readManifestReferences\(\s*defaultRefsJson\s*\)/)
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
def generated_rsc_client_references_iife_body(content)
|
|
1546
|
+
declaration = /^[ \t]*(?:const|let|var)\s+rscClientReferences\s*=\s*\(\s*\(\s*\)\s*=>\s*\{/
|
|
1547
|
+
content.to_enum(:scan, declaration).each do
|
|
1548
|
+
match = Regexp.last_match
|
|
1549
|
+
next unless js_top_level_position?(content, match.begin(0))
|
|
1550
|
+
|
|
1551
|
+
open_brace = match.end(0) - 1
|
|
1552
|
+
close_brace = matching_js_closing_brace(content, open_brace)
|
|
1553
|
+
next unless close_brace
|
|
1554
|
+
next unless content[(close_brace + 1)..].match?(/\A\s*\)\s*\(\s*\)\s*;?/)
|
|
1555
|
+
|
|
1556
|
+
return content[(open_brace + 1)...close_brace]
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
nil
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
def js_code_matches?(content, pattern)
|
|
1563
|
+
search_from = 0
|
|
1564
|
+
while (match = pattern.match(content, search_from))
|
|
1565
|
+
return true if js_code_position?(content, match.begin(0))
|
|
1566
|
+
|
|
1567
|
+
search_from = match.end(0)
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
false
|
|
1533
1571
|
end
|
|
1534
1572
|
|
|
1535
1573
|
def scoped_object_literal_defined?(content, variable_name)
|
|
@@ -183,7 +183,7 @@ module ReactOnRails
|
|
|
183
183
|
⚠️ Procfile.dev not found. Skipping RSC bundle watcher addition.
|
|
184
184
|
|
|
185
185
|
You'll need to add the RSC bundle watcher to your process manager manually:
|
|
186
|
-
rsc-bundle: RSC_BUNDLE_ONLY=true
|
|
186
|
+
rsc-bundle: RSC_BUNDLE_ONLY=true #{shakapacker_watch_command}
|
|
187
187
|
MSG
|
|
188
188
|
return
|
|
189
189
|
end
|
|
@@ -198,7 +198,7 @@ module ReactOnRails
|
|
|
198
198
|
rsc_watcher_line = <<~PROCFILE
|
|
199
199
|
|
|
200
200
|
# React on Rails Pro - RSC bundle watcher
|
|
201
|
-
rsc-bundle: RSC_BUNDLE_ONLY=true
|
|
201
|
+
rsc-bundle: RSC_BUNDLE_ONLY=true #{shakapacker_watch_command}
|
|
202
202
|
PROCFILE
|
|
203
203
|
|
|
204
204
|
append_to_file("Procfile.dev", rsc_watcher_line)
|
|
@@ -354,62 +354,147 @@ module ReactOnRails
|
|
|
354
354
|
say "✅ Updated webpack configs for RSC", :green
|
|
355
355
|
end
|
|
356
356
|
|
|
357
|
+
# Structural matchers shared by the ServerClientOrBoth (SCOB) transforms in
|
|
358
|
+
# update_server_client_or_both_for_rsc / update_scob_default_bundle_output and
|
|
359
|
+
# their verifier counterparts in check_rsc_scob_config. Each keys off a real
|
|
360
|
+
# JS declaration/branch/assignment shape rather than a bare substring, so:
|
|
361
|
+
# * a stray mention in a comment or string literal cannot suppress a
|
|
362
|
+
# transform (which would leave dangling `rscConfig` references →
|
|
363
|
+
# `ReferenceError: rscConfig is not defined` at runtime) or fool the
|
|
364
|
+
# verifier into reporting a partial setup as complete; and
|
|
365
|
+
# * linter whitespace reflow (e.g. `[ clientConfig, serverConfig ]`) is
|
|
366
|
+
# tolerated, matching the rest of this file.
|
|
367
|
+
# A transform's skip-guard and its verifier check use the *same* constant so
|
|
368
|
+
# the two can never disagree on what "already applied" means.
|
|
369
|
+
RSC_WEBPACK_CONFIG_IMPORT =
|
|
370
|
+
%r{const\s+rscWebpackConfig\s*=\s*require\(\s*'\./rscWebpackConfig'\s*\)}
|
|
371
|
+
RSC_CONFIG_DECLARATION = /const\s+rscConfig\s*=\s*rscWebpackConfig\(\s*\)/
|
|
372
|
+
ENVSPECIFIC_RSC_CALL = /envSpecific\(\s*clientConfig\s*,\s*serverConfig\s*,\s*rscConfig\s*\)/
|
|
373
|
+
# The RSC_BUNDLE_ONLY branch, matched by its `process.env.RSC_BUNDLE_ONLY`
|
|
374
|
+
# condition together with the `result = rscConfig` assignment in its body —
|
|
375
|
+
# a bare `RSC_BUNDLE_ONLY` comment mention satisfies neither.
|
|
376
|
+
RSC_BUNDLE_ONLY_BRANCH = /process\.env\.RSC_BUNDLE_ONLY\s*\)[^}]*?result\s*=\s*rscConfig\s*;?/
|
|
377
|
+
# The 2-element default result array we rewrite, and the 3-element RSC-aware
|
|
378
|
+
# array it becomes — both fully whitespace tolerant (inner brackets/commas).
|
|
379
|
+
DEFAULT_RESULT_ARRAY = /result\s*=\s*\[\s*clientConfig\s*,\s*serverConfig\s*\]\s*;?/
|
|
380
|
+
RSC_RESULT_ARRAY = /result\s*=\s*\[\s*clientConfig\s*,\s*serverConfig\s*,\s*rscConfig\s*\]/
|
|
381
|
+
# The RSC-aware default-build log, matched by its `console.log('…')` call
|
|
382
|
+
# shape so a bare comment mention of the message text can't no-op the rewrite.
|
|
383
|
+
RSC_BUNDLES_LOG =
|
|
384
|
+
/console\.log\(\s*'\[React on Rails\] Creating client, server, and RSC bundles\.'\s*\)/
|
|
385
|
+
# The bare `} else {` default-build branch: the one whose body reaches the
|
|
386
|
+
# 2-element default result assignment with no intervening `}`. The
|
|
387
|
+
# RSC_BUNDLE_ONLY branch is spliced immediately before it. `[^}]` cannot span
|
|
388
|
+
# an earlier else's closing brace, so an unrelated earlier `} else {` (e.g. an
|
|
389
|
+
# `if (envSpecific) {…} else {…}`) is never matched, and `\} else \{` never
|
|
390
|
+
# matches `} else if (…) {`. Keyed off the default result assignment, not the
|
|
391
|
+
# `// default …` comment, so comment reflow/reword/removal can't no-op it.
|
|
392
|
+
DEFAULT_BUILD_BRANCH_ANCHOR = /\n(\s*\}\s*else\s*\{[^}]*?#{DEFAULT_RESULT_ARRAY})/
|
|
393
|
+
|
|
394
|
+
# Pre-transform "source" shapes the gsub_file rewrites target (as opposed to
|
|
395
|
+
# the "already applied" markers above). Each is whitespace/semicolon tolerant
|
|
396
|
+
# and anchored to a complete statement so it can't match inside a larger
|
|
397
|
+
# construct; every rewrite is applied via replace_single_match so an
|
|
398
|
+
# incidental extra match in a customized file can't corrupt it.
|
|
399
|
+
#
|
|
400
|
+
# SERVER_WEBPACK_CONFIG_IMPORT tolerates a destructured `{ default: … }` or a
|
|
401
|
+
# plain binding AND an optional trailing `.default`
|
|
402
|
+
# (`require('./serverWebpackConfig').default;`), and captures through the end
|
|
403
|
+
# of the statement so the RSC import is inserted AFTER the complete require —
|
|
404
|
+
# never before a trailing `.default`, which would produce invalid JS.
|
|
405
|
+
SERVER_WEBPACK_CONFIG_IMPORT =
|
|
406
|
+
%r{(const\s+(?:\{[^\}]*\}|\w+)\s*=\s*require\(\s*'\./serverWebpackConfig'\s*\)(?:\s*\.\s*default)?\s*;?)}
|
|
407
|
+
SERVER_CONFIG_INVOCATION = /^(\s*const\s+serverConfig\s*=\s*serverWebpackConfig\(\s*\)\s*;?)\s*$/
|
|
408
|
+
ENVSPECIFIC_DEFAULT_CALL = /envSpecific\(\s*clientConfig\s*,\s*serverConfig\s*\)\s*;?/
|
|
409
|
+
DEFAULT_BUNDLES_LOG =
|
|
410
|
+
/console\.log\(\s*'\[React on Rails\] Creating both client and server bundles\.'\s*\)\s*;?/
|
|
411
|
+
|
|
357
412
|
def update_server_client_or_both_for_rsc
|
|
358
413
|
config_path = resolve_server_client_or_both_path
|
|
359
414
|
return unless config_path
|
|
360
415
|
|
|
416
|
+
# `content` is read once as a pre-run snapshot; every guard below checks
|
|
417
|
+
# this snapshot rather than re-reading the file after each gsub_file.
|
|
418
|
+
# That is safe only because the five transforms are textually
|
|
419
|
+
# independent — each skip-guard matches a distinct real JS shape and none
|
|
420
|
+
# of the inserted strings satisfies a later guard (e.g. the import line
|
|
421
|
+
# `const rscWebpackConfig = require(...)` never matches RSC_CONFIG_DECLARATION,
|
|
422
|
+
# whose identifier is `rscConfig`). Preserve that invariant: do not add a
|
|
423
|
+
# transform whose output would flip a later guard, or re-read `content`.
|
|
361
424
|
content = File.read(File.join(destination_root, config_path))
|
|
362
425
|
|
|
363
|
-
#
|
|
364
|
-
|
|
426
|
+
# Insert the RSC import AFTER the complete server import statement
|
|
427
|
+
# (SERVER_WEBPACK_CONFIG_IMPORT captures through an optional `.default;`).
|
|
428
|
+
unless content.match?(RSC_WEBPACK_CONFIG_IMPORT)
|
|
429
|
+
replace_single_match(
|
|
430
|
+
config_path, content, SERVER_WEBPACK_CONFIG_IMPORT,
|
|
431
|
+
"\\1\nconst rscWebpackConfig = require('./rscWebpackConfig');"
|
|
432
|
+
)
|
|
433
|
+
end
|
|
365
434
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
435
|
+
unless content.match?(RSC_CONFIG_DECLARATION)
|
|
436
|
+
replace_single_match(
|
|
437
|
+
config_path, content, SERVER_CONFIG_INVOCATION,
|
|
438
|
+
"\\1\n\n const rscConfig = rscWebpackConfig();"
|
|
439
|
+
)
|
|
440
|
+
end
|
|
372
441
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
442
|
+
unless content.match?(ENVSPECIFIC_RSC_CALL)
|
|
443
|
+
replace_single_match(
|
|
444
|
+
config_path, content, ENVSPECIFIC_DEFAULT_CALL,
|
|
445
|
+
"envSpecific(clientConfig, serverConfig, rscConfig);"
|
|
446
|
+
)
|
|
447
|
+
end
|
|
379
448
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
insert_rsc_bundle_only_branch(config_path, content)
|
|
450
|
+
|
|
451
|
+
update_scob_default_bundle_output(config_path, content)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def insert_rsc_bundle_only_branch(config_path, content)
|
|
455
|
+
return if content.match?(RSC_BUNDLE_ONLY_BRANCH)
|
|
386
456
|
|
|
387
|
-
# Add RSC_BUNDLE_ONLY handling before the else block
|
|
388
457
|
rsc_bundle_handling = <<~JS.chomp
|
|
389
458
|
} else if (process.env.RSC_BUNDLE_ONLY) {
|
|
390
459
|
// eslint-disable-next-line no-console
|
|
391
460
|
console.log('[React on Rails] Creating only the RSC bundle.');
|
|
392
461
|
result = rscConfig;
|
|
393
462
|
JS
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
463
|
+
# Splice in the branch only when exactly one default-build branch is
|
|
464
|
+
# identifiable (replace_single_match bails on 0 or 2+).
|
|
465
|
+
replace_single_match(
|
|
466
|
+
config_path, content, DEFAULT_BUILD_BRANCH_ANCHOR,
|
|
397
467
|
"\n #{rsc_bundle_handling}\n\\1"
|
|
398
468
|
)
|
|
469
|
+
end
|
|
399
470
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
471
|
+
def update_scob_default_bundle_output(config_path, content)
|
|
472
|
+
unless content.match?(RSC_BUNDLES_LOG)
|
|
473
|
+
replace_single_match(
|
|
474
|
+
config_path, content, DEFAULT_BUNDLES_LOG,
|
|
475
|
+
"console.log('[React on Rails] Creating client, server, and RSC bundles.');"
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
return if content.match?(RSC_RESULT_ARRAY)
|
|
480
|
+
|
|
481
|
+
replace_single_match(
|
|
482
|
+
config_path, content, DEFAULT_RESULT_ARRAY,
|
|
409
483
|
"result = [clientConfig, serverConfig, rscConfig];"
|
|
410
484
|
)
|
|
411
485
|
end
|
|
412
486
|
|
|
487
|
+
# Apply a gsub_file rewrite only when `pattern` matches exactly once in the
|
|
488
|
+
# pre-run snapshot. Thor's gsub_file rewrites every textual match, so bailing
|
|
489
|
+
# on 0 or 2+ matches keeps a customized file (an incidental extra match, or a
|
|
490
|
+
# missing anchor) from being corrupted — a miss is surfaced by
|
|
491
|
+
# check_rsc_scob_config / manual review rather than silently mis-edited.
|
|
492
|
+
def replace_single_match(config_path, content, pattern, replacement)
|
|
493
|
+
return unless content.scan(pattern).length == 1
|
|
494
|
+
|
|
495
|
+
gsub_file(config_path, pattern, replacement)
|
|
496
|
+
end
|
|
497
|
+
|
|
413
498
|
def update_server_webpack_config_for_rsc
|
|
414
499
|
config_path = destination_config_path("config/webpack/serverWebpackConfig.js")
|
|
415
500
|
full_path = File.join(destination_root, config_path)
|
|
@@ -578,7 +663,7 @@ module ReactOnRails
|
|
|
578
663
|
missing.concat(stale_inactive_rsc_plugin_messages(content, "serverWebpackConfig.js"))
|
|
579
664
|
warn_non_object_literal_rsc_plugin_options_for_config(content)
|
|
580
665
|
unless rsc_plugin_client_references_configured?(content, is_server: true)
|
|
581
|
-
missing << "generated
|
|
666
|
+
missing << "generated manifest-backed clientReferences resolver in serverWebpackConfig.js"
|
|
582
667
|
end
|
|
583
668
|
elsif inactive_rsc_plugin_symbol_in_js_code?(content)
|
|
584
669
|
missing << "#{rsc_plugin_class_name} in serverWebpackConfig.js " \
|
|
@@ -600,7 +685,7 @@ module ReactOnRails
|
|
|
600
685
|
missing.concat(stale_inactive_rsc_plugin_messages(content, "clientWebpackConfig.js"))
|
|
601
686
|
warn_non_object_literal_rsc_plugin_options_for_config(content)
|
|
602
687
|
unless rsc_plugin_client_references_configured?(content, is_server: false)
|
|
603
|
-
missing << "generated
|
|
688
|
+
missing << "generated manifest-backed clientReferences resolver in clientWebpackConfig.js"
|
|
604
689
|
end
|
|
605
690
|
elsif inactive_rsc_plugin_symbol_in_js_code?(content)
|
|
606
691
|
missing << "#{rsc_plugin_class_name} in clientWebpackConfig.js " \
|
|
@@ -616,7 +701,24 @@ module ReactOnRails
|
|
|
616
701
|
return [] unless scob_path
|
|
617
702
|
|
|
618
703
|
content = File.read(File.join(destination_root, scob_path))
|
|
619
|
-
|
|
704
|
+
missing = []
|
|
705
|
+
# Each check uses the SAME structural matcher as the corresponding
|
|
706
|
+
# transform's skip-guard, so a comment/string mention can neither suppress
|
|
707
|
+
# the transform nor mask its absence, and the two can never disagree.
|
|
708
|
+
missing << "rscWebpackConfig import in ServerClientOrBoth.js" unless content.match?(RSC_WEBPACK_CONFIG_IMPORT)
|
|
709
|
+
unless content.match?(RSC_CONFIG_DECLARATION)
|
|
710
|
+
missing << "rscConfig declaration (const rscConfig = rscWebpackConfig()) in ServerClientOrBoth.js"
|
|
711
|
+
end
|
|
712
|
+
unless content.match?(ENVSPECIFIC_RSC_CALL)
|
|
713
|
+
missing << "envSpecific(clientConfig, serverConfig, rscConfig) call in ServerClientOrBoth.js"
|
|
714
|
+
end
|
|
715
|
+
# The two default-branch transforms share the `} else {` anchor in
|
|
716
|
+
# insert_rsc_bundle_only_branch, which deliberately no-ops on a customized
|
|
717
|
+
# file where a single default branch can't be identified — so their success
|
|
718
|
+
# is NOT implied by the invocation above and a silent miss must be surfaced.
|
|
719
|
+
missing << "RSC_BUNDLE_ONLY branch in ServerClientOrBoth.js" unless content.match?(RSC_BUNDLE_ONLY_BRANCH)
|
|
720
|
+
missing << "RSC-aware default result array in ServerClientOrBoth.js" unless content.match?(RSC_RESULT_ARRAY)
|
|
721
|
+
missing
|
|
620
722
|
end
|
|
621
723
|
|
|
622
724
|
def warn_non_object_literal_rsc_plugin_options_for_config(content)
|
|
@@ -640,7 +742,7 @@ module ReactOnRails
|
|
|
640
742
|
@non_object_literal_rsc_plugin_options_warned = true
|
|
641
743
|
GeneratorMessages.add_warning(
|
|
642
744
|
"#{rsc_plugin_class_name} calls use non-object-literal options in one or more bundler configs, " \
|
|
643
|
-
"so the generator cannot verify whether
|
|
745
|
+
"so the generator cannot verify whether the generated manifest-backed resolver is configured. " \
|
|
644
746
|
"Please verify your bundler configs manually."
|
|
645
747
|
)
|
|
646
748
|
end
|
|
@@ -149,13 +149,41 @@ A Redux store wasn't registered before a component that depends on it rendered.
|
|
|
149
149
|
|
|
150
150
|
## 6. Diagnose your setup
|
|
151
151
|
|
|
152
|
-
Before guessing, run the doctor
|
|
152
|
+
Before guessing, run the doctor in its stable machine-readable mode — it checks
|
|
153
|
+
configuration, bundles, and dependencies and returns copy-promptable remediation:
|
|
153
154
|
|
|
154
155
|
```bash
|
|
155
|
-
bin/rails react_on_rails:doctor
|
|
156
|
+
bin/rails react_on_rails:doctor FORMAT=json
|
|
156
157
|
```
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
Exit code `0` means there are no errors (warnings may still be present); exit code
|
|
160
|
+
`1` means at least one error must be fixed. Use each non-passing check's stable
|
|
161
|
+
`id`, `severity`, `message`, and `remediation.prompt`, then rerun the command until
|
|
162
|
+
the report no longer contains errors. The JSON contract is documented at
|
|
163
|
+
https://reactonrails.com/docs/api-reference/doctor.
|
|
164
|
+
|
|
165
|
+
## 7. Use the bundled docs and skills first
|
|
166
|
+
|
|
167
|
+
Use the installed gem directory as the canonical, reliable lookup for version-matched
|
|
168
|
+
guidance:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
bundle show react_on_rails
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
From the returned directory, read `docs/agent/README.md` and load the matching workflow
|
|
175
|
+
skill when the task fits:
|
|
176
|
+
|
|
177
|
+
- Install or upgrade: `skills/install-and-upgrade/SKILL.md`
|
|
178
|
+
- Adopt React Server Components (Pro): `skills/rsc-adoption/SKILL.md`
|
|
179
|
+
- Debug streaming SSR (Pro): `skills/streaming-debug/SKILL.md`
|
|
180
|
+
- Run the doctor fix loop: `skills/doctor-fix-loop/SKILL.md`
|
|
181
|
+
|
|
182
|
+
An optional direct-dependency path is `node_modules/react-on-rails/`, but use it only when
|
|
183
|
+
that directory exists. Do not assume it exists: Pro installations may omit the direct base
|
|
184
|
+
npm dependency, and pnpm may isolate a transitive copy.
|
|
185
|
+
|
|
186
|
+
## 8. Where the hosted reference lives
|
|
159
187
|
|
|
160
188
|
- Hosted docs: https://reactonrails.com/docs/
|
|
161
189
|
- Getting-started tutorial: https://reactonrails.com/docs/getting-started/tutorial/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
web: bin/rails server -p ${PORT:-3000}
|
|
2
|
-
js:
|
|
2
|
+
js: <%= shakapacker_watch_command %>
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
# For manual worktree setup (no base-port tool), see .env.example.
|
|
8
8
|
rails: bundle exec rails s -p ${PORT:-3000}
|
|
9
9
|
dev-server: bin/shakapacker-dev-server
|
|
10
|
-
server-bundle: SERVER_BUNDLE_ONLY=true
|
|
10
|
+
server-bundle: SERVER_BUNDLE_ONLY=true <%= shakapacker_watch_command %>
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/serverWebpackConfig.js") %>
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { config } = require('shakapacker');
|
|
4
4
|
const commonWebpackConfig = require('./commonWebpackConfig');
|
|
5
5
|
|
|
6
6
|
const bundler = config.assets_bundler === 'rspack'
|
|
@@ -17,18 +17,18 @@ const rscClientReferences = {
|
|
|
17
17
|
};
|
|
18
18
|
<% end -%>
|
|
19
19
|
|
|
20
|
+
// Normalizes an entry of a webpack/rspack `rule.use` array to its loader path.
|
|
21
|
+
// Entries may be a bare string, a `{ loader, options }` object, or null.
|
|
22
|
+
function getLoaderPath(item) {
|
|
23
|
+
if (typeof item === 'string') return item;
|
|
24
|
+
if (item && typeof item.loader === 'string') return item.loader;
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
<% if use_pro? -%>
|
|
21
29
|
function extractLoader(rule, loaderName) {
|
|
22
30
|
if (!Array.isArray(rule.use)) return null;
|
|
23
|
-
return rule.use.find((item) =>
|
|
24
|
-
let testValue = '';
|
|
25
|
-
if (typeof item === 'string') {
|
|
26
|
-
testValue = item;
|
|
27
|
-
} else if (item && typeof item.loader === 'string') {
|
|
28
|
-
testValue = item.loader;
|
|
29
|
-
}
|
|
30
|
-
return testValue.includes(loaderName);
|
|
31
|
-
});
|
|
31
|
+
return rule.use.find((item) => getLoaderPath(item).includes(loaderName));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
<% end -%>
|
|
@@ -38,10 +38,6 @@ const configureServer = (rscBundle = false) => {
|
|
|
38
38
|
<% else -%>
|
|
39
39
|
const configureServer = () => {
|
|
40
40
|
<% end -%>
|
|
41
|
-
// We need to use "merge" because the clientConfigObject, EVEN after running
|
|
42
|
-
// toWebpackConfig() is a mutable GLOBAL. Thus any changes, like modifying the
|
|
43
|
-
// entry value will result in changing the client config!
|
|
44
|
-
// Using webpack-merge into an empty object avoids this issue.
|
|
45
41
|
const serverWebpackConfig = commonWebpackConfig();
|
|
46
42
|
|
|
47
43
|
// We just want the single server bundle entry
|
|
@@ -63,15 +59,10 @@ const configureServer = () => {
|
|
|
63
59
|
serverWebpackConfig.module.rules.forEach((loader) => {
|
|
64
60
|
if (loader.use && loader.use.filter) {
|
|
65
61
|
loader.use = loader.use.filter((item) => {
|
|
66
|
-
|
|
67
|
-
if (typeof item === 'string') {
|
|
68
|
-
testValue = item;
|
|
69
|
-
} else if (item && typeof item.loader === 'string') {
|
|
70
|
-
testValue = item.loader;
|
|
71
|
-
}
|
|
62
|
+
const loaderPath = getLoaderPath(item);
|
|
72
63
|
return !(
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
loaderPath.includes('mini-css-extract-plugin') ||
|
|
65
|
+
loaderPath.includes('cssExtractLoader') // Rspack uses this path
|
|
75
66
|
);
|
|
76
67
|
});
|
|
77
68
|
}
|
|
@@ -162,29 +153,14 @@ const configureServer = () => {
|
|
|
162
153
|
if (Array.isArray(rule.use)) {
|
|
163
154
|
// remove the mini-css-extract-plugin and style-loader
|
|
164
155
|
rule.use = rule.use.filter((item) => {
|
|
165
|
-
|
|
166
|
-
if (typeof item === 'string') {
|
|
167
|
-
testValue = item;
|
|
168
|
-
} else if (item && typeof item.loader === 'string') {
|
|
169
|
-
testValue = item.loader;
|
|
170
|
-
}
|
|
156
|
+
const loaderPath = getLoaderPath(item);
|
|
171
157
|
return !(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
loaderPath.includes('mini-css-extract-plugin') ||
|
|
159
|
+
loaderPath.includes('cssExtractLoader') || // Rspack uses this path
|
|
160
|
+
loaderPath === 'style-loader'
|
|
175
161
|
);
|
|
176
162
|
});
|
|
177
|
-
const cssLoader = rule.use.find((item) =>
|
|
178
|
-
let testValue = '';
|
|
179
|
-
|
|
180
|
-
if (typeof item === 'string') {
|
|
181
|
-
testValue = item;
|
|
182
|
-
} else if (item && typeof item.loader === 'string') {
|
|
183
|
-
testValue = item.loader;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return testValue.includes('css-loader');
|
|
187
|
-
});
|
|
163
|
+
const cssLoader = rule.use.find((item) => getLoaderPath(item).includes('css-loader'));
|
|
188
164
|
if (cssLoader && cssLoader.options && cssLoader.options.modules) {
|
|
189
165
|
cssLoader.options.modules = {
|
|
190
166
|
...(typeof cssLoader.options.modules === 'object' ? cssLoader.options.modules : {}),
|