Class: ReactOnRails::Doctor

Inherits:
Object
  • Object
show all
Includes:
ConfigPathResolver, RscRspackSupport, ShakapackerConfigHelpers
Defined in:
lib/react_on_rails/doctor.rb

Overview

rubocop:disable Metrics/ClassLength, Metrics/AbcSize

Constant Summary collapse

MESSAGE_COLORS =
{
  error: :red,
  warning: :yellow,
  success: :green,
  info: :blue
}.freeze
RSPEC_HELPER_FILES =
["spec/rails_helper.rb", "spec/spec_helper.rb"].freeze
MINITEST_HELPER_FILE =
"test/test_helper.rb"
DEFAULT_BUILD_TEST_COMMAND =
'config.build_test_command = "RAILS_ENV=test bin/shakapacker"'
SERVER_BUNDLE_SOURCE_EXTENSIONS =
%w[.js .jsx .ts .tsx .mjs .cjs].freeze
CUSTOM_LAUNCHER_INDICATOR_FILES =
%w[dev].freeze
RAILS_SERVER_COMMAND_REGEX =
%r{\b(?:(?:bin/)?rails\s+(?:server|s)|puma|unicorn|rackup|passenger\s+start)\b}
DEPRECATED_RENDERER_CACHE_TASK =

Deprecated-renderer-cache scan (used by check_deprecated_renderer_cache_task): look for references to the old pre_stage_bundle_for_node_renderer task in common deploy-script locations so users on older Procfile/Dockerfile entries get a migration nudge before the task is removed.

"pre_stage_bundle_for_node_renderer"
RENDERER_CACHE_DEPLOY_SCRIPT_PATHS =

Fixed allowlist of single-file deploy-script paths. Each entry is a literal path that may host a deploy hook referencing the deprecated task. Directory globs (e.g., per-stage Capistrano files or per-workflow GitHub Actions YAML) live in RENDERER_CACHE_DEPLOY_SCRIPT_GLOBS so they stay bounded.

[
  "Procfile",
  "Procfile.dev",
  "Procfile.dev-static-assets",
  "Procfile.production",
  "Dockerfile",
  "Dockerfile.production",
  "Dockerfile.staging",
  "Dockerfile.review",
  "docker-compose.yml",
  "docker-compose.yaml",
  "compose.yml",
  "compose.yaml",
  "bin/deploy",
  "bin/release",
  "bin/docker-entrypoint",
  "config/deploy.rb",
  "config/deploy/production.rb",
  "config/deploy/staging.rb",
  ".kamal/deploy.yml",
  "scripts/deploy.sh",
  ".circleci/config.yml",
  ".gitlab-ci.yml",
  "bitbucket-pipelines.yml",
  "Jenkinsfile"
].freeze
RENDERER_CACHE_DEPLOY_SCRIPT_GLOBS =

Bounded glob allowlist for deploy manifests that live in a known directory but use per-environment or per-workflow filenames. Each pattern matches only one directory level (no **) so the scan never recurses into the project tree, and the expansion is capped by RENDERER_CACHE_DEPLOY_SCRIPT_GLOB_MAX_MATCHES.

[
  ".github/workflows/*.yml",
  ".github/workflows/*.yaml",
  "config/deploy/*.rb"
].freeze
RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES =

Per-file safety gate to bound IO during the scan, not a meaningful size limit.

1_048_576
NODE_RENDERER_CONFIG_MAX_BYTES =
1_048_576
NODE_RENDERER_SYNTAX_CHECK_TIMEOUT_SECONDS =
5
NODE_RENDERER_SYNTAX_CHECK_TERMINATION_GRACE_SECONDS =
0.5
NODE_RENDERER_ROLLOUT_GENERATIONS =
2
NODE_RENDERER_CURRENT_GENERATION_MANIFEST_FILENAME =
/\Arorp-generation-v1-[0-9a-f]{64}\.json\z/
NODE_RENDERER_LAUNCHER_PATHS =
%w[
  Procfile
  Procfile.dev
  Procfile.dev-static-assets
  Procfile.dev-prod-assets
  Procfile.production
].freeze
NODE_RENDERER_SCRIPT_REFERENCE_PATTERN =
%r{\bnode\s+(?:\./)?((?:renderer|client)/node-renderer\.js)(?=\s|\z)}
NODE_RENDERER_DIRECT_LAUNCHER_PATTERN =
%r{
  \A[ \t]*[A-Za-z0-9_-]+:[ \t]*
  (?:MAX_VM_POOL_SIZE=(?<assignment>[^\s]+)[ \t]+)?
  node[ \t]+(?:\./)?(?<path>(?:renderer|client)/node-renderer\.js)
  [ \t]*(?:\r?\n)?\z
}x
NODE_RENDERER_JS_STRING_PATTERN =
/
  "(?:\\.|[^"\\])*" |
  '(?:\\.|[^'\\])*' |
  `(?:\\.|[^`\\])*`
/mx
NODE_RENDERER_NESTED_OBJECT_PATTERN =
/
  \{
    (?:#{NODE_RENDERER_JS_STRING_PATTERN}|[^{}"'`])*
  \}
/mx
NODE_RENDERER_CONFIG_OBJECT_PATTERN =
/
  \{
    (?:#{NODE_RENDERER_JS_STRING_PATTERN}|#{NODE_RENDERER_NESTED_OBJECT_PATTERN}|[^{}"'`])*
  \}
/mx
NODE_RENDERER_BARE_CALL_PATTERN =
/(?<![.\p{ID_Continue}$#])reactOnRailsProNodeRenderer\s*\(\s*/
MAX_NODE_RENDERER_BARE_CALL_CANDIDATES =

A canonical launcher needs one bare renderer call. This generous allowance bounds prefix-based reachability work while tolerating benign decoys.

32
NODE_RENDERER_PACKAGE_BINDING_PATTERN =

The canonical export must be a shorthand specifier and the binding must end explicitly. Aliases and trusted-looking expression prefixes are not evidence that the real renderer runs.

/
  (?:\A|[;\n])\s*
  (?:
    const\s*\{\s*
      (?:
        [A-Za-z_$][A-Za-z0-9_$]*(?:\s*:\s*[A-Za-z_$][A-Za-z0-9_$]*)?\s*,\s*
      )*
      reactOnRailsProNodeRenderer
      (?:
        \s*,\s*[A-Za-z_$][A-Za-z0-9_$]*(?:\s*:\s*[A-Za-z_$][A-Za-z0-9_$]*)?
      )*
      \s*,?\s*\}\s*=\s*
      require\s*\(\s*["']react-on-rails-pro-node-renderer["']\s*\)
    |
    import\s*\{\s*
      (?:
        [A-Za-z_$][A-Za-z0-9_$]*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*,\s*
      )*
      reactOnRailsProNodeRenderer
      (?:
        \s*,\s*[A-Za-z_$][A-Za-z0-9_$]*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?
      )*
      \s*,?\s*\}\s+from\s+
      ["']react-on-rails-pro-node-renderer["']
  )\s*(?:;|\z)
/mx
NODE_RENDERER_REQUIRE_IDENTIFIER_PATTERN =
/(?<![.\p{ID_Continue}$#])require\b/
NODE_RENDERER_REQUIRE_FUNCTION_DECLARATION_PATTERN =
/\bfunction\s*\*?\s+require\b/
NODE_RENDERER_ARGUMENTS_IDENTIFIER_PATTERN =
/(?<![.\p{ID_Continue}$#])arguments\b/
NODE_RENDERER_DIRECT_EVAL_PATTERN =
/(?<![.\p{ID_Continue}$#])eval\s*\(/
NODE_RENDERER_LOCAL_BINDING_PATTERN =
%r{
  \b(?:const|let|var)\s+
    (?:reactOnRailsProNodeRenderer\b|\{[^{}]*\breactOnRailsProNodeRenderer\b[^{}]*\})
  |
  \b(?:function|class)\s+reactOnRailsProNodeRenderer\b
  |
  \bimport\s*\{[^{}]*\breactOnRailsProNodeRenderer\b[^{}]*\}\s+from\b
  |
  (?<![.\p{ID_Continue}$#])reactOnRailsProNodeRenderer\b\s*
    (?:\+\+|--|\*\*=|&&=|\|\|=|\?\?=|[+\-*\/%&|^]=|=(?!=|>))
  |
  (?:\+\+|--)\s*(?<![.\p{ID_Continue}$#])reactOnRailsProNodeRenderer\b
}mx
NODE_RENDERER_IDENTIFIER_PATTERN =
/(?<![.\p{ID_Continue}$#])reactOnRailsProNodeRenderer\b/
NODE_RENDERER_GLOBAL_OBJECT_PATTERN =
/(?<![.\p{ID_Continue}$#])(?:globalThis|global)\b/
NODE_RENDERER_UNPROVEN_CALL_CONTROL_PATTERN =
/
  (?:&&|\|\||=>|\?) |
  \b(?:if|else|for|while|do|switch|case|catch|finally|function|return|throw)\b
/x
NODE_RENDERER_OPENING_DELIMITERS =
["(", "[", "{"].freeze
NODE_RENDERER_QUOTE_CHARACTERS =
['"', "'"].freeze
NODE_RENDERER_MATCHING_OPENING_DELIMITERS =
{ ")" => "(", "]" => "[", "}" => "{" }.freeze
NODE_RENDERER_UNPROVEN_INITIALIZER_OPENINGS =
["(", "["].freeze
RENDERER_CACHE_DEPLOY_SCRIPT_GLOB_MAX_MATCHES =

Defense-in-depth cap on how many files a single glob may contribute. Realistic repos have a handful of workflow / deploy-stage files; far more than this is a sign of an unexpectedly broad pattern, not legitimate config.

100
OUTPUT_FORMATS =

Supported output formats. :text is the human-readable default; :json emits a machine-readable report (see JSON_SCHEMA_VERSION below).

%i[text json].freeze
JSON_SCHEMA_VERSION =

DoctorSchema is the canonical machine-readable report contract; bump its version only for breaking changes. No timestamp is included so output is deterministic for a given app state. Exit code semantics match text mode: 1 if any check fails, else 0.

DoctorSchema::VERSION
CHECK_SECTIONS =

Doctor check sections. The :id values are part of the stable JSON schema contract (consumed by agents/tooling) — never rename or reuse them; add new sections with new ids instead.

[
  { id: "environment_prerequisites", title: "Environment Prerequisites", method: :check_environment },
  { id: "react_on_rails_versions", title: "React on Rails Versions", method: :check_react_on_rails_versions },
  { id: "react_on_rails_packages", title: "React on Rails Packages", method: :check_packages },
  { id: "javascript_package_dependencies", title: "JavaScript Package Dependencies",
    method: :check_dependencies },
  { id: "key_configuration_files", title: "Key Configuration Files", method: :check_key_files },
  { id: "configuration_analysis", title: "Configuration Analysis", method: :check_configuration_details },
  { id: "bin_dev_launcher_setup", title: "bin/dev Launcher Setup", method: :check_bin_dev_launcher },
  { id: "rails_integration", title: "Rails Integration", method: :check_rails },
  { id: "bundler_configuration", title: "Bundler Configuration", method: :check_bundler_configuration },
  { id: "testing_setup", title: "Testing Setup", method: :check_testing_setup },
  { id: "development_environment", title: "Development Environment", method: :check_development },
  { id: "react_on_rails_pro_setup", title: "React on Rails Pro Setup", method: :check_pro_setup },
  { id: "node_renderer_rollout_capacity", title: "Node Renderer Rollout Capacity",
    method: :check_node_renderer_rollout_capacity },
  { id: "react_server_components", title: "React Server Components", method: :check_rsc_setup }
].freeze
CHECK_SECTIONS_BY_ID =
CHECK_SECTIONS.to_h { |section| [section[:id], section] }.freeze

Constants included from RscRspackSupport

RscRspackSupport::DECLARED_PACKAGE_DEPENDENCY_FIELDS, RscRspackSupport::GENERIC_DECLARED_PACKAGE_DEPENDENCY_FIELDS, RscRspackSupport::MINIMUM_RSC_RSPACK_MAJOR, RscRspackSupport::NODE_PACKAGE_RESOLUTION_SCRIPT, RscRspackSupport::PACKAGE_NAME_PATTERN, RscRspackSupport::PATH_PROTOCOL_PACKAGE_SPEC_PATTERN, RscRspackSupport::RSC_RSPACK_PACKAGE, RscRspackSupport::RSC_RSPACK_PACKAGE_DEPENDENCY_FIELDS, RscRspackSupport::RSC_RSPACK_UPGRADE_PACKAGE_DEPENDENCY_FIELDS, RscRspackSupport::RSC_RSPACK_V2_PACKAGES

Constants included from ShakapackerConfigHelpers

ShakapackerConfigHelpers::DEFAULT_SHAKAPACKER_CONFIG_PATH, ShakapackerConfigHelpers::SHAKAPACKER_ASSETS_BUNDLER_ENV, ShakapackerConfigHelpers::SUPPORTED_ASSETS_BUNDLERS

Constants included from ConfigPathResolver

ConfigPathResolver::ALL_DEFAULT_CONFIG_CANDIDATES, ConfigPathResolver::RSPACK_DEFAULT_CONFIG_CANDIDATES, ConfigPathResolver::WEBPACK_DEFAULT_CONFIG_CANDIDATES

Instance Method Summary collapse

Constructor Details

#initialize(verbose: false, fix: false, format: :text, only: nil) ⇒ Doctor

Returns a new instance of Doctor.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/react_on_rails/doctor.rb', line 258

def initialize(verbose: false, fix: false, format: :text, only: nil)
  @verbose = verbose
  @fix = fix
  @format = format.respond_to?(:to_sym) ? format.to_sym : format
  unless OUTPUT_FORMATS.include?(@format)
    raise ArgumentError, "Invalid doctor format #{format.inspect}; expected one of #{OUTPUT_FORMATS.join(', ')}"
  end

  @only_explicitly_set = explicit_check_selection?(only)
  @check_sections = normalize_check_sections(only)
  @checker = SystemChecker.new
  @test_output_path_strategy = :unknown
  @rails_environment_loaded = false
  @rsc_artifacts_rebuild_guidance_added = false
  @rsc_registration_entry_warning_added = false
end

Instance Method Details

#run_diagnosisObject



275
276
277
278
279
280
281
282
283
284
# File 'lib/react_on_rails/doctor.rb', line 275

def run_diagnosis
  return run_json_diagnosis if format == :json

  print_header
  run_all_checks
  print_summary
  print_recommendations if should_show_recommendations?

  exit_with_status
end