shakapacker 9.0.0 → 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/.gitignore +3 -0
- data/CHANGELOG.md +108 -11
- data/Gemfile.lock +1 -1
- data/README.md +182 -107
- data/bin/export-bundler-config +11 -0
- data/docs/common-upgrades.md +615 -0
- data/docs/deployment.md +52 -8
- data/docs/releasing.md +197 -0
- data/docs/rspack_migration_guide.md +120 -17
- data/docs/transpiler-migration.md +21 -0
- data/docs/troubleshooting.md +124 -23
- data/docs/typescript-migration.md +2 -1
- data/docs/using_swc_loader.md +108 -8
- data/docs/v9_upgrade.md +45 -0
- data/lib/install/bin/export-bundler-config +11 -0
- data/lib/install/bin/shakapacker +1 -1
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/shakapacker/bundler_switcher.rb +329 -0
- data/lib/shakapacker/configuration.rb +28 -2
- data/lib/shakapacker/doctor.rb +65 -4
- data/lib/shakapacker/rspack_runner.rb +1 -1
- data/lib/shakapacker/runner.rb +1 -1
- data/lib/shakapacker/swc_migrator.rb +14 -6
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/webpack_runner.rb +1 -1
- data/lib/shakapacker.rb +10 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
- data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
- data/lib/tasks/shakapacker.rake +2 -1
- 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/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 +7 -1
- data/yarn.lock +261 -389
- metadata +17 -2
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
|
|
245
|
+
# Run a full install to ensure optional dependencies (like native bindings) are properly resolved
|
|
246
|
+
# This is especially important for packages like @rspack/core that use platform-specific native modules
|
|
247
|
+
unless package_json.manager.install
|
|
248
|
+
puts "❌ Failed to run full install to resolve optional dependencies"
|
|
249
|
+
raise "Failed to run full install"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def get_package_json
|
|
254
|
+
require "package_json"
|
|
255
|
+
PackageJson.read(root_path)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def print_manual_dependency_instructions(bundler, deps_to_install, deps_to_remove)
|
|
259
|
+
puts ""
|
|
260
|
+
puts "⚠️ Dependencies not automatically installed (use --install-deps to auto-install)"
|
|
261
|
+
puts ""
|
|
262
|
+
|
|
263
|
+
package_manager = detect_package_manager
|
|
264
|
+
target_name = bundler == "rspack" ? "rspack" : "webpack"
|
|
265
|
+
old_name = bundler == "rspack" ? "webpack" : "rspack"
|
|
266
|
+
|
|
267
|
+
puts "📦 To install #{target_name} dependencies, run:"
|
|
268
|
+
print_install_commands(package_manager, deps_to_install)
|
|
269
|
+
puts ""
|
|
270
|
+
puts "🗑️ To remove #{old_name} dependencies, run:"
|
|
271
|
+
print_uninstall_commands(package_manager, deps_to_remove)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def detect_package_manager
|
|
275
|
+
get_package_json.manager.binary
|
|
276
|
+
rescue StandardError
|
|
277
|
+
"npm" # Fallback to npm if detection fails
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def print_install_commands(package_manager, deps)
|
|
281
|
+
case package_manager
|
|
282
|
+
when "yarn"
|
|
283
|
+
puts " yarn add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
284
|
+
puts " yarn add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
285
|
+
when "pnpm"
|
|
286
|
+
puts " pnpm add -D #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
287
|
+
puts " pnpm add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
288
|
+
when "bun"
|
|
289
|
+
puts " bun add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
290
|
+
puts " bun add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
291
|
+
else # npm
|
|
292
|
+
puts " npm install --save-dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
293
|
+
puts " npm install --save #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def print_uninstall_commands(package_manager, deps)
|
|
298
|
+
case package_manager
|
|
299
|
+
when "yarn"
|
|
300
|
+
puts " yarn remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
301
|
+
puts " yarn remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
302
|
+
when "pnpm"
|
|
303
|
+
puts " pnpm remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
304
|
+
puts " pnpm remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
305
|
+
when "bun"
|
|
306
|
+
puts " bun remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
307
|
+
puts " bun remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
308
|
+
else # npm
|
|
309
|
+
puts " npm uninstall #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
310
|
+
puts " npm uninstall #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Load YAML config file with Ruby version compatibility
|
|
315
|
+
# Ruby 3.1+ supports aliases: keyword, older versions need YAML.safe_load
|
|
316
|
+
def load_yaml_config(path)
|
|
317
|
+
if YAML.respond_to?(:unsafe_load_file)
|
|
318
|
+
# Ruby 3.1+: Use unsafe_load_file to support aliases/anchors
|
|
319
|
+
YAML.unsafe_load_file(path)
|
|
320
|
+
else
|
|
321
|
+
# Ruby 2.7-3.0: Use safe_load with aliases enabled
|
|
322
|
+
YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
323
|
+
end
|
|
324
|
+
rescue ArgumentError
|
|
325
|
+
# Ruby 2.7 doesn't support aliases keyword - fall back to YAML.load
|
|
326
|
+
YAML.load(File.read(path)) # rubocop:disable Security/YAMLLoad
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
@@ -255,7 +255,21 @@ class Shakapacker::Configuration
|
|
|
255
255
|
rescue ArgumentError
|
|
256
256
|
YAML.load_file(config_path.to_s)
|
|
257
257
|
end
|
|
258
|
-
|
|
258
|
+
|
|
259
|
+
# Try to find environment-specific configuration with fallback
|
|
260
|
+
# Fallback order: requested env → production
|
|
261
|
+
if config[env]
|
|
262
|
+
env_config = config[env]
|
|
263
|
+
elsif config["production"]
|
|
264
|
+
log_fallback(env, "production")
|
|
265
|
+
env_config = config["production"]
|
|
266
|
+
else
|
|
267
|
+
# No suitable configuration found - rely on bundled defaults
|
|
268
|
+
log_fallback(env, "none (will use bundled defaults)")
|
|
269
|
+
env_config = nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
symbolized_config = env_config&.deep_symbolize_keys || {}
|
|
259
273
|
|
|
260
274
|
return symbolized_config
|
|
261
275
|
rescue Errno::ENOENT => e
|
|
@@ -280,7 +294,10 @@ class Shakapacker::Configuration
|
|
|
280
294
|
rescue ArgumentError
|
|
281
295
|
YAML.load_file(path)
|
|
282
296
|
end
|
|
283
|
-
|
|
297
|
+
# Load defaults from bundled shakapacker.yml (always has all environments)
|
|
298
|
+
# Note: This differs from load() which reads user's config and may be missing environments
|
|
299
|
+
# Fallback to production ensures staging and other custom envs get production-like defaults
|
|
300
|
+
HashWithIndifferentAccess.new(config[env] || config["production"])
|
|
284
301
|
end
|
|
285
302
|
end
|
|
286
303
|
|
|
@@ -289,4 +306,13 @@ class Shakapacker::Configuration
|
|
|
289
306
|
|
|
290
307
|
path
|
|
291
308
|
end
|
|
309
|
+
|
|
310
|
+
def log_fallback(requested_env, fallback_env)
|
|
311
|
+
return unless Shakapacker.logger
|
|
312
|
+
|
|
313
|
+
Shakapacker.logger.info(
|
|
314
|
+
"Shakapacker environment '#{requested_env}' not found in #{config_path}, " \
|
|
315
|
+
"falling back to '#{fallback_env}'"
|
|
316
|
+
)
|
|
317
|
+
end
|
|
292
318
|
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(', ')}"
|
|
@@ -326,6 +327,11 @@ module Shakapacker
|
|
|
326
327
|
unless binstub_path.exist?
|
|
327
328
|
@warnings << "Shakapacker binstub not found at bin/shakapacker. Run 'rails shakapacker:binstubs' to create it."
|
|
328
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
|
|
329
335
|
end
|
|
330
336
|
|
|
331
337
|
def check_javascript_transpiler_dependencies
|
|
@@ -453,7 +459,51 @@ module Shakapacker
|
|
|
453
459
|
|
|
454
460
|
if swc_config_path.exist?
|
|
455
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"
|
|
456
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")
|
|
457
507
|
end
|
|
458
508
|
|
|
459
509
|
def check_css_dependencies
|
|
@@ -785,6 +835,11 @@ module Shakapacker
|
|
|
785
835
|
if binstub_path.exist?
|
|
786
836
|
puts "✓ Shakapacker binstub found"
|
|
787
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
|
|
788
843
|
end
|
|
789
844
|
|
|
790
845
|
def print_info_messages
|
|
@@ -828,6 +883,12 @@ module Shakapacker
|
|
|
828
883
|
package_manager = doctor.send(:package_manager)
|
|
829
884
|
puts "To fix missing dependencies, run:"
|
|
830
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"
|
|
831
892
|
end
|
|
832
893
|
|
|
833
894
|
def package_manager_install_command(manager)
|
data/lib/shakapacker/runner.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Shakapacker
|
|
|
24
24
|
].freeze
|
|
25
25
|
def self.run(argv)
|
|
26
26
|
$stdout.sync = true
|
|
27
|
-
|
|
27
|
+
Shakapacker.ensure_node_env!
|
|
28
28
|
|
|
29
29
|
# Create a single runner instance to avoid loading configuration twice.
|
|
30
30
|
# We extend it with the appropriate build command based on the bundler type.
|
|
@@ -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
data/lib/shakapacker.rb
CHANGED
|
@@ -7,6 +7,9 @@ module Shakapacker
|
|
|
7
7
|
extend self
|
|
8
8
|
|
|
9
9
|
DEFAULT_ENV = "development".freeze
|
|
10
|
+
# Environments that use their RAILS_ENV value for NODE_ENV
|
|
11
|
+
# All other environments (production, staging, etc.) use "production" for webpack optimizations
|
|
12
|
+
DEV_TEST_ENVS = %w[development test].freeze
|
|
10
13
|
|
|
11
14
|
def instance=(instance)
|
|
12
15
|
@instance = instance
|
|
@@ -24,6 +27,13 @@ module Shakapacker
|
|
|
24
27
|
ENV["NODE_ENV"] = original
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
# Set NODE_ENV based on RAILS_ENV if not already set
|
|
31
|
+
# - development/test environments use their RAILS_ENV value
|
|
32
|
+
# - all other environments (production, staging, etc.) use "production" for webpack optimizations
|
|
33
|
+
def ensure_node_env!
|
|
34
|
+
ENV["NODE_ENV"] ||= DEV_TEST_ENVS.include?(ENV["RAILS_ENV"]) ? ENV["RAILS_ENV"] : "production"
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
def ensure_log_goes_to_stdout
|
|
28
38
|
old_logger = Shakapacker.logger
|
|
29
39
|
Shakapacker.logger = Logger.new(STDOUT)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
namespace :shakapacker do
|
|
2
|
+
desc <<~DESC
|
|
3
|
+
Export webpack or rspack configuration for debugging and analysis
|
|
4
|
+
|
|
5
|
+
Exports your resolved webpack/rspack configuration in human-readable formats.
|
|
6
|
+
Use this to debug configuration issues, compare environments, or analyze
|
|
7
|
+
client vs server bundle differences.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
rails shakapacker:export_bundler_config [OPTIONS]
|
|
11
|
+
rake shakapacker:export_bundler_config -- [OPTIONS]
|
|
12
|
+
|
|
13
|
+
Quick Start (Recommended):
|
|
14
|
+
rails shakapacker:export_bundler_config --doctor
|
|
15
|
+
|
|
16
|
+
This exports all configs (dev + prod, client + server) to shakapacker-config-exports/
|
|
17
|
+
directory in annotated YAML format - perfect for troubleshooting.
|
|
18
|
+
|
|
19
|
+
Common Options:
|
|
20
|
+
--doctor Export everything for troubleshooting (recommended)
|
|
21
|
+
--save Save current environment configs to files
|
|
22
|
+
--save-dir=<dir> Custom output directory (requires --save)
|
|
23
|
+
--env=development|production|test Specify environment
|
|
24
|
+
--client-only Export only client config
|
|
25
|
+
--server-only Export only server config
|
|
26
|
+
--format=yaml|json|inspect Output format
|
|
27
|
+
--help, -h Show detailed help
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
# Export all configs for troubleshooting
|
|
31
|
+
rails shakapacker:export_bundler_config --doctor
|
|
32
|
+
|
|
33
|
+
# Save production client config
|
|
34
|
+
rails shakapacker:export_bundler_config --save --env=production --client-only
|
|
35
|
+
|
|
36
|
+
# View development config in terminal
|
|
37
|
+
rails shakapacker:export_bundler_config
|
|
38
|
+
|
|
39
|
+
# Show detailed help
|
|
40
|
+
rails shakapacker:export_bundler_config --help
|
|
41
|
+
|
|
42
|
+
Note: When using 'rake', you must use '--' to separate rake options from task arguments.
|
|
43
|
+
Example: rake shakapacker:export_bundler_config -- --doctor
|
|
44
|
+
|
|
45
|
+
The task automatically falls back to the gem version if bin/export-bundler-config
|
|
46
|
+
binstub is not installed. To install all binstubs, run: rails shakapacker:binstubs
|
|
47
|
+
DESC
|
|
48
|
+
task :export_bundler_config do
|
|
49
|
+
# Try to use the binstub if it exists, otherwise use the gem's version
|
|
50
|
+
bin_path = Rails.root.join("bin/export-bundler-config")
|
|
51
|
+
|
|
52
|
+
unless File.exist?(bin_path)
|
|
53
|
+
# Binstub not installed, use the gem's version directly
|
|
54
|
+
gem_bin_path = File.expand_path("../../install/bin/export-bundler-config", __dir__)
|
|
55
|
+
|
|
56
|
+
$stderr.puts "Note: bin/export-bundler-config binstub not found."
|
|
57
|
+
$stderr.puts "Using gem version directly. To install the binstub, run: rake shakapacker:binstubs"
|
|
58
|
+
$stderr.puts ""
|
|
59
|
+
|
|
60
|
+
Dir.chdir(Rails.root) do
|
|
61
|
+
exec("node", gem_bin_path, *ARGV[1..])
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
# Pass through command-line arguments after the task name
|
|
65
|
+
# Use exec to replace the rake process with the export script
|
|
66
|
+
# This ensures proper exit codes and signal handling
|
|
67
|
+
Dir.chdir(Rails.root) do
|
|
68
|
+
exec(bin_path.to_s, *ARGV[1..])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|