shakapacker 9.0.0 → 9.1.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/CHANGELOG.md +77 -11
- data/Gemfile.lock +1 -1
- data/README.md +161 -107
- data/docs/common-upgrades.md +615 -0
- data/docs/rspack_migration_guide.md +92 -17
- data/docs/transpiler-migration.md +21 -0
- data/docs/typescript-migration.md +2 -1
- data/docs/using_swc_loader.md +108 -8
- data/docs/v9_upgrade.md +45 -0
- data/lib/shakapacker/bundler_switcher.rb +322 -0
- data/lib/shakapacker/doctor.rb +49 -4
- data/lib/shakapacker/swc_migrator.rb +14 -6
- data/lib/shakapacker/version.rb +1 -1
- data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
- data/lib/tasks/shakapacker.rake +2 -1
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +30 -0
- data/package/environments/types.ts +22 -14
- data/package/index.ts +12 -9
- data/package/swc/index.ts +5 -3
- data/package/types/README.md +2 -1
- data/package/types/index.ts +1 -0
- data/package/utils/debug.ts +5 -5
- data/package-lock.json +13047 -0
- data/package.json +6 -1
- data/yarn.lock +196 -115
- metadata +7 -2
@@ -0,0 +1,322 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Shakapacker
|
7
|
+
# Provides functionality to switch between webpack and rspack bundlers
|
8
|
+
class BundlerSwitcher
|
9
|
+
SHAKAPACKER_CONFIG = "config/shakapacker.yml"
|
10
|
+
CUSTOM_DEPS_CONFIG = ".shakapacker-switch-bundler-dependencies.yml"
|
11
|
+
|
12
|
+
# Default dependencies for each bundler (package names only, no versions)
|
13
|
+
DEFAULT_RSPACK_DEPS = {
|
14
|
+
dev: %w[@rspack/cli @rspack/plugin-react-refresh],
|
15
|
+
prod: %w[@rspack/core rspack-manifest-plugin]
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
DEFAULT_WEBPACK_DEPS = {
|
19
|
+
dev: %w[webpack webpack-cli webpack-dev-server @pmmmwh/react-refresh-webpack-plugin @swc/core swc-loader],
|
20
|
+
prod: %w[webpack-assets-manifest webpack-merge]
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
attr_reader :root_path
|
24
|
+
|
25
|
+
def initialize(root_path = nil)
|
26
|
+
@root_path = root_path || (defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd))
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_bundler
|
30
|
+
config = load_yaml_config(config_path)
|
31
|
+
config.dig("default", "assets_bundler") || "webpack"
|
32
|
+
end
|
33
|
+
|
34
|
+
def switch_to(bundler, install_deps: false, no_uninstall: false)
|
35
|
+
unless %w[webpack rspack].include?(bundler)
|
36
|
+
raise ArgumentError, "Invalid bundler: #{bundler}. Must be 'webpack' or 'rspack'"
|
37
|
+
end
|
38
|
+
|
39
|
+
current = current_bundler
|
40
|
+
if current == bundler && !install_deps
|
41
|
+
puts "✅ Already using #{bundler}"
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
if current == bundler && install_deps
|
46
|
+
puts "✅ Already using #{bundler} - reinstalling dependencies as requested"
|
47
|
+
manage_dependencies(bundler, install_deps, switching: false, no_uninstall: no_uninstall)
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
update_config(bundler)
|
52
|
+
|
53
|
+
puts "✅ Switched from #{current} to #{bundler}"
|
54
|
+
puts ""
|
55
|
+
puts "📝 Configuration updated in #{SHAKAPACKER_CONFIG}"
|
56
|
+
|
57
|
+
manage_dependencies(bundler, install_deps, no_uninstall: no_uninstall)
|
58
|
+
|
59
|
+
puts ""
|
60
|
+
puts "🎯 Next steps:"
|
61
|
+
puts " 1. Restart your dev server: bin/dev"
|
62
|
+
puts " 2. Verify build works: bin/shakapacker"
|
63
|
+
puts ""
|
64
|
+
puts "💡 Tip: Both webpack and rspack can coexist in package.json during migration"
|
65
|
+
puts " Use --install-deps to automatically manage dependencies, or manage manually"
|
66
|
+
puts " Use --no-uninstall to skip removing old bundler packages (faster switching)"
|
67
|
+
end
|
68
|
+
|
69
|
+
def init_config
|
70
|
+
if File.exist?(custom_config_path)
|
71
|
+
puts "⚠️ #{CUSTOM_DEPS_CONFIG} already exists"
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
config = {
|
76
|
+
"rspack" => {
|
77
|
+
"devDependencies" => DEFAULT_RSPACK_DEPS[:dev],
|
78
|
+
"dependencies" => DEFAULT_RSPACK_DEPS[:prod]
|
79
|
+
},
|
80
|
+
"webpack" => {
|
81
|
+
"devDependencies" => DEFAULT_WEBPACK_DEPS[:dev],
|
82
|
+
"dependencies" => DEFAULT_WEBPACK_DEPS[:prod]
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
File.write(custom_config_path, YAML.dump(config))
|
87
|
+
puts "✅ Created #{CUSTOM_DEPS_CONFIG}"
|
88
|
+
puts ""
|
89
|
+
puts "You can now customize the dependencies for each bundler in this file."
|
90
|
+
puts "The script will automatically use these custom dependencies when switching bundlers."
|
91
|
+
end
|
92
|
+
|
93
|
+
def show_usage
|
94
|
+
current = current_bundler
|
95
|
+
puts "Current bundler: #{current}"
|
96
|
+
puts ""
|
97
|
+
puts "Usage:"
|
98
|
+
puts " rails shakapacker:switch_bundler [webpack|rspack] [OPTIONS]"
|
99
|
+
puts " rake shakapacker:switch_bundler [webpack|rspack] -- [OPTIONS]"
|
100
|
+
puts ""
|
101
|
+
puts "Options:"
|
102
|
+
puts " --install-deps Automatically install/uninstall dependencies"
|
103
|
+
puts " --no-uninstall Skip uninstalling old bundler packages (faster, keeps both bundlers)"
|
104
|
+
puts " --init-config Create #{CUSTOM_DEPS_CONFIG} with default dependencies"
|
105
|
+
puts " --help, -h Show this help message"
|
106
|
+
puts ""
|
107
|
+
puts "Examples:"
|
108
|
+
puts " # Using rails command"
|
109
|
+
puts " rails shakapacker:switch_bundler rspack --install-deps"
|
110
|
+
puts " rails shakapacker:switch_bundler webpack --install-deps --no-uninstall"
|
111
|
+
puts " rails shakapacker:switch_bundler --init-config"
|
112
|
+
puts ""
|
113
|
+
puts " # Using rake command (note the -- separator)"
|
114
|
+
puts " rake shakapacker:switch_bundler rspack -- --install-deps"
|
115
|
+
puts " rake shakapacker:switch_bundler webpack -- --install-deps --no-uninstall"
|
116
|
+
puts " rake shakapacker:switch_bundler -- --init-config"
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def config_path
|
122
|
+
root_path.join(SHAKAPACKER_CONFIG)
|
123
|
+
end
|
124
|
+
|
125
|
+
def custom_config_path
|
126
|
+
root_path.join(CUSTOM_DEPS_CONFIG)
|
127
|
+
end
|
128
|
+
|
129
|
+
def load_dependencies
|
130
|
+
if File.exist?(custom_config_path)
|
131
|
+
puts "📝 Using custom dependencies from #{CUSTOM_DEPS_CONFIG}"
|
132
|
+
begin
|
133
|
+
custom = load_yaml_config(custom_config_path)
|
134
|
+
rescue Psych::SyntaxError => e
|
135
|
+
puts "❌ Error parsing #{CUSTOM_DEPS_CONFIG}: #{e.message}"
|
136
|
+
puts " Please fix the YAML syntax or delete the file to use defaults"
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
rspack_deps = {
|
140
|
+
dev: custom.dig("rspack", "devDependencies") || DEFAULT_RSPACK_DEPS[:dev],
|
141
|
+
prod: custom.dig("rspack", "dependencies") || DEFAULT_RSPACK_DEPS[:prod]
|
142
|
+
}
|
143
|
+
webpack_deps = {
|
144
|
+
dev: custom.dig("webpack", "devDependencies") || DEFAULT_WEBPACK_DEPS[:dev],
|
145
|
+
prod: custom.dig("webpack", "dependencies") || DEFAULT_WEBPACK_DEPS[:prod]
|
146
|
+
}
|
147
|
+
[rspack_deps, webpack_deps]
|
148
|
+
else
|
149
|
+
[DEFAULT_RSPACK_DEPS, DEFAULT_WEBPACK_DEPS]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def update_config(bundler)
|
154
|
+
content = File.read(config_path)
|
155
|
+
|
156
|
+
# Replace assets_bundler value (handles spaces, tabs, and various quote styles)
|
157
|
+
# Only matches uncommented lines
|
158
|
+
content.gsub!(/^([ \t]*assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1#{bundler}\\3")
|
159
|
+
|
160
|
+
# Update javascript_transpiler recommendation for rspack
|
161
|
+
# Only update if not already set to swc and only on uncommented lines
|
162
|
+
if bundler == "rspack" && content !~ /^[ \t]*javascript_transpiler:[ \t]*['"]?swc['"]?/
|
163
|
+
content.gsub!(/^([ \t]*javascript_transpiler:[ \t]*['"]?)\w+(['"]?)/, "\\1swc\\2")
|
164
|
+
end
|
165
|
+
|
166
|
+
File.write(config_path, content)
|
167
|
+
end
|
168
|
+
|
169
|
+
def manage_dependencies(bundler, install_deps, switching: true, no_uninstall: false)
|
170
|
+
rspack_deps, webpack_deps = load_dependencies
|
171
|
+
deps_to_install = bundler == "rspack" ? rspack_deps : webpack_deps
|
172
|
+
deps_to_remove = bundler == "rspack" ? webpack_deps : rspack_deps
|
173
|
+
|
174
|
+
if install_deps
|
175
|
+
puts ""
|
176
|
+
puts "📦 Managing dependencies..."
|
177
|
+
puts ""
|
178
|
+
|
179
|
+
# Show what will be removed (only when switching and not no_uninstall)
|
180
|
+
if switching && !no_uninstall && (!deps_to_remove[:dev].empty? || !deps_to_remove[:prod].empty?)
|
181
|
+
puts " 🗑️ Removing:"
|
182
|
+
deps_to_remove[:dev].each { |dep| puts " - #{dep} (dev)" }
|
183
|
+
deps_to_remove[:prod].each { |dep| puts " - #{dep} (prod)" }
|
184
|
+
puts ""
|
185
|
+
elsif switching && no_uninstall
|
186
|
+
puts " ⏭️ Skipping uninstall (--no-uninstall)"
|
187
|
+
puts ""
|
188
|
+
end
|
189
|
+
|
190
|
+
# Show what will be installed
|
191
|
+
if !deps_to_install[:dev].empty? || !deps_to_install[:prod].empty?
|
192
|
+
puts " 📦 Installing:"
|
193
|
+
deps_to_install[:dev].each { |dep| puts " - #{dep} (dev)" }
|
194
|
+
deps_to_install[:prod].each { |dep| puts " - #{dep} (prod)" }
|
195
|
+
puts ""
|
196
|
+
end
|
197
|
+
|
198
|
+
# Remove old bundler dependencies (only when switching and not no_uninstall)
|
199
|
+
if switching && !no_uninstall
|
200
|
+
remove_dependencies(deps_to_remove)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Install new bundler dependencies
|
204
|
+
install_dependencies(deps_to_install)
|
205
|
+
|
206
|
+
puts " ✅ Dependencies updated"
|
207
|
+
else
|
208
|
+
print_manual_dependency_instructions(bundler, deps_to_install, deps_to_remove)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def remove_dependencies(deps)
|
213
|
+
package_json = get_package_json
|
214
|
+
|
215
|
+
unless deps[:dev].empty?
|
216
|
+
unless package_json.manager.remove(deps[:dev])
|
217
|
+
puts " ⚠️ Warning: Failed to uninstall some dev dependencies"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
unless deps[:prod].empty?
|
222
|
+
unless package_json.manager.remove(deps[:prod])
|
223
|
+
puts " ⚠️ Warning: Failed to uninstall some prod dependencies"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def install_dependencies(deps)
|
229
|
+
package_json = get_package_json
|
230
|
+
|
231
|
+
unless deps[:dev].empty?
|
232
|
+
unless package_json.manager.add(deps[:dev], type: :dev)
|
233
|
+
puts "❌ Failed to install dev dependencies"
|
234
|
+
raise "Failed to install dev dependencies"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
unless deps[:prod].empty?
|
239
|
+
unless package_json.manager.add(deps[:prod], type: :production)
|
240
|
+
puts "❌ Failed to install prod dependencies"
|
241
|
+
raise "Failed to install prod dependencies"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def get_package_json
|
247
|
+
require "package_json"
|
248
|
+
PackageJson.read(root_path)
|
249
|
+
end
|
250
|
+
|
251
|
+
def print_manual_dependency_instructions(bundler, deps_to_install, deps_to_remove)
|
252
|
+
puts ""
|
253
|
+
puts "⚠️ Dependencies not automatically installed (use --install-deps to auto-install)"
|
254
|
+
puts ""
|
255
|
+
|
256
|
+
package_manager = detect_package_manager
|
257
|
+
target_name = bundler == "rspack" ? "rspack" : "webpack"
|
258
|
+
old_name = bundler == "rspack" ? "webpack" : "rspack"
|
259
|
+
|
260
|
+
puts "📦 To install #{target_name} dependencies, run:"
|
261
|
+
print_install_commands(package_manager, deps_to_install)
|
262
|
+
puts ""
|
263
|
+
puts "🗑️ To remove #{old_name} dependencies, run:"
|
264
|
+
print_uninstall_commands(package_manager, deps_to_remove)
|
265
|
+
end
|
266
|
+
|
267
|
+
def detect_package_manager
|
268
|
+
get_package_json.manager.binary
|
269
|
+
rescue StandardError
|
270
|
+
"npm" # Fallback to npm if detection fails
|
271
|
+
end
|
272
|
+
|
273
|
+
def print_install_commands(package_manager, deps)
|
274
|
+
case package_manager
|
275
|
+
when "yarn"
|
276
|
+
puts " yarn add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
277
|
+
puts " yarn add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
278
|
+
when "pnpm"
|
279
|
+
puts " pnpm add -D #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
280
|
+
puts " pnpm add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
281
|
+
when "bun"
|
282
|
+
puts " bun add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
283
|
+
puts " bun add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
284
|
+
else # npm
|
285
|
+
puts " npm install --save-dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
286
|
+
puts " npm install --save #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def print_uninstall_commands(package_manager, deps)
|
291
|
+
case package_manager
|
292
|
+
when "yarn"
|
293
|
+
puts " yarn remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
294
|
+
puts " yarn remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
295
|
+
when "pnpm"
|
296
|
+
puts " pnpm remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
297
|
+
puts " pnpm remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
298
|
+
when "bun"
|
299
|
+
puts " bun remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
300
|
+
puts " bun remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
301
|
+
else # npm
|
302
|
+
puts " npm uninstall #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
303
|
+
puts " npm uninstall #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Load YAML config file with Ruby version compatibility
|
308
|
+
# Ruby 3.1+ supports aliases: keyword, older versions need YAML.safe_load
|
309
|
+
def load_yaml_config(path)
|
310
|
+
if YAML.respond_to?(:unsafe_load_file)
|
311
|
+
# Ruby 3.1+: Use unsafe_load_file to support aliases/anchors
|
312
|
+
YAML.unsafe_load_file(path)
|
313
|
+
else
|
314
|
+
# Ruby 2.7-3.0: Use safe_load with aliases enabled
|
315
|
+
YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
316
|
+
end
|
317
|
+
rescue ArgumentError
|
318
|
+
# Ruby 2.7 doesn't support aliases keyword - fall back to YAML.load
|
319
|
+
YAML.load(File.read(path)) # rubocop:disable Security/YAMLLoad
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
data/lib/shakapacker/doctor.rb
CHANGED
@@ -65,12 +65,12 @@ module Shakapacker
|
|
65
65
|
|
66
66
|
def check_entry_points
|
67
67
|
# Check for invalid configuration first
|
68
|
-
if config.
|
68
|
+
if config.fetch(:source_entry_path) == "/" && config.nested_entries?
|
69
69
|
@issues << "Invalid configuration: cannot use '/' as source_entry_path with nested_entries: true"
|
70
70
|
return # Don't try to check files when config is invalid
|
71
71
|
end
|
72
72
|
|
73
|
-
source_entry_path = config.source_path.join(config.
|
73
|
+
source_entry_path = config.source_path.join(config.fetch(:source_entry_path) || "packs")
|
74
74
|
|
75
75
|
unless source_entry_path.exist?
|
76
76
|
@issues << "Source entry path #{source_entry_path} does not exist"
|
@@ -173,7 +173,8 @@ module Shakapacker
|
|
173
173
|
end
|
174
174
|
|
175
175
|
def check_sri_dependencies
|
176
|
-
|
176
|
+
integrity_config = config.integrity
|
177
|
+
return unless integrity_config&.dig(:enabled)
|
177
178
|
|
178
179
|
bundler = config.assets_bundler
|
179
180
|
if bundler == "webpack"
|
@@ -183,7 +184,7 @@ module Shakapacker
|
|
183
184
|
end
|
184
185
|
|
185
186
|
# Validate hash functions
|
186
|
-
hash_functions =
|
187
|
+
hash_functions = integrity_config.dig(:hash_functions) || ["sha384"]
|
187
188
|
invalid_functions = hash_functions - ["sha256", "sha384", "sha512"]
|
188
189
|
unless invalid_functions.empty?
|
189
190
|
@issues << "Invalid SRI hash functions: #{invalid_functions.join(', ')}"
|
@@ -453,9 +454,53 @@ module Shakapacker
|
|
453
454
|
|
454
455
|
if swc_config_path.exist?
|
455
456
|
@info << "SWC configuration: Using config/swc.config.js (recommended). This config is merged with Shakapacker's defaults."
|
457
|
+
check_swc_config_settings(swc_config_path)
|
456
458
|
end
|
457
459
|
end
|
458
460
|
|
461
|
+
def check_swc_config_settings(config_path)
|
462
|
+
config_content = File.read(config_path, encoding: "UTF-8")
|
463
|
+
|
464
|
+
# Check for loose: true (deprecated default)
|
465
|
+
if config_content.match?(/loose\s*:\s*true/)
|
466
|
+
@warnings << "SWC configuration: 'loose: true' detected in config/swc.config.js. " \
|
467
|
+
"This can cause silent failures with Stimulus controllers and incorrect spread operator behavior. " \
|
468
|
+
"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"
|
470
|
+
end
|
471
|
+
|
472
|
+
# Check for missing keepClassNames with Stimulus
|
473
|
+
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. " \
|
475
|
+
"Without this setting, Stimulus controllers will fail silently. " \
|
476
|
+
"Add 'keepClassNames: true' to jsc config. " \
|
477
|
+
"See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
|
478
|
+
elsif config_content.match?(/keepClassNames\s*:\s*true/)
|
479
|
+
@info << "SWC configuration: 'keepClassNames: true' is set (good for Stimulus compatibility)"
|
480
|
+
end
|
481
|
+
|
482
|
+
# Check for jsc.target and env conflict
|
483
|
+
# Use word boundary to avoid false positives with transform.target or other nested properties
|
484
|
+
if config_content.match?(/jsc\s*:\s*\{[^}]*\btarget\s*:/) && config_content.match?(/env\s*:\s*\{/)
|
485
|
+
@issues << "SWC configuration: Both 'jsc.target' and 'env' are configured. These cannot be used together. " \
|
486
|
+
"Remove 'jsc.target' and use only 'env' (Shakapacker sets this automatically). " \
|
487
|
+
"See: https://github.com/shakacode/shakapacker/blob/main/docs/using_swc_loader.md#using-swc-with-stimulus"
|
488
|
+
end
|
489
|
+
rescue => e
|
490
|
+
# Don't fail doctor if SWC config check has issues
|
491
|
+
@warnings << "Unable to validate SWC configuration: #{e.message}"
|
492
|
+
end
|
493
|
+
|
494
|
+
def stimulus_likely_used?
|
495
|
+
return false unless package_json_exists?
|
496
|
+
|
497
|
+
package_json = read_package_json
|
498
|
+
dependencies = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
499
|
+
|
500
|
+
# Check for @hotwired/stimulus or stimulus package
|
501
|
+
dependencies.key?("@hotwired/stimulus") || dependencies.key?("stimulus")
|
502
|
+
end
|
503
|
+
|
459
504
|
def check_css_dependencies
|
460
505
|
check_dependency("css-loader", @issues, "CSS")
|
461
506
|
check_dependency("style-loader", @issues, "CSS (style-loader)")
|
@@ -51,11 +51,19 @@ module Shakapacker
|
|
51
51
|
// This file is merged with Shakapacker's default SWC configuration
|
52
52
|
// See: https://swc.rs/docs/configuration/compilation
|
53
53
|
|
54
|
+
const { env } = require('shakapacker');
|
55
|
+
|
54
56
|
module.exports = {
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
57
|
+
options: {
|
58
|
+
jsc: {
|
59
|
+
// CRITICAL for Stimulus compatibility: Prevents SWC from mangling class names
|
60
|
+
// which breaks Stimulus's class-based controller discovery mechanism
|
61
|
+
keepClassNames: true,
|
62
|
+
transform: {
|
63
|
+
react: {
|
64
|
+
runtime: "automatic",
|
65
|
+
refresh: env.isDevelopment && env.runningWebpackDevServer
|
66
|
+
}
|
59
67
|
}
|
60
68
|
}
|
61
69
|
}
|
@@ -222,8 +230,8 @@ module Shakapacker
|
|
222
230
|
settings.delete("babel")
|
223
231
|
end
|
224
232
|
|
225
|
-
settings["
|
226
|
-
logger.info " -
|
233
|
+
settings["javascript_transpiler"] = "swc"
|
234
|
+
logger.info " - Set javascript_transpiler to 'swc' for #{env} environment"
|
227
235
|
end
|
228
236
|
|
229
237
|
File.write(config_path, config.to_yaml)
|
data/lib/shakapacker/version.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
require "shakapacker/bundler_switcher"
|
2
|
+
|
3
|
+
namespace :shakapacker do
|
4
|
+
desc <<~DESC
|
5
|
+
Switch between webpack and rspack bundlers
|
6
|
+
|
7
|
+
Easily switch your Shakapacker configuration between webpack and rspack bundlers.
|
8
|
+
This task updates config/shakapacker.yml and optionally manages npm dependencies.
|
9
|
+
|
10
|
+
Usage:
|
11
|
+
rails shakapacker:switch_bundler [webpack|rspack] [OPTIONS]
|
12
|
+
rake shakapacker:switch_bundler [webpack|rspack] -- [OPTIONS]
|
13
|
+
|
14
|
+
Options:
|
15
|
+
--install-deps Automatically install/uninstall bundler dependencies
|
16
|
+
--no-uninstall Skip uninstalling old bundler packages (faster, keeps both bundlers)
|
17
|
+
--init-config Create custom dependencies configuration file
|
18
|
+
--help, -h Show detailed help message
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
# Switch to rspack with automatic dependency management
|
22
|
+
rails shakapacker:switch_bundler rspack --install-deps
|
23
|
+
rake shakapacker:switch_bundler rspack -- --install-deps
|
24
|
+
|
25
|
+
# Fast switching without uninstalling (keeps both bundlers)
|
26
|
+
rails shakapacker:switch_bundler webpack --install-deps --no-uninstall
|
27
|
+
rake shakapacker:switch_bundler rspack -- --install-deps --no-uninstall
|
28
|
+
|
29
|
+
# Switch to rspack (manual dependency management)
|
30
|
+
rails shakapacker:switch_bundler rspack
|
31
|
+
rake shakapacker:switch_bundler rspack
|
32
|
+
|
33
|
+
# Switch back to webpack with dependency management
|
34
|
+
rails shakapacker:switch_bundler webpack --install-deps
|
35
|
+
rake shakapacker:switch_bundler webpack -- --install-deps
|
36
|
+
|
37
|
+
# Create custom dependencies config file
|
38
|
+
rails shakapacker:switch_bundler --init-config
|
39
|
+
rake shakapacker:switch_bundler -- --init-config
|
40
|
+
|
41
|
+
# Show current bundler and usage help
|
42
|
+
rails shakapacker:switch_bundler --help
|
43
|
+
rake shakapacker:switch_bundler -- --help
|
44
|
+
|
45
|
+
Note: When using 'rake', you must use '--' to separate rake options from task arguments.
|
46
|
+
|
47
|
+
What it does:
|
48
|
+
- Updates 'assets_bundler' in config/shakapacker.yml
|
49
|
+
- Preserves YAML comments and structure
|
50
|
+
- Updates 'javascript_transpiler' to 'swc' when switching to rspack
|
51
|
+
- With --install-deps: installs/uninstalls npm dependencies automatically
|
52
|
+
- Without --install-deps: shows manual installation commands
|
53
|
+
|
54
|
+
Custom Dependencies:
|
55
|
+
Create .shakapacker-switch-bundler-dependencies.yml to customize which
|
56
|
+
npm packages are installed/uninstalled during bundler switching.
|
57
|
+
|
58
|
+
See docs/rspack_migration_guide.md for more information.
|
59
|
+
DESC
|
60
|
+
task :switch_bundler do
|
61
|
+
switcher = Shakapacker::BundlerSwitcher.new
|
62
|
+
|
63
|
+
if ARGV.empty? || ARGV.include?("--help") || ARGV.include?("-h")
|
64
|
+
switcher.show_usage
|
65
|
+
elsif ARGV.include?("--init-config")
|
66
|
+
switcher.init_config
|
67
|
+
else
|
68
|
+
bundler = ARGV[1]
|
69
|
+
install_deps = ARGV.include?("--install-deps")
|
70
|
+
no_uninstall = ARGV.include?("--no-uninstall")
|
71
|
+
|
72
|
+
if bundler.nil? || bundler.start_with?("-")
|
73
|
+
switcher.show_usage
|
74
|
+
else
|
75
|
+
switcher.switch_to(bundler, install_deps: install_deps, no_uninstall: no_uninstall)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Prevent rake from trying to execute arguments as tasks
|
80
|
+
ARGV.each { |arg| task arg.to_sym {} }
|
81
|
+
end
|
82
|
+
end
|
data/lib/tasks/shakapacker.rake
CHANGED
@@ -9,7 +9,8 @@ tasks = {
|
|
9
9
|
"shakapacker:check_binstubs" => "Verifies that bin/shakapacker is present",
|
10
10
|
"shakapacker:binstubs" => "Installs Shakapacker binstubs in this application",
|
11
11
|
"shakapacker:verify_install" => "Verifies if Shakapacker is installed",
|
12
|
-
"shakapacker:doctor" => "Checks for configuration issues and missing dependencies"
|
12
|
+
"shakapacker:doctor" => "Checks for configuration issues and missing dependencies",
|
13
|
+
"shakapacker:switch_bundler" => "Switch between webpack and rspack bundlers"
|
13
14
|
}.freeze
|
14
15
|
|
15
16
|
desc "Lists all available tasks in Shakapacker"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
/**
|
2
|
+
* Compile-time type tests for RspackPlugin backward compatibility
|
3
|
+
* This file ensures that RspackPlugin is correctly aliased to RspackPluginInstance
|
4
|
+
* and maintains backward compatibility for existing consumers.
|
5
|
+
*
|
6
|
+
* These tests will fail at compile time if the types are not compatible.
|
7
|
+
*/
|
8
|
+
|
9
|
+
import type { RspackPlugin, RspackPluginInstance } from '../types'
|
10
|
+
|
11
|
+
// Test 1: RspackPlugin should be assignable to RspackPluginInstance
|
12
|
+
const testPluginToInstance = (plugin: RspackPlugin): RspackPluginInstance => plugin
|
13
|
+
|
14
|
+
// Test 2: RspackPluginInstance should be assignable to RspackPlugin
|
15
|
+
const testInstanceToPlugin = (instance: RspackPluginInstance): RspackPlugin => instance
|
16
|
+
|
17
|
+
// Test 3: Array compatibility
|
18
|
+
const testArrayCompatibility = (plugins: RspackPlugin[]): RspackPluginInstance[] => plugins
|
19
|
+
const testArrayCompatibilityReverse = (
|
20
|
+
instances: RspackPluginInstance[]
|
21
|
+
): RspackPlugin[] => instances
|
22
|
+
|
23
|
+
// Test 4: Optional parameter compatibility
|
24
|
+
const testOptionalParam = (plugin?: RspackPlugin): RspackPluginInstance | undefined => plugin
|
25
|
+
const testOptionalParamReverse = (
|
26
|
+
instance?: RspackPluginInstance
|
27
|
+
): RspackPlugin | undefined => instance
|
28
|
+
|
29
|
+
// Export a dummy value to make this a module
|
30
|
+
export const __typeTestsComplete = true
|
@@ -3,8 +3,12 @@
|
|
3
3
|
* These types are exported for consumer use
|
4
4
|
*/
|
5
5
|
|
6
|
-
import type {
|
6
|
+
import type {
|
7
|
+
Configuration as WebpackConfiguration,
|
8
|
+
WebpackPluginInstance
|
9
|
+
} from "webpack"
|
7
10
|
import type { Configuration as DevServerConfiguration } from "webpack-dev-server"
|
11
|
+
import type { RspackPluginInstance as ImportedRspackPluginInstance } from "@rspack/core"
|
8
12
|
|
9
13
|
/**
|
10
14
|
* Webpack configuration extended with dev server support
|
@@ -15,15 +19,17 @@ export interface WebpackConfigWithDevServer extends WebpackConfiguration {
|
|
15
19
|
}
|
16
20
|
|
17
21
|
/**
|
18
|
-
* Rspack plugin interface
|
19
|
-
*
|
22
|
+
* Rspack plugin instance interface
|
23
|
+
* Uses the RspackPluginInstance type from @rspack/core
|
20
24
|
*/
|
21
|
-
export
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
export type RspackPluginInstance = ImportedRspackPluginInstance
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Rspack plugin type alias
|
29
|
+
* @deprecated Use RspackPluginInstance instead
|
30
|
+
* Retained for backward compatibility with existing code
|
31
|
+
*/
|
32
|
+
export type RspackPlugin = RspackPluginInstance
|
27
33
|
|
28
34
|
/**
|
29
35
|
* Rspack dev server configuration
|
@@ -52,7 +58,7 @@ export interface RspackConfigWithDevServer {
|
|
52
58
|
mode?: "development" | "production" | "none"
|
53
59
|
devtool?: string | false
|
54
60
|
devServer?: RspackDevServerConfig
|
55
|
-
plugins?:
|
61
|
+
plugins?: RspackPluginInstance[]
|
56
62
|
module?: WebpackConfiguration["module"]
|
57
63
|
resolve?: WebpackConfiguration["resolve"]
|
58
64
|
entry?: WebpackConfiguration["entry"]
|
@@ -76,15 +82,17 @@ export interface CompressionPluginOptions {
|
|
76
82
|
/**
|
77
83
|
* Compression plugin constructor type
|
78
84
|
*/
|
79
|
-
export type CompressionPluginConstructor = new (
|
85
|
+
export type CompressionPluginConstructor = new (
|
86
|
+
options: CompressionPluginOptions
|
87
|
+
) => WebpackPluginInstance
|
80
88
|
|
81
89
|
/**
|
82
90
|
* React Refresh plugin types
|
83
91
|
*/
|
84
92
|
export interface ReactRefreshWebpackPlugin {
|
85
|
-
new(options?: Record<string, unknown>): WebpackPluginInstance
|
93
|
+
new (options?: Record<string, unknown>): WebpackPluginInstance
|
86
94
|
}
|
87
95
|
|
88
96
|
export interface ReactRefreshRspackPlugin {
|
89
|
-
new(options?: Record<string, unknown>):
|
90
|
-
}
|
97
|
+
new (options?: Record<string, unknown>): RspackPluginInstance
|
98
|
+
}
|
data/package/index.ts
CHANGED
@@ -16,18 +16,21 @@ const inliningCss = require("./utils/inliningCss")
|
|
16
16
|
const rulesPath = resolve(__dirname, "rules", `${config.assets_bundler}.js`)
|
17
17
|
const rules = require(rulesPath)
|
18
18
|
|
19
|
-
const generateWebpackConfig = (
|
19
|
+
const generateWebpackConfig = (
|
20
|
+
extraConfig: Configuration = {},
|
21
|
+
...extraArgs: unknown[]
|
22
|
+
): Configuration => {
|
20
23
|
if (extraArgs.length > 0) {
|
21
24
|
throw new Error(
|
22
25
|
`Invalid usage: generateWebpackConfig() accepts only one configuration object.\n\n` +
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
`You passed ${extraArgs.length + 1} arguments. Only one extra config may be passed here - use webpack-merge to merge configs before passing them to Shakapacker.\n\n` +
|
27
|
+
`Example:\n` +
|
28
|
+
` const { merge } = require('webpack-merge')\n` +
|
29
|
+
` const mergedConfig = merge(config1, config2, config3)\n` +
|
30
|
+
` const finalConfig = generateWebpackConfig(mergedConfig)\n\n` +
|
31
|
+
`Or if using ES6:\n` +
|
32
|
+
` import { merge } from 'webpack-merge'\n` +
|
33
|
+
` const finalConfig = generateWebpackConfig(merge(config1, config2))`
|
31
34
|
)
|
32
35
|
}
|
33
36
|
|