shakapacker 10.1.0 → 10.3.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.
@@ -1,13 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbconfig"
4
+ require "yaml"
5
5
 
6
- # Keep helper logic in sync across:
7
- # - lib/install/bin/shakapacker-config
8
- # - lib/install/bin/diff-bundler-config
9
- # - spec/dummy/bin/shakapacker-config
10
- # - package/configExporter/cli.ts (createBinStub).
6
+ # This binstub is managed by Shakapacker. Delete it and rerun the install or init command to regenerate it.
11
7
  def shakapacker_app_root
12
8
  candidate = File.expand_path("..", __dir__)
13
9
  return candidate if File.exist?(File.join(candidate, "Gemfile"))
@@ -17,59 +13,136 @@ def shakapacker_app_root
17
13
  Dir.pwd
18
14
  end
19
15
 
20
- def shakapacker_executable_candidates(executable)
21
- extensions = [
22
- RbConfig::CONFIG["EXEEXT"],
23
- *ENV.fetch("PATHEXT", "").split(File::PATH_SEPARATOR)
24
- ].compact.reject(&:empty?)
25
- return [executable] if extensions.empty? || File.extname(executable) != ""
16
+ def shakapacker_node_binary
17
+ "node"
18
+ end
26
19
 
27
- ([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
28
24
  end
29
25
 
30
- def shakapacker_find_executable(executable)
31
- ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
32
- shakapacker_executable_candidates(executable).each do |candidate|
33
- executable_path = File.join(path, candidate)
34
- return executable_path if File.file?(executable_path) && File.executable?(executable_path)
35
- end
36
- end
26
+ def shakapacker_exec_env(launch_dir)
27
+ return {} unless ENV.key?("PATH")
37
28
 
38
- nil
29
+ path_entries = ENV["PATH"].split(File::PATH_SEPARATOR, -1)
30
+ path_entries = [""] if path_entries.empty?
31
+
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 }
39
37
  end
40
38
 
41
- def shakapacker_node_binary
42
- node_bin = shakapacker_find_executable("node")
43
- return node_bin if node_bin
39
+ def shakapacker_exec_node(script_path, argv, launch_dir)
40
+ exec_env = shakapacker_exec_env(launch_dir)
44
41
 
45
- warn '[Shakapacker] Could not find Node.js executable "node". ' \
46
- "Install Node.js and try again."
47
- 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
48
49
  end
49
50
 
50
51
  def shakapacker_node_env
51
52
  %w[development test].include?(ENV["RAILS_ENV"]) ? "development" : "production"
52
53
  end
53
54
 
55
+ def shakapacker_load_yaml_file(path)
56
+ YAML.load_file(path, aliases: true)
57
+ rescue ArgumentError
58
+ YAML.load_file(path)
59
+ end
60
+
61
+ def shakapacker_source_path(app_root)
62
+ config_path = ENV["SHAKAPACKER_CONFIG"] || File.join("config", "shakapacker.yml")
63
+ config_path = File.expand_path(config_path, app_root)
64
+ return nil unless File.file?(config_path)
65
+
66
+ config = shakapacker_load_yaml_file(config_path)
67
+ return nil unless config.is_a?(Hash)
68
+
69
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
70
+ env_config = config[env] || config["production"]
71
+ return nil unless env_config.is_a?(Hash)
72
+
73
+ source_path = env_config["source_path"]
74
+ source_path if source_path.is_a?(String)
75
+ rescue Psych::Exception, SystemCallError, ArgumentError
76
+ nil
77
+ end
78
+
79
+ def shakapacker_path_within?(path, parent)
80
+ path == parent || path.start_with?("#{parent}#{File::SEPARATOR}")
81
+ end
82
+
83
+ def shakapacker_package_root_marker?(path)
84
+ %w[
85
+ package.json
86
+ bun.lockb
87
+ pnpm-lock.yaml
88
+ yarn.lock
89
+ package-lock.json
90
+ node_modules
91
+ ].any? { |entry| File.exist?(File.join(path, entry)) }
92
+ end
93
+
94
+ def shakapacker_client_root(app_root)
95
+ source_path = shakapacker_source_path(app_root)
96
+ return app_root unless source_path && !source_path.empty?
97
+
98
+ app_root = File.expand_path(app_root)
99
+ current = File.expand_path(source_path, app_root)
100
+ return app_root unless shakapacker_path_within?(current, app_root)
101
+
102
+ loop do
103
+ return current if shakapacker_package_root_marker?(current)
104
+ break if current == app_root
105
+
106
+ parent = File.dirname(current)
107
+ break if parent == current
108
+
109
+ current = parent
110
+ end
111
+
112
+ app_root
113
+ end
114
+
115
+ def shakapacker_package_script_path(package_root, package_script)
116
+ File.join(
117
+ package_root,
118
+ "node_modules",
119
+ "shakapacker",
120
+ "package",
121
+ "bin",
122
+ package_script
123
+ )
124
+ end
125
+
126
+ def shakapacker_package_script_paths(app_root, client_root, package_script)
127
+ [client_root, app_root].uniq.map do |package_root|
128
+ shakapacker_package_script_path(package_root, package_script)
129
+ end
130
+ end
131
+
54
132
  ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
55
133
  ENV["NODE_ENV"] ||= shakapacker_node_env
56
134
 
135
+ launch_dir = Dir.pwd
57
136
  app_root = shakapacker_app_root
58
- node_bin = shakapacker_node_binary
59
- script_path = File.join(
60
- app_root,
61
- "node_modules",
62
- "shakapacker",
63
- "package",
64
- "bin",
65
- "diff-bundler-config.cjs"
66
- )
67
-
68
- unless File.file?(script_path)
69
- warn "[Shakapacker] Could not find #{script_path}. Run your package manager install command and try again."
137
+ client_root = shakapacker_client_root(app_root)
138
+ script_paths = shakapacker_package_script_paths(app_root, client_root, "diff-bundler-config.cjs")
139
+ script_path = script_paths.find { |path| File.file?(path) }
140
+
141
+ unless script_path
142
+ warn "[Shakapacker] Could not find #{script_paths.join(' or ')}. Run your package manager install command and try again."
70
143
  exit 1
71
144
  end
72
145
 
73
146
  Dir.chdir(app_root) do
74
- exec node_bin, script_path, *ARGV
147
+ shakapacker_exec_node(script_path, ARGV, launch_dir)
75
148
  end
@@ -1,13 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbconfig"
4
+ require "yaml"
5
5
 
6
- # Keep helper logic in sync across:
7
- # - lib/install/bin/shakapacker-config
8
- # - lib/install/bin/diff-bundler-config
9
- # - spec/dummy/bin/shakapacker-config
10
- # - package/configExporter/cli.ts (createBinStub).
6
+ # This binstub is managed by Shakapacker. Delete it and rerun the install or init command to regenerate it.
11
7
  def shakapacker_app_root
12
8
  candidate = File.expand_path("..", __dir__)
13
9
  return candidate if File.exist?(File.join(candidate, "Gemfile"))
@@ -17,59 +13,136 @@ def shakapacker_app_root
17
13
  Dir.pwd
18
14
  end
19
15
 
20
- def shakapacker_executable_candidates(executable)
21
- extensions = [
22
- RbConfig::CONFIG["EXEEXT"],
23
- *ENV.fetch("PATHEXT", "").split(File::PATH_SEPARATOR)
24
- ].compact.reject(&:empty?)
25
- return [executable] if extensions.empty? || File.extname(executable) != ""
16
+ def shakapacker_node_binary
17
+ "node"
18
+ end
26
19
 
27
- ([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
28
24
  end
29
25
 
30
- def shakapacker_find_executable(executable)
31
- ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
32
- shakapacker_executable_candidates(executable).each do |candidate|
33
- executable_path = File.join(path, candidate)
34
- return executable_path if File.file?(executable_path) && File.executable?(executable_path)
35
- end
36
- end
26
+ def shakapacker_exec_env(launch_dir)
27
+ return {} unless ENV.key?("PATH")
37
28
 
38
- nil
29
+ path_entries = ENV["PATH"].split(File::PATH_SEPARATOR, -1)
30
+ path_entries = [""] if path_entries.empty?
31
+
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 }
39
37
  end
40
38
 
41
- def shakapacker_node_binary
42
- node_bin = shakapacker_find_executable("node")
43
- return node_bin if node_bin
39
+ def shakapacker_exec_node(script_path, argv, launch_dir)
40
+ exec_env = shakapacker_exec_env(launch_dir)
44
41
 
45
- warn '[Shakapacker] Could not find Node.js executable "node". ' \
46
- "Install Node.js and try again."
47
- 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
48
49
  end
49
50
 
50
51
  def shakapacker_node_env
51
52
  %w[development test].include?(ENV["RAILS_ENV"]) ? "development" : "production"
52
53
  end
53
54
 
55
+ def shakapacker_load_yaml_file(path)
56
+ YAML.load_file(path, aliases: true)
57
+ rescue ArgumentError
58
+ YAML.load_file(path)
59
+ end
60
+
61
+ def shakapacker_source_path(app_root)
62
+ config_path = ENV["SHAKAPACKER_CONFIG"] || File.join("config", "shakapacker.yml")
63
+ config_path = File.expand_path(config_path, app_root)
64
+ return nil unless File.file?(config_path)
65
+
66
+ config = shakapacker_load_yaml_file(config_path)
67
+ return nil unless config.is_a?(Hash)
68
+
69
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
70
+ env_config = config[env] || config["production"]
71
+ return nil unless env_config.is_a?(Hash)
72
+
73
+ source_path = env_config["source_path"]
74
+ source_path if source_path.is_a?(String)
75
+ rescue Psych::Exception, SystemCallError, ArgumentError
76
+ nil
77
+ end
78
+
79
+ def shakapacker_path_within?(path, parent)
80
+ path == parent || path.start_with?("#{parent}#{File::SEPARATOR}")
81
+ end
82
+
83
+ def shakapacker_package_root_marker?(path)
84
+ %w[
85
+ package.json
86
+ bun.lockb
87
+ pnpm-lock.yaml
88
+ yarn.lock
89
+ package-lock.json
90
+ node_modules
91
+ ].any? { |entry| File.exist?(File.join(path, entry)) }
92
+ end
93
+
94
+ def shakapacker_client_root(app_root)
95
+ source_path = shakapacker_source_path(app_root)
96
+ return app_root unless source_path && !source_path.empty?
97
+
98
+ app_root = File.expand_path(app_root)
99
+ current = File.expand_path(source_path, app_root)
100
+ return app_root unless shakapacker_path_within?(current, app_root)
101
+
102
+ loop do
103
+ return current if shakapacker_package_root_marker?(current)
104
+ break if current == app_root
105
+
106
+ parent = File.dirname(current)
107
+ break if parent == current
108
+
109
+ current = parent
110
+ end
111
+
112
+ app_root
113
+ end
114
+
115
+ def shakapacker_package_script_path(package_root, package_script)
116
+ File.join(
117
+ package_root,
118
+ "node_modules",
119
+ "shakapacker",
120
+ "package",
121
+ "bin",
122
+ package_script
123
+ )
124
+ end
125
+
126
+ def shakapacker_package_script_paths(app_root, client_root, package_script)
127
+ [client_root, app_root].uniq.map do |package_root|
128
+ shakapacker_package_script_path(package_root, package_script)
129
+ end
130
+ end
131
+
54
132
  ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
55
133
  ENV["NODE_ENV"] ||= shakapacker_node_env
56
134
 
135
+ launch_dir = Dir.pwd
57
136
  app_root = shakapacker_app_root
58
- node_bin = shakapacker_node_binary
59
- script_path = File.join(
60
- app_root,
61
- "node_modules",
62
- "shakapacker",
63
- "package",
64
- "bin",
65
- "shakapacker-config.cjs"
66
- )
67
-
68
- unless File.file?(script_path)
69
- warn "[Shakapacker] Could not find #{script_path}. Run your package manager install command and try again."
137
+ client_root = shakapacker_client_root(app_root)
138
+ script_paths = shakapacker_package_script_paths(app_root, client_root, "shakapacker-config.cjs")
139
+ script_path = script_paths.find { |path| File.file?(path) }
140
+
141
+ unless script_path
142
+ warn "[Shakapacker] Could not find #{script_paths.join(' or ')}. Run your package manager install command and try again."
70
143
  exit 1
71
144
  end
72
145
 
73
146
  Dir.chdir(app_root) do
74
- exec node_bin, script_path, *ARGV
147
+ shakapacker_exec_node(script_path, ARGV, launch_dir)
75
148
  end
@@ -35,6 +35,12 @@ default: &default
35
35
  public_output_path: packs
36
36
  cache_path: tmp/shakapacker
37
37
  webpack_compile_output: true
38
+
39
+ # Extra command-line flags passed to webpack/rspack when compiling.
40
+ # Do not include "--", Shakapacker wrapper flags, help/version flags, watch flags/forms, or managed flags.
41
+ # Example: ["--progress", "--fail-on-warnings"]
42
+ webpack_compile_flags: []
43
+
38
44
  # See https://github.com/shakacode/shakapacker#deployment
39
45
  shakapacker_precompile: true
40
46
 
@@ -53,13 +59,15 @@ default: &default
53
59
  cache_manifest: false
54
60
 
55
61
  # Select JavaScript transpiler to use
56
- # 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.
57
65
  # Use 'none' when providing a completely custom webpack configuration
58
66
  # Note: When using rspack, swc is used automatically regardless of this setting
59
67
  javascript_transpiler: "swc"
60
68
 
61
69
  # Select assets bundler to use
62
- # Available options: 'webpack' (default) or 'rspack'
70
+ # Available options: 'webpack' or 'rspack'
63
71
  assets_bundler: "webpack"
64
72
 
65
73
  # Path to the directory containing webpack/rspack config files
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "rspack": {
3
- "@rspack/cli": "^1.0.0 || ^2.0.0-0",
4
- "@rspack/core": "^1.0.0 || ^2.0.0-0",
5
- "rspack-manifest-plugin": "^5.0.0"
3
+ "@rspack/cli": "^2.0.0",
4
+ "@rspack/core": "^2.0.0",
5
+ "@rspack/dev-server": "^2.0.0",
6
+ "rspack-manifest-plugin": "^5.2.2"
6
7
  },
7
8
  "webpack": {
8
9
  "mini-css-extract-plugin": "^2.0.0",
@@ -10,13 +11,14 @@
10
11
  "webpack": "^5.101.0",
11
12
  "webpack-assets-manifest": "^5.0.6 || ^6.0.0",
12
13
  "webpack-cli": "^4.9.2 || ^5.0.0 || ^6.0.0 || ^7.0.0",
13
- "webpack-dev-server": "^5.2.2",
14
+ "webpack-dev-server": "^5.2.2 || ^6.0.0",
14
15
  "webpack-merge": "^5.8.0 || ^6.0.0",
15
16
  "webpack-subresource-integrity": "^5.1.0"
16
17
  },
17
18
  "common": {
18
19
  "compression-webpack-plugin": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
19
20
  "css-loader": "^6.0.0 || ^7.0.0",
21
+ "sass": "^1.50.0",
20
22
  "sass-loader": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
21
23
  "style-loader": "^3.0.0 || ^4.0.0"
22
24
  },
@@ -15,6 +15,31 @@ require "json"
15
15
  @package_json ||= PackageJson.new
16
16
  install_dir = File.expand_path(File.dirname(__FILE__))
17
17
 
18
+ # Read the existing app's bundler (if any) before copy_file can overwrite it, so a
19
+ # re-install can default to that bundler instead of silently switching it.
20
+ config_path = Rails.root.join("config/shakapacker.yml")
21
+ shakapacker_config_preexisting = config_path.exist?
22
+ existing_assets_bundler =
23
+ File.read(config_path)[/assets_bundler:\s*"([^"]+)"/, 1] if shakapacker_config_preexisting
24
+
25
+ # New installs default to rspack; an existing app keeps its current bundler on a
26
+ # re-install unless overridden by the env var/argument or a FORCE overwrite (see
27
+ # Env.resolve_assets_bundler for the precedence rules). The bundled shakapacker.yml
28
+ # ships "webpack" for backward compatibility and is rewritten below when the chosen
29
+ # bundler differs.
30
+ assets_bundler = Shakapacker::Install::Env.resolve_assets_bundler(
31
+ env_value: ENV["SHAKAPACKER_ASSETS_BUNDLER"],
32
+ existing_bundler: existing_assets_bundler,
33
+ force: @conflict_option[:force]
34
+ )
35
+
36
+ # Fail fast on a misspelled SHAKAPACKER_ASSETS_BUNDLER instead of failing later
37
+ # with a confusing missing-config-directory or peer-lookup error.
38
+ unless Shakapacker::Install::Env::VALID_BUNDLERS.include?(assets_bundler)
39
+ say "❌ Unknown bundler '#{assets_bundler}'. Set SHAKAPACKER_ASSETS_BUNDLER to one of: #{Shakapacker::Install::Env::VALID_BUNDLERS.join(", ")}.", :red
40
+ exit 1
41
+ end
42
+
18
43
  # Installation strategy:
19
44
  # - USE_BABEL_PACKAGES installs both babel AND swc for compatibility
20
45
  # - Otherwise install only the specified transpiler
@@ -35,7 +60,6 @@ else
35
60
  end
36
61
 
37
62
  # Copy config file
38
- shakapacker_config_preexisting = Rails.root.join("config/shakapacker.yml").exist?
39
63
  copy_file "#{install_dir}/config/shakapacker.yml", "config/shakapacker.yml", @conflict_option
40
64
 
41
65
  # Update config to match the selected transpiler
@@ -46,14 +70,59 @@ if Shakapacker::Install::Env.update_transpiler_config?(
46
70
  config_preexisting: shakapacker_config_preexisting
47
71
  )
48
72
  gsub_file "config/shakapacker.yml", 'javascript_transpiler: "swc"', "javascript_transpiler: \"#{@transpiler_to_install}\""
49
- say " 📝 Updated config/shakapacker.yml to use #{@transpiler_to_install} transpiler", :green
73
+ # Unlike the bundler rewrite below (which only runs on installer-owned files and
74
+ # aborts on a miss), this can also run against a pre-existing user config whose
75
+ # transpiler line may legitimately differ from the shipped "swc" literal. A no-op
76
+ # there is not an error, so only claim success when the value actually landed.
77
+ if File.read(config_path).include?("javascript_transpiler: \"#{@transpiler_to_install}\"")
78
+ say " 📝 Updated config/shakapacker.yml to use #{@transpiler_to_install} transpiler", :green
79
+ end
80
+ end
81
+
82
+ # Update config to match the selected bundler (see update_assets_bundler_config?
83
+ # for when the installer rewrites the shipped "webpack" default vs. preserves an
84
+ # existing config).
85
+ if Shakapacker::Install::Env.update_assets_bundler_config?(
86
+ assets_bundler_to_install: assets_bundler,
87
+ conflict_option: @conflict_option,
88
+ config_preexisting: shakapacker_config_preexisting
89
+ )
90
+ gsub_file "config/shakapacker.yml", 'assets_bundler: "webpack"', "assets_bundler: \"#{assets_bundler}\""
91
+ # gsub_file silently no-ops if the shipped literal is ever reformatted, so verify
92
+ # the value landed. Abort rather than continue, since the installer is about to set
93
+ # up the chosen bundler's dependencies and config, and a mismatched bundler value
94
+ # would produce a broken install. This runs before any dependencies are installed.
95
+ if File.read(config_path).include?("assets_bundler: \"#{assets_bundler}\"")
96
+ say " 📝 Updated config/shakapacker.yml to use #{assets_bundler} bundler", :green
97
+ else
98
+ say "❌ Could not set assets_bundler to \"#{assets_bundler}\" in config/shakapacker.yml " \
99
+ "— the expected 'assets_bundler: \"webpack\"' line was not found. Aborting so the " \
100
+ "install doesn't proceed with a mismatched bundler config.", :red
101
+ exit 1
102
+ end
103
+ else
104
+ # The bundler config was left as-is (an existing app's config is preserved unless
105
+ # FORCE overwrites it). Report the value present, and warn if it differs from the
106
+ # bundler whose dependencies/config are being installed — usually when a bundler is
107
+ # requested explicitly (env var or task argument) against a preserved config, but
108
+ # also when a preserved config holds an unrecognized bundler and resolve_assets_bundler
109
+ # falls back to rspack. Either case would otherwise be a silent mismatch. Only act
110
+ # when we can read the value back — never guess, or the message could contradict the file.
111
+ retained_bundler = File.read(config_path)[/assets_bundler:\s*"([^"]+)"/, 1]
112
+ if retained_bundler && retained_bundler != assets_bundler
113
+ say "⚠️ Installing #{assets_bundler} dependencies, but config/shakapacker.yml keeps " \
114
+ "assets_bundler: \"#{retained_bundler}\". To switch an existing app's bundler, run " \
115
+ "`bin/rake shakapacker:switch_bundler #{assets_bundler} -- --install-deps`, " \
116
+ "or re-run the installer with FORCE=true to overwrite the config.", :yellow
117
+ elsif retained_bundler
118
+ say " 📝 Keeping assets_bundler: \"#{retained_bundler}\" in config/shakapacker.yml", :green
119
+ end
50
120
  end
51
121
 
52
122
  # Detect TypeScript usage
53
123
  # Auto-detect from tsconfig.json or explicit via SHAKAPACKER_USE_TYPESCRIPT env var
54
124
  @use_typescript = File.exist?(Rails.root.join("tsconfig.json")) ||
55
125
  Shakapacker::Install::Env.truthy_env?("SHAKAPACKER_USE_TYPESCRIPT")
56
- assets_bundler = ENV["SHAKAPACKER_ASSETS_BUNDLER"] || "webpack"
57
126
  config_extension = @use_typescript ? "ts" : "js"
58
127
 
59
128
  say "Copying #{assets_bundler} core config (#{config_extension.upcase})"
@@ -224,9 +293,12 @@ Dir.chdir(Rails.root) do
224
293
  end
225
294
 
226
295
  # Inline fetch_peer_dependencies and fetch_common_dependencies
227
- peers = PackageJson.read(install_dir).fetch(ENV["SHAKAPACKER_ASSETS_BUNDLER"] || "webpack")
296
+ peers = PackageJson.read(install_dir).fetch(assets_bundler)
228
297
  common_deps = Shakapacker::Install::Env.truthy_env?("SKIP_COMMON_LOADERS") ? {} : PackageJson.read(install_dir).fetch("common")
229
- peers = peers.merge(common_deps)
298
+ peers = common_deps.merge(peers)
299
+ if assets_bundler == "rspack" && common_deps.key?("css-loader")
300
+ peers["css-loader"] = "^7.1.4"
301
+ end
230
302
 
231
303
  # Add transpiler-specific dependencies based on detected/configured transpiler
232
304
  # Inline the logic here since methods can't be called before they're defined in Rails templates
@@ -255,7 +327,9 @@ Dir.chdir(Rails.root) do
255
327
  peers = peers.merge(esbuild_deps)
256
328
  end
257
329
 
258
- dev_dependency_packages = ["webpack-dev-server"]
330
+ # Lists both dev servers for classification only; just the one matching the chosen
331
+ # bundler appears in `peers`, so only that package is actually installed.
332
+ dev_dependency_packages = ["webpack-dev-server", "@rspack/dev-server"]
259
333
 
260
334
  dependencies_to_add = []
261
335
  dev_dependencies_to_add = []
@@ -290,12 +364,17 @@ Dir.chdir(Rails.root) do
290
364
  exit 1
291
365
  end
292
366
 
293
- say "Installing webpack-dev-server for live reloading as a development dependency"
294
- begin
295
- @package_json.manager.add!(dev_dependencies_to_add, type: :dev)
296
- rescue PackageJson::Error
297
- say "Shakapacker installation failed 😭 See above for details.", :red
298
- exit 1
367
+ if dev_dependencies_to_add.any?
368
+ # Strip the trailing @version, keeping the package name; the regex drops only
369
+ # the last @-segment so scoped names (e.g. @rspack/dev-server) survive.
370
+ dev_dependency_names = dev_dependencies_to_add.map { |entry| entry.sub(/@[^@]+\z/, "") }
371
+ say "Installing development dependencies: #{dev_dependency_names.join(", ")}"
372
+ begin
373
+ @package_json.manager.add!(dev_dependencies_to_add, type: :dev)
374
+ rescue PackageJson::Error
375
+ say "Shakapacker installation failed 😭 See above for details.", :red
376
+ exit 1
377
+ end
299
378
  end
300
379
 
301
380
  # Configure babel preset in package.json if babel is being used
@@ -1,7 +1,7 @@
1
1
  module Shakapacker
2
2
  class BaseStrategy
3
- def initialize
4
- @config = Shakapacker.config
3
+ def initialize(instance = Shakapacker.instance)
4
+ @instance = instance
5
5
  end
6
6
 
7
7
  def after_compile_hook
@@ -10,7 +10,13 @@ module Shakapacker
10
10
 
11
11
  private
12
12
 
13
- attr_reader :config
13
+ def config
14
+ @instance.config
15
+ end
16
+
17
+ def env
18
+ @instance.env
19
+ end
14
20
 
15
21
  def default_watched_paths
16
22
  [
@@ -18,7 +24,7 @@ module Shakapacker
18
24
  "#{config.source_path}{,/**/*}",
19
25
  "package.json", "package-lock.json", "yarn.lock",
20
26
  "pnpm-lock.yaml", "bun.lockb",
21
- "config/webpack{,/**/*}"
27
+ "config/{webpack,rspack}{,/**/*}"
22
28
  ].freeze
23
29
  end
24
30
  end