shakapacker 9.1.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 (123) 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 +7 -0
  12. data/CHANGELOG.md +50 -4
  13. data/CLAUDE.md +6 -1
  14. data/CONTRIBUTING.md +0 -1
  15. data/Gemfile.lock +1 -1
  16. data/README.md +35 -14
  17. data/TODO.md +10 -2
  18. data/TODO_v9.md +13 -3
  19. data/bin/export-bundler-config +11 -0
  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 +68 -6
  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 +195 -0
  34. data/docs/rspack.md +25 -21
  35. data/docs/rspack_migration_guide.md +363 -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 +122 -23
  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 +11 -0
  52. data/lib/install/bin/shakapacker +1 -1
  53. data/lib/install/bin/shakapacker-dev-server +1 -1
  54. data/lib/install/config/shakapacker.yml +16 -5
  55. data/lib/shakapacker/bundler_switcher.rb +7 -0
  56. data/lib/shakapacker/compiler.rb +80 -0
  57. data/lib/shakapacker/configuration.rb +56 -2
  58. data/lib/shakapacker/dev_server_runner.rb +140 -1
  59. data/lib/shakapacker/doctor.rb +302 -57
  60. data/lib/shakapacker/instance.rb +8 -3
  61. data/lib/shakapacker/rspack_runner.rb +1 -1
  62. data/lib/shakapacker/runner.rb +245 -9
  63. data/lib/shakapacker/version.rb +1 -1
  64. data/lib/shakapacker/webpack_runner.rb +1 -1
  65. data/lib/shakapacker.rb +10 -0
  66. data/lib/tasks/shakapacker/doctor.rake +42 -2
  67. data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
  68. data/package/babel/preset.ts +7 -4
  69. data/package/config.ts +42 -30
  70. data/package/configExporter/cli.ts +1274 -0
  71. data/package/configExporter/configDocs.ts +102 -0
  72. data/package/configExporter/configFile.ts +520 -0
  73. data/package/configExporter/fileWriter.ts +96 -0
  74. data/package/configExporter/index.ts +13 -0
  75. data/package/configExporter/types.ts +70 -0
  76. data/package/configExporter/yamlSerializer.ts +280 -0
  77. data/package/dev_server.ts +1 -1
  78. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
  79. data/package/environments/base.ts +18 -13
  80. data/package/environments/development.ts +1 -1
  81. data/package/environments/production.ts +4 -1
  82. data/package/index.d.ts +50 -3
  83. data/package/index.d.ts.template +50 -0
  84. data/package/index.ts +7 -7
  85. data/package/loaders.d.ts +2 -2
  86. data/package/optimization/rspack.ts +1 -1
  87. data/package/plugins/rspack.ts +15 -4
  88. data/package/plugins/webpack.ts +7 -3
  89. data/package/rspack/index.ts +10 -2
  90. data/package/rules/raw.ts +3 -2
  91. data/package/rules/sass.ts +1 -1
  92. data/package/types/README.md +15 -13
  93. data/package/types/index.ts +5 -5
  94. data/package/types.ts +0 -1
  95. data/package/utils/defaultConfigPath.ts +4 -1
  96. data/package/utils/errorCodes.ts +129 -100
  97. data/package/utils/errorHelpers.ts +34 -29
  98. data/package/utils/getStyleRule.ts +5 -2
  99. data/package/utils/helpers.ts +21 -11
  100. data/package/utils/pathValidation.ts +43 -35
  101. data/package/utils/requireOrError.ts +1 -1
  102. data/package/utils/snakeToCamelCase.ts +1 -1
  103. data/package/utils/typeGuards.ts +132 -83
  104. data/package/utils/validateDependencies.ts +1 -1
  105. data/package/webpack-types.d.ts +3 -3
  106. data/package/webpackDevServerConfig.ts +22 -10
  107. data/package-lock.json +2 -2
  108. data/package.json +37 -28
  109. data/scripts/type-check-no-emit.js +1 -1
  110. data/test/configExporter/configFile.test.js +392 -0
  111. data/test/configExporter/integration.test.js +275 -0
  112. data/test/helpers.js +1 -1
  113. data/test/package/configExporter.test.js +154 -0
  114. data/test/package/helpers.test.js +2 -2
  115. data/test/package/rules/sass-version-parsing.test.js +71 -0
  116. data/test/package/rules/sass.test.js +2 -4
  117. data/test/package/rules/sass1.test.js +1 -3
  118. data/test/package/rules/sass16.test.js +23 -0
  119. data/tools/README.md +15 -5
  120. data/tsconfig.eslint.json +2 -9
  121. data/yarn.lock +1635 -1442
  122. metadata +29 -3
  123. 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,9 +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
401
+ end
402
+
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.")
329
406
  end
330
407
  end
331
408
 
@@ -389,7 +466,16 @@ module Shakapacker
389
466
  # Rspack has built-in SWC support
390
467
  @info << "Rspack has built-in SWC support - no additional loaders needed"
391
468
  if package_installed?("swc-loader")
392
- @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}.")
393
479
  end
394
480
  end
395
481
  end
@@ -413,26 +499,32 @@ module Shakapacker
413
499
  ]
414
500
 
415
501
  babel_config_exists = babel_configs.any?(&:exist?)
502
+ babel_in_package_json = false
416
503
 
417
504
  # Check if package.json has babel config
418
505
  if package_json_exists?
419
506
  package_json = read_package_json
420
- babel_config_exists ||= package_json.key?("babel")
507
+ babel_in_package_json = package_json.key?("babel")
508
+ babel_config_exists ||= babel_in_package_json
421
509
  end
422
510
 
423
511
  transpiler = config.javascript_transpiler
424
512
 
425
513
  if babel_config_exists && transpiler != "babel"
426
- @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.")
427
519
  end
428
520
 
429
521
  # Check for redundant dependencies
430
522
  if transpiler == "swc" && package_installed?("babel-loader")
431
- @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")
432
524
  end
433
525
 
434
526
  if transpiler == "esbuild" && package_installed?("babel-loader")
435
- @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")
436
528
  end
437
529
 
438
530
  # Check for SWC configuration conflicts
@@ -446,10 +538,10 @@ module Shakapacker
446
538
  swc_config_path = root_path.join("config/swc.config.js")
447
539
 
448
540
  if swcrc_path.exist?
449
- @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. " \
450
542
  "Please migrate to config/swc.config.js which properly merges with Shakapacker defaults. " \
451
543
  "To migrate: Move your custom settings from .swcrc to config/swc.config.js (see docs for format). " \
452
- "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")
453
545
  end
454
546
 
455
547
  if swc_config_path.exist?
@@ -463,18 +555,18 @@ module Shakapacker
463
555
 
464
556
  # Check for loose: true (deprecated default)
465
557
  if config_content.match?(/loose\s*:\s*true/)
466
- @warnings << "SWC configuration: 'loose: true' detected in config/swc.config.js. " \
558
+ add_warning("SWC configuration: 'loose: true' detected in config/swc.config.js. " \
467
559
  "This can cause silent failures with Stimulus controllers and incorrect spread operator behavior. " \
468
560
  "Consider removing this setting to use Shakapacker's default 'loose: false' (spec-compliant). " \
469
- "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")
470
562
  end
471
563
 
472
564
  # Check for missing keepClassNames with Stimulus
473
565
  if stimulus_likely_used? && !config_content.match?(/keepClassNames\s*:\s*true/)
474
- @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. " \
475
567
  "Without this setting, Stimulus controllers will fail silently. " \
476
568
  "Add 'keepClassNames: true' to jsc config. " \
477
- "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")
478
570
  elsif config_content.match?(/keepClassNames\s*:\s*true/)
479
571
  @info << "SWC configuration: 'keepClassNames: true' is set (good for Stimulus compatibility)"
480
572
  end
@@ -488,7 +580,7 @@ module Shakapacker
488
580
  end
489
581
  rescue => e
490
582
  # Don't fail doctor if SWC config check has issues
491
- @warnings << "Unable to validate SWC configuration: #{e.message}"
583
+ add_warning("Unable to validate SWC configuration: #{e.message}")
492
584
  end
493
585
 
494
586
  def stimulus_likely_used?
@@ -538,19 +630,13 @@ module Shakapacker
538
630
  @issues << " Using exportLocalsConvention: 'camelCase' with namedExport: true will cause build errors"
539
631
  @issues << " Change to 'camelCaseOnly' or 'dashesOnly'. See docs/v9_upgrade.md for details"
540
632
  end
541
-
542
- # Warn if CSS modules are used but no configuration is found
543
- if !config_content.match(/namedExport/) && !config_content.match(/exportLocalsConvention/)
544
- @info << "CSS module files found but no explicit CSS modules configuration detected"
545
- @info << " v9 defaults: namedExport: true, exportLocalsConvention: 'camelCaseOnly'"
546
- end
547
633
  end
548
634
 
549
635
  # Check for common v8 to v9 migration issues
550
636
  check_css_modules_import_patterns
551
637
  rescue => e
552
638
  # Don't fail doctor if CSS modules check has issues
553
- @warnings << "Unable to validate CSS modules configuration: #{e.message}"
639
+ add_warning("Unable to validate CSS modules configuration: #{e.message}")
554
640
  end
555
641
 
556
642
  def check_css_modules_import_patterns
@@ -567,10 +653,10 @@ module Shakapacker
567
653
 
568
654
  # Check for v8 default import pattern with .module.css
569
655
  if v8_pattern.match?(content)
570
- @warnings << "Potential v8-style CSS module imports detected (using default import)"
571
- @warnings << " v9 uses named exports. Update to: import { className } from './styles.module.css'"
572
- @warnings << " Or use: import * as styles from './styles.module.css' (TypeScript)"
573
- @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")
574
660
  break # Stop after finding first occurrence
575
661
  end
576
662
  end
@@ -638,7 +724,7 @@ module Shakapacker
638
724
 
639
725
  def check_optional_dependency(package_name, warnings_array, description)
640
726
  unless package_installed?(package_name)
641
- warnings_array << "Optional dependency '#{package_name}' for #{description} is not installed"
727
+ add_warning("Optional dependency '#{package_name}' for #{description} is not installed")
642
728
  end
643
729
  end
644
730
 
@@ -754,25 +840,91 @@ module Shakapacker
754
840
 
755
841
  private
756
842
 
843
+ def verbose?
844
+ doctor.options[:verbose]
845
+ end
846
+
757
847
  def print_header
758
848
  puts "Running Shakapacker doctor..."
759
849
  puts "=" * 60
850
+ puts ""
851
+ if verbose?
852
+ puts "Mode: Verbose (showing all checks)"
853
+ puts ""
854
+ end
760
855
  end
761
856
 
762
857
  def print_checks
763
858
  if doctor.config.config_path.exist?
764
- 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
765
866
  print_transpiler_status
766
867
  print_bundler_status
767
868
  print_css_status
869
+ elsif verbose?
870
+ puts "✗ Configuration file not found"
768
871
  end
769
872
 
770
873
  print_node_status
771
874
  print_package_manager_status
772
875
  print_binstub_status
876
+ print_verbose_checks if verbose?
773
877
  print_info_messages
774
878
  end
775
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
+
776
928
  def print_transpiler_status
777
929
  transpiler = doctor.config.javascript_transpiler
778
930
  return if transpiler.nil? || transpiler == "none"
@@ -826,9 +978,20 @@ module Shakapacker
826
978
  end
827
979
 
828
980
  def print_binstub_status
829
- binstub_path = doctor.root_path.join("bin/shakapacker")
830
- if binstub_path.exist?
831
- puts "✓ Shakapacker 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
832
995
  end
833
996
  end
834
997
 
@@ -843,13 +1006,14 @@ module Shakapacker
843
1006
 
844
1007
  def print_summary
845
1008
  puts "=" * 60
1009
+ puts ""
846
1010
 
847
1011
  if doctor.issues.empty? && doctor.warnings.empty?
848
1012
  puts "✅ No issues found! Shakapacker appears to be configured correctly."
849
1013
  else
850
1014
  print_issues if doctor.issues.any?
851
1015
  print_warnings if doctor.warnings.any?
852
- print_fix_instructions
1016
+ print_fix_instructions if has_dependency_issues?
853
1017
  end
854
1018
  end
855
1019
 
@@ -862,17 +1026,98 @@ module Shakapacker
862
1026
  end
863
1027
 
864
1028
  def print_warnings
865
- puts "⚠️ Warnings (#{doctor.warnings.length}):"
866
- doctor.warnings.each_with_index do |warning, index|
867
- 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
868
1059
  end
869
1060
  puts ""
870
1061
  end
871
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
+
872
1111
  def print_fix_instructions
873
1112
  package_manager = doctor.send(:package_manager)
874
1113
  puts "To fix missing dependencies, run:"
875
1114
  puts " #{package_manager_install_command(package_manager)}"
1115
+ puts ""
1116
+ puts "For debugging configuration issues, export your webpack/rspack config:"
1117
+ puts " bin/export-bundler-config --doctor"
1118
+ puts " (Exports annotated YAML configs for dev and production - best for troubleshooting)"
1119
+ puts ""
1120
+ puts " See 'bin/export-bundler-config --help' for more options"
876
1121
  end
877
1122
 
878
1123
  def package_manager_install_command(manager)
@@ -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
@@ -6,7 +6,7 @@ module Shakapacker
6
6
  class RspackRunner < Shakapacker::Runner
7
7
  def self.run(argv)
8
8
  $stdout.sync = true
9
- ENV["NODE_ENV"] ||= (ENV["RAILS_ENV"] == "production") ? "production" : "development"
9
+ Shakapacker.ensure_node_env!
10
10
  new(argv).run
11
11
  end
12
12