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.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.eslintrc.fast.js +40 -0
  4. data/.eslintrc.js +48 -0
  5. data/.github/STATUS.md +1 -0
  6. data/.github/workflows/claude-code-review.yml +54 -0
  7. data/.github/workflows/claude.yml +50 -0
  8. data/.github/workflows/dummy.yml +9 -4
  9. data/.github/workflows/generator.yml +32 -10
  10. data/.github/workflows/node.yml +23 -1
  11. data/.github/workflows/ruby.yml +33 -2
  12. data/.github/workflows/test-bundlers.yml +170 -0
  13. data/.gitignore +20 -0
  14. data/.husky/pre-commit +2 -0
  15. data/.npmignore +56 -0
  16. data/.prettierignore +3 -0
  17. data/.rubocop.yml +1 -0
  18. data/.yalcignore +26 -0
  19. data/CHANGELOG.md +302 -16
  20. data/CLAUDE.md +29 -0
  21. data/CONTRIBUTING.md +138 -20
  22. data/Gemfile.lock +83 -89
  23. data/README.md +343 -105
  24. data/Rakefile +39 -4
  25. data/TODO.md +50 -0
  26. data/TODO_v9.md +87 -0
  27. data/bin/export-bundler-config +11 -0
  28. data/conductor-setup.sh +70 -0
  29. data/conductor.json +7 -0
  30. data/docs/cdn_setup.md +379 -0
  31. data/docs/common-upgrades.md +615 -0
  32. data/docs/css-modules-export-mode.md +512 -0
  33. data/docs/deployment.md +62 -9
  34. data/docs/optional-peer-dependencies.md +198 -0
  35. data/docs/peer-dependencies.md +60 -0
  36. data/docs/react.md +6 -14
  37. data/docs/releasing.md +197 -0
  38. data/docs/rspack.md +190 -0
  39. data/docs/rspack_migration_guide.md +305 -0
  40. data/docs/subresource_integrity.md +54 -0
  41. data/docs/transpiler-migration.md +209 -0
  42. data/docs/transpiler-performance.md +179 -0
  43. data/docs/troubleshooting.md +157 -22
  44. data/docs/typescript-migration.md +379 -0
  45. data/docs/typescript.md +99 -0
  46. data/docs/using_esbuild_loader.md +3 -3
  47. data/docs/using_swc_loader.md +112 -10
  48. data/docs/v6_upgrade.md +10 -0
  49. data/docs/v8_upgrade.md +3 -5
  50. data/docs/v9_upgrade.md +458 -0
  51. data/gemfiles/Gemfile-rails.6.0.x +2 -1
  52. data/gemfiles/Gemfile-rails.6.1.x +1 -1
  53. data/gemfiles/Gemfile-rails.7.0.x +2 -2
  54. data/gemfiles/Gemfile-rails.7.1.x +1 -2
  55. data/gemfiles/Gemfile-rails.7.2.x +11 -0
  56. data/gemfiles/Gemfile-rails.8.0.x +11 -0
  57. data/lib/install/bin/export-bundler-config +11 -0
  58. data/lib/install/bin/shakapacker +4 -6
  59. data/lib/install/bin/shakapacker-dev-server +1 -1
  60. data/lib/install/config/rspack/rspack.config.js +6 -0
  61. data/lib/install/config/rspack/rspack.config.ts +7 -0
  62. data/lib/install/config/shakapacker.yml +25 -5
  63. data/lib/install/config/webpack/webpack.config.ts +7 -0
  64. data/lib/install/package.json +38 -0
  65. data/lib/install/template.rb +194 -44
  66. data/lib/shakapacker/bundler_switcher.rb +329 -0
  67. data/lib/shakapacker/compiler.rb +2 -1
  68. data/lib/shakapacker/compiler_strategy.rb +2 -2
  69. data/lib/shakapacker/configuration.rb +173 -2
  70. data/lib/shakapacker/dev_server_runner.rb +29 -8
  71. data/lib/shakapacker/digest_strategy.rb +2 -1
  72. data/lib/shakapacker/doctor.rb +905 -0
  73. data/lib/shakapacker/helper.rb +64 -16
  74. data/lib/shakapacker/manifest.rb +10 -3
  75. data/lib/shakapacker/mtime_strategy.rb +1 -1
  76. data/lib/shakapacker/railtie.rb +4 -4
  77. data/lib/shakapacker/rspack_runner.rb +19 -0
  78. data/lib/shakapacker/runner.rb +159 -10
  79. data/lib/shakapacker/swc_migrator.rb +384 -0
  80. data/lib/shakapacker/utils/manager.rb +15 -2
  81. data/lib/shakapacker/version.rb +1 -1
  82. data/lib/shakapacker/version_checker.rb +2 -2
  83. data/lib/shakapacker/webpack_runner.rb +6 -43
  84. data/lib/shakapacker.rb +22 -11
  85. data/lib/tasks/shakapacker/doctor.rake +8 -0
  86. data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
  87. data/lib/tasks/shakapacker/install.rake +12 -2
  88. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  89. data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
  90. data/lib/tasks/shakapacker.rake +2 -0
  91. data/package/.npmignore +4 -0
  92. data/package/babel/preset.ts +56 -0
  93. data/package/config.ts +175 -0
  94. data/package/configExporter/cli.ts +683 -0
  95. data/package/configExporter/configDocs.ts +102 -0
  96. data/package/configExporter/fileWriter.ts +92 -0
  97. data/package/configExporter/index.ts +5 -0
  98. data/package/configExporter/types.ts +36 -0
  99. data/package/configExporter/yamlSerializer.ts +266 -0
  100. data/package/{dev_server.js → dev_server.ts} +8 -5
  101. data/package/env.ts +92 -0
  102. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +30 -0
  103. data/package/environments/{base.js → base.ts} +56 -60
  104. data/package/environments/development.ts +90 -0
  105. data/package/environments/production.ts +80 -0
  106. data/package/environments/test.ts +53 -0
  107. data/package/environments/types.ts +98 -0
  108. data/package/esbuild/index.ts +42 -0
  109. data/package/index.d.ts +3 -60
  110. data/package/index.ts +55 -0
  111. data/package/loaders.d.ts +28 -0
  112. data/package/optimization/rspack.ts +36 -0
  113. data/package/optimization/webpack.ts +57 -0
  114. data/package/plugins/rspack.ts +103 -0
  115. data/package/plugins/webpack.ts +62 -0
  116. data/package/rspack/index.ts +64 -0
  117. data/package/rules/{babel.js → babel.ts} +2 -2
  118. data/package/rules/{coffee.js → coffee.ts} +1 -1
  119. data/package/rules/css.ts +3 -0
  120. data/package/rules/{erb.js → erb.ts} +1 -1
  121. data/package/rules/esbuild.ts +10 -0
  122. data/package/rules/file.ts +40 -0
  123. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  124. data/package/rules/{less.js → less.ts} +4 -4
  125. data/package/rules/raw.ts +25 -0
  126. data/package/rules/rspack.ts +176 -0
  127. data/package/rules/{sass.js → sass.ts} +7 -3
  128. data/package/rules/{stylus.js → stylus.ts} +4 -8
  129. data/package/rules/swc.ts +10 -0
  130. data/package/rules/webpack.ts +16 -0
  131. data/package/swc/index.ts +56 -0
  132. data/package/types/README.md +88 -0
  133. data/package/types/index.ts +61 -0
  134. data/package/types.ts +108 -0
  135. data/package/utils/configPath.ts +6 -0
  136. data/package/utils/debug.ts +49 -0
  137. data/package/utils/defaultConfigPath.ts +4 -0
  138. data/package/utils/errorCodes.ts +219 -0
  139. data/package/utils/errorHelpers.ts +143 -0
  140. data/package/utils/getStyleRule.ts +64 -0
  141. data/package/utils/helpers.ts +85 -0
  142. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  143. data/package/utils/pathValidation.ts +139 -0
  144. data/package/utils/requireOrError.ts +15 -0
  145. data/package/utils/snakeToCamelCase.ts +5 -0
  146. data/package/utils/typeGuards.ts +342 -0
  147. data/package/utils/validateDependencies.ts +61 -0
  148. data/package/webpack-types.d.ts +33 -0
  149. data/package/webpackDevServerConfig.ts +117 -0
  150. data/package-lock.json +13047 -0
  151. data/package.json +154 -18
  152. data/scripts/remove-use-strict.js +45 -0
  153. data/scripts/type-check-no-emit.js +27 -0
  154. data/test/helpers.js +1 -1
  155. data/test/package/config.test.js +43 -0
  156. data/test/package/env.test.js +42 -7
  157. data/test/package/environments/base.test.js +5 -1
  158. data/test/package/rules/babel.test.js +16 -0
  159. data/test/package/rules/esbuild.test.js +1 -1
  160. data/test/package/rules/raw.test.js +40 -7
  161. data/test/package/rules/swc.test.js +1 -1
  162. data/test/package/rules/webpack.test.js +35 -0
  163. data/test/package/staging.test.js +4 -3
  164. data/test/package/transpiler-defaults.test.js +127 -0
  165. data/test/peer-dependencies.sh +85 -0
  166. data/test/scripts/remove-use-strict.test.js +125 -0
  167. data/test/typescript/build.test.js +118 -0
  168. data/test/typescript/environments.test.js +107 -0
  169. data/test/typescript/pathValidation.test.js +142 -0
  170. data/test/typescript/securityValidation.test.js +182 -0
  171. data/tools/README.md +124 -0
  172. data/tools/css-modules-v9-codemod.js +179 -0
  173. data/tsconfig.eslint.json +16 -0
  174. data/tsconfig.json +38 -0
  175. data/yarn.lock +4165 -2706
  176. metadata +129 -41
  177. data/package/babel/preset.js +0 -37
  178. data/package/config.js +0 -54
  179. data/package/env.js +0 -48
  180. data/package/environments/development.js +0 -13
  181. data/package/environments/production.js +0 -88
  182. data/package/environments/test.js +0 -3
  183. data/package/esbuild/index.js +0 -40
  184. data/package/index.js +0 -40
  185. data/package/rules/css.js +0 -3
  186. data/package/rules/esbuild.js +0 -10
  187. data/package/rules/file.js +0 -29
  188. data/package/rules/index.js +0 -20
  189. data/package/rules/raw.js +0 -5
  190. data/package/rules/swc.js +0 -10
  191. data/package/swc/index.js +0 -50
  192. data/package/utils/configPath.js +0 -4
  193. data/package/utils/defaultConfigPath.js +0 -2
  194. data/package/utils/getStyleRule.js +0 -40
  195. data/package/utils/helpers.js +0 -58
  196. data/package/utils/snakeToCamelCase.js +0 -5
  197. data/package/webpackDevServerConfig.js +0 -71
  198. data/test/package/rules/index.test.js +0 -16
@@ -0,0 +1,384 @@
1
+ require "yaml"
2
+ require "json"
3
+ require "fileutils"
4
+ require "logger"
5
+ require "pathname"
6
+
7
+ module Shakapacker
8
+ class SwcMigrator
9
+ attr_reader :root_path, :logger
10
+
11
+ # Babel packages safe to remove when migrating to SWC
12
+ # Note: @babel/core and @babel/eslint-parser are excluded as they may be needed for ESLint
13
+ BABEL_PACKAGES = [
14
+ "@babel/plugin-proposal-class-properties",
15
+ "@babel/plugin-proposal-object-rest-spread",
16
+ "@babel/plugin-syntax-dynamic-import",
17
+ "@babel/plugin-transform-destructuring",
18
+ "@babel/plugin-transform-regenerator",
19
+ "@babel/plugin-transform-runtime",
20
+ "@babel/preset-env",
21
+ "@babel/preset-react",
22
+ "@babel/preset-typescript",
23
+ "@babel/runtime",
24
+ "babel-loader",
25
+ "babel-plugin-macros",
26
+ "babel-plugin-transform-react-remove-prop-types"
27
+ ].freeze
28
+
29
+ # Babel packages that may be needed for ESLint - only remove if user explicitly confirms
30
+ ESLINT_BABEL_PACKAGES = [
31
+ "@babel/core",
32
+ "@babel/eslint-parser"
33
+ ].freeze
34
+
35
+ SWC_PACKAGES = {
36
+ "@swc/core" => "^1.7.39",
37
+ "swc-loader" => "^0.2.6"
38
+ }.freeze
39
+
40
+ ESLINT_CONFIG_FILES = %w[
41
+ .eslintrc
42
+ .eslintrc.js
43
+ .eslintrc.cjs
44
+ .eslintrc.yaml
45
+ .eslintrc.yml
46
+ .eslintrc.json
47
+ ].freeze
48
+
49
+ DEFAULT_SWC_CONFIG = <<~JS.freeze
50
+ // config/swc.config.js
51
+ // This file is merged with Shakapacker's default SWC configuration
52
+ // See: https://swc.rs/docs/configuration/compilation
53
+
54
+ const { env } = require('shakapacker');
55
+
56
+ module.exports = {
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
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ JS
72
+
73
+ def initialize(root_path, logger: nil)
74
+ @root_path = Pathname.new(root_path)
75
+ @logger = logger || Logger.new($stdout)
76
+ end
77
+
78
+ def migrate_to_swc(run_installer: true)
79
+ logger.info "🔄 Starting migration from Babel to SWC..."
80
+
81
+ results = {
82
+ config_updated: update_shakapacker_config,
83
+ packages_installed: install_swc_packages,
84
+ swc_config_created: create_swc_config,
85
+ babel_packages_found: find_babel_packages
86
+ }
87
+
88
+ logger.info "🎉 Migration to SWC complete!"
89
+ logger.info " Note: SWC is approximately 20x faster than Babel for transpilation."
90
+ logger.info " Please test your application thoroughly after migration."
91
+ logger.info "\n📝 Configuration Info:"
92
+ logger.info " - config/swc.config.js is merged with Shakapacker's default SWC configuration"
93
+ logger.info " - You can customize config/swc.config.js to add additional options"
94
+ logger.info " - Avoid using .swcrc as it overrides Shakapacker defaults completely"
95
+
96
+ # Show cleanup recommendations if babel packages found
97
+ if results[:babel_packages_found].any?
98
+ logger.info "\n🧹 Cleanup Recommendations:"
99
+ logger.info " Found the following Babel packages in your package.json:"
100
+ results[:babel_packages_found].each do |package|
101
+ logger.info " - #{package}"
102
+ end
103
+ logger.info "\n To remove them, run:"
104
+ logger.info " rails shakapacker:clean_babel_packages"
105
+ end
106
+
107
+ # Suggest running doctor to verify configuration
108
+ logger.info "\n🩺 Run 'rails shakapacker:doctor' to verify your configuration"
109
+
110
+ # Run package manager install if packages were added
111
+ if run_installer && results[:packages_installed].any?
112
+ run_package_manager_install
113
+ end
114
+
115
+ results
116
+ end
117
+
118
+ def clean_babel_packages(run_installer: true)
119
+ logger.info "🧹 Removing Babel packages..."
120
+
121
+ package_json_path = root_path.join("package.json")
122
+ unless package_json_path.exist?
123
+ logger.error "❌ No package.json found"
124
+ return { removed_packages: [], config_files_deleted: [], preserved_packages: [] }
125
+ end
126
+
127
+ # Check if ESLint uses Babel parser
128
+ preserved_for_eslint = []
129
+ if eslint_uses_babel?
130
+ logger.info "\n⚠️ ESLint configuration detected that uses Babel parser"
131
+ logger.info " Preserving @babel/core and @babel/eslint-parser for ESLint compatibility"
132
+ logger.info " To switch ESLint parser:"
133
+ logger.info " 1. For TypeScript: use @typescript-eslint/parser"
134
+ logger.info " 2. For JavaScript: use espree (ESLint's default parser)"
135
+ preserved_for_eslint = ESLINT_BABEL_PACKAGES
136
+ end
137
+
138
+ removed_packages = remove_babel_from_package_json(package_json_path, preserve: preserved_for_eslint)
139
+ deleted_files = delete_babel_config_files
140
+
141
+ if removed_packages.any?
142
+ logger.info "✅ Babel packages removed successfully!"
143
+ run_package_manager_install if run_installer
144
+ else
145
+ logger.info "ℹ️ No Babel packages found to remove"
146
+ end
147
+
148
+ { removed_packages: removed_packages, config_files_deleted: deleted_files, preserved_packages: preserved_for_eslint }
149
+ end
150
+
151
+ def find_babel_packages
152
+ package_json_path = root_path.join("package.json")
153
+ return [] unless package_json_path.exist?
154
+
155
+ begin
156
+ package_json = JSON.parse(File.read(package_json_path))
157
+ dependencies = package_json["dependencies"] || {}
158
+ dev_dependencies = package_json["devDependencies"] || {}
159
+ all_deps = dependencies.merge(dev_dependencies)
160
+
161
+ # Find all babel packages (including ESLint-related ones for display)
162
+ all_babel_packages = BABEL_PACKAGES + ESLINT_BABEL_PACKAGES
163
+ found_packages = all_babel_packages.select { |pkg| all_deps.key?(pkg) }
164
+ found_packages
165
+ rescue JSON::ParserError => e
166
+ logger.error "Failed to parse package.json: #{e.message}"
167
+ []
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def eslint_uses_babel?
174
+ # Check for ESLint config files
175
+ # Note: This is a heuristic check that may have false positives (e.g., in comments),
176
+ # but false positives only result in an extra warning, which is safer than silently
177
+ # breaking ESLint configurations.
178
+ ESLINT_CONFIG_FILES.each do |config_file|
179
+ config_path = root_path.join(config_file)
180
+ next unless config_path.exist?
181
+
182
+ content = File.read(config_path)
183
+ # Check for Babel parser references
184
+ return true if content.match?(/@babel\/eslint-parser|babel-eslint/)
185
+ end
186
+
187
+ # Check package.json for eslintConfig
188
+ package_json_path = root_path.join("package.json")
189
+ if package_json_path.exist?
190
+ begin
191
+ package_json = JSON.parse(File.read(package_json_path))
192
+ if package_json["eslintConfig"]
193
+ # Check parser field explicitly
194
+ parser = package_json["eslintConfig"]["parser"]
195
+ return true if parser && parser.match?(/@babel\/eslint-parser|babel-eslint/)
196
+
197
+ # Also check entire config for babel parser references (catches nested configs)
198
+ return true if package_json["eslintConfig"].to_json.match?(/@babel\/eslint-parser|babel-eslint/)
199
+ end
200
+
201
+ # Check if Babel ESLint packages are installed
202
+ dependencies = package_json["dependencies"] || {}
203
+ dev_dependencies = package_json["devDependencies"] || {}
204
+ all_deps = dependencies.merge(dev_dependencies)
205
+ return true if all_deps.key?("@babel/eslint-parser") || all_deps.key?("babel-eslint")
206
+ rescue JSON::ParserError => e
207
+ logger.debug "Could not parse package.json for ESLint detection: #{e.message}"
208
+ end
209
+ end
210
+
211
+ false
212
+ end
213
+
214
+ def update_shakapacker_config
215
+ config_path = root_path.join("config/shakapacker.yml")
216
+ return false unless config_path.exist?
217
+
218
+ logger.info "📝 Updating shakapacker.yml..."
219
+ config = begin
220
+ YAML.load_file(config_path, aliases: true)
221
+ rescue ArgumentError
222
+ YAML.load_file(config_path)
223
+ end
224
+
225
+ config.each do |env, settings|
226
+ next unless settings.is_a?(Hash)
227
+
228
+ if settings["babel"]
229
+ logger.info " - Removing babel config from #{env} environment"
230
+ settings.delete("babel")
231
+ end
232
+
233
+ settings["javascript_transpiler"] = "swc"
234
+ logger.info " - Set javascript_transpiler to 'swc' for #{env} environment"
235
+ end
236
+
237
+ File.write(config_path, config.to_yaml)
238
+ logger.info "✅ shakapacker.yml updated"
239
+ true
240
+ rescue StandardError => e
241
+ logger.error "Failed to update config: #{e.message}"
242
+ false
243
+ end
244
+
245
+ def install_swc_packages
246
+ package_json_path = root_path.join("package.json")
247
+ return {} unless package_json_path.exist?
248
+
249
+ logger.info "📦 Installing SWC dependencies..."
250
+ package_json = JSON.parse(File.read(package_json_path))
251
+
252
+ dependencies = package_json["dependencies"] || {}
253
+ dev_dependencies = package_json["devDependencies"] || {}
254
+ installed = {}
255
+
256
+ SWC_PACKAGES.each do |package, version|
257
+ unless dependencies[package] || dev_dependencies[package]
258
+ logger.info " - Adding #{package}@#{version}"
259
+ dev_dependencies[package] = version
260
+ installed[package] = version
261
+ else
262
+ logger.info " - #{package} already installed"
263
+ end
264
+ end
265
+
266
+ if installed.any?
267
+ package_json["devDependencies"] = dev_dependencies
268
+ File.write(package_json_path, JSON.pretty_generate(package_json) + "\n")
269
+ logger.info "✅ package.json updated with SWC dependencies"
270
+ end
271
+
272
+ installed
273
+ rescue StandardError => e
274
+ logger.error "Failed to install packages: #{e.message}"
275
+ {}
276
+ end
277
+
278
+ def create_swc_config
279
+ config_dir = root_path.join("config")
280
+ swc_config_path = config_dir.join("swc.config.js")
281
+
282
+ if swc_config_path.exist?
283
+ logger.info "ℹ️ config/swc.config.js already exists"
284
+ return false
285
+ end
286
+
287
+ FileUtils.mkdir_p(config_dir) unless config_dir.exist?
288
+
289
+ logger.info "📄 Creating config/swc.config.js..."
290
+ File.write(swc_config_path, DEFAULT_SWC_CONFIG)
291
+ logger.info "✅ config/swc.config.js created"
292
+ true
293
+ rescue StandardError => e
294
+ logger.error "Failed to create config/swc.config.js: #{e.message}"
295
+ false
296
+ end
297
+
298
+ def remove_babel_from_package_json(package_json_path, preserve: [])
299
+ package_json = JSON.parse(File.read(package_json_path))
300
+ dependencies = package_json["dependencies"] || {}
301
+ dev_dependencies = package_json["devDependencies"] || {}
302
+ removed_packages = []
303
+
304
+ BABEL_PACKAGES.each do |package|
305
+ next if preserve.include?(package)
306
+
307
+ if dependencies.delete(package)
308
+ removed_packages << package
309
+ logger.info " - Removed #{package} from dependencies"
310
+ end
311
+ if dev_dependencies.delete(package)
312
+ removed_packages << package
313
+ logger.info " - Removed #{package} from devDependencies"
314
+ end
315
+ end
316
+
317
+ # Log preserved packages
318
+ preserve.each do |package|
319
+ if dependencies[package] || dev_dependencies[package]
320
+ logger.info " - Preserved #{package} (needed for ESLint)"
321
+ end
322
+ end
323
+
324
+ if removed_packages.any?
325
+ package_json["dependencies"] = dependencies
326
+ package_json["devDependencies"] = dev_dependencies
327
+ File.write(package_json_path, JSON.pretty_generate(package_json) + "\n")
328
+ logger.info "✅ package.json updated"
329
+ end
330
+
331
+ removed_packages.uniq
332
+ rescue StandardError => e
333
+ logger.error "Failed to remove packages: #{e.message}"
334
+ []
335
+ end
336
+
337
+ def delete_babel_config_files
338
+ deleted_files = []
339
+ babel_config_files = [".babelrc", "babel.config.js", ".babelrc.js", "babel.config.json"]
340
+
341
+ babel_config_files.each do |file|
342
+ file_path = root_path.join(file)
343
+ if file_path.exist?
344
+ logger.info " - Removing #{file}"
345
+ File.delete(file_path)
346
+ deleted_files << file
347
+ end
348
+ end
349
+
350
+ deleted_files
351
+ rescue StandardError => e
352
+ logger.error "Failed to delete config files: #{e.message}"
353
+ []
354
+ end
355
+
356
+ def run_package_manager_install
357
+ logger.info "\n🔧 Running npm/yarn install..."
358
+
359
+ yarn_lock = root_path.join("yarn.lock")
360
+ pnpm_lock = root_path.join("pnpm-lock.yaml")
361
+
362
+ if yarn_lock.exist?
363
+ system("yarn install")
364
+ elsif pnpm_lock.exist?
365
+ system("pnpm install")
366
+ else
367
+ system("npm install")
368
+ end
369
+ end
370
+
371
+ def package_manager
372
+ yarn_lock = root_path.join("yarn.lock")
373
+ pnpm_lock = root_path.join("pnpm-lock.yaml")
374
+
375
+ if yarn_lock.exist?
376
+ "yarn"
377
+ elsif pnpm_lock.exist?
378
+ "pnpm"
379
+ else
380
+ "npm"
381
+ end
382
+ end
383
+ end
384
+ end
@@ -16,7 +16,7 @@ module Shakapacker
16
16
 
17
17
  # Emits a warning if it's not obvious what package manager to use
18
18
  def self.error_unless_package_manager_is_obvious!
19
- return unless PackageJson.read.fetch("packageManager", nil).nil?
19
+ return unless PackageJson.read(rails_root).fetch("packageManager", nil).nil?
20
20
 
21
21
  guessed_binary = guess_binary
22
22
 
@@ -35,7 +35,7 @@ module Shakapacker
35
35
  #
36
36
  # @return [String]
37
37
  def self.guess_binary
38
- MANAGER_LOCKS.find { |_, lock| File.exist?(lock) }&.first || "npm"
38
+ MANAGER_LOCKS.find { |_, lock| File.exist?(rails_root.join(lock)) }&.first || "npm"
39
39
  end
40
40
 
41
41
  # Guesses the version of the package manager to use by calling `<manager> --version`
@@ -53,6 +53,19 @@ module Shakapacker
53
53
 
54
54
  stdout.chomp
55
55
  end
56
+
57
+ private
58
+ def self.rails_root
59
+ if defined?(APP_ROOT)
60
+ Pathname.new(APP_ROOT)
61
+ elsif ENV["APP_ROOT"]
62
+ Pathname.new(ENV["APP_ROOT"])
63
+ elsif defined?(Rails)
64
+ Rails.root
65
+ else
66
+ raise "can only be called from a rails environment or with APP_ROOT defined"
67
+ end
68
+ end
56
69
  end
57
70
  end
58
71
  end
@@ -1,4 +1,4 @@
1
1
  module Shakapacker
2
2
  # Change the version in package.json too, please!
3
- VERSION = "8.0.2".freeze
3
+ VERSION = "9.2.0".freeze
4
4
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require "shakapacker/version"
2
+ require_relative "version"
3
3
 
4
4
  module Shakapacker
5
5
  class VersionChecker
@@ -126,7 +126,7 @@ module Shakapacker
126
126
  end
127
127
 
128
128
  def relative_path?
129
- raw.match(%r{(\.\.|\Afile:///)}).present?
129
+ raw.match(%r{(\.\.|\Afile:)}).present?
130
130
  end
131
131
 
132
132
  def git_url?
@@ -1,50 +1,13 @@
1
1
  require "shellwords"
2
- require "shakapacker/runner"
2
+
3
+ require_relative "runner"
3
4
 
4
5
  module Shakapacker
5
6
  class WebpackRunner < Shakapacker::Runner
6
- WEBPACK_COMMANDS = [
7
- "help",
8
- "h",
9
- "--help",
10
- "-h",
11
- "version",
12
- "v",
13
- "--version",
14
- "-v",
15
- "info",
16
- "i"
17
- ].freeze
18
-
19
- def run
20
- env = Shakapacker::Compiler.env
21
- env["SHAKAPACKER_CONFIG"] = @shakapacker_config
22
- env["NODE_OPTIONS"] = ENV["NODE_OPTIONS"] || ""
23
-
24
- cmd = build_cmd
25
-
26
- if @argv.delete("--debug-shakapacker")
27
- env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --inspect-brk"
28
- end
29
-
30
- if @argv.delete "--trace-deprecation"
31
- env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --trace-deprecation"
32
- end
33
-
34
- if @argv.delete "--no-deprecation"
35
- env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --no-deprecation"
36
- end
37
-
38
- # Webpack commands are not compatible with --config option.
39
- if (@argv & WEBPACK_COMMANDS).empty?
40
- cmd += ["--config", @webpack_config]
41
- end
42
-
43
- cmd += @argv
44
-
45
- Dir.chdir(@app_path) do
46
- Kernel.exec env, *cmd
47
- end
7
+ def self.run(argv)
8
+ $stdout.sync = true
9
+ Shakapacker.ensure_node_env!
10
+ new(argv).run
48
11
  end
49
12
 
50
13
  private
data/lib/shakapacker.rb CHANGED
@@ -6,7 +6,10 @@ require "active_support/tagged_logging"
6
6
  module Shakapacker
7
7
  extend self
8
8
 
9
- DEFAULT_ENV = "production".freeze
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)
@@ -37,13 +47,14 @@ module Shakapacker
37
47
  delegate :bootstrap, :clean, :clobber, :compile, to: :commands
38
48
  end
39
49
 
40
- require "shakapacker/instance"
41
- require "shakapacker/env"
42
- require "shakapacker/configuration"
43
- require "shakapacker/manifest"
44
- require "shakapacker/compiler"
45
- require "shakapacker/commands"
46
- require "shakapacker/dev_server"
47
- require "shakapacker/deprecation_helper"
48
-
49
- require "shakapacker/railtie" if defined?(Rails)
50
+ require_relative "shakapacker/instance"
51
+ require_relative "shakapacker/env"
52
+ require_relative "shakapacker/configuration"
53
+ require_relative "shakapacker/manifest"
54
+ require_relative "shakapacker/compiler"
55
+ require_relative "shakapacker/commands"
56
+ require_relative "shakapacker/dev_server"
57
+ require_relative "shakapacker/doctor"
58
+ require_relative "shakapacker/deprecation_helper"
59
+
60
+ require_relative "shakapacker/railtie" if defined?(Rails)
@@ -0,0 +1,8 @@
1
+ require "shakapacker/doctor"
2
+
3
+ namespace :shakapacker do
4
+ desc "Checks for common Shakapacker configuration issues and missing dependencies"
5
+ task doctor: :environment do
6
+ Shakapacker::Doctor.new.run
7
+ end
8
+ end
@@ -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
@@ -2,10 +2,20 @@ install_template_path = File.expand_path("../../install/template.rb", __dir__).f
2
2
  bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin")
3
3
 
4
4
  namespace :shakapacker do
5
- desc "Install Shakapacker in this application"
6
- task install: [:check_node] do |task|
5
+ desc "Install Shakapacker in this application (use SHAKAPACKER_ASSETS_BUNDLER=rspack for Rspack, --typescript for TypeScript)"
6
+ task :install, [:bundler, :typescript] => [:check_node] do |task, args|
7
7
  Shakapacker::Configuration.installing = true
8
8
 
9
+ if args[:bundler] == "rspack" || ENV["SHAKAPACKER_ASSETS_BUNDLER"] == "rspack"
10
+ ENV["SHAKAPACKER_ASSETS_BUNDLER"] = "rspack"
11
+ end
12
+
13
+ # Set typescript flag if passed as argument
14
+ # Accepts: typescript, true, or any truthy value
15
+ if args[:typescript] && args[:typescript] != "false"
16
+ ENV["SHAKAPACKER_USE_TYPESCRIPT"] = "true"
17
+ end
18
+
9
19
  prefix = task.name.split(/#|shakapacker:install/).first
10
20
 
11
21
  if Rails::VERSION::MAJOR >= 5
@@ -0,0 +1,13 @@
1
+ require "shakapacker/swc_migrator"
2
+
3
+ namespace :shakapacker do
4
+ desc "Migrate from Babel to SWC transpiler"
5
+ task :migrate_to_swc do
6
+ Shakapacker::SwcMigrator.new(Rails.root).migrate_to_swc
7
+ end
8
+
9
+ desc "Remove Babel packages after migrating to SWC"
10
+ task :clean_babel_packages do
11
+ Shakapacker::SwcMigrator.new(Rails.root).clean_babel_packages
12
+ end
13
+ end