shakapacker 9.2.0 → 9.3.0.beta.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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
  4. data/.github/workflows/claude-code-review.yml +4 -5
  5. data/.github/workflows/claude.yml +1 -2
  6. data/.github/workflows/dummy.yml +4 -4
  7. data/.github/workflows/generator.yml +9 -9
  8. data/.github/workflows/node.yml +11 -2
  9. data/.github/workflows/ruby.yml +16 -16
  10. data/.github/workflows/test-bundlers.yml +9 -9
  11. data/.gitignore +4 -0
  12. data/CHANGELOG.md +19 -4
  13. data/CLAUDE.md +6 -1
  14. data/CONTRIBUTING.md +0 -1
  15. data/Gemfile.lock +1 -1
  16. data/README.md +14 -14
  17. data/TODO.md +10 -2
  18. data/TODO_v9.md +13 -3
  19. data/bin/export-bundler-config +1 -1
  20. data/conductor-setup.sh +1 -1
  21. data/conductor.json +1 -1
  22. data/docs/cdn_setup.md +13 -8
  23. data/docs/common-upgrades.md +2 -1
  24. data/docs/configuration.md +630 -0
  25. data/docs/css-modules-export-mode.md +120 -100
  26. data/docs/customizing_babel_config.md +16 -16
  27. data/docs/deployment.md +18 -0
  28. data/docs/developing_shakapacker.md +6 -0
  29. data/docs/optional-peer-dependencies.md +9 -4
  30. data/docs/peer-dependencies.md +17 -6
  31. data/docs/precompile_hook.md +342 -0
  32. data/docs/react.md +57 -47
  33. data/docs/releasing.md +0 -2
  34. data/docs/rspack.md +25 -21
  35. data/docs/rspack_migration_guide.md +335 -8
  36. data/docs/sprockets.md +1 -0
  37. data/docs/style_loader_vs_mini_css.md +12 -12
  38. data/docs/subresource_integrity.md +13 -7
  39. data/docs/transpiler-performance.md +40 -19
  40. data/docs/troubleshooting.md +0 -2
  41. data/docs/typescript-migration.md +48 -39
  42. data/docs/typescript.md +12 -8
  43. data/docs/using_esbuild_loader.md +10 -10
  44. data/docs/v6_upgrade.md +33 -20
  45. data/docs/v7_upgrade.md +8 -6
  46. data/docs/v8_upgrade.md +13 -12
  47. data/docs/v9_upgrade.md +2 -1
  48. data/eslint.config.fast.js +134 -0
  49. data/eslint.config.js +140 -0
  50. data/knip.ts +54 -0
  51. data/lib/install/bin/export-bundler-config +1 -1
  52. data/lib/install/config/shakapacker.yml +16 -5
  53. data/lib/shakapacker/compiler.rb +80 -0
  54. data/lib/shakapacker/configuration.rb +33 -5
  55. data/lib/shakapacker/dev_server_runner.rb +140 -1
  56. data/lib/shakapacker/doctor.rb +294 -65
  57. data/lib/shakapacker/instance.rb +8 -3
  58. data/lib/shakapacker/runner.rb +244 -8
  59. data/lib/shakapacker/version.rb +1 -1
  60. data/lib/tasks/shakapacker/doctor.rake +42 -2
  61. data/package/babel/preset.ts +7 -4
  62. data/package/config.ts +42 -30
  63. data/package/configExporter/cli.ts +799 -208
  64. data/package/configExporter/configFile.ts +520 -0
  65. data/package/configExporter/fileWriter.ts +12 -8
  66. data/package/configExporter/index.ts +9 -1
  67. data/package/configExporter/types.ts +36 -2
  68. data/package/configExporter/yamlSerializer.ts +22 -8
  69. data/package/dev_server.ts +1 -1
  70. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
  71. data/package/environments/base.ts +18 -13
  72. data/package/environments/development.ts +1 -1
  73. data/package/environments/production.ts +4 -1
  74. data/package/index.d.ts +50 -3
  75. data/package/index.d.ts.template +50 -0
  76. data/package/index.ts +7 -7
  77. data/package/loaders.d.ts +2 -2
  78. data/package/optimization/rspack.ts +1 -1
  79. data/package/plugins/rspack.ts +15 -4
  80. data/package/plugins/webpack.ts +7 -3
  81. data/package/rspack/index.ts +10 -2
  82. data/package/rules/raw.ts +3 -2
  83. data/package/rules/sass.ts +1 -1
  84. data/package/types/README.md +15 -13
  85. data/package/types/index.ts +5 -5
  86. data/package/types.ts +0 -1
  87. data/package/utils/defaultConfigPath.ts +4 -1
  88. data/package/utils/errorCodes.ts +129 -100
  89. data/package/utils/errorHelpers.ts +34 -29
  90. data/package/utils/getStyleRule.ts +5 -2
  91. data/package/utils/helpers.ts +21 -11
  92. data/package/utils/pathValidation.ts +43 -35
  93. data/package/utils/requireOrError.ts +1 -1
  94. data/package/utils/snakeToCamelCase.ts +1 -1
  95. data/package/utils/typeGuards.ts +132 -83
  96. data/package/utils/validateDependencies.ts +1 -1
  97. data/package/webpack-types.d.ts +3 -3
  98. data/package/webpackDevServerConfig.ts +22 -10
  99. data/package-lock.json +2 -2
  100. data/package.json +36 -28
  101. data/scripts/type-check-no-emit.js +1 -1
  102. data/test/configExporter/configFile.test.js +392 -0
  103. data/test/configExporter/integration.test.js +275 -0
  104. data/test/helpers.js +1 -1
  105. data/test/package/configExporter.test.js +154 -0
  106. data/test/package/helpers.test.js +2 -2
  107. data/test/package/rules/sass-version-parsing.test.js +71 -0
  108. data/test/package/rules/sass.test.js +2 -4
  109. data/test/package/rules/sass1.test.js +1 -3
  110. data/test/package/rules/sass16.test.js +23 -0
  111. data/tools/README.md +15 -5
  112. data/tsconfig.eslint.json +2 -9
  113. data/yarn.lock +1894 -1492
  114. metadata +19 -3
  115. data/.eslintignore +0 -5
@@ -5,17 +5,28 @@ require "semantic_range"
5
5
 
6
6
  module Shakapacker
7
7
  class Doctor
8
- attr_reader :config, :root_path, :issues, :warnings, :info
8
+ attr_reader :config, :root_path, :issues, :warnings, :info, :options
9
9
 
10
- def initialize(config = nil, root_path = nil)
10
+ # Warning categories for better organization
11
+ CATEGORY_ACTION_REQUIRED = :action_required
12
+ CATEGORY_RECOMMENDED = :recommended
13
+ CATEGORY_INFO = :info
14
+
15
+ def initialize(config = nil, root_path = nil, options = {})
11
16
  @config = config || Shakapacker.config
12
17
  @root_path = root_path || (defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd))
13
18
  @issues = []
14
- @warnings = []
19
+ @warnings = [] # Now stores hashes: { category: :symbol, message: "..." }
15
20
  @info = []
21
+ @options = options
16
22
  end
17
23
 
18
24
  def run
25
+ if options[:help]
26
+ print_help
27
+ return
28
+ end
29
+
19
30
  perform_checks
20
31
  report_results
21
32
  end
@@ -26,6 +37,49 @@ module Shakapacker
26
37
 
27
38
  private
28
39
 
40
+ def add_warning(message, category = CATEGORY_RECOMMENDED)
41
+ @warnings << { category: category, message: message }
42
+ end
43
+
44
+ def add_action_required(message)
45
+ add_warning(message, CATEGORY_ACTION_REQUIRED)
46
+ end
47
+
48
+ def add_info_warning(message)
49
+ add_warning(message, CATEGORY_INFO)
50
+ end
51
+
52
+ def print_help
53
+ puts <<~HELP
54
+ Shakapacker Doctor - Diagnostic tool for Shakapacker configuration
55
+
56
+ Usage:
57
+ bin/rails shakapacker:doctor [options]
58
+
59
+ Options:
60
+ --help Show this help message
61
+ --verbose Show detailed information about all checks
62
+
63
+ Description:
64
+ The doctor command checks for common configuration issues and missing
65
+ dependencies in your Shakapacker setup, including:
66
+
67
+ • Configuration file validity
68
+ • Entry points and output paths
69
+ • Node.js and package manager installation
70
+ • Required npm dependencies
71
+ • JavaScript transpiler configuration
72
+ • CSS and CSS Modules setup
73
+ • Binstubs presence
74
+ • Version consistency
75
+ • Legacy file detection
76
+
77
+ Exit codes:
78
+ 0 - No issues found
79
+ 1 - Issues detected (see output for details)
80
+ HELP
81
+ end
82
+
29
83
  def perform_checks
30
84
  # Core configuration checks
31
85
  check_config_file
@@ -80,7 +134,7 @@ module Shakapacker
80
134
  # Check for at least one entry point
81
135
  entry_files = Dir.glob(File.join(source_entry_path, "**/*.{js,jsx,ts,tsx,coffee}"))
82
136
  if entry_files.empty?
83
- @warnings << "No entry point files found in #{source_entry_path}"
137
+ add_warning("No entry point files found in #{source_entry_path}")
84
138
  end
85
139
  end
86
140
 
@@ -109,7 +163,7 @@ module Shakapacker
109
163
  begin
110
164
  manifest_content = JSON.parse(File.read(manifest_path))
111
165
  if manifest_content.empty?
112
- @warnings << "Manifest file is empty - you may need to run 'rails assets:precompile'"
166
+ add_warning("Manifest file is empty - you may need to run 'bin/rails assets:precompile'")
113
167
  end
114
168
  rescue JSON::ParserError
115
169
  @issues << "Manifest file #{manifest_path} contains invalid JSON"
@@ -125,13 +179,17 @@ module Shakapacker
125
179
 
126
180
  def check_deprecated_config
127
181
  config_file = File.read(config.config_path)
182
+ config_relative_path = config.config_path.relative_path_from(root_path)
128
183
 
129
184
  if config_file.include?("webpack_loader:")
130
- @warnings << "Deprecated config: 'webpack_loader' should be renamed to 'javascript_transpiler'"
185
+ add_action_required("Deprecated config: 'webpack_loader' should be renamed to 'javascript_transpiler' in #{config_relative_path}")
131
186
  end
132
187
 
133
- if config_file.include?("bundler:")
134
- @warnings << "Deprecated config: 'bundler' should be renamed to 'assets_bundler'"
188
+ # Check for standalone "bundler:" but not "assets_bundler:"
189
+ # Match "bundler:" at start of line or preceded by non-underscore character
190
+ if config_file.match?(/^\s*bundler:/m) || config_file.match?(/[^_]bundler:/)
191
+ add_action_required("Deprecated config: 'bundler' should be renamed to 'assets_bundler' in #{config_relative_path}.")
192
+ add_action_required(" Fix: Open #{config_relative_path} and change 'bundler:' to 'assets_bundler:'.")
135
193
  end
136
194
  rescue => e
137
195
  # Ignore read errors as config file check already handles missing file
@@ -146,15 +204,18 @@ module Shakapacker
146
204
  package_json.dig("devDependencies", "shakapacker")
147
205
 
148
206
  if npm_version
207
+ # Skip version check for github/file references
208
+ return if npm_version.start_with?("github:", "git+", "file:", "link:", "./", "../", "/")
209
+
149
210
  gem_version = Shakapacker::VERSION rescue nil
150
211
  if gem_version && !versions_compatible?(gem_version, npm_version)
151
- @warnings << "Version mismatch: shakapacker gem is #{gem_version} but npm package is #{npm_version}"
212
+ add_info_warning("Version mismatch: shakapacker gem is #{gem_version} but npm package is #{npm_version}")
152
213
  end
153
214
  end
154
215
 
155
- # Check if ensure_consistent_versioning is enabled and warn if versions might mismatch
216
+ # Check if ensure_consistent_versioning is enabled
156
217
  if config.ensure_consistent_versioning?
157
- @info << "Version consistency checking is enabled"
218
+ @info << "Version consistency checking: enabled (ensures shakapacker gem and npm package versions match at runtime)"
158
219
  end
159
220
  end
160
221
 
@@ -163,7 +224,7 @@ module Shakapacker
163
224
  node_env = ENV["NODE_ENV"]
164
225
 
165
226
  if rails_env && node_env && rails_env != node_env
166
- @warnings << "Environment mismatch: Rails.env is '#{rails_env}' but NODE_ENV is '#{node_env}'"
227
+ add_warning("Environment mismatch: Rails.env is '#{rails_env}' but NODE_ENV is '#{node_env}'")
167
228
  end
168
229
 
169
230
  # Check SHAKAPACKER_ASSET_HOST for production
@@ -206,7 +267,7 @@ module Shakapacker
206
267
 
207
268
  # Check for conflicting installations
208
269
  if package_installed?("webpack") && package_installed?("@rspack/core")
209
- @warnings << "Both webpack and rspack are installed - ensure assets_bundler is set correctly"
270
+ add_warning("Both webpack and rspack are installed - ensure assets_bundler is set correctly")
210
271
  end
211
272
  end
212
273
 
@@ -242,7 +303,7 @@ module Shakapacker
242
303
 
243
304
  # Check for case sensitivity issues
244
305
  if File.exist?(root_path.join("App")) || File.exist?(root_path.join("APP"))
245
- @warnings << "Potential case sensitivity issue detected on Windows filesystem"
306
+ add_warning("Potential case sensitivity issue detected on Windows filesystem")
246
307
  end
247
308
  end
248
309
  end
@@ -254,7 +315,8 @@ module Shakapacker
254
315
  # Check if manifest is recent (within last 24 hours)
255
316
  manifest_age_hours = (Time.now - File.mtime(manifest_path)) / 3600
256
317
 
257
- if manifest_age_hours > 24
318
+ if manifest_age_hours > 24 && options[:verbose]
319
+ # Only show age in verbose mode - it's not actionable information
258
320
  @info << "Assets were last compiled #{manifest_age_hours.round} hours ago. Consider recompiling if you've made changes."
259
321
  end
260
322
 
@@ -263,15 +325,16 @@ module Shakapacker
263
325
  if source_files.any?
264
326
  newest_source = source_files.map { |f| File.mtime(f) }.max
265
327
  if newest_source > File.mtime(manifest_path)
266
- @warnings << "Source files have been modified after last asset compilation. Run 'rails assets:precompile'"
328
+ add_warning("Source files have been modified after last asset compilation. Run 'bin/rails assets:precompile'")
267
329
  end
268
330
  end
269
331
  else
270
332
  rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
271
333
  if rails_env == "production"
272
- @issues << "No compiled assets found (manifest.json missing). Run 'rails assets:precompile'"
273
- else
274
- @info << "Assets not yet compiled. Run 'rails assets:precompile' or start the dev server"
334
+ @issues << "No compiled assets found (manifest.json missing). Run 'bin/rails assets:precompile'"
335
+ elsif options[:verbose]
336
+ # Only show in verbose mode for non-production environments
337
+ @info << "Assets not yet compiled. Run 'bin/rails assets:precompile' or start the dev server"
275
338
  end
276
339
  end
277
340
  end
@@ -287,7 +350,7 @@ module Shakapacker
287
350
  legacy_files.each do |file|
288
351
  file_path = root_path.join(file)
289
352
  if file_path.exist?
290
- @warnings << "Legacy webpacker file found: #{file} - consider removing after migration"
353
+ add_warning("Legacy webpacker file found: #{file} - consider removing after migration")
291
354
  end
292
355
  end
293
356
  end
@@ -302,7 +365,7 @@ module Shakapacker
302
365
  if version_match
303
366
  major = version_match[1].to_i
304
367
  if major < 14
305
- @warnings << "Node.js version #{node_version} is outdated. Recommend upgrading to v14 or higher"
368
+ add_warning("Node.js version #{node_version} is outdated. Recommend upgrading to v14 or higher")
306
369
  end
307
370
  end
308
371
  else
@@ -313,7 +376,7 @@ module Shakapacker
313
376
  rescue Errno::EACCES
314
377
  @issues << "Permission denied when checking Node.js version"
315
378
  rescue StandardError => e
316
- @warnings << "Unable to check Node.js version: #{e.message}"
379
+ add_warning("Unable to check Node.js version: #{e.message}")
317
380
  end
318
381
 
319
382
  def check_package_manager
@@ -323,14 +386,23 @@ module Shakapacker
323
386
  end
324
387
 
325
388
  def check_binstub
326
- binstub_path = root_path.join("bin/shakapacker")
327
- unless binstub_path.exist?
328
- @warnings << "Shakapacker binstub not found at bin/shakapacker. Run 'rails shakapacker:binstubs' to create it."
389
+ missing_binstubs = []
390
+
391
+ expected_binstubs = {
392
+ "bin/shakapacker" => "Main Shakapacker binstub",
393
+ "bin/shakapacker-dev-server" => "Development server binstub",
394
+ "bin/export-bundler-config" => "Config export binstub"
395
+ }
396
+
397
+ expected_binstubs.each do |path, description|
398
+ unless root_path.join(path).exist?
399
+ missing_binstubs << "#{path} (#{description})"
400
+ end
329
401
  end
330
402
 
331
- export_config_binstub = root_path.join("bin/export-bundler-config")
332
- unless export_config_binstub.exist?
333
- @warnings << "Config export binstub not found at bin/export-bundler-config. Run 'rails shakapacker:binstubs' to create it."
403
+ unless missing_binstubs.empty?
404
+ add_action_required("Missing binstubs: #{missing_binstubs.join(', ')}.")
405
+ add_action_required(" Fix: Run 'bin/rails shakapacker:binstubs' to create them.")
334
406
  end
335
407
  end
336
408
 
@@ -394,7 +466,16 @@ module Shakapacker
394
466
  # Rspack has built-in SWC support
395
467
  @info << "Rspack has built-in SWC support - no additional loaders needed"
396
468
  if package_installed?("swc-loader")
397
- @warnings << "swc-loader is not needed with Rspack (SWC is built-in) - consider removing it"
469
+ package_manager = detect_package_manager
470
+ remove_cmd = case package_manager
471
+ when "yarn" then "yarn remove swc-loader"
472
+ when "npm" then "npm uninstall swc-loader"
473
+ when "pnpm" then "pnpm remove swc-loader"
474
+ when "bun" then "bun remove swc-loader"
475
+ else "npm uninstall swc-loader"
476
+ end
477
+ add_warning("swc-loader is not needed with Rspack (SWC is built-in). Rspack includes SWC transpilation natively, so this package is redundant.")
478
+ add_warning(" Fix: Remove it with: #{remove_cmd}.")
398
479
  end
399
480
  end
400
481
  end
@@ -418,26 +499,32 @@ module Shakapacker
418
499
  ]
419
500
 
420
501
  babel_config_exists = babel_configs.any?(&:exist?)
502
+ babel_in_package_json = false
421
503
 
422
504
  # Check if package.json has babel config
423
505
  if package_json_exists?
424
506
  package_json = read_package_json
425
- babel_config_exists ||= package_json.key?("babel")
507
+ babel_in_package_json = package_json.key?("babel")
508
+ babel_config_exists ||= babel_in_package_json
426
509
  end
427
510
 
428
511
  transpiler = config.javascript_transpiler
429
512
 
430
513
  if babel_config_exists && transpiler != "babel"
431
- @warnings << "Babel configuration files found but javascript_transpiler is '#{transpiler}'. Consider removing Babel configs or setting javascript_transpiler: 'babel'"
514
+ babel_files = babel_configs.select(&:exist?).map { |f| f.relative_path_from(root_path) }
515
+ babel_files << "package.json" if babel_in_package_json
516
+ babel_files_str = babel_files.join(", ")
517
+ add_warning("Babel configuration files found (#{babel_files_str}) but javascript_transpiler is '#{transpiler}'. These Babel configs are ignored by Shakapacker (though they may still be used by ESLint or other tools).")
518
+ add_warning(" Fix: Remove Babel config files if not needed, or set javascript_transpiler: 'babel' in shakapacker.yml to use Babel for transpilation.")
432
519
  end
433
520
 
434
521
  # Check for redundant dependencies
435
522
  if transpiler == "swc" && package_installed?("babel-loader")
436
- @warnings << "Both SWC and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size"
523
+ add_warning("Both SWC and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size")
437
524
  end
438
525
 
439
526
  if transpiler == "esbuild" && package_installed?("babel-loader")
440
- @warnings << "Both esbuild and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size"
527
+ add_warning("Both esbuild and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size")
441
528
  end
442
529
 
443
530
  # Check for SWC configuration conflicts
@@ -451,10 +538,10 @@ module Shakapacker
451
538
  swc_config_path = root_path.join("config/swc.config.js")
452
539
 
453
540
  if swcrc_path.exist?
454
- @warnings << "SWC configuration: .swcrc file detected. This file completely overrides Shakapacker's default SWC settings and may cause build failures. " \
541
+ add_warning("SWC configuration: .swcrc file detected. This file completely overrides Shakapacker's default SWC settings and may cause build failures. " \
455
542
  "Please migrate to config/swc.config.js which properly merges with Shakapacker defaults. " \
456
543
  "To migrate: Move your custom settings from .swcrc to config/swc.config.js (see docs for format). " \
457
- "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md"
544
+ "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md")
458
545
  end
459
546
 
460
547
  if swc_config_path.exist?
@@ -468,18 +555,18 @@ module Shakapacker
468
555
 
469
556
  # Check for loose: true (deprecated default)
470
557
  if config_content.match?(/loose\s*:\s*true/)
471
- @warnings << "SWC configuration: 'loose: true' detected in config/swc.config.js. " \
558
+ add_warning("SWC configuration: 'loose: true' detected in config/swc.config.js. " \
472
559
  "This can cause silent failures with Stimulus controllers and incorrect spread operator behavior. " \
473
560
  "Consider removing this setting to use Shakapacker's default 'loose: false' (spec-compliant). " \
474
- "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
561
+ "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus")
475
562
  end
476
563
 
477
564
  # Check for missing keepClassNames with Stimulus
478
565
  if stimulus_likely_used? && !config_content.match?(/keepClassNames\s*:\s*true/)
479
- @warnings << "SWC configuration: Stimulus appears to be in use, but 'keepClassNames: true' is not set in config/swc.config.js. " \
566
+ add_warning("SWC configuration: Stimulus appears to be in use, but 'keepClassNames: true' is not set in config/swc.config.js. " \
480
567
  "Without this setting, Stimulus controllers will fail silently. " \
481
568
  "Add 'keepClassNames: true' to jsc config. " \
482
- "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
569
+ "See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus")
483
570
  elsif config_content.match?(/keepClassNames\s*:\s*true/)
484
571
  @info << "SWC configuration: 'keepClassNames: true' is set (good for Stimulus compatibility)"
485
572
  end
@@ -493,7 +580,7 @@ module Shakapacker
493
580
  end
494
581
  rescue => e
495
582
  # Don't fail doctor if SWC config check has issues
496
- @warnings << "Unable to validate SWC configuration: #{e.message}"
583
+ add_warning("Unable to validate SWC configuration: #{e.message}")
497
584
  end
498
585
 
499
586
  def stimulus_likely_used?
@@ -543,19 +630,13 @@ module Shakapacker
543
630
  @issues << " Using exportLocalsConvention: 'camelCase' with namedExport: true will cause build errors"
544
631
  @issues << " Change to 'camelCaseOnly' or 'dashesOnly'. See docs/v9_upgrade.md for details"
545
632
  end
546
-
547
- # Warn if CSS modules are used but no configuration is found
548
- if !config_content.match(/namedExport/) && !config_content.match(/exportLocalsConvention/)
549
- @info << "CSS module files found but no explicit CSS modules configuration detected"
550
- @info << " v9 defaults: namedExport: true, exportLocalsConvention: 'camelCaseOnly'"
551
- end
552
633
  end
553
634
 
554
635
  # Check for common v8 to v9 migration issues
555
636
  check_css_modules_import_patterns
556
637
  rescue => e
557
638
  # Don't fail doctor if CSS modules check has issues
558
- @warnings << "Unable to validate CSS modules configuration: #{e.message}"
639
+ add_warning("Unable to validate CSS modules configuration: #{e.message}")
559
640
  end
560
641
 
561
642
  def check_css_modules_import_patterns
@@ -572,10 +653,10 @@ module Shakapacker
572
653
 
573
654
  # Check for v8 default import pattern with .module.css
574
655
  if v8_pattern.match?(content)
575
- @warnings << "Potential v8-style CSS module imports detected (using default import)"
576
- @warnings << " v9 uses named exports. Update to: import { className } from './styles.module.css'"
577
- @warnings << " Or use: import * as styles from './styles.module.css' (TypeScript)"
578
- @warnings << " See docs/v9_upgrade.md for migration guide"
656
+ add_warning("Potential v8-style CSS module imports detected (using default import)")
657
+ add_warning(" v9 uses named exports. Update to: import { className } from './styles.module.css'")
658
+ add_warning(" Or use: import * as styles from './styles.module.css' (TypeScript)")
659
+ add_warning(" See docs/v9_upgrade.md for migration guide")
579
660
  break # Stop after finding first occurrence
580
661
  end
581
662
  end
@@ -643,7 +724,7 @@ module Shakapacker
643
724
 
644
725
  def check_optional_dependency(package_name, warnings_array, description)
645
726
  unless package_installed?(package_name)
646
- warnings_array << "Optional dependency '#{package_name}' for #{description} is not installed"
727
+ add_warning("Optional dependency '#{package_name}' for #{description} is not installed")
647
728
  end
648
729
  end
649
730
 
@@ -759,25 +840,91 @@ module Shakapacker
759
840
 
760
841
  private
761
842
 
843
+ def verbose?
844
+ doctor.options[:verbose]
845
+ end
846
+
762
847
  def print_header
763
848
  puts "Running Shakapacker doctor..."
764
849
  puts "=" * 60
850
+ puts ""
851
+ if verbose?
852
+ puts "Mode: Verbose (showing all checks)"
853
+ puts ""
854
+ end
765
855
  end
766
856
 
767
857
  def print_checks
768
858
  if doctor.config.config_path.exist?
769
- puts "✓ Configuration file found"
859
+ config_relative_path = doctor.config.config_path.relative_path_from(doctor.root_path)
860
+ puts "✓ Configuration file found (#{config_relative_path})"
861
+ if verbose?
862
+ puts " Assets bundler: #{doctor.config.assets_bundler}"
863
+ puts " Source path: #{doctor.config.source_path.relative_path_from(doctor.root_path)}"
864
+ puts " Public output path: #{doctor.config.public_output_path.relative_path_from(doctor.root_path)}"
865
+ end
770
866
  print_transpiler_status
771
867
  print_bundler_status
772
868
  print_css_status
869
+ elsif verbose?
870
+ puts "✗ Configuration file not found"
773
871
  end
774
872
 
775
873
  print_node_status
776
874
  print_package_manager_status
777
875
  print_binstub_status
876
+ print_verbose_checks if verbose?
778
877
  print_info_messages
779
878
  end
780
879
 
880
+ def print_verbose_checks
881
+ puts "\nVerbose diagnostics:"
882
+
883
+ # Show environment info
884
+ rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
885
+ node_env = ENV["NODE_ENV"]
886
+ puts " • Rails environment: #{rails_env || 'not set'}"
887
+ puts " • Node environment: #{node_env || 'not set'}"
888
+
889
+ # Show gem/npm versions
890
+ if doctor.send(:package_json_exists?)
891
+ package_json = doctor.send(:read_package_json)
892
+ npm_version = package_json.dig("dependencies", "shakapacker") ||
893
+ package_json.dig("devDependencies", "shakapacker")
894
+ puts " • Shakapacker gem version: #{Shakapacker::VERSION}"
895
+ puts " • Shakapacker npm version: #{npm_version || 'not installed'}"
896
+ end
897
+
898
+ # Show paths
899
+ puts " • Root path: #{doctor.root_path}"
900
+ if doctor.config.config_path.exist?
901
+ puts " • Cache path: #{doctor.config.cache_path}"
902
+ puts " • Manifest path: #{doctor.config.manifest_path}"
903
+ end
904
+
905
+ # Show environment-specific shakapacker.yml configuration values
906
+ if doctor.config.config_path.exist?
907
+ puts "\nConfiguration values for '#{doctor.config.env}' environment:"
908
+ config_data = doctor.config.send(:data)
909
+ if config_data.any?
910
+ config_data.each do |key, value|
911
+ # Format the value nicely - truncate long arrays/hashes
912
+ formatted_value = case value
913
+ when Array
914
+ value.length > 3 ? "#{value.first(3).inspect}... (#{value.length} items)" : value.inspect
915
+ when Hash
916
+ value.length > 3 ? "{...} (#{value.length} keys)" : value.inspect
917
+ else
918
+ value.inspect
919
+ end
920
+ puts " • #{key}: #{formatted_value}"
921
+ end
922
+ else
923
+ puts " (using bundled defaults - no environment-specific config found)"
924
+ end
925
+ end
926
+ end
927
+
781
928
  def print_transpiler_status
782
929
  transpiler = doctor.config.javascript_transpiler
783
930
  return if transpiler.nil? || transpiler == "none"
@@ -831,14 +978,20 @@ module Shakapacker
831
978
  end
832
979
 
833
980
  def print_binstub_status
834
- binstub_path = doctor.root_path.join("bin/shakapacker")
835
- if binstub_path.exist?
836
- puts "✓ Shakapacker binstub found"
837
- end
838
-
839
- export_config_binstub = doctor.root_path.join("bin/export-bundler-config")
840
- if export_config_binstub.exist?
841
- puts "✓ Config export binstub found"
981
+ binstubs = [
982
+ "bin/shakapacker",
983
+ "bin/shakapacker-dev-server",
984
+ "bin/export-bundler-config"
985
+ ]
986
+
987
+ existing_binstubs = binstubs.select { |b| doctor.root_path.join(b).exist? }
988
+
989
+ if existing_binstubs.length == binstubs.length
990
+ puts "✓ All Shakapacker binstubs found (#{existing_binstubs.join(', ')})"
991
+ elsif existing_binstubs.any?
992
+ existing_binstubs.each do |binstub|
993
+ puts "✓ #{binstub} found"
994
+ end
842
995
  end
843
996
  end
844
997
 
@@ -853,13 +1006,14 @@ module Shakapacker
853
1006
 
854
1007
  def print_summary
855
1008
  puts "=" * 60
1009
+ puts ""
856
1010
 
857
1011
  if doctor.issues.empty? && doctor.warnings.empty?
858
1012
  puts "✅ No issues found! Shakapacker appears to be configured correctly."
859
1013
  else
860
1014
  print_issues if doctor.issues.any?
861
1015
  print_warnings if doctor.warnings.any?
862
- print_fix_instructions
1016
+ print_fix_instructions if has_dependency_issues?
863
1017
  end
864
1018
  end
865
1019
 
@@ -872,13 +1026,88 @@ module Shakapacker
872
1026
  end
873
1027
 
874
1028
  def print_warnings
875
- puts "⚠️ Warnings (#{doctor.warnings.length}):"
876
- doctor.warnings.each_with_index do |warning, index|
877
- puts " #{index + 1}. #{warning}"
1029
+ # Count only main items (not sub-items)
1030
+ main_item_count = doctor.warnings.count { |w| !w[:message].start_with?(" ") }
1031
+ puts "⚠️ Warnings (#{main_item_count}):"
1032
+ puts ""
1033
+
1034
+ item_number = 0
1035
+ doctor.warnings.each do |warning|
1036
+ category_prefix = case warning[:category]
1037
+ when :action_required then "[REQUIRED]"
1038
+ when :info then "[INFO]"
1039
+ when :recommended then "[RECOMMENDED]"
1040
+ else ""
1041
+ end
1042
+
1043
+ # Sub-items start with whitespace (indented fix instructions)
1044
+ is_subitem = warning[:message].start_with?(" ")
1045
+
1046
+ if is_subitem
1047
+ # Fix instructions align at column 16 (length of "N. [RECOMMENDED] ")
1048
+ # This ensures all Fix lines align vertically regardless of category
1049
+ subitem_prefix = " " * 15
1050
+ wrapped = wrap_text(warning[:message], 100, subitem_prefix)
1051
+ puts wrapped
1052
+ else
1053
+ item_number += 1
1054
+ # Format: N. [CATEGORY] Message
1055
+ prefix = "#{item_number}. #{category_prefix} "
1056
+ wrapped = wrap_text(warning[:message], 100, prefix)
1057
+ puts wrapped
1058
+ end
878
1059
  end
879
1060
  puts ""
880
1061
  end
881
1062
 
1063
+ def wrap_text(text, max_width, prefix)
1064
+ # Strip leading whitespace from sub-items
1065
+ text = text.strip
1066
+
1067
+ # Calculate available width for text
1068
+ available_width = max_width - prefix.length
1069
+ return prefix + text if text.length <= available_width
1070
+
1071
+ # Wrap long lines
1072
+ words = text.split(" ")
1073
+ lines = []
1074
+ current_line = []
1075
+ current_length = 0
1076
+
1077
+ words.each do |word|
1078
+ word_length = word.length + (current_line.empty? ? 0 : 1) # +1 for space
1079
+
1080
+ if current_length + word_length <= available_width
1081
+ current_line << word
1082
+ current_length += word_length
1083
+ else
1084
+ lines << current_line.join(" ") unless current_line.empty?
1085
+ current_line = [word]
1086
+ current_length = word.length
1087
+ end
1088
+ end
1089
+ lines << current_line.join(" ") unless current_line.empty?
1090
+
1091
+ # Format output
1092
+ result = prefix + lines[0]
1093
+ lines[1..].each do |line|
1094
+ result += "\n" + (" " * prefix.length) + line
1095
+ end
1096
+ result
1097
+ end
1098
+
1099
+ def has_dependency_issues?
1100
+ # Check if any issues or warnings are about missing npm/package dependencies
1101
+ # Exclude optional dependencies - only show install instructions for required dependencies
1102
+ all_messages = doctor.issues + doctor.warnings.map { |w| w[:message] }
1103
+ all_messages.any? do |msg|
1104
+ next if msg.include?("Optional")
1105
+ (msg.include?("Missing") && msg.include?("dependency")) ||
1106
+ msg.include?("not installed") ||
1107
+ msg.include?("is not installed")
1108
+ end
1109
+ end
1110
+
882
1111
  def print_fix_instructions
883
1112
  package_manager = doctor.send(:package_manager)
884
1113
  puts "To fix missing dependencies, run:"
@@ -5,9 +5,14 @@ class Shakapacker::Instance
5
5
 
6
6
  attr_reader :root_path, :config_path
7
7
 
8
- def initialize(root_path: Rails.root, config_path: Rails.root.join("config/shakapacker.yml"))
9
- @root_path = root_path
10
- @config_path = Pathname.new(ENV["SHAKAPACKER_CONFIG"] || config_path)
8
+ def initialize(root_path: nil, config_path: nil)
9
+ # Use Rails.root if Rails is defined and no root_path is provided
10
+ @root_path = root_path || (defined?(Rails) && Rails&.root) || Pathname.new(Dir.pwd)
11
+
12
+ # Use the determined root_path to construct the default config path
13
+ default_config_path = @root_path.join("config/shakapacker.yml")
14
+
15
+ @config_path = Pathname.new(ENV["SHAKAPACKER_CONFIG"] || config_path || default_config_path)
11
16
  end
12
17
 
13
18
  def env