shakapacker 8.0.2 → 9.2.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/.eslintignore +1 -0
- data/.eslintrc.fast.js +40 -0
- data/.eslintrc.js +48 -0
- data/.github/STATUS.md +1 -0
- data/.github/workflows/claude-code-review.yml +54 -0
- data/.github/workflows/claude.yml +50 -0
- data/.github/workflows/dummy.yml +9 -4
- data/.github/workflows/generator.yml +32 -10
- data/.github/workflows/node.yml +23 -1
- data/.github/workflows/ruby.yml +33 -2
- data/.github/workflows/test-bundlers.yml +170 -0
- data/.gitignore +20 -0
- data/.husky/pre-commit +2 -0
- data/.npmignore +56 -0
- data/.prettierignore +3 -0
- data/.rubocop.yml +1 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +302 -16
- data/CLAUDE.md +29 -0
- data/CONTRIBUTING.md +138 -20
- data/Gemfile.lock +83 -89
- data/README.md +343 -105
- data/Rakefile +39 -4
- data/TODO.md +50 -0
- data/TODO_v9.md +87 -0
- data/bin/export-bundler-config +11 -0
- data/conductor-setup.sh +70 -0
- data/conductor.json +7 -0
- data/docs/cdn_setup.md +379 -0
- data/docs/common-upgrades.md +615 -0
- data/docs/css-modules-export-mode.md +512 -0
- data/docs/deployment.md +62 -9
- data/docs/optional-peer-dependencies.md +198 -0
- data/docs/peer-dependencies.md +60 -0
- data/docs/react.md +6 -14
- data/docs/releasing.md +197 -0
- data/docs/rspack.md +190 -0
- data/docs/rspack_migration_guide.md +305 -0
- data/docs/subresource_integrity.md +54 -0
- data/docs/transpiler-migration.md +209 -0
- data/docs/transpiler-performance.md +179 -0
- data/docs/troubleshooting.md +157 -22
- data/docs/typescript-migration.md +379 -0
- data/docs/typescript.md +99 -0
- data/docs/using_esbuild_loader.md +3 -3
- data/docs/using_swc_loader.md +112 -10
- data/docs/v6_upgrade.md +10 -0
- data/docs/v8_upgrade.md +3 -5
- data/docs/v9_upgrade.md +458 -0
- data/gemfiles/Gemfile-rails.6.0.x +2 -1
- data/gemfiles/Gemfile-rails.6.1.x +1 -1
- data/gemfiles/Gemfile-rails.7.0.x +2 -2
- data/gemfiles/Gemfile-rails.7.1.x +1 -2
- data/gemfiles/Gemfile-rails.7.2.x +11 -0
- data/gemfiles/Gemfile-rails.8.0.x +11 -0
- data/lib/install/bin/export-bundler-config +11 -0
- data/lib/install/bin/shakapacker +4 -6
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/install/config/rspack/rspack.config.js +6 -0
- data/lib/install/config/rspack/rspack.config.ts +7 -0
- data/lib/install/config/shakapacker.yml +25 -5
- data/lib/install/config/webpack/webpack.config.ts +7 -0
- data/lib/install/package.json +38 -0
- data/lib/install/template.rb +194 -44
- data/lib/shakapacker/bundler_switcher.rb +329 -0
- data/lib/shakapacker/compiler.rb +2 -1
- data/lib/shakapacker/compiler_strategy.rb +2 -2
- data/lib/shakapacker/configuration.rb +173 -2
- data/lib/shakapacker/dev_server_runner.rb +29 -8
- data/lib/shakapacker/digest_strategy.rb +2 -1
- data/lib/shakapacker/doctor.rb +905 -0
- data/lib/shakapacker/helper.rb +64 -16
- data/lib/shakapacker/manifest.rb +10 -3
- data/lib/shakapacker/mtime_strategy.rb +1 -1
- data/lib/shakapacker/railtie.rb +4 -4
- data/lib/shakapacker/rspack_runner.rb +19 -0
- data/lib/shakapacker/runner.rb +159 -10
- data/lib/shakapacker/swc_migrator.rb +384 -0
- data/lib/shakapacker/utils/manager.rb +15 -2
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/version_checker.rb +2 -2
- data/lib/shakapacker/webpack_runner.rb +6 -43
- data/lib/shakapacker.rb +22 -11
- data/lib/tasks/shakapacker/doctor.rake +8 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
- data/lib/tasks/shakapacker/install.rake +12 -2
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
- data/lib/tasks/shakapacker.rake +2 -0
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +56 -0
- data/package/config.ts +175 -0
- data/package/configExporter/cli.ts +683 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/fileWriter.ts +92 -0
- data/package/configExporter/index.ts +5 -0
- data/package/configExporter/types.ts +36 -0
- data/package/configExporter/yamlSerializer.ts +266 -0
- data/package/{dev_server.js → dev_server.ts} +8 -5
- data/package/env.ts +92 -0
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +30 -0
- data/package/environments/{base.js → base.ts} +56 -60
- data/package/environments/development.ts +90 -0
- data/package/environments/production.ts +80 -0
- data/package/environments/test.ts +53 -0
- data/package/environments/types.ts +98 -0
- data/package/esbuild/index.ts +42 -0
- data/package/index.d.ts +3 -60
- data/package/index.ts +55 -0
- data/package/loaders.d.ts +28 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/webpack.ts +57 -0
- data/package/plugins/rspack.ts +103 -0
- data/package/plugins/webpack.ts +62 -0
- data/package/rspack/index.ts +64 -0
- data/package/rules/{babel.js → babel.ts} +2 -2
- data/package/rules/{coffee.js → coffee.ts} +1 -1
- data/package/rules/css.ts +3 -0
- data/package/rules/{erb.js → erb.ts} +1 -1
- data/package/rules/esbuild.ts +10 -0
- data/package/rules/file.ts +40 -0
- data/package/rules/{jscommon.js → jscommon.ts} +4 -4
- data/package/rules/{less.js → less.ts} +4 -4
- data/package/rules/raw.ts +25 -0
- data/package/rules/rspack.ts +176 -0
- data/package/rules/{sass.js → sass.ts} +7 -3
- data/package/rules/{stylus.js → stylus.ts} +4 -8
- data/package/rules/swc.ts +10 -0
- data/package/rules/webpack.ts +16 -0
- data/package/swc/index.ts +56 -0
- data/package/types/README.md +88 -0
- data/package/types/index.ts +61 -0
- data/package/types.ts +108 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/debug.ts +49 -0
- data/package/utils/defaultConfigPath.ts +4 -0
- data/package/utils/errorCodes.ts +219 -0
- data/package/utils/errorHelpers.ts +143 -0
- data/package/utils/getStyleRule.ts +64 -0
- data/package/utils/helpers.ts +85 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/pathValidation.ts +139 -0
- data/package/utils/requireOrError.ts +15 -0
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +342 -0
- data/package/utils/validateDependencies.ts +61 -0
- data/package/webpack-types.d.ts +33 -0
- data/package/webpackDevServerConfig.ts +117 -0
- data/package-lock.json +13047 -0
- data/package.json +154 -18
- data/scripts/remove-use-strict.js +45 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/test/helpers.js +1 -1
- data/test/package/config.test.js +43 -0
- data/test/package/env.test.js +42 -7
- data/test/package/environments/base.test.js +5 -1
- data/test/package/rules/babel.test.js +16 -0
- data/test/package/rules/esbuild.test.js +1 -1
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/swc.test.js +1 -1
- data/test/package/rules/webpack.test.js +35 -0
- data/test/package/staging.test.js +4 -3
- data/test/package/transpiler-defaults.test.js +127 -0
- data/test/peer-dependencies.sh +85 -0
- data/test/scripts/remove-use-strict.test.js +125 -0
- data/test/typescript/build.test.js +118 -0
- data/test/typescript/environments.test.js +107 -0
- data/test/typescript/pathValidation.test.js +142 -0
- data/test/typescript/securityValidation.test.js +182 -0
- data/tools/README.md +124 -0
- data/tools/css-modules-v9-codemod.js +179 -0
- data/tsconfig.eslint.json +16 -0
- data/tsconfig.json +38 -0
- data/yarn.lock +4165 -2706
- metadata +129 -41
- data/package/babel/preset.js +0 -37
- data/package/config.js +0 -54
- data/package/env.js +0 -48
- data/package/environments/development.js +0 -13
- data/package/environments/production.js +0 -88
- data/package/environments/test.js +0 -3
- data/package/esbuild/index.js +0 -40
- data/package/index.js +0 -40
- data/package/rules/css.js +0 -3
- data/package/rules/esbuild.js +0 -10
- data/package/rules/file.js +0 -29
- data/package/rules/index.js +0 -20
- data/package/rules/raw.js +0 -5
- data/package/rules/swc.js +0 -10
- data/package/swc/index.js +0 -50
- data/package/utils/configPath.js +0 -4
- data/package/utils/defaultConfigPath.js +0 -2
- data/package/utils/getStyleRule.js +0 -40
- data/package/utils/helpers.js +0 -58
- data/package/utils/snakeToCamelCase.js +0 -5
- data/package/webpackDevServerConfig.js +0 -71
- data/test/package/rules/index.test.js +0 -16
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "pathname"
|
|
3
|
+
require "open3"
|
|
4
|
+
require "semantic_range"
|
|
5
|
+
|
|
6
|
+
module Shakapacker
|
|
7
|
+
class Doctor
|
|
8
|
+
attr_reader :config, :root_path, :issues, :warnings, :info
|
|
9
|
+
|
|
10
|
+
def initialize(config = nil, root_path = nil)
|
|
11
|
+
@config = config || Shakapacker.config
|
|
12
|
+
@root_path = root_path || (defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd))
|
|
13
|
+
@issues = []
|
|
14
|
+
@warnings = []
|
|
15
|
+
@info = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
perform_checks
|
|
20
|
+
report_results
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def success?
|
|
24
|
+
@issues.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def perform_checks
|
|
30
|
+
# Core configuration checks
|
|
31
|
+
check_config_file
|
|
32
|
+
check_entry_points if config_exists?
|
|
33
|
+
check_output_paths if config_exists?
|
|
34
|
+
check_deprecated_config if config_exists?
|
|
35
|
+
|
|
36
|
+
# Environment checks
|
|
37
|
+
check_node_installation
|
|
38
|
+
check_package_manager
|
|
39
|
+
check_binstub
|
|
40
|
+
check_version_consistency
|
|
41
|
+
check_environment_consistency
|
|
42
|
+
|
|
43
|
+
# Dependency checks
|
|
44
|
+
check_javascript_transpiler_dependencies if config_exists?
|
|
45
|
+
check_css_dependencies
|
|
46
|
+
check_css_modules_configuration
|
|
47
|
+
check_bundler_dependencies if config_exists?
|
|
48
|
+
check_file_type_dependencies if config_exists?
|
|
49
|
+
check_sri_dependencies if config_exists?
|
|
50
|
+
check_peer_dependencies
|
|
51
|
+
|
|
52
|
+
# Platform and migration checks
|
|
53
|
+
check_windows_platform
|
|
54
|
+
check_legacy_webpacker_files
|
|
55
|
+
|
|
56
|
+
# Build and compilation checks
|
|
57
|
+
check_assets_compilation if config_exists?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check_config_file
|
|
61
|
+
unless config.config_path.exist?
|
|
62
|
+
@issues << "Configuration file not found at #{config.config_path}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_entry_points
|
|
67
|
+
# Check for invalid configuration first
|
|
68
|
+
if config.fetch(:source_entry_path) == "/" && config.nested_entries?
|
|
69
|
+
@issues << "Invalid configuration: cannot use '/' as source_entry_path with nested_entries: true"
|
|
70
|
+
return # Don't try to check files when config is invalid
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
source_entry_path = config.source_path.join(config.fetch(:source_entry_path) || "packs")
|
|
74
|
+
|
|
75
|
+
unless source_entry_path.exist?
|
|
76
|
+
@issues << "Source entry path #{source_entry_path} does not exist"
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check for at least one entry point
|
|
81
|
+
entry_files = Dir.glob(File.join(source_entry_path, "**/*.{js,jsx,ts,tsx,coffee}"))
|
|
82
|
+
if entry_files.empty?
|
|
83
|
+
@warnings << "No entry point files found in #{source_entry_path}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_output_paths
|
|
88
|
+
public_output_path = config.public_output_path
|
|
89
|
+
|
|
90
|
+
# Check if output directory is writable
|
|
91
|
+
if public_output_path.exist?
|
|
92
|
+
unless File.writable?(public_output_path)
|
|
93
|
+
@issues << "Public output path #{public_output_path} is not writable"
|
|
94
|
+
end
|
|
95
|
+
elsif public_output_path.parent.exist?
|
|
96
|
+
unless File.writable?(public_output_path.parent)
|
|
97
|
+
@issues << "Cannot create public output path #{public_output_path} (parent directory not writable)"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check manifest.json
|
|
102
|
+
manifest_path = config.manifest_path
|
|
103
|
+
if manifest_path.exist?
|
|
104
|
+
unless File.readable?(manifest_path)
|
|
105
|
+
@issues << "Manifest file #{manifest_path} exists but is not readable"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if manifest is stale
|
|
109
|
+
begin
|
|
110
|
+
manifest_content = JSON.parse(File.read(manifest_path))
|
|
111
|
+
if manifest_content.empty?
|
|
112
|
+
@warnings << "Manifest file is empty - you may need to run 'rails assets:precompile'"
|
|
113
|
+
end
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
@issues << "Manifest file #{manifest_path} contains invalid JSON"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check cache path
|
|
120
|
+
cache_path = config.cache_path
|
|
121
|
+
if cache_path.exist? && !File.writable?(cache_path)
|
|
122
|
+
@issues << "Cache path #{cache_path} is not writable"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def check_deprecated_config
|
|
127
|
+
config_file = File.read(config.config_path)
|
|
128
|
+
|
|
129
|
+
if config_file.include?("webpack_loader:")
|
|
130
|
+
@warnings << "Deprecated config: 'webpack_loader' should be renamed to 'javascript_transpiler'"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if config_file.include?("bundler:")
|
|
134
|
+
@warnings << "Deprecated config: 'bundler' should be renamed to 'assets_bundler'"
|
|
135
|
+
end
|
|
136
|
+
rescue => e
|
|
137
|
+
# Ignore read errors as config file check already handles missing file
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def check_version_consistency
|
|
141
|
+
return unless package_json_exists?
|
|
142
|
+
|
|
143
|
+
# Check if shakapacker npm package version matches gem version
|
|
144
|
+
package_json = read_package_json
|
|
145
|
+
npm_version = package_json.dig("dependencies", "shakapacker") ||
|
|
146
|
+
package_json.dig("devDependencies", "shakapacker")
|
|
147
|
+
|
|
148
|
+
if npm_version
|
|
149
|
+
gem_version = Shakapacker::VERSION rescue nil
|
|
150
|
+
if gem_version && !versions_compatible?(gem_version, npm_version)
|
|
151
|
+
@warnings << "Version mismatch: shakapacker gem is #{gem_version} but npm package is #{npm_version}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if ensure_consistent_versioning is enabled and warn if versions might mismatch
|
|
156
|
+
if config.ensure_consistent_versioning?
|
|
157
|
+
@info << "Version consistency checking is enabled"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def check_environment_consistency
|
|
162
|
+
rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
|
|
163
|
+
node_env = ENV["NODE_ENV"]
|
|
164
|
+
|
|
165
|
+
if rails_env && node_env && rails_env != node_env
|
|
166
|
+
@warnings << "Environment mismatch: Rails.env is '#{rails_env}' but NODE_ENV is '#{node_env}'"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Check SHAKAPACKER_ASSET_HOST for production
|
|
170
|
+
if rails_env == "production" && ENV["SHAKAPACKER_ASSET_HOST"].nil?
|
|
171
|
+
@info << "SHAKAPACKER_ASSET_HOST not set - assets will be served from the application host"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def check_sri_dependencies
|
|
176
|
+
integrity_config = config.integrity
|
|
177
|
+
return unless integrity_config&.dig(:enabled)
|
|
178
|
+
|
|
179
|
+
bundler = config.assets_bundler
|
|
180
|
+
if bundler == "webpack"
|
|
181
|
+
unless package_installed?("webpack-subresource-integrity")
|
|
182
|
+
@issues << "SRI is enabled but 'webpack-subresource-integrity' is not installed"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Validate hash functions
|
|
187
|
+
hash_functions = integrity_config.dig(:hash_functions) || ["sha384"]
|
|
188
|
+
invalid_functions = hash_functions - ["sha256", "sha384", "sha512"]
|
|
189
|
+
unless invalid_functions.empty?
|
|
190
|
+
@issues << "Invalid SRI hash functions: #{invalid_functions.join(', ')}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def check_peer_dependencies
|
|
195
|
+
return unless package_json_exists?
|
|
196
|
+
|
|
197
|
+
bundler = config.assets_bundler
|
|
198
|
+
package_json = read_package_json
|
|
199
|
+
all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
|
200
|
+
|
|
201
|
+
if bundler == "webpack"
|
|
202
|
+
check_webpack_peer_deps(all_deps)
|
|
203
|
+
elsif bundler == "rspack"
|
|
204
|
+
check_rspack_peer_deps(all_deps)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Check for conflicting installations
|
|
208
|
+
if package_installed?("webpack") && package_installed?("@rspack/core")
|
|
209
|
+
@warnings << "Both webpack and rspack are installed - ensure assets_bundler is set correctly"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def check_webpack_peer_deps(deps)
|
|
214
|
+
essential_webpack = {
|
|
215
|
+
"webpack" => "^5.76.0",
|
|
216
|
+
"webpack-cli" => "^4.9.2 || ^5.0.0"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
essential_webpack.each do |package, version|
|
|
220
|
+
unless deps[package]
|
|
221
|
+
@issues << "Missing essential webpack dependency: #{package} (#{version})"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def check_rspack_peer_deps(deps)
|
|
227
|
+
essential_rspack = {
|
|
228
|
+
"@rspack/cli" => "^1.0.0",
|
|
229
|
+
"@rspack/core" => "^1.0.0"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
essential_rspack.each do |package, version|
|
|
233
|
+
unless deps[package]
|
|
234
|
+
@issues << "Missing essential rspack dependency: #{package} (#{version})"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def check_windows_platform
|
|
240
|
+
if Gem.win_platform?
|
|
241
|
+
@info << "Windows detected: You may need to run shakapacker scripts with 'ruby bin/shakapacker'"
|
|
242
|
+
|
|
243
|
+
# Check for case sensitivity issues
|
|
244
|
+
if File.exist?(root_path.join("App")) || File.exist?(root_path.join("APP"))
|
|
245
|
+
@warnings << "Potential case sensitivity issue detected on Windows filesystem"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def check_assets_compilation
|
|
251
|
+
manifest_path = config.manifest_path
|
|
252
|
+
|
|
253
|
+
if manifest_path.exist?
|
|
254
|
+
# Check if manifest is recent (within last 24 hours)
|
|
255
|
+
manifest_age_hours = (Time.now - File.mtime(manifest_path)) / 3600
|
|
256
|
+
|
|
257
|
+
if manifest_age_hours > 24
|
|
258
|
+
@info << "Assets were last compiled #{manifest_age_hours.round} hours ago. Consider recompiling if you've made changes."
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Check if source files are newer than manifest
|
|
262
|
+
source_files = Dir.glob(File.join(config.source_path, "**/*.{js,jsx,ts,tsx,css,scss,sass}"))
|
|
263
|
+
if source_files.any?
|
|
264
|
+
newest_source = source_files.map { |f| File.mtime(f) }.max
|
|
265
|
+
if newest_source > File.mtime(manifest_path)
|
|
266
|
+
@warnings << "Source files have been modified after last asset compilation. Run 'rails assets:precompile'"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
else
|
|
270
|
+
rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
|
|
271
|
+
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"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def check_legacy_webpacker_files
|
|
280
|
+
legacy_files = [
|
|
281
|
+
"config/webpacker.yml",
|
|
282
|
+
"config/webpack/webpacker.yml",
|
|
283
|
+
"bin/webpack",
|
|
284
|
+
"bin/webpack-dev-server"
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
legacy_files.each do |file|
|
|
288
|
+
file_path = root_path.join(file)
|
|
289
|
+
if file_path.exist?
|
|
290
|
+
@warnings << "Legacy webpacker file found: #{file} - consider removing after migration"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def check_node_installation
|
|
296
|
+
stdout, stderr, status = Open3.capture3("node", "--version")
|
|
297
|
+
|
|
298
|
+
if status.success?
|
|
299
|
+
node_version = stdout.strip
|
|
300
|
+
# Check minimum Node version (14.0.0 for modern tooling)
|
|
301
|
+
version_match = node_version.match(/v(\d+)\.(\d+)\.(\d+)/)
|
|
302
|
+
if version_match
|
|
303
|
+
major = version_match[1].to_i
|
|
304
|
+
if major < 14
|
|
305
|
+
@warnings << "Node.js version #{node_version} is outdated. Recommend upgrading to v14 or higher"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
else
|
|
309
|
+
@issues << "Node.js command failed: #{stderr}"
|
|
310
|
+
end
|
|
311
|
+
rescue Errno::ENOENT
|
|
312
|
+
@issues << "Node.js is not installed or not in PATH"
|
|
313
|
+
rescue Errno::EACCES
|
|
314
|
+
@issues << "Permission denied when checking Node.js version"
|
|
315
|
+
rescue StandardError => e
|
|
316
|
+
@warnings << "Unable to check Node.js version: #{e.message}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def check_package_manager
|
|
320
|
+
unless package_manager
|
|
321
|
+
@issues << "No package manager lock file found (package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb)"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
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."
|
|
329
|
+
end
|
|
330
|
+
|
|
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."
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def check_javascript_transpiler_dependencies
|
|
338
|
+
transpiler = config.javascript_transpiler
|
|
339
|
+
|
|
340
|
+
# Default to SWC for v9+ if not configured
|
|
341
|
+
if transpiler.nil?
|
|
342
|
+
@info << "No javascript_transpiler configured - defaulting to SWC (20x faster than Babel)"
|
|
343
|
+
transpiler = "swc"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
return if transpiler == "none"
|
|
347
|
+
|
|
348
|
+
bundler = config.assets_bundler
|
|
349
|
+
|
|
350
|
+
case transpiler
|
|
351
|
+
when "babel"
|
|
352
|
+
check_babel_dependencies
|
|
353
|
+
check_babel_performance_suggestion
|
|
354
|
+
when "swc"
|
|
355
|
+
check_swc_dependencies(bundler)
|
|
356
|
+
when "esbuild"
|
|
357
|
+
check_esbuild_dependencies
|
|
358
|
+
else
|
|
359
|
+
# Generic check for other transpilers
|
|
360
|
+
loader_name = "#{transpiler}-loader"
|
|
361
|
+
unless package_installed?(loader_name)
|
|
362
|
+
@issues << "Missing required dependency '#{loader_name}' for JavaScript transpiler '#{transpiler}'"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
check_transpiler_config_consistency
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def check_babel_dependencies
|
|
370
|
+
unless package_installed?("babel-loader")
|
|
371
|
+
@issues << "Missing required dependency 'babel-loader' for JavaScript transpiler 'babel'"
|
|
372
|
+
end
|
|
373
|
+
unless package_installed?("@babel/core")
|
|
374
|
+
@issues << "Missing required dependency '@babel/core' for Babel transpiler"
|
|
375
|
+
end
|
|
376
|
+
unless package_installed?("@babel/preset-env")
|
|
377
|
+
@issues << "Missing required dependency '@babel/preset-env' for Babel transpiler"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def check_babel_performance_suggestion
|
|
382
|
+
@info << "Consider switching to SWC for 20x faster compilation. Set javascript_transpiler: 'swc' in shakapacker.yml"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def check_swc_dependencies(bundler)
|
|
386
|
+
if bundler == "webpack"
|
|
387
|
+
unless package_installed?("@swc/core")
|
|
388
|
+
@issues << "Missing required dependency '@swc/core' for SWC transpiler"
|
|
389
|
+
end
|
|
390
|
+
unless package_installed?("swc-loader")
|
|
391
|
+
@issues << "Missing required dependency 'swc-loader' for SWC with webpack"
|
|
392
|
+
end
|
|
393
|
+
elsif bundler == "rspack"
|
|
394
|
+
# Rspack has built-in SWC support
|
|
395
|
+
@info << "Rspack has built-in SWC support - no additional loaders needed"
|
|
396
|
+
if package_installed?("swc-loader")
|
|
397
|
+
@warnings << "swc-loader is not needed with Rspack (SWC is built-in) - consider removing it"
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def check_esbuild_dependencies
|
|
403
|
+
unless package_installed?("esbuild")
|
|
404
|
+
@issues << "Missing required dependency 'esbuild' for esbuild transpiler"
|
|
405
|
+
end
|
|
406
|
+
unless package_installed?("esbuild-loader")
|
|
407
|
+
@issues << "Missing required dependency 'esbuild-loader' for esbuild transpiler"
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def check_transpiler_config_consistency
|
|
412
|
+
babel_configs = [
|
|
413
|
+
root_path.join(".babelrc"),
|
|
414
|
+
root_path.join(".babelrc.js"),
|
|
415
|
+
root_path.join(".babelrc.json"),
|
|
416
|
+
root_path.join("babel.config.js"),
|
|
417
|
+
root_path.join("babel.config.json")
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
babel_config_exists = babel_configs.any?(&:exist?)
|
|
421
|
+
|
|
422
|
+
# Check if package.json has babel config
|
|
423
|
+
if package_json_exists?
|
|
424
|
+
package_json = read_package_json
|
|
425
|
+
babel_config_exists ||= package_json.key?("babel")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
transpiler = config.javascript_transpiler
|
|
429
|
+
|
|
430
|
+
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'"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Check for redundant dependencies
|
|
435
|
+
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"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
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"
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Check for SWC configuration conflicts
|
|
444
|
+
if transpiler == "swc"
|
|
445
|
+
check_swc_config_conflicts
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def check_swc_config_conflicts
|
|
450
|
+
swcrc_path = root_path.join(".swcrc")
|
|
451
|
+
swc_config_path = root_path.join("config/swc.config.js")
|
|
452
|
+
|
|
453
|
+
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. " \
|
|
455
|
+
"Please migrate to config/swc.config.js which properly merges with Shakapacker defaults. " \
|
|
456
|
+
"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"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
if swc_config_path.exist?
|
|
461
|
+
@info << "SWC configuration: Using config/swc.config.js (recommended). This config is merged with Shakapacker's defaults."
|
|
462
|
+
check_swc_config_settings(swc_config_path)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def check_swc_config_settings(config_path)
|
|
467
|
+
config_content = File.read(config_path, encoding: "UTF-8")
|
|
468
|
+
|
|
469
|
+
# Check for loose: true (deprecated default)
|
|
470
|
+
if config_content.match?(/loose\s*:\s*true/)
|
|
471
|
+
@warnings << "SWC configuration: 'loose: true' detected in config/swc.config.js. " \
|
|
472
|
+
"This can cause silent failures with Stimulus controllers and incorrect spread operator behavior. " \
|
|
473
|
+
"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"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Check for missing keepClassNames with Stimulus
|
|
478
|
+
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. " \
|
|
480
|
+
"Without this setting, Stimulus controllers will fail silently. " \
|
|
481
|
+
"Add 'keepClassNames: true' to jsc config. " \
|
|
482
|
+
"See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
|
|
483
|
+
elsif config_content.match?(/keepClassNames\s*:\s*true/)
|
|
484
|
+
@info << "SWC configuration: 'keepClassNames: true' is set (good for Stimulus compatibility)"
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Check for jsc.target and env conflict
|
|
488
|
+
# Use word boundary to avoid false positives with transform.target or other nested properties
|
|
489
|
+
if config_content.match?(/jsc\s*:\s*\{[^}]*\btarget\s*:/) && config_content.match?(/env\s*:\s*\{/)
|
|
490
|
+
@issues << "SWC configuration: Both 'jsc.target' and 'env' are configured. These cannot be used together. " \
|
|
491
|
+
"Remove 'jsc.target' and use only 'env' (Shakapacker sets this automatically). " \
|
|
492
|
+
"See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
|
|
493
|
+
end
|
|
494
|
+
rescue => e
|
|
495
|
+
# Don't fail doctor if SWC config check has issues
|
|
496
|
+
@warnings << "Unable to validate SWC configuration: #{e.message}"
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def stimulus_likely_used?
|
|
500
|
+
return false unless package_json_exists?
|
|
501
|
+
|
|
502
|
+
package_json = read_package_json
|
|
503
|
+
dependencies = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
|
504
|
+
|
|
505
|
+
# Check for @hotwired/stimulus or stimulus package
|
|
506
|
+
dependencies.key?("@hotwired/stimulus") || dependencies.key?("stimulus")
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def check_css_dependencies
|
|
510
|
+
check_dependency("css-loader", @issues, "CSS")
|
|
511
|
+
check_dependency("style-loader", @issues, "CSS (style-loader)")
|
|
512
|
+
check_optional_dependency("mini-css-extract-plugin", @warnings, "CSS extraction")
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def check_css_modules_configuration
|
|
516
|
+
# Check for CSS module files in the project
|
|
517
|
+
return unless config_exists?
|
|
518
|
+
|
|
519
|
+
source_path = config.source_path
|
|
520
|
+
return unless source_path.exist?
|
|
521
|
+
|
|
522
|
+
# Performance optimization: Just check if ANY CSS module file exists
|
|
523
|
+
# Using .first with early return is much faster than globbing all files
|
|
524
|
+
css_module_exists = Dir.glob(File.join(source_path, "**/*.module.{css,scss,sass}")).first
|
|
525
|
+
return unless css_module_exists
|
|
526
|
+
|
|
527
|
+
# Check webpack configuration for CSS modules settings
|
|
528
|
+
webpack_config_paths = [
|
|
529
|
+
root_path.join("config/webpack/webpack.config.js"),
|
|
530
|
+
root_path.join("config/webpack/webpack.config.ts"),
|
|
531
|
+
root_path.join("config/webpack/commonWebpackConfig.js"),
|
|
532
|
+
root_path.join("config/webpack/commonWebpackConfig.ts")
|
|
533
|
+
]
|
|
534
|
+
|
|
535
|
+
webpack_config_paths.each do |config_path|
|
|
536
|
+
next unless config_path.exist?
|
|
537
|
+
|
|
538
|
+
config_content = File.read(config_path)
|
|
539
|
+
|
|
540
|
+
# Check for the invalid configuration: namedExport: true with exportLocalsConvention: 'camelCase'
|
|
541
|
+
if config_content.match(/namedExport\s*:\s*true/) && config_content.match(/exportLocalsConvention\s*:\s*['"]camelCase['"]/)
|
|
542
|
+
@issues << "CSS Modules: Invalid configuration detected in #{config_path.relative_path_from(root_path)}"
|
|
543
|
+
@issues << " Using exportLocalsConvention: 'camelCase' with namedExport: true will cause build errors"
|
|
544
|
+
@issues << " Change to 'camelCaseOnly' or 'dashesOnly'. See docs/v9_upgrade.md for details"
|
|
545
|
+
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
|
+
end
|
|
553
|
+
|
|
554
|
+
# Check for common v8 to v9 migration issues
|
|
555
|
+
check_css_modules_import_patterns
|
|
556
|
+
rescue => e
|
|
557
|
+
# Don't fail doctor if CSS modules check has issues
|
|
558
|
+
@warnings << "Unable to validate CSS modules configuration: #{e.message}"
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def check_css_modules_import_patterns
|
|
562
|
+
# Look for JavaScript/TypeScript files that might have v8-style imports
|
|
563
|
+
source_path = config.source_path
|
|
564
|
+
|
|
565
|
+
# Use lazy evaluation with Enumerator to avoid loading all file paths into memory
|
|
566
|
+
# Stop after checking 50 files or finding a match
|
|
567
|
+
v8_pattern = /import\s+\w+\s+from\s+['"][^'"]*\.module\.(css|scss|sass)['"]/
|
|
568
|
+
|
|
569
|
+
Dir.glob(File.join(source_path, "**/*.{js,jsx,ts,tsx}")).lazy.take(50).each do |file|
|
|
570
|
+
# Read file and check for v8 pattern
|
|
571
|
+
content = File.read(file)
|
|
572
|
+
|
|
573
|
+
# Check for v8 default import pattern with .module.css
|
|
574
|
+
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"
|
|
579
|
+
break # Stop after finding first occurrence
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
rescue => e
|
|
583
|
+
# Don't fail doctor if import pattern check has issues
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def check_bundler_dependencies
|
|
587
|
+
bundler = config.assets_bundler
|
|
588
|
+
case bundler
|
|
589
|
+
when "webpack"
|
|
590
|
+
check_dependency("webpack", @issues, "webpack")
|
|
591
|
+
check_dependency("webpack-cli", @issues, "webpack CLI")
|
|
592
|
+
when "rspack"
|
|
593
|
+
check_dependency("@rspack/core", @issues, "Rspack")
|
|
594
|
+
check_dependency("@rspack/cli", @issues, "Rspack CLI")
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def check_file_type_dependencies
|
|
599
|
+
source_path = config.source_path
|
|
600
|
+
return unless source_path.exist?
|
|
601
|
+
|
|
602
|
+
check_typescript_dependencies if typescript_files_exist?
|
|
603
|
+
check_sass_dependencies if sass_files_exist?
|
|
604
|
+
check_less_dependencies if less_files_exist?
|
|
605
|
+
check_stylus_dependencies if stylus_files_exist?
|
|
606
|
+
check_postcss_dependencies if postcss_config_exists?
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def check_typescript_dependencies
|
|
610
|
+
transpiler = config.javascript_transpiler
|
|
611
|
+
if transpiler == "babel"
|
|
612
|
+
check_optional_dependency("@babel/preset-typescript", @warnings, "TypeScript with Babel")
|
|
613
|
+
elsif transpiler != "esbuild" && transpiler != "swc"
|
|
614
|
+
check_optional_dependency("ts-loader", @warnings, "TypeScript")
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def check_sass_dependencies
|
|
619
|
+
check_dependency("sass-loader", @issues, "Sass/SCSS")
|
|
620
|
+
check_dependency("sass", @issues, "Sass/SCSS (sass package)")
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def check_less_dependencies
|
|
624
|
+
check_dependency("less-loader", @issues, "Less")
|
|
625
|
+
check_dependency("less", @issues, "Less (less package)")
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def check_stylus_dependencies
|
|
629
|
+
check_dependency("stylus-loader", @issues, "Stylus")
|
|
630
|
+
check_dependency("stylus", @issues, "Stylus (stylus package)")
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def check_postcss_dependencies
|
|
634
|
+
check_dependency("postcss", @issues, "PostCSS")
|
|
635
|
+
check_dependency("postcss-loader", @issues, "PostCSS")
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def check_dependency(package_name, issues_array, description)
|
|
639
|
+
unless package_installed?(package_name)
|
|
640
|
+
issues_array << "Missing required dependency '#{package_name}' for #{description}"
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def check_optional_dependency(package_name, warnings_array, description)
|
|
645
|
+
unless package_installed?(package_name)
|
|
646
|
+
warnings_array << "Optional dependency '#{package_name}' for #{description} is not installed"
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def package_installed?(package_name)
|
|
651
|
+
return false unless package_json_exists?
|
|
652
|
+
|
|
653
|
+
package_json = read_package_json
|
|
654
|
+
dependencies = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
|
655
|
+
dependencies.key?(package_name)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def package_json_exists?
|
|
659
|
+
package_json_path.exist?
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def package_json_path
|
|
663
|
+
root_path.join("package.json")
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def read_package_json
|
|
667
|
+
@package_json ||= begin
|
|
668
|
+
JSON.parse(File.read(package_json_path))
|
|
669
|
+
rescue JSON::ParserError
|
|
670
|
+
{}
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def config_exists?
|
|
675
|
+
config.config_path.exist?
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def typescript_files_exist?
|
|
679
|
+
# Use .first for early exit optimization
|
|
680
|
+
!Dir.glob(File.join(config.source_path, "**/*.{ts,tsx}")).first.nil?
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def sass_files_exist?
|
|
684
|
+
!Dir.glob(File.join(config.source_path, "**/*.{sass,scss}")).first.nil?
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def less_files_exist?
|
|
688
|
+
!Dir.glob(File.join(config.source_path, "**/*.less")).first.nil?
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def stylus_files_exist?
|
|
692
|
+
!Dir.glob(File.join(config.source_path, "**/*.{styl,stylus}")).first.nil?
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def postcss_config_exists?
|
|
696
|
+
root_path.join("postcss.config.js").exist?
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def package_manager
|
|
700
|
+
@package_manager ||= detect_package_manager
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def detect_package_manager
|
|
704
|
+
return "bun" if File.exist?(root_path.join("bun.lockb"))
|
|
705
|
+
return "pnpm" if File.exist?(root_path.join("pnpm-lock.yaml"))
|
|
706
|
+
return "yarn" if File.exist?(root_path.join("yarn.lock"))
|
|
707
|
+
return "npm" if File.exist?(root_path.join("package-lock.json"))
|
|
708
|
+
nil
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def versions_compatible?(gem_version, npm_version)
|
|
712
|
+
# Handle pre-release versions and ranges properly
|
|
713
|
+
npm_clean = npm_version.gsub(/[\^~]/, "")
|
|
714
|
+
|
|
715
|
+
# Extract version without pre-release suffix
|
|
716
|
+
gem_base = gem_version.split("-").first
|
|
717
|
+
npm_base = npm_clean.split("-").first
|
|
718
|
+
|
|
719
|
+
# Compare major versions
|
|
720
|
+
gem_major = gem_base.split(".").first
|
|
721
|
+
npm_major = npm_base.split(".").first
|
|
722
|
+
|
|
723
|
+
if gem_major != npm_major
|
|
724
|
+
return false
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# For same major version, check if npm version satisfies gem version
|
|
728
|
+
begin
|
|
729
|
+
# Use semantic versioning if available
|
|
730
|
+
if defined?(SemanticRange)
|
|
731
|
+
SemanticRange.satisfies?(gem_version, npm_version)
|
|
732
|
+
else
|
|
733
|
+
gem_major == npm_major
|
|
734
|
+
end
|
|
735
|
+
rescue StandardError
|
|
736
|
+
# Fallback to simple major version comparison
|
|
737
|
+
gem_major == npm_major
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def report_results
|
|
742
|
+
reporter = Reporter.new(self)
|
|
743
|
+
reporter.print_report
|
|
744
|
+
exit(1) unless success?
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
class Reporter
|
|
748
|
+
attr_reader :doctor
|
|
749
|
+
|
|
750
|
+
def initialize(doctor)
|
|
751
|
+
@doctor = doctor
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def print_report
|
|
755
|
+
print_header
|
|
756
|
+
print_checks
|
|
757
|
+
print_summary
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
private
|
|
761
|
+
|
|
762
|
+
def print_header
|
|
763
|
+
puts "Running Shakapacker doctor..."
|
|
764
|
+
puts "=" * 60
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def print_checks
|
|
768
|
+
if doctor.config.config_path.exist?
|
|
769
|
+
puts "✓ Configuration file found"
|
|
770
|
+
print_transpiler_status
|
|
771
|
+
print_bundler_status
|
|
772
|
+
print_css_status
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
print_node_status
|
|
776
|
+
print_package_manager_status
|
|
777
|
+
print_binstub_status
|
|
778
|
+
print_info_messages
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def print_transpiler_status
|
|
782
|
+
transpiler = doctor.config.javascript_transpiler
|
|
783
|
+
return if transpiler.nil? || transpiler == "none"
|
|
784
|
+
|
|
785
|
+
loader_name = "#{transpiler}-loader"
|
|
786
|
+
if doctor.send(:package_installed?, loader_name)
|
|
787
|
+
puts "✓ JavaScript transpiler: #{loader_name} is installed"
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def print_bundler_status
|
|
792
|
+
bundler = doctor.config.assets_bundler
|
|
793
|
+
case bundler
|
|
794
|
+
when "webpack"
|
|
795
|
+
print_package_status("webpack", "webpack")
|
|
796
|
+
print_package_status("webpack-cli", "webpack CLI")
|
|
797
|
+
when "rspack"
|
|
798
|
+
print_package_status("@rspack/core", "Rspack")
|
|
799
|
+
print_package_status("@rspack/cli", "Rspack CLI")
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def print_css_status
|
|
804
|
+
print_package_status("css-loader", "CSS")
|
|
805
|
+
print_package_status("style-loader", "CSS (style-loader)")
|
|
806
|
+
print_package_status("mini-css-extract-plugin", "CSS extraction (optional)")
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def print_package_status(package_name, description)
|
|
810
|
+
if doctor.send(:package_installed?, package_name)
|
|
811
|
+
puts "✓ #{description}: #{package_name} is installed"
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def print_node_status
|
|
816
|
+
begin
|
|
817
|
+
stdout, stderr, status = Open3.capture3("node", "--version")
|
|
818
|
+
if status.success?
|
|
819
|
+
puts "✓ Node.js #{stdout.strip} found"
|
|
820
|
+
end
|
|
821
|
+
rescue Errno::ENOENT, Errno::EACCES, StandardError
|
|
822
|
+
# Error already added to issues
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def print_package_manager_status
|
|
827
|
+
package_manager = doctor.send(:package_manager)
|
|
828
|
+
if package_manager
|
|
829
|
+
puts "✓ Package manager: #{package_manager}"
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
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"
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def print_info_messages
|
|
846
|
+
return if doctor.info.empty?
|
|
847
|
+
|
|
848
|
+
puts "\nℹ️ Information:"
|
|
849
|
+
doctor.info.each do |info|
|
|
850
|
+
puts " • #{info}"
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def print_summary
|
|
855
|
+
puts "=" * 60
|
|
856
|
+
|
|
857
|
+
if doctor.issues.empty? && doctor.warnings.empty?
|
|
858
|
+
puts "✅ No issues found! Shakapacker appears to be configured correctly."
|
|
859
|
+
else
|
|
860
|
+
print_issues if doctor.issues.any?
|
|
861
|
+
print_warnings if doctor.warnings.any?
|
|
862
|
+
print_fix_instructions
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def print_issues
|
|
867
|
+
puts "❌ Issues found (#{doctor.issues.length}):"
|
|
868
|
+
doctor.issues.each_with_index do |issue, index|
|
|
869
|
+
puts " #{index + 1}. #{issue}"
|
|
870
|
+
end
|
|
871
|
+
puts ""
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def print_warnings
|
|
875
|
+
puts "⚠️ Warnings (#{doctor.warnings.length}):"
|
|
876
|
+
doctor.warnings.each_with_index do |warning, index|
|
|
877
|
+
puts " #{index + 1}. #{warning}"
|
|
878
|
+
end
|
|
879
|
+
puts ""
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def print_fix_instructions
|
|
883
|
+
package_manager = doctor.send(:package_manager)
|
|
884
|
+
puts "To fix missing dependencies, run:"
|
|
885
|
+
puts " #{package_manager_install_command(package_manager)}"
|
|
886
|
+
puts ""
|
|
887
|
+
puts "For debugging configuration issues, export your webpack/rspack config:"
|
|
888
|
+
puts " bin/export-bundler-config --doctor"
|
|
889
|
+
puts " (Exports annotated YAML configs for dev and production - best for troubleshooting)"
|
|
890
|
+
puts ""
|
|
891
|
+
puts " See 'bin/export-bundler-config --help' for more options"
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def package_manager_install_command(manager)
|
|
895
|
+
case manager
|
|
896
|
+
when "bun" then "bun add -D [package-name]"
|
|
897
|
+
when "pnpm" then "pnpm add -D [package-name]"
|
|
898
|
+
when "yarn" then "yarn add -D [package-name]"
|
|
899
|
+
when "npm" then "npm install --save-dev [package-name]"
|
|
900
|
+
else "npm install --save-dev [package-name]"
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|