shakapacker 10.2.0 → 10.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 441942bb193dab20df1d0956197e3d441adb6fef5e25001a4eb1f05c69543312
4
- data.tar.gz: 4aea05e5fa5f72da3b65fd108514400ca6c7140c85cdf7d10aa1d6edf6118d43
3
+ metadata.gz: 66455bdf084a5e63422cf5741b2ce549b2dc77dcd36553e8e5fa7dcc6ce34286
4
+ data.tar.gz: a7030178f8ec26e3c1243fbdeb806c2746d6b14484bc99142a35f3fa3f57e9dd
5
5
  SHA512:
6
- metadata.gz: a2d3907c1a2d5121c7b70f72cb46c8ea09157e263241803b67ce2d250f7183121442bc0e0b6387d1552008565a3802e5a57c6447f4f6bee0d3e3f9214f16786f
7
- data.tar.gz: 3b59158ac878b1403defdc3a22293a2559bb99d13c106175d2c72d637596ad4dfbdd5174aea80222070f50ee911bb7e585a1cae830886bb75be9f942f310e6ed
6
+ metadata.gz: ec0476cbbaaa7044f9f06a3fe207caad61350c309164deffa47d5990e583b025c03e235b4e0e110adac394461fce264b887a71de6159a43ce5664e7a8a133992
7
+ data.tar.gz: 3d625e63b6f838c9febaaad2dda13d43cd7486748d7e39d6ff09e5675d06ab8b85414b9d323c41a3b09fca5adb19ec193b6d90d5651dce92f8573cd18e23937f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,5 @@
1
1
  - For the changelog of versions prior to v6, see the [5.x stable branch of rails/webpacker](https://github.com/rails/webpacker/tree/5-x-stable).
2
+ - **Please see the [v10 Upgrade Guide](./docs/v10_upgrade.md) for upgrading to version 10 and accounting for breaking changes.**
2
3
  - **Please see the [v9 Upgrade Guide](./docs/v9_upgrade.md) for upgrading to version 9 and accounting for breaking changes.**
3
4
  - Please see the [v8 Upgrade Guide](./docs/v8_upgrade.md) for upgrading to version 8 and accounting for breaking changes.
4
5
  - Please see the [v7 Upgrade Guide](./docs/v7_upgrade.md) for upgrading to new spelling in version 7.
@@ -9,6 +10,32 @@
9
10
 
10
11
  ## [Unreleased]
11
12
 
13
+ ## [v10.3.1] - August 3, 2026
14
+
15
+ ### Fixed
16
+
17
+ - **Fixed 502 responses for proxied dev server assets under `rack-proxy` v1.** [PR #1222](https://github.com/shakacode/shakapacker/pull/1222) by [jcbpl](https://github.com/jcbpl). Fixes [#1220](https://github.com/shakacode/shakapacker/issues/1220).
18
+ - **Fixed dev-server liveness checks treating refused macOS 27 connections as running.** The Ruby probe now verifies the connected socket's `SO_ERROR` result before proxying asset requests, avoiding false-positive dev-server detection and resulting 502 responses. [PR #1225](https://github.com/shakacode/shakapacker/pull/1225) by [justin808](https://github.com/justin808). Fixes [#1224](https://github.com/shakacode/shakapacker/issues/1224).
19
+ - **Fixed webpack Babel, SWC, and esbuild rules skipping explicitly included `.cjs` files.** [PR #1219](https://github.com/shakacode/shakapacker/pull/1219) by [oiahoon](https://github.com/oiahoon). Fixes [#1218](https://github.com/shakacode/shakapacker/issues/1218).
20
+ - **Added a `shakapacker:doctor` warning for Rspack React Refresh v2 configs that still use the v1 default-export constructor pattern.** [PR #1207](https://github.com/shakacode/shakapacker/pull/1207) by [justin808](https://github.com/justin808). Existing configs with `const ReactRefreshPlugin = require("@rspack/plugin-react-refresh")` followed by `new ReactRefreshPlugin()` can fail after upgrading to `@rspack/plugin-react-refresh` v2 with `ReactRefreshPlugin is not a constructor`; Doctor now points to the affected JS/TS config file and suggests the named-export/default/module compatibility form. Fixes [#1204](https://github.com/shakacode/shakapacker/issues/1204).
21
+ - **Fixed the missing-`@babel/core` failure to report an actionable install message.** [PR #1212](https://github.com/shakacode/shakapacker/pull/1212) by [justin808](https://github.com/justin808). Babel-transpiled builds whose app lacks `@babel/core` previously surfaced a raw module-resolution error from the Babel 8 compatibility check; the rule now explains which package to install and how to switch `javascript_transpiler` instead. Refs [#1163](https://github.com/shakacode/shakapacker/issues/1163).
22
+
23
+ ### Documentation
24
+
25
+ - **Documented the required `css-loader@^7.1.4` in the v10 Rspack upgrade instructions.** [PR #1211](https://github.com/shakacode/shakapacker/pull/1211) by [justin808](https://github.com/justin808). The v10 upgrade guide's copy-paste Rspack v2 commands omitted `css-loader`, so apps following them could upgrade into an unsatisfied peer dependency.
26
+
27
+ ## [v10.3.0] - July 5, 2026
28
+
29
+ ### Added
30
+
31
+ - **Allowed `webpack-dev-server` v6**. [PR #1102](https://github.com/shakacode/shakapacker/pull/1102) by [G-Rath](https://github.com/G-Rath).
32
+
33
+ ### Fixed
34
+
35
+ - **Fixed implicit SWC defaults for existing webpack/Babel apps without `swc-loader`.** [PR #1206](https://github.com/shakacode/shakapacker/pull/1206) by [justin808](https://github.com/justin808). Webpack apps that omit both `javascript_transpiler` and the deprecated `webpack_loader` now fall back to Babel with a warning when Shakapacker's bundled SWC default is active, `swc-loader` is missing, and Babel is present. Explicit transpiler settings, webpack apps with `swc-loader`, and Rspack's built-in SWC path keep their existing behavior. Closes [#1203](https://github.com/shakacode/shakapacker/issues/1203).
36
+ - **Fixed JavaScript config loading for missing Rails environments to use the production fallback.** [PR #1206](https://github.com/shakacode/shakapacker/pull/1206) by [justin808](https://github.com/justin808). When `RAILS_ENV` has no matching section in `config/shakapacker.yml`, the Node package config now merges the `production` section instead of only bundled defaults, matching Ruby configuration loading and honoring explicit production `javascript_transpiler`, `source_path`, `dev_server`, and related settings for custom environments such as staging.
37
+ - **Fixed helper binstubs delegating Node resolution to Ruby `exec` in unset and empty `PATH` environments.** [PR #1200](https://github.com/shakacode/shakapacker/pull/1200) and [PR #1201](https://github.com/shakacode/shakapacker/pull/1201) by [justin808](https://github.com/justin808). Restores shell-compatible Node lookup for `bin/shakapacker-config` and `bin/diff-bundler-config` after the `v10.2.0` Ruby-binstub regression, while keeping friendly missing-Node errors for `ENOENT` and `EACCES`.
38
+
12
39
  ## [v10.2.0] - July 3, 2026
13
40
 
14
41
  ### Added
@@ -973,7 +1000,9 @@ Note: [Rubygem is 6.3.0.pre.rc.1](https://rubygems.org/gems/shakapacker/versions
973
1000
 
974
1001
  See [CHANGELOG.md in rails/webpacker (up to v5.4.3)](https://github.com/rails/webpacker/blob/master/CHANGELOG.md)
975
1002
 
976
- [Unreleased]: https://github.com/shakacode/shakapacker/compare/v10.2.0...main
1003
+ [Unreleased]: https://github.com/shakacode/shakapacker/compare/v10.3.1...main
1004
+ [v10.3.1]: https://github.com/shakacode/shakapacker/compare/v10.3.0...v10.3.1
1005
+ [v10.3.0]: https://github.com/shakacode/shakapacker/compare/v10.2.0...v10.3.0
977
1006
  [v10.2.0]: https://github.com/shakacode/shakapacker/compare/v10.1.0...v10.2.0
978
1007
  [v10.1.0]: https://github.com/shakacode/shakapacker/compare/v10.0.0...v10.1.0
979
1008
  [v10.0.0]: https://github.com/shakacode/shakapacker/compare/v9.7.0...v10.0.0
data/README.md CHANGED
@@ -114,7 +114,8 @@ and release-to-release upgrade paths.
114
114
 
115
115
  Older major-version docs:
116
116
 
117
- - [v10.0.0 release notes](https://github.com/shakacode/shakapacker/releases/tag/v10.0.0)
117
+ - [v10 upgrade guide](https://github.com/shakacode/shakapacker/blob/main/docs/v10_upgrade.md)
118
+ ([docs site](https://shakapacker.com/docs/v10_upgrade/))
118
119
  - [v9 upgrade guide](https://shakapacker.com/docs/v9_upgrade/)
119
120
  - [v8 upgrade guide](https://shakapacker.com/docs/v8_upgrade/)
120
121
  - [v7 upgrade guide](https://shakapacker.com/docs/v7_upgrade/)
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbconfig"
5
4
  require "yaml"
6
5
 
7
6
  # This binstub is managed by Shakapacker. Delete it and rerun the install or init command to regenerate it.
@@ -14,36 +13,39 @@ def shakapacker_app_root
14
13
  Dir.pwd
15
14
  end
16
15
 
17
- def shakapacker_executable_candidates(executable)
18
- extensions = [
19
- RbConfig::CONFIG["EXEEXT"],
20
- *ENV.fetch("PATHEXT", "").split(File::PATH_SEPARATOR)
21
- ].compact.reject(&:empty?)
22
- return [executable] if extensions.empty? || File.extname(executable) != ""
16
+ def shakapacker_node_binary
17
+ "node"
18
+ end
23
19
 
24
- ([executable] + extensions.map { |extension| "#{executable}#{extension}" }).uniq
20
+ def shakapacker_warn_missing_node
21
+ warn '[Shakapacker] Could not find Node.js executable "node". ' \
22
+ "Install Node.js and try again."
23
+ exit 1
25
24
  end
26
25
 
27
- def shakapacker_find_executable(executable)
28
- ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
29
- search_path = path.empty? ? Dir.pwd : path
26
+ def shakapacker_exec_env(launch_dir)
27
+ return {} unless ENV.key?("PATH")
30
28
 
31
- shakapacker_executable_candidates(executable).each do |candidate|
32
- executable_path = File.join(search_path, candidate)
33
- return executable_path if File.file?(executable_path) && File.executable?(executable_path)
34
- end
35
- end
29
+ path_entries = ENV["PATH"].split(File::PATH_SEPARATOR, -1)
30
+ path_entries = [""] if path_entries.empty?
36
31
 
37
- nil
32
+ normalized_path = path_entries.map do |entry|
33
+ entry.empty? ? launch_dir : File.absolute_path(entry, launch_dir)
34
+ end.join(File::PATH_SEPARATOR)
35
+
36
+ { "PATH" => normalized_path }
38
37
  end
39
38
 
40
- def shakapacker_node_binary
41
- node_bin = shakapacker_find_executable("node")
42
- return node_bin if node_bin
39
+ def shakapacker_exec_node(script_path, argv, launch_dir)
40
+ exec_env = shakapacker_exec_env(launch_dir)
43
41
 
44
- warn '[Shakapacker] Could not find Node.js executable "node". ' \
45
- "Install Node.js and try again."
46
- exit 1
42
+ if exec_env.empty?
43
+ exec shakapacker_node_binary, script_path, *argv
44
+ else
45
+ exec exec_env, shakapacker_node_binary, script_path, *argv
46
+ end
47
+ rescue Errno::ENOENT, Errno::EACCES
48
+ shakapacker_warn_missing_node
47
49
  end
48
50
 
49
51
  def shakapacker_node_env
@@ -130,9 +132,9 @@ end
130
132
  ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
131
133
  ENV["NODE_ENV"] ||= shakapacker_node_env
132
134
 
135
+ launch_dir = Dir.pwd
133
136
  app_root = shakapacker_app_root
134
137
  client_root = shakapacker_client_root(app_root)
135
- node_bin = shakapacker_node_binary
136
138
  script_paths = shakapacker_package_script_paths(app_root, client_root, "diff-bundler-config.cjs")
137
139
  script_path = script_paths.find { |path| File.file?(path) }
138
140
 
@@ -142,5 +144,5 @@ unless script_path
142
144
  end
143
145
 
144
146
  Dir.chdir(app_root) do
145
- exec node_bin, script_path, *ARGV
147
+ shakapacker_exec_node(script_path, ARGV, launch_dir)
146
148
  end
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbconfig"
5
4
  require "yaml"
6
5
 
7
6
  # This binstub is managed by Shakapacker. Delete it and rerun the install or init command to regenerate it.
@@ -14,36 +13,39 @@ def shakapacker_app_root
14
13
  Dir.pwd
15
14
  end
16
15
 
17
- def shakapacker_executable_candidates(executable)
18
- extensions = [
19
- RbConfig::CONFIG["EXEEXT"],
20
- *ENV.fetch("PATHEXT", "").split(File::PATH_SEPARATOR)
21
- ].compact.reject(&:empty?)
22
- return [executable] if extensions.empty? || File.extname(executable) != ""
16
+ def shakapacker_node_binary
17
+ "node"
18
+ end
23
19
 
24
- ([executable] + extensions.map { |extension| "#{executable}#{extension}" }).uniq
20
+ def shakapacker_warn_missing_node
21
+ warn '[Shakapacker] Could not find Node.js executable "node". ' \
22
+ "Install Node.js and try again."
23
+ exit 1
25
24
  end
26
25
 
27
- def shakapacker_find_executable(executable)
28
- ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
29
- search_path = path.empty? ? Dir.pwd : path
26
+ def shakapacker_exec_env(launch_dir)
27
+ return {} unless ENV.key?("PATH")
30
28
 
31
- shakapacker_executable_candidates(executable).each do |candidate|
32
- executable_path = File.join(search_path, candidate)
33
- return executable_path if File.file?(executable_path) && File.executable?(executable_path)
34
- end
35
- end
29
+ path_entries = ENV["PATH"].split(File::PATH_SEPARATOR, -1)
30
+ path_entries = [""] if path_entries.empty?
36
31
 
37
- nil
32
+ normalized_path = path_entries.map do |entry|
33
+ entry.empty? ? launch_dir : File.absolute_path(entry, launch_dir)
34
+ end.join(File::PATH_SEPARATOR)
35
+
36
+ { "PATH" => normalized_path }
38
37
  end
39
38
 
40
- def shakapacker_node_binary
41
- node_bin = shakapacker_find_executable("node")
42
- return node_bin if node_bin
39
+ def shakapacker_exec_node(script_path, argv, launch_dir)
40
+ exec_env = shakapacker_exec_env(launch_dir)
43
41
 
44
- warn '[Shakapacker] Could not find Node.js executable "node". ' \
45
- "Install Node.js and try again."
46
- exit 1
42
+ if exec_env.empty?
43
+ exec shakapacker_node_binary, script_path, *argv
44
+ else
45
+ exec exec_env, shakapacker_node_binary, script_path, *argv
46
+ end
47
+ rescue Errno::ENOENT, Errno::EACCES
48
+ shakapacker_warn_missing_node
47
49
  end
48
50
 
49
51
  def shakapacker_node_env
@@ -130,9 +132,9 @@ end
130
132
  ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
131
133
  ENV["NODE_ENV"] ||= shakapacker_node_env
132
134
 
135
+ launch_dir = Dir.pwd
133
136
  app_root = shakapacker_app_root
134
137
  client_root = shakapacker_client_root(app_root)
135
- node_bin = shakapacker_node_binary
136
138
  script_paths = shakapacker_package_script_paths(app_root, client_root, "shakapacker-config.cjs")
137
139
  script_path = script_paths.find { |path| File.file?(path) }
138
140
 
@@ -142,5 +144,5 @@ unless script_path
142
144
  end
143
145
 
144
146
  Dir.chdir(app_root) do
145
- exec node_bin, script_path, *ARGV
147
+ shakapacker_exec_node(script_path, ARGV, launch_dir)
146
148
  end
@@ -59,7 +59,9 @@ default: &default
59
59
  cache_manifest: false
60
60
 
61
61
  # Select JavaScript transpiler to use
62
- # Available options: 'swc' (default, 20x faster), 'babel', 'esbuild', or 'none'
62
+ # Available options: 'swc' (bundled default), 'babel', 'esbuild', or 'none'
63
+ # Webpack apps that omit this setting use the bundled SWC default when swc-loader is
64
+ # installed, but fall back to Babel with a warning when only Babel tooling is present.
63
65
  # Use 'none' when providing a completely custom webpack configuration
64
66
  # Note: When using rspack, swc is used automatically regardless of this setting
65
67
  javascript_transpiler: "swc"
@@ -11,7 +11,7 @@
11
11
  "webpack": "^5.101.0",
12
12
  "webpack-assets-manifest": "^5.0.6 || ^6.0.0",
13
13
  "webpack-cli": "^4.9.2 || ^5.0.0 || ^6.0.0 || ^7.0.0",
14
- "webpack-dev-server": "^5.2.2",
14
+ "webpack-dev-server": "^5.2.2 || ^6.0.0",
15
15
  "webpack-merge": "^5.8.0 || ^6.0.0",
16
16
  "webpack-subresource-integrity": "^5.1.0"
17
17
  },
@@ -50,6 +50,13 @@ class Shakapacker::Configuration
50
50
  /\A(?:--config|-c|--node-env|--nodeEnv|--bundler|--build|--init|--list-builds)(?:=.*)?\z/
51
51
  private_constant :SHAKAPACKER_MANAGED_COMPILE_FLAG_PATTERN
52
52
 
53
+ IMPLICIT_SWC_BABEL_FALLBACK_WARNING =
54
+ "`javascript_transpiler` is not set in config/shakapacker.yml. " \
55
+ "Shakapacker defaults to SWC, but swc-loader is not installed and Babel was detected, so Babel will be used. " \
56
+ "Set `javascript_transpiler: babel` (or `swc`) explicitly to silence this message. " \
57
+ "See https://github.com/shakacode/shakapacker/blob/main/docs/transpiler-migration.md"
58
+ private_constant :IMPLICIT_SWC_BABEL_FALLBACK_WARNING
59
+
53
60
  DISALLOWED_WEBPACK_COMPILE_FLAGS =
54
61
  (SHAKAPACKER_NODE_FLAGS + SHAKAPACKER_RUNNER_COMMANDS + SHAKAPACKER_WATCH_FLAGS +
55
62
  SHAKAPACKER_MANAGED_COMPILE_FLAGS).freeze
@@ -357,24 +364,54 @@ class Shakapacker::Configuration
357
364
  # Resolution order:
358
365
  # 1. javascript_transpiler setting in config file
359
366
  # 2. webpack_loader setting in config file (deprecated)
360
- # 3. Default based on bundler (swc for rspack, babel for webpack)
367
+ # 3. Bundled defaults
368
+ # 4. Babel fallback for implicit webpack/SWC defaults when swc-loader is missing
361
369
  #
362
370
  # Validates that the configured transpiler matches installed packages.
363
371
  #
364
372
  # @return [String] "babel", "swc", or "esbuild"
365
373
  def javascript_transpiler
366
- # Show deprecation warning if using old 'webpack_loader' key
367
- if data.has_key?(:webpack_loader) && !data.has_key?(:javascript_transpiler)
368
- $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."
369
- end
374
+ return @javascript_transpiler if defined?(@javascript_transpiler)
375
+
376
+ @javascript_transpiler =
377
+ begin
378
+ javascript_transpiler_configured = config_value_configured?(:javascript_transpiler)
379
+ webpack_loader_configured = config_value_present?(:webpack_loader)
380
+
381
+ # Show deprecation warning if using old 'webpack_loader' key
382
+ if webpack_loader_configured && !javascript_transpiler_configured
383
+ $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."
384
+ end
385
+
386
+ # Use explicit config if set, otherwise default based on bundler
387
+ current_javascript_transpiler =
388
+ if javascript_transpiler_configured
389
+ fetch(:javascript_transpiler)
390
+ elsif data.has_key?(:javascript_transpiler)
391
+ defaults[:javascript_transpiler]
392
+ else
393
+ fetch(:javascript_transpiler)
394
+ end
395
+
396
+ transpiler =
397
+ if webpack_loader_configured && !javascript_transpiler_configured
398
+ fetch(:webpack_loader) || default_javascript_transpiler
399
+ else
400
+ current_javascript_transpiler || fetch(:webpack_loader) || default_javascript_transpiler
401
+ end
370
402
 
371
- # Use explicit config if set, otherwise default based on bundler
372
- transpiler = fetch(:javascript_transpiler) || fetch(:webpack_loader) || default_javascript_transpiler
403
+ if !javascript_transpiler_configured &&
404
+ !webpack_loader_configured &&
405
+ implicit_swc_to_babel_fallback?(transpiler)
406
+ $stderr.puts IMPLICIT_SWC_BABEL_FALLBACK_WARNING
407
+ transpiler = "babel"
408
+ end
373
409
 
374
- # Validate transpiler configuration
375
- validate_transpiler_configuration(transpiler) unless self.class.installing
410
+ # Validate transpiler configuration
411
+ validate_transpiler_configuration(transpiler) unless self.class.installing
376
412
 
377
- transpiler
413
+ transpiler
414
+ end
378
415
  end
379
416
 
380
417
  # Deprecated alias for {#javascript_transpiler}
@@ -461,42 +498,124 @@ class Shakapacker::Configuration
461
498
  rspack? ? "swc" : "babel"
462
499
  end
463
500
 
501
+ def implicit_swc_to_babel_fallback?(transpiler)
502
+ webpack? &&
503
+ transpiler == "swc" &&
504
+ !package_dependency?("swc-loader") &&
505
+ package_dependency?("babel-loader")
506
+ end
507
+
508
+ def package_dependency?(package_name)
509
+ declared_package_dependencies.key?(package_name)
510
+ end
511
+
512
+ def config_value_present?(key)
513
+ return false unless data.has_key?(key)
514
+
515
+ value = data[key]
516
+ value.is_a?(String) && !value.strip.empty?
517
+ end
518
+
519
+ def config_value_configured?(key)
520
+ return false unless data.has_key?(key)
521
+
522
+ value = data[key]
523
+ return false if value.nil?
524
+ return false if value.is_a?(String) && value.strip.empty?
525
+
526
+ true
527
+ end
528
+
529
+ def declared_package_dependencies
530
+ return @declared_package_dependencies if defined?(@declared_package_dependencies)
531
+
532
+ @declared_package_dependencies =
533
+ package_json_paths.reverse_each.each_with_object({}) do |path, dependencies|
534
+ package_json = parse_package_json(path)
535
+ next unless package_json
536
+
537
+ dependencies.merge!(installable_package_dependencies(package_json))
538
+ end
539
+ end
540
+
541
+ def installable_package_dependencies(package_json)
542
+ (package_json["optionalDependencies"] || {})
543
+ .merge(package_json["devDependencies"] || {})
544
+ .merge(package_json["dependencies"] || {})
545
+ end
546
+
547
+ def package_json_paths
548
+ @package_json_paths ||= package_root_paths
549
+ .map { |path| path.join("package.json") }
550
+ .select(&:exist?)
551
+ end
552
+
553
+ def parse_package_json(path)
554
+ JSON.parse(File.read(path))
555
+ rescue JSON::ParserError, SystemCallError
556
+ nil
557
+ end
558
+
559
+ def package_root_paths
560
+ @package_root_paths ||= [javascript_package_root_path, root_path].uniq
561
+ end
562
+
563
+ def javascript_package_root_path
564
+ @javascript_package_root_path ||= begin
565
+ resolved_source_path = source_path.expand_path
566
+ app_root = root_path.expand_path
567
+
568
+ if path_within?(resolved_source_path, app_root)
569
+ current = resolved_source_path
570
+ loop do
571
+ break current if current.join("package.json").exist?
572
+ break root_path if current == app_root
573
+
574
+ parent = current.dirname
575
+ break root_path if parent == current
576
+
577
+ current = parent
578
+ end
579
+ else
580
+ root_path
581
+ end
582
+ rescue StandardError
583
+ root_path
584
+ end
585
+ end
586
+
587
+ def path_within?(path, parent)
588
+ path.to_s == parent.to_s || path.to_s.start_with?("#{parent}#{File::SEPARATOR}")
589
+ end
590
+
464
591
  def validate_transpiler_configuration(transpiler)
465
592
  return unless ENV["NODE_ENV"] != "test" # Skip validation in test environment
466
593
 
467
594
  # Skip validation if transpiler is set to 'none' (custom webpack config)
468
595
  return if transpiler == "none"
469
596
 
470
- # Check if package.json exists
471
- package_json_path = root_path.join("package.json")
472
- return unless package_json_path.exist?
597
+ all_deps = declared_package_dependencies
598
+ return if all_deps.empty?
473
599
 
474
- begin
475
- package_json = JSON.parse(File.read(package_json_path))
476
- all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
477
-
478
- # Check for transpiler mismatch
479
- has_babel = all_deps.keys.any? { |pkg| pkg.start_with?("@babel/", "babel-") }
480
- has_swc = all_deps.keys.any? { |pkg| pkg.include?("swc") }
481
- has_esbuild = all_deps.keys.any? { |pkg| pkg.include?("esbuild") }
482
-
483
- case transpiler
484
- when "babel"
485
- if !has_babel && has_swc
486
- warn_transpiler_mismatch("Babel", "SWC packages found but Babel is configured")
487
- end
488
- when "swc"
489
- if !has_swc && has_babel
490
- warn_transpiler_mismatch("SWC", "Babel packages found but SWC is configured")
491
- end
492
- when "esbuild"
493
- if !has_esbuild && (has_babel || has_swc)
494
- other = has_babel ? "Babel" : "SWC"
495
- warn_transpiler_mismatch("esbuild", "#{other} packages found but esbuild is configured")
496
- end
600
+ # Check for transpiler mismatch
601
+ has_babel = all_deps.keys.any? { |pkg| pkg.start_with?("@babel/", "babel-") }
602
+ has_swc = all_deps.keys.any? { |pkg| pkg.include?("swc") }
603
+ has_esbuild = all_deps.keys.any? { |pkg| pkg.include?("esbuild") }
604
+
605
+ case transpiler
606
+ when "babel"
607
+ if !has_babel && has_swc
608
+ warn_transpiler_mismatch("Babel", "SWC packages found but Babel is configured")
609
+ end
610
+ when "swc"
611
+ if !has_swc && has_babel
612
+ warn_transpiler_mismatch("SWC", "Babel packages found but SWC is configured")
613
+ end
614
+ when "esbuild"
615
+ if !has_esbuild && (has_babel || has_swc)
616
+ other = has_babel ? "Babel" : "SWC"
617
+ warn_transpiler_mismatch("esbuild", "#{other} packages found but esbuild is configured")
497
618
  end
498
- rescue JSON::ParserError
499
- # Ignore if package.json is malformed
500
619
  end
501
620
  end
502
621
 
@@ -47,8 +47,13 @@ class Shakapacker::DevServer
47
47
  # @return [Boolean] true if the dev server is running
48
48
  def running?
49
49
  if config.dev_server.present?
50
- Socket.tcp(host, port, connect_timeout: connect_timeout).close
51
- true
50
+ socket = Socket.tcp(host, port, connect_timeout: connect_timeout)
51
+
52
+ begin
53
+ socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int.zero?
54
+ ensure
55
+ socket.close
56
+ end
52
57
  else
53
58
  false
54
59
  end
@@ -11,6 +11,10 @@ class Shakapacker::DevServerProxy < Rack::Proxy
11
11
 
12
12
  def perform_request(env)
13
13
  if env["PATH_INFO"].start_with?("/#{public_output_uri_path}") && dev_server.running?
14
+ # rack-proxy v1 refuses to derive the backend from the Host header.
15
+ env["rack.backend"] = URI::Generic.build(
16
+ scheme: dev_server.protocol, host: dev_server.host, port: dev_server.port
17
+ )
14
18
  env["HTTP_HOST"] = env["HTTP_X_FORWARDED_HOST"] = dev_server.host
15
19
  env["HTTP_X_FORWARDED_SERVER"] = dev_server.host_with_port
16
20
  env["HTTP_PORT"] = env["HTTP_X_FORWARDED_PORT"] = dev_server.port.to_s
@@ -59,6 +59,9 @@ module Shakapacker
59
59
  "@rspack/plugin-react-refresh" => "^2.0.0"
60
60
  }.freeze
61
61
 
62
+ RSPACK_REACT_REFRESH_PACKAGE = "@rspack/plugin-react-refresh".freeze
63
+ VERSION_UPPER_BOUND_OPERATORS = %w[< <=].freeze
64
+
62
65
  CUSTOM_HYBRID_LOADER_DEPS = %w[
63
66
  babel-loader
64
67
  esbuild-loader
@@ -170,6 +173,7 @@ module Shakapacker
170
173
  check_css_modules_configuration
171
174
  check_bundler_dependencies if config_exists?
172
175
  check_rspack_cache_configuration if config_exists?
176
+ check_rspack_react_refresh_plugin_constructor if config_exists?
173
177
  check_file_type_dependencies if config_exists?
174
178
  check_sri_dependencies if config_exists?
175
179
  check_peer_dependencies
@@ -511,12 +515,14 @@ module Shakapacker
511
515
 
512
516
  def check_javascript_transpiler_dependencies
513
517
  transpiler = explicit_javascript_transpiler
518
+ implicit_babel_fallback = false
514
519
 
515
520
  if transpiler.nil?
516
- @info << "No javascript_transpiler configured - defaulting to SWC (20x faster than Babel)"
517
- transpiler = "swc"
521
+ transpiler = javascript_transpiler
522
+ implicit_babel_fallback = transpiler == "babel"
518
523
  end
519
524
 
525
+ @resolved_javascript_transpiler = transpiler
520
526
  return if transpiler == "none"
521
527
 
522
528
  bundler = assets_bundler
@@ -541,7 +547,7 @@ module Shakapacker
541
547
  case transpiler
542
548
  when "babel"
543
549
  check_babel_dependencies
544
- check_babel_performance_suggestion
550
+ check_babel_performance_suggestion unless implicit_babel_fallback
545
551
  when "swc"
546
552
  check_swc_dependencies(bundler) unless inferred_hybrid_graph
547
553
  when "esbuild"
@@ -573,6 +579,20 @@ module Shakapacker
573
579
  @info << "Consider switching to SWC for 20x faster compilation. Set javascript_transpiler: 'swc' in shakapacker.yml"
574
580
  end
575
581
 
582
+ def implicit_javascript_transpiler
583
+ if assets_bundler == "webpack" && !package_installed?("swc-loader") && package_installed?("babel-loader")
584
+ @info << "`javascript_transpiler` is not set in config/shakapacker.yml. " \
585
+ "Shakapacker defaults to SWC, but swc-loader is not installed and Babel was detected, so Babel will be used. " \
586
+ "Set `javascript_transpiler: babel` (or `swc`) explicitly to silence this message. " \
587
+ "See https://github.com/shakacode/shakapacker/blob/main/docs/transpiler-migration.md"
588
+ "babel"
589
+ else
590
+ @info << "No javascript_transpiler configured - using bundled SWC default. " \
591
+ "Set javascript_transpiler: 'swc' or 'babel' explicitly in shakapacker.yml to silence this message."
592
+ "swc"
593
+ end
594
+ end
595
+
576
596
  def check_swc_dependencies(bundler)
577
597
  if bundler == "webpack"
578
598
  unless package_installed?("@swc/core")
@@ -862,14 +882,89 @@ module Shakapacker
862
882
  "See https://rspack.rs/config/cache for options.")
863
883
  end
864
884
 
885
+ def check_rspack_react_refresh_plugin_constructor
886
+ return unless rspack_react_refresh_check_applicable?
887
+ return unless rspack_react_refresh_plugin_v2_or_newer?
888
+
889
+ rspack_react_refresh_config_paths.each do |path|
890
+ content = File.read(path)
891
+ next unless legacy_rspack_react_refresh_constructor?(content)
892
+
893
+ relative = config_path_for_warning(path)
894
+ add_action_required("Rspack React Refresh config #{relative} uses the " \
895
+ "#{RSPACK_REACT_REFRESH_PACKAGE} v1 default-export constructor pattern. " \
896
+ "With #{RSPACK_REACT_REFRESH_PACKAGE} v2, rspack may fail with " \
897
+ "'ReactRefreshPlugin is not a constructor'.")
898
+ add_fix_hint("Use a compat constructor: const ReactRefresh = require(\"#{RSPACK_REACT_REFRESH_PACKAGE}\"); " \
899
+ "const ReactRefreshRspackPlugin = ReactRefresh.ReactRefreshRspackPlugin || " \
900
+ "ReactRefresh.default || ReactRefresh; then call new ReactRefreshRspackPlugin().")
901
+ rescue SystemCallError => e
902
+ add_info_warning("Unable to validate Rspack React Refresh config #{config_path_for_warning(path)}: #{e.message}")
903
+ end
904
+ end
905
+
906
+ def rspack_react_refresh_check_applicable?
907
+ assets_bundler == "rspack" || unconfigured_hybrid_loader_graph?
908
+ end
909
+
910
+ def rspack_react_refresh_plugin_v2_or_newer?
911
+ version = installed_package_version(RSPACK_REACT_REFRESH_PACKAGE)
912
+ return version >= Gem::Version.new("2.0.0") if version
913
+
914
+ package_specifier_allows_version_or_newer?(
915
+ package_json_dependency_version(RSPACK_REACT_REFRESH_PACKAGE),
916
+ Gem::Version.new("2.0.0")
917
+ )
918
+ end
919
+
920
+ def rspack_react_refresh_config_paths
921
+ config_dir = rspack_react_refresh_config_dir
922
+ config_dir_path = Pathname.new(File.join(root_path.to_s, config_dir))
923
+ return [] unless config_dir_path.directory?
924
+
925
+ path = active_bundler_config_path_in(config_dir)
926
+ path ? [path] : []
927
+ end
928
+
929
+ def rspack_react_refresh_config_dir
930
+ config_dir = config.assets_bundler_config_path.to_s
931
+
932
+ if assets_bundler == "rspack" || same_directory_hybrid_config_present?(config_dir)
933
+ config_dir
934
+ else
935
+ "config/rspack"
936
+ end
937
+ end
938
+
939
+ def legacy_rspack_react_refresh_constructor?(content)
940
+ stripped = strip_rspack_config_comments_and_literals(content, preserve_literals: [RSPACK_REACT_REFRESH_PACKAGE])
941
+ constructor_names = []
942
+
943
+ constructor_names.concat(stripped.scan(
944
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*["']#{Regexp.escape(RSPACK_REACT_REFRESH_PACKAGE)}["']\s*\)(?=\s*(?:[;,\)]|$))/
945
+ ).flatten)
946
+ constructor_names.concat(stripped.scan(
947
+ /\bimport\s+([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\}\s*)?\s+from\s*["']#{Regexp.escape(RSPACK_REACT_REFRESH_PACKAGE)}["']/
948
+ ).flatten)
949
+ constructor_names.concat(stripped.scan(
950
+ /\bimport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["']#{Regexp.escape(RSPACK_REACT_REFRESH_PACKAGE)}["']/
951
+ ).flatten)
952
+
953
+ constructor_names.uniq.any? do |constructor_name|
954
+ stripped.match?(/\bnew\s+#{Regexp.escape(constructor_name)}\s*\(/)
955
+ end
956
+ end
957
+
865
958
  # Returns the single active config path the runner would load, or nil. Mirrors the
866
959
  # resolution order in Shakapacker::Runner#find_rspack_config_with_fallback so the
867
960
  # doctor inspects the same file the build will actually use (and so unused sibling
868
961
  # configs in the same directory don't trigger spurious warnings).
869
962
  # NOTE: keep this candidate list in sync with Runner#find_rspack_config_with_fallback.
870
963
  def active_assets_bundler_config_path
871
- config_dir = config.assets_bundler_config_path.to_s
964
+ active_bundler_config_path_in(config.assets_bundler_config_path.to_s)
965
+ end
872
966
 
967
+ def active_bundler_config_path_in(config_dir)
873
968
  candidates = %w[ts js].map { |ext| Pathname.new(File.join(root_path.to_s, config_dir, "rspack.config.#{ext}")) }
874
969
  candidates += %w[ts js].map { |ext| Pathname.new(File.join(root_path.to_s, config_dir, "webpack.config.#{ext}")) }
875
970
  if default_rspack_config_dir?(config_dir)
@@ -928,7 +1023,7 @@ module Shakapacker
928
1023
  stripped.gsub(%r{/(?:\\.|\[[^\]\n]*\]|[^/\n\\\[])+?/[gimsuy]*}, "")
929
1024
  end
930
1025
 
931
- def strip_rspack_config_comments_and_literals(content)
1026
+ def strip_rspack_config_comments_and_literals(content, preserve_literals: [])
932
1027
  stripped = +""
933
1028
  index = 0
934
1029
 
@@ -971,8 +1066,12 @@ module Shakapacker
971
1066
  end
972
1067
 
973
1068
  literal = content[index...literal_end]
974
- stripped << '""'
975
- stripped << literal.gsub(/[^\n]/, "")
1069
+ if preserved_js_string_literal?(literal, char, preserve_literals)
1070
+ stripped << literal
1071
+ else
1072
+ stripped << '""'
1073
+ stripped << literal.gsub(/[^\n]/, "")
1074
+ end
976
1075
  index = literal_end
977
1076
  elsif char == "`"
978
1077
  literal_end = index + 1
@@ -1013,6 +1112,12 @@ module Shakapacker
1013
1112
  stripped
1014
1113
  end
1015
1114
 
1115
+ def preserved_js_string_literal?(literal, quote, preserve_literals)
1116
+ return false unless literal.end_with?(quote)
1117
+
1118
+ preserve_literals.include?(literal[1...-1])
1119
+ end
1120
+
1016
1121
  def direct_export_cache_disabled?(stripped)
1017
1122
  # Match `cache: false` only near an exported config object at brace depth
1018
1123
  # 1. This avoids warning on local base config objects while still
@@ -1167,8 +1272,9 @@ module Shakapacker
1167
1272
  return versions.min
1168
1273
  end
1169
1274
 
1275
+ return package_version_from_range_specifier(cleaned) if cleaned.match?(/[<>=]/)
1170
1276
  return nil unless cleaned.match?(/\A[\^~]?\d/)
1171
- return nil if cleaned.match?(/(\s|\|\||[<>=:]|\A(?:git|file|link|workspace|npm):)/)
1277
+ return nil if cleaned.match?(/(\s|\|\||:|\A(?:git|file|link|workspace|npm):)/)
1172
1278
 
1173
1279
  match = cleaned.sub(/\A[\^~]/, "").match(/\A(\d+(?:\.\d+){0,2})(?:-[0-9A-Za-z.-]+)?\z/)
1174
1280
  match && Gem::Version.new(match[1])
@@ -1176,6 +1282,48 @@ module Shakapacker
1176
1282
  nil
1177
1283
  end
1178
1284
 
1285
+ def package_version_from_range_specifier(specifier)
1286
+ lower_bounds = specifier.scan(/(?:\A|\s)(?:>=|>)\s*(\d+(?:\.\d+){0,2})(?:-[0-9A-Za-z.-]+)?/)
1287
+ versions = lower_bounds.flatten.map { |version| Gem::Version.new(version) }
1288
+
1289
+ versions.max
1290
+ rescue ArgumentError
1291
+ nil
1292
+ end
1293
+
1294
+ def package_specifier_allows_version_or_newer?(specifier, minimum)
1295
+ return false unless specifier
1296
+
1297
+ cleaned = specifier.strip
1298
+ if cleaned.include?("||")
1299
+ return cleaned.split("||").any? { |part| package_specifier_allows_version_or_newer?(part, minimum) }
1300
+ end
1301
+ return false if cleaned.match?(/(:|\A(?:git|file|link|workspace|npm):)/)
1302
+
1303
+ if cleaned.match?(/[<>=]/)
1304
+ return package_range_specifier_allows_version_or_newer?(cleaned, minimum)
1305
+ end
1306
+
1307
+ version = package_version_from_specifier(cleaned)
1308
+ version && version >= minimum
1309
+ end
1310
+
1311
+ def package_range_specifier_allows_version_or_newer?(specifier, minimum)
1312
+ comparators = specifier.scan(/(?:\A|\s)(<=|<|>=|>|=)\s*(\d+(?:\.\d+){0,2})(?:-[0-9A-Za-z.-]+)?/)
1313
+ return false if comparators.empty?
1314
+
1315
+ equalities = comparators.select { |operator, _version| operator == "=" }
1316
+ return equalities.any? { |_operator, version| Gem::Version.new(version) >= minimum } if equalities.any?
1317
+
1318
+ upper_bounds = comparators.select { |operator, _version| VERSION_UPPER_BOUND_OPERATORS.include?(operator) }
1319
+ upper_bounds.all? do |operator, version|
1320
+ parsed = Gem::Version.new(version)
1321
+ operator == "<" ? parsed > minimum : parsed >= minimum
1322
+ end
1323
+ rescue ArgumentError
1324
+ false
1325
+ end
1326
+
1179
1327
  def declared_package_dependencies
1180
1328
  @declared_package_dependencies ||= begin
1181
1329
  package_json_paths.reverse_each.each_with_object({}) do |path, dependencies|
@@ -1271,13 +1419,20 @@ module Shakapacker
1271
1419
 
1272
1420
  def javascript_transpiler_configured?
1273
1421
  !javascript_transpiler_env_override.nil? ||
1274
- config_key_defined?(:javascript_transpiler) ||
1275
- config_key_defined?(:webpack_loader)
1422
+ config_key_configured?(:javascript_transpiler) ||
1423
+ config_key_present?(:webpack_loader)
1276
1424
  end
1277
1425
 
1278
1426
  def javascript_transpiler
1279
- transpiler = javascript_transpiler_env_override || config.javascript_transpiler
1280
- blank_config_value?(transpiler) ? default_javascript_transpiler : transpiler
1427
+ return @resolved_javascript_transpiler if defined?(@resolved_javascript_transpiler)
1428
+
1429
+ @resolved_javascript_transpiler =
1430
+ if javascript_transpiler_configured?
1431
+ transpiler = javascript_transpiler_env_override || config.javascript_transpiler
1432
+ blank_config_value?(transpiler) ? default_javascript_transpiler : transpiler
1433
+ else
1434
+ implicit_javascript_transpiler
1435
+ end
1281
1436
  end
1282
1437
 
1283
1438
  def explicit_javascript_transpiler
@@ -1329,7 +1484,7 @@ module Shakapacker
1329
1484
  end
1330
1485
 
1331
1486
  def blank_config_value?(value)
1332
- value.nil? || (value.respond_to?(:empty?) && value.empty?)
1487
+ value.nil? || (value.is_a?(String) && value.strip.empty?) || (value.respond_to?(:empty?) && value.empty?)
1333
1488
  end
1334
1489
 
1335
1490
  def default_javascript_transpiler
@@ -1337,14 +1492,15 @@ module Shakapacker
1337
1492
  end
1338
1493
 
1339
1494
  def config_key_present?(key)
1340
- !blank_config_value?(config_value(key))
1495
+ value = config_value(key)
1496
+ value.is_a?(String) && !value.strip.empty?
1341
1497
  end
1342
1498
 
1343
- def config_key_defined?(key)
1499
+ def config_key_configured?(key)
1344
1500
  return false unless config.respond_to?(:data)
1345
1501
 
1346
1502
  data = config.data
1347
- data.respond_to?(:key?) && data.key?(key)
1503
+ data.respond_to?(:key?) && data.key?(key) && !blank_config_value?(data[key])
1348
1504
  end
1349
1505
 
1350
1506
  def config_value(key)
@@ -1383,7 +1539,7 @@ module Shakapacker
1383
1539
  if path_within?(source_path, app_root)
1384
1540
  current = source_path
1385
1541
  loop do
1386
- break current if package_root_marker?(current)
1542
+ break current if current.join("package.json").exist?
1387
1543
  break root_path if current == app_root
1388
1544
 
1389
1545
  parent = current.dirname
@@ -1458,7 +1614,7 @@ module Shakapacker
1458
1614
  def detect_package_manager
1459
1615
  root_package_manager = package_manager_for(root_path)
1460
1616
 
1461
- package_root_paths.each do |package_root|
1617
+ package_manager_root_paths.each do |package_root|
1462
1618
  next if package_root == root_path
1463
1619
 
1464
1620
  package_manager_name = package_manager_for(package_root)
@@ -1478,6 +1634,34 @@ module Shakapacker
1478
1634
  nil
1479
1635
  end
1480
1636
 
1637
+ def package_manager_root_paths
1638
+ @package_manager_root_paths ||= [javascript_package_manager_root_path, root_path].uniq
1639
+ end
1640
+
1641
+ def javascript_package_manager_root_path
1642
+ @javascript_package_manager_root_path ||= begin
1643
+ source_path = config.source_path.expand_path
1644
+ app_root = root_path.expand_path
1645
+
1646
+ if path_within?(source_path, app_root)
1647
+ current = source_path
1648
+ loop do
1649
+ break current if package_root_marker?(current)
1650
+ break root_path if current == app_root
1651
+
1652
+ parent = current.dirname
1653
+ break root_path if parent == current
1654
+
1655
+ current = parent
1656
+ end
1657
+ else
1658
+ root_path
1659
+ end
1660
+ rescue StandardError
1661
+ root_path
1662
+ end
1663
+ end
1664
+
1481
1665
  def versions_compatible?(gem_version, npm_version)
1482
1666
  # Handle pre-release versions and ranges properly
1483
1667
  npm_clean = npm_version.gsub(/[\^~]/, "")
@@ -1,4 +1,4 @@
1
1
  module Shakapacker
2
2
  # Change the version in package.json too, please!
3
- VERSION = "10.2.0".freeze
3
+ VERSION = "10.3.1".freeze
4
4
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "10.2.0",
3
+ "version": "10.3.1",
4
4
  "description": "Use webpack to manage app-like JavaScript modules in Rails",
5
5
  "homepage": "https://github.com/shakacode/shakapacker",
6
6
  "bugs": {
@@ -127,7 +127,7 @@
127
127
  "webpack": "^5.101.0",
128
128
  "webpack-assets-manifest": "^5.0.6 || ^6.0.0",
129
129
  "webpack-cli": "^4.9.2 || ^5.0.0 || ^6.0.0 || ^7.0.0",
130
- "webpack-dev-server": "^5.2.2",
130
+ "webpack-dev-server": "^5.2.2 || ^6.0.0",
131
131
  "webpack-subresource-integrity": "^5.1.0"
132
132
  },
133
133
  "peerDependenciesMeta": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shakapacker
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.2.0
4
+ version: 10.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -225,7 +225,7 @@ homepage: https://github.com/shakacode/shakapacker
225
225
  licenses:
226
226
  - MIT
227
227
  metadata:
228
- source_code_uri: https://github.com/shakacode/shakapacker/tree/v10.2.0
228
+ source_code_uri: https://github.com/shakacode/shakapacker/tree/v10.3.1
229
229
  rdoc_options: []
230
230
  require_paths:
231
231
  - lib