shakapacker 8.4.0 → 9.0.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 (166) 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 +8 -4
  9. data/.github/workflows/generator.yml +17 -14
  10. data/.github/workflows/node.yml +23 -1
  11. data/.github/workflows/ruby.yml +11 -0
  12. data/.github/workflows/test-bundlers.yml +170 -0
  13. data/.gitignore +17 -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 +156 -18
  20. data/CLAUDE.md +29 -0
  21. data/CONTRIBUTING.md +138 -20
  22. data/Gemfile.lock +3 -3
  23. data/README.md +130 -5
  24. data/Rakefile +39 -4
  25. data/TODO.md +50 -0
  26. data/TODO_v9.md +87 -0
  27. data/conductor-setup.sh +70 -0
  28. data/conductor.json +7 -0
  29. data/docs/cdn_setup.md +379 -0
  30. data/docs/css-modules-export-mode.md +512 -0
  31. data/docs/deployment.md +10 -1
  32. data/docs/optional-peer-dependencies.md +198 -0
  33. data/docs/peer-dependencies.md +60 -0
  34. data/docs/rspack.md +190 -0
  35. data/docs/rspack_migration_guide.md +202 -0
  36. data/docs/transpiler-migration.md +188 -0
  37. data/docs/transpiler-performance.md +179 -0
  38. data/docs/troubleshooting.md +5 -0
  39. data/docs/typescript-migration.md +378 -0
  40. data/docs/typescript.md +99 -0
  41. data/docs/using_esbuild_loader.md +3 -3
  42. data/docs/using_swc_loader.md +5 -3
  43. data/docs/v6_upgrade.md +10 -0
  44. data/docs/v9_upgrade.md +413 -0
  45. data/lib/install/bin/shakapacker +3 -5
  46. data/lib/install/config/rspack/rspack.config.js +6 -0
  47. data/lib/install/config/rspack/rspack.config.ts +7 -0
  48. data/lib/install/config/shakapacker.yml +12 -2
  49. data/lib/install/config/webpack/webpack.config.ts +7 -0
  50. data/lib/install/package.json +38 -0
  51. data/lib/install/template.rb +194 -44
  52. data/lib/shakapacker/configuration.rb +141 -0
  53. data/lib/shakapacker/dev_server_runner.rb +25 -5
  54. data/lib/shakapacker/doctor.rb +844 -0
  55. data/lib/shakapacker/manifest.rb +4 -2
  56. data/lib/shakapacker/rspack_runner.rb +19 -0
  57. data/lib/shakapacker/runner.rb +144 -4
  58. data/lib/shakapacker/swc_migrator.rb +376 -0
  59. data/lib/shakapacker/utils/manager.rb +2 -0
  60. data/lib/shakapacker/version.rb +1 -1
  61. data/lib/shakapacker/version_checker.rb +1 -1
  62. data/lib/shakapacker/webpack_runner.rb +4 -42
  63. data/lib/shakapacker.rb +2 -1
  64. data/lib/tasks/shakapacker/doctor.rake +8 -0
  65. data/lib/tasks/shakapacker/install.rake +12 -2
  66. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  67. data/lib/tasks/shakapacker.rake +1 -0
  68. data/package/.npmignore +4 -0
  69. data/package/babel/preset.ts +56 -0
  70. data/package/config.ts +175 -0
  71. data/package/{dev_server.js → dev_server.ts} +8 -5
  72. data/package/env.ts +92 -0
  73. data/package/environments/base.ts +138 -0
  74. data/package/environments/development.ts +90 -0
  75. data/package/environments/production.ts +80 -0
  76. data/package/environments/test.ts +53 -0
  77. data/package/environments/types.ts +90 -0
  78. data/package/esbuild/index.ts +42 -0
  79. data/package/index.d.ts +3 -97
  80. data/package/index.ts +52 -0
  81. data/package/loaders.d.ts +28 -0
  82. data/package/optimization/rspack.ts +36 -0
  83. data/package/optimization/webpack.ts +57 -0
  84. data/package/plugins/rspack.ts +103 -0
  85. data/package/plugins/webpack.ts +62 -0
  86. data/package/rspack/index.ts +64 -0
  87. data/package/rules/{babel.js → babel.ts} +2 -2
  88. data/package/rules/{coffee.js → coffee.ts} +1 -1
  89. data/package/rules/css.ts +3 -0
  90. data/package/rules/{erb.js → erb.ts} +1 -1
  91. data/package/rules/esbuild.ts +10 -0
  92. data/package/rules/file.ts +40 -0
  93. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  94. data/package/rules/{less.js → less.ts} +4 -4
  95. data/package/rules/raw.ts +25 -0
  96. data/package/rules/rspack.ts +176 -0
  97. data/package/rules/{sass.js → sass.ts} +7 -3
  98. data/package/rules/{stylus.js → stylus.ts} +4 -8
  99. data/package/rules/swc.ts +10 -0
  100. data/package/rules/{index.js → webpack.ts} +1 -1
  101. data/package/swc/index.ts +54 -0
  102. data/package/types/README.md +87 -0
  103. data/package/types/index.ts +60 -0
  104. data/package/types.ts +108 -0
  105. data/package/utils/configPath.ts +6 -0
  106. data/package/utils/debug.ts +49 -0
  107. data/package/utils/defaultConfigPath.ts +4 -0
  108. data/package/utils/errorCodes.ts +219 -0
  109. data/package/utils/errorHelpers.ts +143 -0
  110. data/package/utils/getStyleRule.ts +64 -0
  111. data/package/utils/helpers.ts +85 -0
  112. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  113. data/package/utils/pathValidation.ts +139 -0
  114. data/package/utils/requireOrError.ts +15 -0
  115. data/package/utils/snakeToCamelCase.ts +5 -0
  116. data/package/utils/typeGuards.ts +342 -0
  117. data/package/utils/validateDependencies.ts +61 -0
  118. data/package/webpack-types.d.ts +33 -0
  119. data/package/webpackDevServerConfig.ts +117 -0
  120. data/package.json +134 -9
  121. data/scripts/remove-use-strict.js +45 -0
  122. data/scripts/type-check-no-emit.js +27 -0
  123. data/test/package/config.test.js +3 -0
  124. data/test/package/env.test.js +42 -7
  125. data/test/package/environments/base.test.js +5 -1
  126. data/test/package/rules/babel.test.js +16 -0
  127. data/test/package/rules/esbuild.test.js +1 -1
  128. data/test/package/rules/raw.test.js +40 -7
  129. data/test/package/rules/swc.test.js +1 -1
  130. data/test/package/rules/webpack.test.js +35 -0
  131. data/test/package/staging.test.js +4 -3
  132. data/test/package/transpiler-defaults.test.js +127 -0
  133. data/test/peer-dependencies.sh +85 -0
  134. data/test/scripts/remove-use-strict.test.js +125 -0
  135. data/test/typescript/build.test.js +118 -0
  136. data/test/typescript/environments.test.js +107 -0
  137. data/test/typescript/pathValidation.test.js +142 -0
  138. data/test/typescript/securityValidation.test.js +182 -0
  139. data/tools/README.md +124 -0
  140. data/tools/css-modules-v9-codemod.js +179 -0
  141. data/tsconfig.eslint.json +16 -0
  142. data/tsconfig.json +38 -0
  143. data/yarn.lock +2704 -767
  144. metadata +111 -41
  145. data/package/babel/preset.js +0 -48
  146. data/package/config.js +0 -56
  147. data/package/env.js +0 -48
  148. data/package/environments/base.js +0 -171
  149. data/package/environments/development.js +0 -13
  150. data/package/environments/production.js +0 -88
  151. data/package/environments/test.js +0 -3
  152. data/package/esbuild/index.js +0 -40
  153. data/package/index.js +0 -40
  154. data/package/rules/css.js +0 -3
  155. data/package/rules/esbuild.js +0 -10
  156. data/package/rules/file.js +0 -29
  157. data/package/rules/raw.js +0 -5
  158. data/package/rules/swc.js +0 -10
  159. data/package/swc/index.js +0 -50
  160. data/package/utils/configPath.js +0 -4
  161. data/package/utils/defaultConfigPath.js +0 -2
  162. data/package/utils/getStyleRule.js +0 -40
  163. data/package/utils/helpers.js +0 -62
  164. data/package/utils/snakeToCamelCase.js +0 -5
  165. data/package/webpackDevServerConfig.js +0 -71
  166. data/test/package/rules/index.test.js +0 -16
@@ -2,26 +2,72 @@ require "shakapacker/utils/misc"
2
2
  require "shakapacker/utils/manager"
3
3
  require "shakapacker/utils/version_syntax_converter"
4
4
  require "package_json"
5
+ require "yaml"
6
+ require "json"
5
7
 
6
8
  # Install Shakapacker
7
9
 
8
10
  force_option = ENV["FORCE"] ? { force: true } : {}
9
11
 
10
- copy_file "#{__dir__}/config/shakapacker.yml", "config/shakapacker.yml", force_option
11
- remove_file "#{__dir__}/package.json" if force_option[:force]
12
+ # Initialize variables for use throughout the template
13
+ # Using instance variable to avoid method definition issues in Rails templates
14
+ @package_json ||= PackageJson.new
15
+ install_dir = File.expand_path(File.dirname(__FILE__))
12
16
 
13
- say "Copying webpack core config"
14
- directory "#{__dir__}/config/webpack", "config/webpack", force_option
17
+ # Installation strategy:
18
+ # - USE_BABEL_PACKAGES installs both babel AND swc for compatibility
19
+ # - Otherwise install only the specified transpiler
20
+ if ENV["USE_BABEL_PACKAGES"] == "true" || ENV["USE_BABEL_PACKAGES"] == "1"
21
+ @transpiler_to_install = "babel"
22
+ say "📦 Installing Babel packages (USE_BABEL_PACKAGES is set)", :yellow
23
+ say "✨ Also installing SWC packages for default config compatibility", :green
24
+ elsif ENV["JAVASCRIPT_TRANSPILER"]
25
+ @transpiler_to_install = ENV["JAVASCRIPT_TRANSPILER"]
26
+ say "📦 Installing #{@transpiler_to_install} packages", :blue
27
+ else
28
+ # Default to swc (matches the default in shakapacker.yml)
29
+ @transpiler_to_install = "swc"
30
+ say "✨ Installing SWC packages (20x faster than Babel)", :green
31
+ end
32
+
33
+ # Copy config file
34
+ copy_file "#{install_dir}/config/shakapacker.yml", "config/shakapacker.yml", force_option
35
+
36
+ # Update config if USE_BABEL_PACKAGES is set to ensure babel is used at runtime
37
+ if @transpiler_to_install == "babel" && !ENV["JAVASCRIPT_TRANSPILER"]
38
+ # When USE_BABEL_PACKAGES is set, update the config to use babel
39
+ gsub_file "config/shakapacker.yml", "javascript_transpiler: 'swc'", "javascript_transpiler: 'babel'"
40
+ say " 📝 Updated config/shakapacker.yml to use Babel transpiler", :green
41
+ end
42
+
43
+ # Detect TypeScript usage
44
+ # Auto-detect from tsconfig.json or explicit via SHAKAPACKER_USE_TYPESCRIPT env var
45
+ @use_typescript = File.exist?(Rails.root.join("tsconfig.json")) ||
46
+ ENV["SHAKAPACKER_USE_TYPESCRIPT"] == "true"
47
+ assets_bundler = ENV["SHAKAPACKER_ASSETS_BUNDLER"] || "webpack"
48
+ config_extension = @use_typescript ? "ts" : "js"
49
+
50
+ say "Copying #{assets_bundler} core config (#{config_extension.upcase})"
51
+ config_file = "#{assets_bundler}.config.#{config_extension}"
52
+ source_config = "#{install_dir}/config/#{assets_bundler}/#{config_file}"
53
+ dest_config = "config/#{assets_bundler}/#{config_file}"
54
+
55
+ empty_directory "config/#{assets_bundler}"
56
+ copy_file source_config, dest_config, force_option
57
+
58
+ if @use_typescript
59
+ say " ✨ Using TypeScript config for enhanced type safety", :green
60
+ end
15
61
 
16
62
  if Dir.exist?(Shakapacker.config.source_path)
17
63
  say "The packs app source directory already exists"
18
64
  else
19
65
  say "Creating packs app source directory"
20
66
  empty_directory "app/javascript/packs"
21
- copy_file "#{__dir__}/application.js", "app/javascript/packs/application.js"
67
+ copy_file "#{install_dir}/application.js", "app/javascript/packs/application.js"
22
68
  end
23
69
 
24
- apply "#{__dir__}/binstubs.rb"
70
+ apply "#{install_dir}/binstubs.rb"
25
71
 
26
72
  git_ignore_path = Rails.root.join(".gitignore")
27
73
  if File.exist?(git_ignore_path)
@@ -44,17 +90,8 @@ else
44
90
  say %( Add <%= javascript_pack_tag "application" %> within the <head> tag in your custom layout.)
45
91
  end
46
92
 
47
- def package_json
48
- @package_json ||= PackageJson.new
49
- end
50
-
51
93
  # setup the package manager with default values
52
- package_json.merge! do |pj|
53
- babel = pj.fetch("babel", {})
54
-
55
- babel["presets"] ||= []
56
- babel["presets"].push("./node_modules/shakapacker/package/babel/preset.js")
57
-
94
+ @package_json.merge! do |pj|
58
95
  package_manager = pj.fetch("packageManager") do
59
96
  "#{Shakapacker::Utils::Manager.guess_binary}@#{Shakapacker::Utils::Manager.guess_version}"
60
97
  end
@@ -63,7 +100,6 @@ package_json.merge! do |pj|
63
100
  "name" => "app",
64
101
  "private" => true,
65
102
  "version" => "0.1.0",
66
- "babel" => babel,
67
103
  "browserslist" => [
68
104
  "defaults"
69
105
  ],
@@ -75,7 +111,7 @@ Shakapacker::Utils::Manager.error_unless_package_manager_is_obvious!
75
111
 
76
112
  # Ensure there is `system!("bin/yarn")` command in `./bin/setup` file
77
113
  if (setup_path = Rails.root.join("bin/setup")).exist?
78
- native_install_command = package_json.manager.native_install_command.join(" ")
114
+ native_install_command = @package_json.manager.native_install_command.join(" ")
79
115
 
80
116
  say "Run #{native_install_command} during bin/setup"
81
117
 
@@ -104,40 +140,126 @@ if (setup_path = Rails.root.join("bin/setup")).exist?
104
140
  end
105
141
  end
106
142
 
107
- def add_dependencies(dependencies, type)
108
- package_json.manager.add!(dependencies, type: type)
109
- rescue PackageJson::Error
110
- say "Shakapacker installation failed 😭 See above for details.", :red
111
- exit 1
112
- end
143
+ Dir.chdir(Rails.root) do
144
+ # In CI, use the pre-packed tarball if available
145
+ if ENV["SHAKAPACKER_NPM_PACKAGE"]
146
+ package_path = ENV["SHAKAPACKER_NPM_PACKAGE"]
113
147
 
114
- def fetch_peer_dependencies
115
- PackageJson.read("#{__dir__}/../../").fetch("peerDependencies")
116
- end
148
+ # Validate package path to prevent directory traversal and invalid file types
149
+ begin
150
+ # Resolve to absolute path
151
+ absolute_path = File.expand_path(package_path)
117
152
 
118
- Dir.chdir(Rails.root) do
119
- npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
120
- say "Installing shakapacker@#{npm_version}"
121
- add_dependencies(["shakapacker@#{npm_version}"], :production)
122
-
123
- package_json.merge! do |pj|
124
- {
125
- "dependencies" => pj["dependencies"].merge({
126
- # TODO: workaround for test suite - long-run need to actually account for diff pkg manager behaviour
127
- "shakapacker" => pj["dependencies"]["shakapacker"].delete_prefix("^")
128
- })
129
- }
153
+ # Reject paths containing directory traversal
154
+ if package_path.include?("..") || absolute_path.include?("..")
155
+ say " Security Error: Package path contains directory traversal: #{package_path}", :red
156
+ exit 1
157
+ end
158
+
159
+ # Ensure filename ends with .tgz or .tar.gz
160
+ unless absolute_path.end_with?(".tgz", ".tar.gz")
161
+ say "❌ Security Error: Package must be a .tgz or .tar.gz file: #{package_path}", :red
162
+ exit 1
163
+ end
164
+
165
+ # Check existence only after validation
166
+ if File.exist?(absolute_path)
167
+ say "📦 Installing shakapacker from local package: #{absolute_path}", :cyan
168
+ begin
169
+ @package_json.manager.add!([absolute_path], type: :production)
170
+ rescue PackageJson::Error
171
+ say "Shakapacker installation failed 😭 See above for details.", :red
172
+ exit 1
173
+ end
174
+ else
175
+ say "⚠️ SHAKAPACKER_NPM_PACKAGE set but file not found: #{absolute_path}", :yellow
176
+ say "Falling back to npm registry...", :yellow
177
+ npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
178
+ begin
179
+ @package_json.manager.add!(["shakapacker@#{npm_version}"], type: :production)
180
+ rescue PackageJson::Error
181
+ say "Shakapacker installation failed 😭 See above for details.", :red
182
+ exit 1
183
+ end
184
+ end
185
+ rescue => e
186
+ say "❌ Error validating package path: #{e.message}", :red
187
+ exit 1
188
+ end
189
+ else
190
+ npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
191
+ say "Installing shakapacker@#{npm_version}"
192
+ begin
193
+ @package_json.manager.add!(["shakapacker@#{npm_version}"], type: :production)
194
+ rescue PackageJson::Error
195
+ say "Shakapacker installation failed 😭 See above for details.", :red
196
+ exit 1
197
+ end
198
+ end
199
+
200
+ @package_json.merge! do |pj|
201
+ if pj["dependencies"] && pj["dependencies"]["shakapacker"]
202
+ {
203
+ "dependencies" => pj["dependencies"].merge({
204
+ # TODO: workaround for test suite - long-run need to actually account for diff pkg manager behaviour
205
+ "shakapacker" => pj["dependencies"]["shakapacker"].delete_prefix("^")
206
+ })
207
+ }
208
+ else
209
+ pj
210
+ end
211
+ end
212
+
213
+ # Inline fetch_peer_dependencies and fetch_common_dependencies
214
+ peers = PackageJson.read(install_dir).fetch(ENV["SHAKAPACKER_ASSETS_BUNDLER"] || "webpack")
215
+ common_deps = ENV["SKIP_COMMON_LOADERS"] ? {} : PackageJson.read(install_dir).fetch("common")
216
+ peers = peers.merge(common_deps)
217
+
218
+ # Add transpiler-specific dependencies based on detected/configured transpiler
219
+ # Inline the logic here since methods can't be called before they're defined in Rails templates
220
+
221
+ # Install transpiler-specific dependencies
222
+ # When USE_BABEL_PACKAGES is set, install both babel AND swc
223
+ # This ensures backward compatibility while supporting the default config
224
+ if @transpiler_to_install == "babel"
225
+ # Install babel packages
226
+ babel_deps = PackageJson.read(install_dir).fetch("babel")
227
+ peers = peers.merge(babel_deps)
228
+
229
+ # Also install SWC since that's what the default config uses
230
+ # This ensures the runtime works regardless of config
231
+ swc_deps = PackageJson.read(install_dir).fetch("swc")
232
+ peers = peers.merge(swc_deps)
233
+
234
+ say "ℹ️ Installing both Babel and SWC packages for compatibility:", :blue
235
+ say " - Babel packages are installed as requested via USE_BABEL_PACKAGES", :blue
236
+ say " - SWC packages are also installed to ensure the default config works", :blue
237
+ say " - Your actual transpiler will be determined by your shakapacker.yml configuration", :blue
238
+ elsif @transpiler_to_install == "swc"
239
+ swc_deps = PackageJson.read(install_dir).fetch("swc")
240
+ peers = peers.merge(swc_deps)
241
+ elsif @transpiler_to_install == "esbuild"
242
+ esbuild_deps = PackageJson.read(install_dir).fetch("esbuild")
243
+ peers = peers.merge(esbuild_deps)
130
244
  end
131
245
 
132
- peers = fetch_peer_dependencies
133
246
  dev_dependency_packages = ["webpack-dev-server"]
134
247
 
135
248
  dependencies_to_add = []
136
249
  dev_dependencies_to_add = []
137
250
 
138
251
  peers.each do |(package, version)|
139
- major_version = version.split("||").last.match(/(\d+)/)[1]
140
- entry = "#{package}@#{major_version}"
252
+ # Handle versions like "^1.3.0" or ">= 4 || 5"
253
+ if version.start_with?("^", "~") || version.match?(/^\d+\.\d+/)
254
+ # Already has proper version format, use as-is
255
+ entry = "#{package}@#{version}"
256
+ else
257
+ # Extract major version from complex version strings like ">= 4 || 5"
258
+ # Fallback to "latest" if no version number found
259
+ version_match = version.split("||").last.match(/(\d+)/)
260
+ major_version = version_match ? version_match[1] : "latest"
261
+ entry = "#{package}@#{major_version}"
262
+ end
141
263
 
142
264
  if dev_dependency_packages.include? package
143
265
  dev_dependencies_to_add << entry
@@ -147,8 +269,36 @@ Dir.chdir(Rails.root) do
147
269
  end
148
270
 
149
271
  say "Adding shakapacker peerDependencies"
150
- add_dependencies(dependencies_to_add, :production)
272
+ begin
273
+ @package_json.manager.add!(dependencies_to_add, type: :production)
274
+ rescue PackageJson::Error
275
+ say "Shakapacker installation failed 😭 See above for details.", :red
276
+ exit 1
277
+ end
151
278
 
152
279
  say "Installing webpack-dev-server for live reloading as a development dependency"
153
- add_dependencies(dev_dependencies_to_add, :dev)
280
+ begin
281
+ @package_json.manager.add!(dev_dependencies_to_add, type: :dev)
282
+ rescue PackageJson::Error
283
+ say "Shakapacker installation failed 😭 See above for details.", :red
284
+ exit 1
285
+ end
286
+
287
+ # Configure babel preset in package.json if babel is being used
288
+ if @transpiler_to_install == "babel"
289
+ @package_json.merge! do |pj|
290
+ babel = pj.fetch("babel", {})
291
+ babel["presets"] ||= []
292
+ unless babel["presets"].include?("./node_modules/shakapacker/package/babel/preset.js")
293
+ babel["presets"].push("./node_modules/shakapacker/package/babel/preset.js")
294
+ end
295
+ { "babel" => babel }
296
+ end
297
+ end
298
+ end
299
+
300
+ # Helper methods defined at the end (Rails template convention)
301
+
302
+ def package_json
303
+ @package_json
154
304
  end
@@ -1,4 +1,5 @@
1
1
  require "yaml"
2
+ require "json"
2
3
  require "active_support/core_ext/hash/keys"
3
4
  require "active_support/core_ext/hash/indifferent_access"
4
5
 
@@ -68,6 +69,13 @@ class Shakapacker::Configuration
68
69
  root_path.join(fetch(:public_root_path))
69
70
  end
70
71
 
72
+ def private_output_path
73
+ private_path = fetch(:private_output_path)
74
+ return nil unless private_path
75
+ validate_output_paths!
76
+ root_path.join(private_path)
77
+ end
78
+
71
79
  def public_output_path
72
80
  public_path.join(fetch(:public_output_path))
73
81
  end
@@ -88,6 +96,107 @@ class Shakapacker::Configuration
88
96
  fetch(:compiler_strategy)
89
97
  end
90
98
 
99
+ def assets_bundler
100
+ # Show deprecation warning if using old 'bundler' key
101
+ if data.has_key?(:bundler) && !data.has_key?(:assets_bundler)
102
+ $stderr.puts "⚠️ DEPRECATION WARNING: The 'bundler' configuration option is deprecated. Please use 'assets_bundler' instead to avoid confusion with Ruby's Bundler gem manager."
103
+ end
104
+ ENV["SHAKAPACKER_ASSETS_BUNDLER"] || fetch(:assets_bundler) || fetch(:bundler) || "webpack"
105
+ end
106
+
107
+ # Deprecated: Use assets_bundler instead
108
+ def bundler
109
+ assets_bundler
110
+ end
111
+
112
+ def rspack?
113
+ assets_bundler == "rspack"
114
+ end
115
+
116
+ def webpack?
117
+ assets_bundler == "webpack"
118
+ end
119
+
120
+ def javascript_transpiler
121
+ # Show deprecation warning if using old 'webpack_loader' key
122
+ if data.has_key?(:webpack_loader) && !data.has_key?(:javascript_transpiler)
123
+ $stderr.puts "⚠️ DEPRECATION WARNING: The 'webpack_loader' configuration option is deprecated. Please use 'javascript_transpiler' instead as it better reflects its purpose of configuring JavaScript transpilation regardless of the bundler used."
124
+ end
125
+
126
+ # Use explicit config if set, otherwise default based on bundler
127
+ transpiler = fetch(:javascript_transpiler) || fetch(:webpack_loader) || default_javascript_transpiler
128
+
129
+ # Validate transpiler configuration
130
+ validate_transpiler_configuration(transpiler) unless self.class.installing
131
+
132
+ transpiler
133
+ end
134
+
135
+ # Deprecated: Use javascript_transpiler instead
136
+ def webpack_loader
137
+ javascript_transpiler
138
+ end
139
+
140
+ private
141
+
142
+ def default_javascript_transpiler
143
+ # RSpack has built-in SWC support, use it by default
144
+ rspack? ? "swc" : "babel"
145
+ end
146
+
147
+ def validate_transpiler_configuration(transpiler)
148
+ return unless ENV["NODE_ENV"] != "test" # Skip validation in test environment
149
+
150
+ # Check if package.json exists
151
+ package_json_path = root_path.join("package.json")
152
+ return unless package_json_path.exist?
153
+
154
+ begin
155
+ package_json = JSON.parse(File.read(package_json_path))
156
+ all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
157
+
158
+ # Check for transpiler mismatch
159
+ has_babel = all_deps.keys.any? { |pkg| pkg.start_with?("@babel/", "babel-") }
160
+ has_swc = all_deps.keys.any? { |pkg| pkg.include?("swc") }
161
+ has_esbuild = all_deps.keys.any? { |pkg| pkg.include?("esbuild") }
162
+
163
+ case transpiler
164
+ when "babel"
165
+ if !has_babel && has_swc
166
+ warn_transpiler_mismatch("Babel", "SWC packages found but Babel is configured")
167
+ end
168
+ when "swc"
169
+ if !has_swc && has_babel
170
+ warn_transpiler_mismatch("SWC", "Babel packages found but SWC is configured")
171
+ end
172
+ when "esbuild"
173
+ if !has_esbuild && (has_babel || has_swc)
174
+ other = has_babel ? "Babel" : "SWC"
175
+ warn_transpiler_mismatch("esbuild", "#{other} packages found but esbuild is configured")
176
+ end
177
+ end
178
+ rescue JSON::ParserError
179
+ # Ignore if package.json is malformed
180
+ end
181
+ end
182
+
183
+ def warn_transpiler_mismatch(configured, message)
184
+ $stderr.puts <<~WARNING
185
+ ⚠️ Transpiler Configuration Mismatch Detected:
186
+ #{message}
187
+ Configured transpiler: #{configured}
188
+ #{' '}
189
+ This might cause unexpected behavior or build failures.
190
+ #{' '}
191
+ To fix this:
192
+ 1. Run 'rails shakapacker:migrate_to_swc' to migrate to SWC (recommended for 20x faster builds)
193
+ 2. Or install the correct packages for #{configured}
194
+ 3. Or update your shakapacker.yml to match your installed packages
195
+ WARNING
196
+ end
197
+
198
+ public
199
+
91
200
  def fetch(key)
92
201
  data.fetch(key, defaults[key])
93
202
  end
@@ -104,6 +213,38 @@ class Shakapacker::Configuration
104
213
  end
105
214
 
106
215
  private
216
+ def validate_output_paths!
217
+ # Skip validation if already validated to avoid redundant checks
218
+ return if @validated_output_paths
219
+ @validated_output_paths = true
220
+
221
+ # Only validate when both paths are configured
222
+ return unless fetch(:private_output_path) && fetch(:public_output_path)
223
+
224
+ private_path_str, public_path_str = resolve_paths_for_comparison
225
+
226
+ if private_path_str == public_path_str
227
+ raise "Shakapacker configuration error: private_output_path and public_output_path must be different. " \
228
+ "Both paths resolve to '#{private_path_str}'. " \
229
+ "The private_output_path is for server-side bundles (e.g., SSR) that should not be served publicly."
230
+ end
231
+ end
232
+
233
+ def resolve_paths_for_comparison
234
+ private_full_path = root_path.join(fetch(:private_output_path))
235
+ public_full_path = root_path.join(fetch(:public_root_path), fetch(:public_output_path))
236
+
237
+ # Create directories if they don't exist (for testing)
238
+ private_full_path.mkpath unless private_full_path.exist?
239
+ public_full_path.mkpath unless public_full_path.exist?
240
+
241
+ # Use realpath to resolve symbolic links and relative paths
242
+ [private_full_path.realpath.to_s, public_full_path.realpath.to_s]
243
+ rescue Errno::ENOENT
244
+ # If paths don't exist yet, fall back to cleanpath for comparison
245
+ [private_full_path.cleanpath.to_s, public_full_path.cleanpath.to_s]
246
+ end
247
+
107
248
  def data
108
249
  @data ||= load
109
250
  end
@@ -7,6 +7,10 @@ require_relative "runner"
7
7
 
8
8
  module Shakapacker
9
9
  class DevServerRunner < Shakapacker::Runner
10
+ def self.run(argv)
11
+ new(argv).run
12
+ end
13
+
10
14
  def run
11
15
  load_config
12
16
  detect_unsupported_switches!
@@ -75,12 +79,19 @@ module Shakapacker
75
79
  env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --inspect-brk --trace-warnings"
76
80
  end
77
81
 
82
+ # Add config file
78
83
  cmd += ["--config", @webpack_config]
79
- cmd += ["--progress", "--color"] if @pretty
80
84
 
81
- # Default behavior of webpack-dev-server is @hot = true
82
- cmd += ["--hot", "only"] if @hot == "only"
83
- cmd += ["--no-hot"] if !@hot
85
+ # Add assets bundler-specific flags
86
+ if webpack?
87
+ cmd += ["--progress", "--color"] if @pretty
88
+ # Default behavior of webpack-dev-server is @hot = true
89
+ cmd += ["--hot", "only"] if @hot == "only"
90
+ cmd += ["--no-hot"] if !@hot
91
+ elsif rspack?
92
+ # Rspack supports --hot but not --no-hot or --progress/--color
93
+ cmd += ["--hot"] if @hot && @hot != false
94
+ end
84
95
 
85
96
  cmd += @argv
86
97
 
@@ -90,7 +101,16 @@ module Shakapacker
90
101
  end
91
102
 
92
103
  def build_cmd
93
- package_json.manager.native_exec_command("webpack", ["serve"])
104
+ command = @config.rspack? ? "rspack" : "webpack"
105
+ package_json.manager.native_exec_command(command, ["serve"])
106
+ end
107
+
108
+ def webpack?
109
+ @config.webpack?
110
+ end
111
+
112
+ def rspack?
113
+ @config.rspack?
94
114
  end
95
115
  end
96
116
  end