shakapacker 9.0.0.beta.4 → 9.0.0.beta.5
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/.github/workflows/dummy.yml +4 -0
- data/.github/workflows/generator.yml +7 -0
- data/.github/workflows/node.yml +22 -0
- data/.github/workflows/ruby.yml +11 -0
- data/.github/workflows/test-bundlers.yml +18 -0
- data/.gitignore +20 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +58 -40
- data/Gemfile.lock +1 -1
- data/README.md +3 -1
- data/docs/typescript.md +99 -0
- data/docs/v9_upgrade.md +14 -1
- data/lib/install/template.rb +8 -1
- data/lib/shakapacker/configuration.rb +58 -1
- data/lib/shakapacker/doctor.rb +752 -0
- data/lib/shakapacker/swc_migrator.rb +292 -0
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker.rb +1 -0
- data/lib/tasks/shakapacker/doctor.rake +8 -0
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker.rake +1 -0
- data/package/config.ts +162 -0
- data/package/{dev_server.js → dev_server.ts} +8 -5
- data/package/env.ts +67 -0
- data/package/environments/base.js +21 -31
- data/package/environments/base.ts +137 -0
- data/package/index.d.ts +3 -150
- data/package/{index.js → index.ts} +17 -8
- data/package/loaders.d.ts +27 -0
- data/package/types.ts +108 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/{debug.js → debug.ts} +7 -7
- data/package/utils/defaultConfigPath.ts +4 -0
- data/package/utils/errorHelpers.ts +77 -0
- data/package/utils/{getStyleRule.js → getStyleRule.ts} +17 -20
- data/package/utils/helpers.ts +85 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/{requireOrError.js → requireOrError.ts} +2 -2
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +228 -0
- data/package/utils/{validateDependencies.js → validateDependencies.ts} +4 -4
- data/package/webpack-types.d.ts +32 -0
- data/package/webpackDevServerConfig.ts +117 -0
- data/package.json +6 -2
- data/test/typescript/build.test.js +117 -0
- data/tsconfig.json +39 -0
- data/yarn.lock +1 -1
- metadata +31 -17
- data/package/config.js +0 -80
- data/package/env.js +0 -48
- data/package/utils/configPath.js +0 -4
- data/package/utils/defaultConfigPath.js +0 -2
- data/package/utils/helpers.js +0 -127
- data/package/utils/snakeToCamelCase.js +0 -5
- data/package/utils/validateCssModulesConfig.js +0 -91
- data/package/webpackDevServerConfig.js +0 -73
@@ -0,0 +1,752 @@
|
|
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_bundler_dependencies if config_exists?
|
47
|
+
check_file_type_dependencies if config_exists?
|
48
|
+
check_sri_dependencies if config_exists?
|
49
|
+
check_peer_dependencies
|
50
|
+
|
51
|
+
# Platform and migration checks
|
52
|
+
check_windows_platform
|
53
|
+
check_legacy_webpacker_files
|
54
|
+
|
55
|
+
# Build and compilation checks
|
56
|
+
check_assets_compilation if config_exists?
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_config_file
|
60
|
+
unless config.config_path.exist?
|
61
|
+
@issues << "Configuration file not found at #{config.config_path}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_entry_points
|
66
|
+
# Check for invalid configuration first
|
67
|
+
if config.data[:source_entry_path] == "/" && config.nested_entries?
|
68
|
+
@issues << "Invalid configuration: cannot use '/' as source_entry_path with nested_entries: true"
|
69
|
+
return # Don't try to check files when config is invalid
|
70
|
+
end
|
71
|
+
|
72
|
+
source_entry_path = config.source_path.join(config.data[:source_entry_path] || "packs")
|
73
|
+
|
74
|
+
unless source_entry_path.exist?
|
75
|
+
@issues << "Source entry path #{source_entry_path} does not exist"
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check for at least one entry point
|
80
|
+
entry_files = Dir.glob(File.join(source_entry_path, "**/*.{js,jsx,ts,tsx,coffee}"))
|
81
|
+
if entry_files.empty?
|
82
|
+
@warnings << "No entry point files found in #{source_entry_path}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def check_output_paths
|
87
|
+
public_output_path = config.public_output_path
|
88
|
+
|
89
|
+
# Check if output directory is writable
|
90
|
+
if public_output_path.exist?
|
91
|
+
unless File.writable?(public_output_path)
|
92
|
+
@issues << "Public output path #{public_output_path} is not writable"
|
93
|
+
end
|
94
|
+
elsif public_output_path.parent.exist?
|
95
|
+
unless File.writable?(public_output_path.parent)
|
96
|
+
@issues << "Cannot create public output path #{public_output_path} (parent directory not writable)"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check manifest.json
|
101
|
+
manifest_path = config.manifest_path
|
102
|
+
if manifest_path.exist?
|
103
|
+
unless File.readable?(manifest_path)
|
104
|
+
@issues << "Manifest file #{manifest_path} exists but is not readable"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check if manifest is stale
|
108
|
+
begin
|
109
|
+
manifest_content = JSON.parse(File.read(manifest_path))
|
110
|
+
if manifest_content.empty?
|
111
|
+
@warnings << "Manifest file is empty - you may need to run 'rails assets:precompile'"
|
112
|
+
end
|
113
|
+
rescue JSON::ParserError
|
114
|
+
@issues << "Manifest file #{manifest_path} contains invalid JSON"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check cache path
|
119
|
+
cache_path = config.cache_path
|
120
|
+
if cache_path.exist? && !File.writable?(cache_path)
|
121
|
+
@issues << "Cache path #{cache_path} is not writable"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def check_deprecated_config
|
126
|
+
config_file = File.read(config.config_path)
|
127
|
+
|
128
|
+
if config_file.include?("webpack_loader:")
|
129
|
+
@warnings << "Deprecated config: 'webpack_loader' should be renamed to 'javascript_transpiler'"
|
130
|
+
end
|
131
|
+
|
132
|
+
if config_file.include?("bundler:")
|
133
|
+
@warnings << "Deprecated config: 'bundler' should be renamed to 'assets_bundler'"
|
134
|
+
end
|
135
|
+
rescue => e
|
136
|
+
# Ignore read errors as config file check already handles missing file
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_version_consistency
|
140
|
+
return unless package_json_exists?
|
141
|
+
|
142
|
+
# Check if shakapacker npm package version matches gem version
|
143
|
+
package_json = read_package_json
|
144
|
+
npm_version = package_json.dig("dependencies", "shakapacker") ||
|
145
|
+
package_json.dig("devDependencies", "shakapacker")
|
146
|
+
|
147
|
+
if npm_version
|
148
|
+
gem_version = Shakapacker::VERSION rescue nil
|
149
|
+
if gem_version && !versions_compatible?(gem_version, npm_version)
|
150
|
+
@warnings << "Version mismatch: shakapacker gem is #{gem_version} but npm package is #{npm_version}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Check if ensure_consistent_versioning is enabled and warn if versions might mismatch
|
155
|
+
if config.ensure_consistent_versioning?
|
156
|
+
@info << "Version consistency checking is enabled"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def check_environment_consistency
|
161
|
+
rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
|
162
|
+
node_env = ENV["NODE_ENV"]
|
163
|
+
|
164
|
+
if rails_env && node_env && rails_env != node_env
|
165
|
+
@warnings << "Environment mismatch: Rails.env is '#{rails_env}' but NODE_ENV is '#{node_env}'"
|
166
|
+
end
|
167
|
+
|
168
|
+
# Check SHAKAPACKER_ASSET_HOST for production
|
169
|
+
if rails_env == "production" && ENV["SHAKAPACKER_ASSET_HOST"].nil?
|
170
|
+
@info << "SHAKAPACKER_ASSET_HOST not set - assets will be served from the application host"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def check_sri_dependencies
|
175
|
+
return unless config.data.dig(:integrity, :enabled)
|
176
|
+
|
177
|
+
bundler = config.assets_bundler
|
178
|
+
if bundler == "webpack"
|
179
|
+
unless package_installed?("webpack-subresource-integrity")
|
180
|
+
@issues << "SRI is enabled but 'webpack-subresource-integrity' is not installed"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Validate hash functions
|
185
|
+
hash_functions = config.data.dig(:integrity, :hash_functions) || ["sha384"]
|
186
|
+
invalid_functions = hash_functions - ["sha256", "sha384", "sha512"]
|
187
|
+
unless invalid_functions.empty?
|
188
|
+
@issues << "Invalid SRI hash functions: #{invalid_functions.join(', ')}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def check_peer_dependencies
|
193
|
+
return unless package_json_exists?
|
194
|
+
|
195
|
+
bundler = config.assets_bundler
|
196
|
+
package_json = read_package_json
|
197
|
+
all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
198
|
+
|
199
|
+
if bundler == "webpack"
|
200
|
+
check_webpack_peer_deps(all_deps)
|
201
|
+
elsif bundler == "rspack"
|
202
|
+
check_rspack_peer_deps(all_deps)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Check for conflicting installations
|
206
|
+
if package_installed?("webpack") && package_installed?("@rspack/core")
|
207
|
+
@warnings << "Both webpack and rspack are installed - ensure assets_bundler is set correctly"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def check_webpack_peer_deps(deps)
|
212
|
+
essential_webpack = {
|
213
|
+
"webpack" => "^5.76.0",
|
214
|
+
"webpack-cli" => "^4.9.2 || ^5.0.0",
|
215
|
+
"webpack-merge" => "^5.8.0 || ^6.0.0"
|
216
|
+
}
|
217
|
+
|
218
|
+
essential_webpack.each do |package, version|
|
219
|
+
unless deps[package]
|
220
|
+
@issues << "Missing essential webpack dependency: #{package} (#{version})"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def check_rspack_peer_deps(deps)
|
226
|
+
essential_rspack = {
|
227
|
+
"@rspack/cli" => "^1.0.0",
|
228
|
+
"@rspack/core" => "^1.0.0"
|
229
|
+
}
|
230
|
+
|
231
|
+
essential_rspack.each do |package, version|
|
232
|
+
unless deps[package]
|
233
|
+
@issues << "Missing essential rspack dependency: #{package} (#{version})"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def check_windows_platform
|
239
|
+
if Gem.win_platform?
|
240
|
+
@info << "Windows detected: You may need to run shakapacker scripts with 'ruby bin/shakapacker'"
|
241
|
+
|
242
|
+
# Check for case sensitivity issues
|
243
|
+
if File.exist?(root_path.join("App")) || File.exist?(root_path.join("APP"))
|
244
|
+
@warnings << "Potential case sensitivity issue detected on Windows filesystem"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def check_assets_compilation
|
250
|
+
manifest_path = config.manifest_path
|
251
|
+
|
252
|
+
if manifest_path.exist?
|
253
|
+
# Check if manifest is recent (within last 24 hours)
|
254
|
+
manifest_age_hours = (Time.now - File.mtime(manifest_path)) / 3600
|
255
|
+
|
256
|
+
if manifest_age_hours > 24
|
257
|
+
@info << "Assets were last compiled #{manifest_age_hours.round} hours ago. Consider recompiling if you've made changes."
|
258
|
+
end
|
259
|
+
|
260
|
+
# Check if source files are newer than manifest
|
261
|
+
source_files = Dir.glob(File.join(config.source_path, "**/*.{js,jsx,ts,tsx,css,scss,sass}"))
|
262
|
+
if source_files.any?
|
263
|
+
newest_source = source_files.map { |f| File.mtime(f) }.max
|
264
|
+
if newest_source > File.mtime(manifest_path)
|
265
|
+
@warnings << "Source files have been modified after last asset compilation. Run 'rails assets:precompile'"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
else
|
269
|
+
rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
|
270
|
+
if rails_env == "production"
|
271
|
+
@issues << "No compiled assets found (manifest.json missing). Run 'rails assets:precompile'"
|
272
|
+
else
|
273
|
+
@info << "Assets not yet compiled. Run 'rails assets:precompile' or start the dev server"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def check_legacy_webpacker_files
|
279
|
+
legacy_files = [
|
280
|
+
"config/webpacker.yml",
|
281
|
+
"config/webpack/webpacker.yml",
|
282
|
+
"bin/webpack",
|
283
|
+
"bin/webpack-dev-server"
|
284
|
+
]
|
285
|
+
|
286
|
+
legacy_files.each do |file|
|
287
|
+
file_path = root_path.join(file)
|
288
|
+
if file_path.exist?
|
289
|
+
@warnings << "Legacy webpacker file found: #{file} - consider removing after migration"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def check_node_installation
|
295
|
+
stdout, stderr, status = Open3.capture3("node", "--version")
|
296
|
+
|
297
|
+
if status.success?
|
298
|
+
node_version = stdout.strip
|
299
|
+
# Check minimum Node version (14.0.0 for modern tooling)
|
300
|
+
version_match = node_version.match(/v(\d+)\.(\d+)\.(\d+)/)
|
301
|
+
if version_match
|
302
|
+
major = version_match[1].to_i
|
303
|
+
if major < 14
|
304
|
+
@warnings << "Node.js version #{node_version} is outdated. Recommend upgrading to v14 or higher"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
else
|
308
|
+
@issues << "Node.js command failed: #{stderr}"
|
309
|
+
end
|
310
|
+
rescue Errno::ENOENT
|
311
|
+
@issues << "Node.js is not installed or not in PATH"
|
312
|
+
rescue Errno::EACCES
|
313
|
+
@issues << "Permission denied when checking Node.js version"
|
314
|
+
rescue StandardError => e
|
315
|
+
@warnings << "Unable to check Node.js version: #{e.message}"
|
316
|
+
end
|
317
|
+
|
318
|
+
def check_package_manager
|
319
|
+
unless package_manager
|
320
|
+
@issues << "No package manager lock file found (package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb)"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def check_binstub
|
325
|
+
binstub_path = root_path.join("bin/shakapacker")
|
326
|
+
unless binstub_path.exist?
|
327
|
+
@warnings << "Shakapacker binstub not found at bin/shakapacker. Run 'rails shakapacker:binstubs' to create it."
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def check_javascript_transpiler_dependencies
|
332
|
+
transpiler = config.javascript_transpiler
|
333
|
+
|
334
|
+
# Default to SWC for v9+ if not configured
|
335
|
+
if transpiler.nil?
|
336
|
+
@info << "No javascript_transpiler configured - defaulting to SWC (20x faster than Babel)"
|
337
|
+
transpiler = "swc"
|
338
|
+
end
|
339
|
+
|
340
|
+
return if transpiler == "none"
|
341
|
+
|
342
|
+
bundler = config.assets_bundler
|
343
|
+
|
344
|
+
case transpiler
|
345
|
+
when "babel"
|
346
|
+
check_babel_dependencies
|
347
|
+
check_babel_performance_suggestion
|
348
|
+
when "swc"
|
349
|
+
check_swc_dependencies(bundler)
|
350
|
+
when "esbuild"
|
351
|
+
check_esbuild_dependencies
|
352
|
+
else
|
353
|
+
# Generic check for other transpilers
|
354
|
+
loader_name = "#{transpiler}-loader"
|
355
|
+
unless package_installed?(loader_name)
|
356
|
+
@issues << "Missing required dependency '#{loader_name}' for JavaScript transpiler '#{transpiler}'"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
check_transpiler_config_consistency
|
361
|
+
end
|
362
|
+
|
363
|
+
def check_babel_dependencies
|
364
|
+
unless package_installed?("babel-loader")
|
365
|
+
@issues << "Missing required dependency 'babel-loader' for JavaScript transpiler 'babel'"
|
366
|
+
end
|
367
|
+
unless package_installed?("@babel/core")
|
368
|
+
@issues << "Missing required dependency '@babel/core' for Babel transpiler"
|
369
|
+
end
|
370
|
+
unless package_installed?("@babel/preset-env")
|
371
|
+
@issues << "Missing required dependency '@babel/preset-env' for Babel transpiler"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def check_babel_performance_suggestion
|
376
|
+
@info << "Consider switching to SWC for 20x faster compilation. Set javascript_transpiler: 'swc' in shakapacker.yml"
|
377
|
+
end
|
378
|
+
|
379
|
+
def check_swc_dependencies(bundler)
|
380
|
+
if bundler == "webpack"
|
381
|
+
unless package_installed?("@swc/core")
|
382
|
+
@issues << "Missing required dependency '@swc/core' for SWC transpiler"
|
383
|
+
end
|
384
|
+
unless package_installed?("swc-loader")
|
385
|
+
@issues << "Missing required dependency 'swc-loader' for SWC with webpack"
|
386
|
+
end
|
387
|
+
elsif bundler == "rspack"
|
388
|
+
# Rspack has built-in SWC support
|
389
|
+
@info << "Rspack has built-in SWC support - no additional loaders needed"
|
390
|
+
if package_installed?("swc-loader")
|
391
|
+
@warnings << "swc-loader is not needed with Rspack (SWC is built-in) - consider removing it"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def check_esbuild_dependencies
|
397
|
+
unless package_installed?("esbuild")
|
398
|
+
@issues << "Missing required dependency 'esbuild' for esbuild transpiler"
|
399
|
+
end
|
400
|
+
unless package_installed?("esbuild-loader")
|
401
|
+
@issues << "Missing required dependency 'esbuild-loader' for esbuild transpiler"
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def check_transpiler_config_consistency
|
406
|
+
babel_configs = [
|
407
|
+
root_path.join(".babelrc"),
|
408
|
+
root_path.join(".babelrc.js"),
|
409
|
+
root_path.join(".babelrc.json"),
|
410
|
+
root_path.join("babel.config.js"),
|
411
|
+
root_path.join("babel.config.json")
|
412
|
+
]
|
413
|
+
|
414
|
+
babel_config_exists = babel_configs.any?(&:exist?)
|
415
|
+
|
416
|
+
# Check if package.json has babel config
|
417
|
+
if package_json_exists?
|
418
|
+
package_json = read_package_json
|
419
|
+
babel_config_exists ||= package_json.key?("babel")
|
420
|
+
end
|
421
|
+
|
422
|
+
transpiler = config.javascript_transpiler
|
423
|
+
|
424
|
+
if babel_config_exists && transpiler != "babel"
|
425
|
+
@warnings << "Babel configuration files found but javascript_transpiler is '#{transpiler}'. Consider removing Babel configs or setting javascript_transpiler: 'babel'"
|
426
|
+
end
|
427
|
+
|
428
|
+
# Check for redundant dependencies
|
429
|
+
if transpiler == "swc" && package_installed?("babel-loader")
|
430
|
+
@warnings << "Both SWC and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size"
|
431
|
+
end
|
432
|
+
|
433
|
+
if transpiler == "esbuild" && package_installed?("babel-loader")
|
434
|
+
@warnings << "Both esbuild and Babel dependencies are installed. Consider removing Babel dependencies to reduce node_modules size"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def check_css_dependencies
|
439
|
+
check_dependency("css-loader", @issues, "CSS")
|
440
|
+
check_dependency("style-loader", @issues, "CSS (style-loader)")
|
441
|
+
check_optional_dependency("mini-css-extract-plugin", @warnings, "CSS extraction")
|
442
|
+
end
|
443
|
+
|
444
|
+
def check_bundler_dependencies
|
445
|
+
bundler = config.assets_bundler
|
446
|
+
case bundler
|
447
|
+
when "webpack"
|
448
|
+
check_dependency("webpack", @issues, "webpack")
|
449
|
+
check_dependency("webpack-cli", @issues, "webpack CLI")
|
450
|
+
when "rspack"
|
451
|
+
check_dependency("@rspack/core", @issues, "Rspack")
|
452
|
+
check_dependency("@rspack/cli", @issues, "Rspack CLI")
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def check_file_type_dependencies
|
457
|
+
source_path = config.source_path
|
458
|
+
return unless source_path.exist?
|
459
|
+
|
460
|
+
check_typescript_dependencies if typescript_files_exist?
|
461
|
+
check_sass_dependencies if sass_files_exist?
|
462
|
+
check_less_dependencies if less_files_exist?
|
463
|
+
check_stylus_dependencies if stylus_files_exist?
|
464
|
+
check_postcss_dependencies if postcss_config_exists?
|
465
|
+
end
|
466
|
+
|
467
|
+
def check_typescript_dependencies
|
468
|
+
transpiler = config.javascript_transpiler
|
469
|
+
if transpiler == "babel"
|
470
|
+
check_optional_dependency("@babel/preset-typescript", @warnings, "TypeScript with Babel")
|
471
|
+
elsif transpiler != "esbuild" && transpiler != "swc"
|
472
|
+
check_optional_dependency("ts-loader", @warnings, "TypeScript")
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def check_sass_dependencies
|
477
|
+
check_dependency("sass-loader", @issues, "Sass/SCSS")
|
478
|
+
check_dependency("sass", @issues, "Sass/SCSS (sass package)")
|
479
|
+
end
|
480
|
+
|
481
|
+
def check_less_dependencies
|
482
|
+
check_dependency("less-loader", @issues, "Less")
|
483
|
+
check_dependency("less", @issues, "Less (less package)")
|
484
|
+
end
|
485
|
+
|
486
|
+
def check_stylus_dependencies
|
487
|
+
check_dependency("stylus-loader", @issues, "Stylus")
|
488
|
+
check_dependency("stylus", @issues, "Stylus (stylus package)")
|
489
|
+
end
|
490
|
+
|
491
|
+
def check_postcss_dependencies
|
492
|
+
check_dependency("postcss", @issues, "PostCSS")
|
493
|
+
check_dependency("postcss-loader", @issues, "PostCSS")
|
494
|
+
end
|
495
|
+
|
496
|
+
def check_dependency(package_name, issues_array, description)
|
497
|
+
unless package_installed?(package_name)
|
498
|
+
issues_array << "Missing required dependency '#{package_name}' for #{description}"
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def check_optional_dependency(package_name, warnings_array, description)
|
503
|
+
unless package_installed?(package_name)
|
504
|
+
warnings_array << "Optional dependency '#{package_name}' for #{description} is not installed"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
def package_installed?(package_name)
|
509
|
+
return false unless package_json_exists?
|
510
|
+
|
511
|
+
package_json = read_package_json
|
512
|
+
dependencies = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
513
|
+
dependencies.key?(package_name)
|
514
|
+
end
|
515
|
+
|
516
|
+
def package_json_exists?
|
517
|
+
package_json_path.exist?
|
518
|
+
end
|
519
|
+
|
520
|
+
def package_json_path
|
521
|
+
root_path.join("package.json")
|
522
|
+
end
|
523
|
+
|
524
|
+
def read_package_json
|
525
|
+
@package_json ||= begin
|
526
|
+
JSON.parse(File.read(package_json_path))
|
527
|
+
rescue JSON::ParserError
|
528
|
+
{}
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def config_exists?
|
533
|
+
config.config_path.exist?
|
534
|
+
end
|
535
|
+
|
536
|
+
def typescript_files_exist?
|
537
|
+
# Use .first for early exit optimization
|
538
|
+
!Dir.glob(File.join(config.source_path, "**/*.{ts,tsx}")).first.nil?
|
539
|
+
end
|
540
|
+
|
541
|
+
def sass_files_exist?
|
542
|
+
!Dir.glob(File.join(config.source_path, "**/*.{sass,scss}")).first.nil?
|
543
|
+
end
|
544
|
+
|
545
|
+
def less_files_exist?
|
546
|
+
!Dir.glob(File.join(config.source_path, "**/*.less")).first.nil?
|
547
|
+
end
|
548
|
+
|
549
|
+
def stylus_files_exist?
|
550
|
+
!Dir.glob(File.join(config.source_path, "**/*.{styl,stylus}")).first.nil?
|
551
|
+
end
|
552
|
+
|
553
|
+
def postcss_config_exists?
|
554
|
+
root_path.join("postcss.config.js").exist?
|
555
|
+
end
|
556
|
+
|
557
|
+
def package_manager
|
558
|
+
@package_manager ||= detect_package_manager
|
559
|
+
end
|
560
|
+
|
561
|
+
def detect_package_manager
|
562
|
+
return "bun" if File.exist?(root_path.join("bun.lockb"))
|
563
|
+
return "pnpm" if File.exist?(root_path.join("pnpm-lock.yaml"))
|
564
|
+
return "yarn" if File.exist?(root_path.join("yarn.lock"))
|
565
|
+
return "npm" if File.exist?(root_path.join("package-lock.json"))
|
566
|
+
nil
|
567
|
+
end
|
568
|
+
|
569
|
+
def versions_compatible?(gem_version, npm_version)
|
570
|
+
# Handle pre-release versions and ranges properly
|
571
|
+
npm_clean = npm_version.gsub(/[\^~]/, "")
|
572
|
+
|
573
|
+
# Extract version without pre-release suffix
|
574
|
+
gem_base = gem_version.split("-").first
|
575
|
+
npm_base = npm_clean.split("-").first
|
576
|
+
|
577
|
+
# Compare major versions
|
578
|
+
gem_major = gem_base.split(".").first
|
579
|
+
npm_major = npm_base.split(".").first
|
580
|
+
|
581
|
+
if gem_major != npm_major
|
582
|
+
return false
|
583
|
+
end
|
584
|
+
|
585
|
+
# For same major version, check if npm version satisfies gem version
|
586
|
+
begin
|
587
|
+
# Use semantic versioning if available
|
588
|
+
if defined?(SemanticRange)
|
589
|
+
SemanticRange.satisfies?(gem_version, npm_version)
|
590
|
+
else
|
591
|
+
gem_major == npm_major
|
592
|
+
end
|
593
|
+
rescue StandardError
|
594
|
+
# Fallback to simple major version comparison
|
595
|
+
gem_major == npm_major
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def report_results
|
600
|
+
reporter = Reporter.new(self)
|
601
|
+
reporter.print_report
|
602
|
+
exit(1) unless success?
|
603
|
+
end
|
604
|
+
|
605
|
+
class Reporter
|
606
|
+
attr_reader :doctor
|
607
|
+
|
608
|
+
def initialize(doctor)
|
609
|
+
@doctor = doctor
|
610
|
+
end
|
611
|
+
|
612
|
+
def print_report
|
613
|
+
print_header
|
614
|
+
print_checks
|
615
|
+
print_summary
|
616
|
+
end
|
617
|
+
|
618
|
+
private
|
619
|
+
|
620
|
+
def print_header
|
621
|
+
puts "Running Shakapacker doctor..."
|
622
|
+
puts "=" * 60
|
623
|
+
end
|
624
|
+
|
625
|
+
def print_checks
|
626
|
+
if doctor.config.config_path.exist?
|
627
|
+
puts "✓ Configuration file found"
|
628
|
+
print_transpiler_status
|
629
|
+
print_bundler_status
|
630
|
+
print_css_status
|
631
|
+
end
|
632
|
+
|
633
|
+
print_node_status
|
634
|
+
print_package_manager_status
|
635
|
+
print_binstub_status
|
636
|
+
print_info_messages
|
637
|
+
end
|
638
|
+
|
639
|
+
def print_transpiler_status
|
640
|
+
transpiler = doctor.config.javascript_transpiler
|
641
|
+
return if transpiler.nil? || transpiler == "none"
|
642
|
+
|
643
|
+
loader_name = "#{transpiler}-loader"
|
644
|
+
if doctor.send(:package_installed?, loader_name)
|
645
|
+
puts "✓ JavaScript transpiler: #{loader_name} is installed"
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
def print_bundler_status
|
650
|
+
bundler = doctor.config.assets_bundler
|
651
|
+
case bundler
|
652
|
+
when "webpack"
|
653
|
+
print_package_status("webpack", "webpack")
|
654
|
+
print_package_status("webpack-cli", "webpack CLI")
|
655
|
+
when "rspack"
|
656
|
+
print_package_status("@rspack/core", "Rspack")
|
657
|
+
print_package_status("@rspack/cli", "Rspack CLI")
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
def print_css_status
|
662
|
+
print_package_status("css-loader", "CSS")
|
663
|
+
print_package_status("style-loader", "CSS (style-loader)")
|
664
|
+
print_package_status("mini-css-extract-plugin", "CSS extraction (optional)")
|
665
|
+
end
|
666
|
+
|
667
|
+
def print_package_status(package_name, description)
|
668
|
+
if doctor.send(:package_installed?, package_name)
|
669
|
+
puts "✓ #{description}: #{package_name} is installed"
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
def print_node_status
|
674
|
+
begin
|
675
|
+
stdout, stderr, status = Open3.capture3("node", "--version")
|
676
|
+
if status.success?
|
677
|
+
puts "✓ Node.js #{stdout.strip} found"
|
678
|
+
end
|
679
|
+
rescue Errno::ENOENT, Errno::EACCES, StandardError
|
680
|
+
# Error already added to issues
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
def print_package_manager_status
|
685
|
+
package_manager = doctor.send(:package_manager)
|
686
|
+
if package_manager
|
687
|
+
puts "✓ Package manager: #{package_manager}"
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
def print_binstub_status
|
692
|
+
binstub_path = doctor.root_path.join("bin/shakapacker")
|
693
|
+
if binstub_path.exist?
|
694
|
+
puts "✓ Shakapacker binstub found"
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
def print_info_messages
|
699
|
+
return if doctor.info.empty?
|
700
|
+
|
701
|
+
puts "\nℹ️ Information:"
|
702
|
+
doctor.info.each do |info|
|
703
|
+
puts " • #{info}"
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
def print_summary
|
708
|
+
puts "=" * 60
|
709
|
+
|
710
|
+
if doctor.issues.empty? && doctor.warnings.empty?
|
711
|
+
puts "✅ No issues found! Shakapacker appears to be configured correctly."
|
712
|
+
else
|
713
|
+
print_issues if doctor.issues.any?
|
714
|
+
print_warnings if doctor.warnings.any?
|
715
|
+
print_fix_instructions
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
def print_issues
|
720
|
+
puts "❌ Issues found (#{doctor.issues.length}):"
|
721
|
+
doctor.issues.each_with_index do |issue, index|
|
722
|
+
puts " #{index + 1}. #{issue}"
|
723
|
+
end
|
724
|
+
puts ""
|
725
|
+
end
|
726
|
+
|
727
|
+
def print_warnings
|
728
|
+
puts "⚠️ Warnings (#{doctor.warnings.length}):"
|
729
|
+
doctor.warnings.each_with_index do |warning, index|
|
730
|
+
puts " #{index + 1}. #{warning}"
|
731
|
+
end
|
732
|
+
puts ""
|
733
|
+
end
|
734
|
+
|
735
|
+
def print_fix_instructions
|
736
|
+
package_manager = doctor.send(:package_manager)
|
737
|
+
puts "To fix missing dependencies, run:"
|
738
|
+
puts " #{package_manager_install_command(package_manager)}"
|
739
|
+
end
|
740
|
+
|
741
|
+
def package_manager_install_command(manager)
|
742
|
+
case manager
|
743
|
+
when "bun" then "bun add -D [package-name]"
|
744
|
+
when "pnpm" then "pnpm add -D [package-name]"
|
745
|
+
when "yarn" then "yarn add -D [package-name]"
|
746
|
+
when "npm" then "npm install --save-dev [package-name]"
|
747
|
+
else "npm install --save-dev [package-name]"
|
748
|
+
end
|
749
|
+
end
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|