shakapacker 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,7 +112,7 @@ module Shakapacker::Helper
112
112
  def javascript_pack_tag(*names, defer: true, async: false, early_hints: nil, **options)
113
113
  if @javascript_pack_tag_loaded
114
114
  raise "To prevent duplicated chunks on the page, you should call javascript_pack_tag only once on the page. " \
115
- "Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helpers-javascript_pack_tag-and-stylesheet_pack_tag for the usage guide"
115
+ "Please refer to https://github.com/shakacode/shakapacker/blob/main/docs/api-reference.md#view-helpers-javascript_pack_tag-and-stylesheet_pack_tag for the usage guide"
116
116
  end
117
117
 
118
118
  # Collect all packs (queue + direct args)
@@ -330,7 +330,7 @@ module Shakapacker::Helper
330
330
  def append_stylesheet_pack_tag(*names)
331
331
  if @stylesheet_pack_tag_loaded
332
332
  raise "You can only call append_stylesheet_pack_tag before stylesheet_pack_tag helper. " \
333
- "Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helper-append_javascript_pack_tag-prepend_javascript_pack_tag-and-append_stylesheet_pack_tag for the usage guide"
333
+ "Please refer to https://github.com/shakacode/shakapacker/blob/main/docs/api-reference.md#view-helper-append_javascript_pack_tag-prepend_javascript_pack_tag-and-append_stylesheet_pack_tag for the usage guide"
334
334
  end
335
335
 
336
336
  @stylesheet_pack_tag_queue ||= []
@@ -357,7 +357,7 @@ module Shakapacker::Helper
357
357
  def update_javascript_pack_tag_queue(defer:, async:)
358
358
  if @javascript_pack_tag_loaded
359
359
  raise "You can only call #{caller_locations(1..1).first.base_label} before javascript_pack_tag helper. " \
360
- "Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helper-append_javascript_pack_tag-prepend_javascript_pack_tag-and-append_stylesheet_pack_tag for the usage guide"
360
+ "Please refer to https://github.com/shakacode/shakapacker/blob/main/docs/api-reference.md#view-helper-append_javascript_pack_tag-prepend_javascript_pack_tag-and-append_stylesheet_pack_tag for the usage guide"
361
361
  end
362
362
 
363
363
  # When both async and defer are specified, async takes precedence per HTML5 spec
@@ -2,6 +2,7 @@ module Shakapacker
2
2
  module Install
3
3
  module Env
4
4
  TRUTHY_VALUES = %w[true 1 yes].freeze
5
+ VALID_BUNDLERS = %w[webpack rspack].freeze
5
6
 
6
7
  module_function
7
8
 
@@ -28,6 +29,55 @@ module Shakapacker
28
29
 
29
30
  !config_preexisting
30
31
  end
32
+
33
+ # Keep bundled runtime defaults backward compatible while allowing the
34
+ # installer to write an explicit new-project bundler choice. Takes the same
35
+ # inputs as update_transpiler_config? for consistency, but is deliberately
36
+ # more conservative: switching an existing app's bundler is impactful, so we
37
+ # only rewrite a config the installer owns (a fresh install, or an explicit
38
+ # FORCE overwrite) and never a pre-existing one (interactive or SKIP mode).
39
+ def update_assets_bundler_config?(assets_bundler_to_install:, conflict_option:, config_preexisting:)
40
+ # The bundled shakapacker.yml already ships assets_bundler: "webpack", so
41
+ # rewriting it to "webpack" would be a no-op. This relies on that shipped default.
42
+ return false if assets_bundler_to_install == "webpack"
43
+ return true if conflict_option[:force]
44
+
45
+ !config_preexisting
46
+ end
47
+
48
+ # Resolve which bundler the installer should set up. Precedence: an explicit
49
+ # SHAKAPACKER_ASSETS_BUNDLER env var (or task argument, which sets that env var)
50
+ # always wins; otherwise a FORCE overwrite installs the new-project default;
51
+ # otherwise an existing app keeps its current bundler (when it is a recognized
52
+ # value) so a re-install never silently switches it; brand-new installs fall
53
+ # back to rspack. Returning the env var verbatim lets the caller's strict
54
+ # VALID_BUNDLERS check still reject a misspelled value.
55
+ def resolve_assets_bundler(env_value:, existing_bundler:, force:)
56
+ return env_value if env_value
57
+ return "rspack" if force
58
+ return existing_bundler if VALID_BUNDLERS.include?(existing_bundler)
59
+
60
+ "rspack"
61
+ end
62
+
63
+ # Apply an optional `shakapacker:install[bundler]` task argument. A
64
+ # recognized bundler (webpack or rspack) is written to
65
+ # SHAKAPACKER_ASSETS_BUNDLER so the install template picks it up,
66
+ # overriding any value already set in that env var (the explicit argument
67
+ # wins). An unrecognized value returns an error message (and leaves the
68
+ # env var unset) so the caller can abort, mirroring the template's strict
69
+ # validation of SHAKAPACKER_ASSETS_BUNDLER. Returns nil when the argument
70
+ # is valid or absent.
71
+ def apply_bundler_arg(bundler_arg)
72
+ return nil if bundler_arg.nil? || bundler_arg.empty?
73
+
74
+ if VALID_BUNDLERS.include?(bundler_arg)
75
+ ENV["SHAKAPACKER_ASSETS_BUNDLER"] = bundler_arg
76
+ nil
77
+ else
78
+ "Unknown bundler '#{bundler_arg}'. Valid values: #{VALID_BUNDLERS.join(", ")}."
79
+ end
80
+ end
31
81
  end
32
82
  end
33
83
  end
@@ -81,7 +81,7 @@ class Shakapacker::Instance
81
81
  # @return [Shakapacker::CompilerStrategy] the compiler strategy
82
82
  # @api private
83
83
  def strategy
84
- @strategy ||= Shakapacker::CompilerStrategy.from_config
84
+ @strategy ||= Shakapacker::CompilerStrategy.from_config(self)
85
85
  end
86
86
 
87
87
  # Returns the compiler for this instance
@@ -19,7 +19,7 @@ module Shakapacker
19
19
  end
20
20
 
21
21
  def latest_modified_timestamp
22
- if Rails.env.development?
22
+ if env.development?
23
23
  warn <<~MSG.strip
24
24
  Shakapacker::Compiler - Slow setup for development
25
25
 
@@ -26,6 +26,23 @@ module Shakapacker
26
26
  "i"
27
27
  ].freeze
28
28
 
29
+ SHAKAPACKER_NODE_FLAGS = Configuration::SHAKAPACKER_NODE_FLAGS
30
+ private_constant :SHAKAPACKER_NODE_FLAGS
31
+
32
+ RSPACK_RUNNER_EXTENSION = Module.new do
33
+ def build_cmd
34
+ package_json.manager.native_exec_command("rspack")
35
+ end
36
+ end
37
+ private_constant :RSPACK_RUNNER_EXTENSION
38
+
39
+ WEBPACK_RUNNER_EXTENSION = Module.new do
40
+ def build_cmd
41
+ package_json.manager.native_exec_command("webpack")
42
+ end
43
+ end
44
+ private_constant :WEBPACK_RUNNER_EXTENSION
45
+
29
46
  def self.json_output?(argv)
30
47
  argv.include?("--json") || argv.include?("-j")
31
48
  end
@@ -34,31 +51,39 @@ module Shakapacker
34
51
  json_output?(argv) ? $stderr : $stdout
35
52
  end
36
53
 
54
+ def self.split_passthrough_argv(argv)
55
+ separator_index = argv.index("--")
56
+ return [argv.dup, []] unless separator_index
57
+
58
+ [argv[0...separator_index], argv[(separator_index + 1)..]]
59
+ end
60
+
37
61
  def self.run(argv)
38
62
  $stdout.sync = true
63
+ runner_argv, passthrough_argv = split_passthrough_argv(argv)
39
64
 
40
65
  # Show Shakapacker help and exit (don't call bundler)
41
66
  # Support --help, -h, and --help=verbose formats
42
- help_verbose = argv.any? { |arg| arg == "--help=verbose" }
43
- if argv.include?("--help") || argv.include?("-h") || help_verbose
67
+ help_verbose = runner_argv.any? { |arg| arg == "--help=verbose" }
68
+ if runner_argv.include?("--help") || runner_argv.include?("-h") || help_verbose
44
69
  print_help(verbose: help_verbose)
45
70
  exit(0)
46
- elsif argv.include?("--version") || argv.include?("-v")
71
+ elsif runner_argv.include?("--version") || runner_argv.include?("-v")
47
72
  print_version
48
73
  exit(0)
49
- elsif argv.include?("--init")
74
+ elsif runner_argv.include?("--init")
50
75
  init_config_file
51
76
  exit(0)
52
- elsif argv.include?("--list-builds")
77
+ elsif runner_argv.include?("--list-builds")
53
78
  list_builds
54
79
  exit(0)
55
80
  end
56
81
 
57
82
  # Check for --bundler flag
58
83
  bundler_override = nil
59
- bundler_index = argv.index("--bundler")
84
+ bundler_index = runner_argv.index("--bundler")
60
85
  if bundler_index
61
- bundler_value = argv[bundler_index + 1]
86
+ bundler_value = runner_argv[bundler_index + 1]
62
87
  unless bundler_value && %w[webpack rspack].include?(bundler_value)
63
88
  $stderr.puts "[Shakapacker] Error: --bundler requires 'webpack' or 'rspack'"
64
89
  $stderr.puts "Usage: bin/shakapacker --bundler <webpack|rspack>"
@@ -68,9 +93,9 @@ module Shakapacker
68
93
  end
69
94
 
70
95
  # Check for --build flag
71
- build_index = argv.index("--build")
96
+ build_index = runner_argv.index("--build")
72
97
  if build_index
73
- build_name = argv[build_index + 1]
98
+ build_name = runner_argv[build_index + 1]
74
99
 
75
100
  unless build_name
76
101
  $stderr.puts "[Shakapacker] Error: --build requires a build name"
@@ -93,7 +118,7 @@ module Shakapacker
93
118
  build_config = loader.resolve_build_config(build_name, **resolve_opts)
94
119
 
95
120
  # Remove --build and build name from argv
96
- remaining_argv = argv.dup
121
+ remaining_argv = runner_argv.dup
97
122
  remaining_argv.delete_at(build_index + 1)
98
123
  remaining_argv.delete_at(build_index)
99
124
 
@@ -108,17 +133,18 @@ module Shakapacker
108
133
 
109
134
  # If this build uses dev server, delegate to DevServerRunner
110
135
  if loader.uses_dev_server?(build_config)
111
- log = log_output_for(argv)
136
+ # Include passthrough --json so Shakapacker logs stay off stdout when bundler JSON is requested.
137
+ log = log_output_for(runner_argv + passthrough_argv)
112
138
  log.puts "[Shakapacker] Build '#{build_name}' requires dev server"
113
139
  log.puts "[Shakapacker] Running: bin/shakapacker-dev-server --build #{build_name}"
114
140
  log.puts ""
115
141
  require_relative "dev_server_runner"
116
- DevServerRunner.run_with_build_config(remaining_argv, build_config)
142
+ DevServerRunner.run_with_build_config(remaining_argv, build_config, passthrough_argv)
117
143
  return
118
144
  end
119
145
 
120
146
  # Otherwise run with this build config
121
- run_with_build_config(remaining_argv, build_config)
147
+ run_with_build_config(remaining_argv, build_config, passthrough_argv)
122
148
  return
123
149
  rescue ArgumentError => e
124
150
  $stderr.puts "[Shakapacker] #{e.message}"
@@ -129,7 +155,7 @@ module Shakapacker
129
155
  Shakapacker.ensure_node_env!
130
156
 
131
157
  # Remove --bundler flag from argv if present (not using --build)
132
- remaining_argv = argv.dup
158
+ remaining_argv = runner_argv.dup
133
159
  if bundler_index
134
160
  bundler_idx = remaining_argv.index("--bundler")
135
161
  if bundler_idx
@@ -144,39 +170,26 @@ module Shakapacker
144
170
 
145
171
  # Create a single runner instance to avoid loading configuration twice.
146
172
  # We extend it with the appropriate build command based on the bundler type.
147
- runner = new(remaining_argv, nil, bundler_override)
173
+ runner = new(remaining_argv, nil, bundler_override, passthrough_argv)
148
174
 
149
175
  # Determine which bundler to use (override takes precedence)
150
176
  use_rspack = bundler_override ? (bundler_override == "rspack") : runner.config.rspack?
151
177
 
152
178
  if use_rspack
153
179
  require_relative "rspack_runner"
154
- # Extend the runner instance with rspack-specific methods
155
- # This avoids creating a new RspackRunner which would reload the configuration
156
- runner.extend(Module.new do
157
- def build_cmd
158
- package_json.manager.native_exec_command("rspack")
159
- end
160
-
161
- def assets_bundler_commands
162
- BASE_COMMANDS + %w[build watch]
163
- end
164
- end)
180
+ # Extend the runner instance to avoid creating a new RspackRunner and reloading configuration.
181
+ runner.extend(RSPACK_RUNNER_EXTENSION)
165
182
  runner.run
166
183
  else
167
184
  require_relative "webpack_runner"
168
185
  # Extend the runner instance with webpack-specific methods
169
186
  # This avoids creating a new WebpackRunner which would reload the configuration
170
- runner.extend(Module.new do
171
- def build_cmd
172
- package_json.manager.native_exec_command("webpack")
173
- end
174
- end)
187
+ runner.extend(WEBPACK_RUNNER_EXTENSION)
175
188
  runner.run
176
189
  end
177
190
  end
178
191
 
179
- def self.run_with_build_config(argv, build_config)
192
+ def self.run_with_build_config(argv, build_config, passthrough_argv = [])
180
193
  $stdout.sync = true
181
194
  Shakapacker.ensure_node_env!
182
195
 
@@ -189,7 +202,8 @@ module Shakapacker
189
202
  # This ensures the bundler override (from --bundler or build config) is respected
190
203
  ENV["SHAKAPACKER_ASSETS_BUNDLER"] = build_config[:bundler]
191
204
 
192
- log = log_output_for(argv)
205
+ # Include passthrough --json so Shakapacker logs stay off stdout when bundler JSON is requested.
206
+ log = log_output_for(argv + passthrough_argv)
193
207
  log.puts "[Shakapacker] Running build: #{build_config[:name]}"
194
208
  log.puts "[Shakapacker] Description: #{build_config[:description]}" if build_config[:description]
195
209
  log.puts "[Shakapacker] Bundler: #{build_config[:bundler]}"
@@ -197,37 +211,31 @@ module Shakapacker
197
211
 
198
212
  # Create runner with modified argv and bundler from build_config
199
213
  # The build_config[:bundler] already has any CLI --bundler override applied
200
- runner = new(argv, build_config, build_config[:bundler])
214
+ runner = new(argv, build_config, build_config[:bundler], passthrough_argv)
201
215
 
202
216
  # Use bundler from build_config (which includes CLI override)
203
217
  if build_config[:bundler] == "rspack"
204
218
  require_relative "rspack_runner"
205
- runner.extend(Module.new do
206
- def build_cmd
207
- package_json.manager.native_exec_command("rspack")
208
- end
209
-
210
- def assets_bundler_commands
211
- BASE_COMMANDS + %w[build watch]
212
- end
213
- end)
219
+ runner.extend(RSPACK_RUNNER_EXTENSION)
214
220
  runner.run
215
221
  else
216
222
  require_relative "webpack_runner"
217
- runner.extend(Module.new do
218
- def build_cmd
219
- package_json.manager.native_exec_command("webpack")
220
- end
221
- end)
223
+ runner.extend(WEBPACK_RUNNER_EXTENSION)
222
224
  runner.run
223
225
  end
224
226
  end
225
227
 
226
- def initialize(argv, build_config = nil, bundler_override = nil)
227
- @argv = argv
228
+ def initialize(argv, build_config = nil, bundler_override = nil, passthrough_argv = nil)
229
+ @argv, @passthrough_argv =
230
+ unless passthrough_argv.nil?
231
+ [argv, passthrough_argv]
232
+ else
233
+ self.class.split_passthrough_argv(argv)
234
+ end
228
235
  @build_config = build_config
229
236
  @bundler_override = bundler_override
230
- @json_output = self.class.json_output?(argv)
237
+ # Bundler --json writes machine-readable output to stdout; route Shakapacker logs to stderr too.
238
+ @json_output = self.class.json_output?(bundler_argv)
231
239
 
232
240
  @app_path = File.expand_path(".", Dir.pwd)
233
241
  @shakapacker_config = ENV["SHAKAPACKER_CONFIG"] || File.join(@app_path, "config/shakapacker.yml")
@@ -260,6 +268,9 @@ module Shakapacker
260
268
  cmd = build_cmd
261
269
  log_output.puts "[Shakapacker] Base command: #{cmd.join(" ")}"
262
270
 
271
+ detect_shakapacker_flags_in_passthrough!
272
+
273
+ # Shakapacker-owned Node flags must appear before --; passthrough args belong to the bundler.
263
274
  if @argv.delete("--debug-shakapacker")
264
275
  log_output.puts "[Shakapacker] Debug mode enabled (--debug-shakapacker)"
265
276
  env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --inspect-brk"
@@ -276,18 +287,19 @@ module Shakapacker
276
287
  end
277
288
 
278
289
  # Commands are not compatible with --config option.
279
- if (@argv & assets_bundler_commands).empty?
290
+ incompatible_args = config_incompatible_args
291
+ if incompatible_args.empty?
280
292
  log_output.puts "[Shakapacker] Adding config file: #{@webpack_config}"
281
293
  cmd += ["--config", @webpack_config]
282
294
  else
283
- log_output.puts "[Shakapacker] Skipping config file (running assets bundler command: #{(@argv & assets_bundler_commands).join(", ")})"
295
+ log_output.puts "[Shakapacker] Skipping config file (running assets bundler command: #{incompatible_args.join(", ")})"
284
296
  end
285
297
 
286
- cmd += @argv
298
+ cmd += bundler_argv
287
299
  log_output.puts "[Shakapacker] Final command: #{cmd.join(" ")}"
288
300
  log_output.puts "[Shakapacker] Working directory: #{@app_path}"
289
301
 
290
- watch_mode = @argv.include?("--watch") || @argv.include?("-w")
302
+ watch_mode = bundler_argv.include?("--watch") || bundler_argv.include?("-w")
291
303
  start_time = Time.now unless watch_mode
292
304
 
293
305
  Dir.chdir(@app_path) do
@@ -319,10 +331,33 @@ module Shakapacker
319
331
 
320
332
  protected
321
333
 
334
+ def bundler_argv
335
+ @argv + @passthrough_argv
336
+ end
337
+
322
338
  def assets_bundler_commands
323
339
  BASE_COMMANDS
324
340
  end
325
341
 
342
+ def config_incompatible_args
343
+ bundler_argv & assets_bundler_commands
344
+ end
345
+
346
+ def detect_shakapacker_flags_in_passthrough!(flags = SHAKAPACKER_NODE_FLAGS)
347
+ misplaced_flags = @passthrough_argv & flags
348
+ return if misplaced_flags.empty?
349
+
350
+ log_output.puts(
351
+ "The following Shakapacker-specific options must appear before --: #{misplaced_flags.join(' ')}."
352
+ )
353
+ exit_after_shakapacker_flag_error(1)
354
+ end
355
+
356
+ def exit_after_shakapacker_flag_error(status)
357
+ log_output.flush
358
+ exit(status)
359
+ end
360
+
326
361
  def print_config_not_found_error(bundler_type, config_path, config_dir)
327
362
  $stderr.puts "[Shakapacker] ERROR: #{bundler_type} config #{config_path} not found."
328
363
  $stderr.puts ""
@@ -354,6 +389,9 @@ module Shakapacker
354
389
  --bundler <webpack|rspack>
355
390
  Override bundler (defaults to shakapacker.yml)
356
391
 
392
+ Put Shakapacker-specific options before --. Arguments after -- are
393
+ passed directly to the selected bundler.
394
+
357
395
  Build configurations (config/shakapacker-builds.yml):
358
396
  --init Create config/shakapacker-builds.yml
359
397
  --list-builds List available builds
@@ -2,12 +2,18 @@
2
2
 
3
3
  require "English"
4
4
  require "rake/file_utils"
5
+ require "shellwords"
5
6
 
6
7
  module Shakapacker
7
8
  module Utils
8
9
  class Misc
9
10
  extend FileUtils
10
11
 
12
+ NODE_BINSTUB_EXECUTABLES = %w[node nodejs].freeze
13
+ ENV_FLAGS_WITH_ARGUMENTS = %w[-u --unset -C --chdir -P --path -a --argv0].freeze
14
+ private_constant :NODE_BINSTUB_EXECUTABLES
15
+ private_constant :ENV_FLAGS_WITH_ARGUMENTS
16
+
11
17
  def self.uncommitted_changes?(message_handler)
12
18
  return false if ENV["COVERAGE"] == "true"
13
19
 
@@ -31,6 +37,38 @@ module Shakapacker
31
37
  def self.sh_in_dir(dir, *shell_commands)
32
38
  shell_commands.flatten.each { |shell_command| sh %(cd '#{dir}' && #{shell_command.strip}) }
33
39
  end
40
+
41
+ def self.js_binstub_executable(path)
42
+ return nil unless File.file?(path)
43
+
44
+ shebang = File.open(path, "rb") { |f| f.gets }.to_s.chomp
45
+ return nil unless shebang.start_with?("#!")
46
+
47
+ shebang_tokens = begin
48
+ Shellwords.split(shebang.delete_prefix("#!"))
49
+ rescue ArgumentError
50
+ return nil
51
+ end
52
+ executable = shebang_tokens.first.to_s
53
+
54
+ if File.basename(executable) == "env"
55
+ shebang_tokens = shebang_tokens.drop(1)
56
+ while (env_token = shebang_tokens.first)
57
+ if env_token.start_with?("-")
58
+ env_flag = shebang_tokens.shift
59
+ shebang_tokens.shift if ENV_FLAGS_WITH_ARGUMENTS.include?(env_flag)
60
+ elsif env_token.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/)
61
+ shebang_tokens.shift
62
+ else
63
+ break
64
+ end
65
+ end
66
+ executable = shebang_tokens.first.to_s
67
+ end
68
+
69
+ # Preserve direct interpreter paths so stale absolute Node shebangs fail with actionable binstub guidance.
70
+ NODE_BINSTUB_EXECUTABLES.include?(File.basename(executable)) ? executable : nil
71
+ end
34
72
  end
35
73
  end
36
74
  end
@@ -1,4 +1,4 @@
1
1
  module Shakapacker
2
2
  # Change the version in package.json too, please!
3
- VERSION = "10.1.0".freeze
3
+ VERSION = "10.2.0".freeze
4
4
  end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require "yaml"
3
+
2
4
  require_relative "version"
3
5
 
4
6
  module Shakapacker
@@ -224,13 +226,19 @@ module Shakapacker
224
226
 
225
227
  def from_pnpm_lock
226
228
  require "yaml"
229
+ require "date"
230
+
231
+ # pnpm >= 10.16 writes a `time:` section; permit Time/Date so Psych 4+ doesn't raise DisallowedClass.
232
+ content = safe_load_pnpm_lock(File.read(@pnpm_lock))
227
233
 
228
- content = YAML.load_file(@pnpm_lock)
234
+ packages = content.is_a?(Hash) ? (content["packages"] || {}) : {}
229
235
 
230
- content.fetch("packages", {}).each do |key, value|
236
+ packages.each do |key, value|
231
237
  # git-based constraints will include a "version" key with their pseudo semantic version
232
- return value["version"] if key.start_with?("shakapacker") && value.key?("version")
233
- return value["version"] if value["name"] == "shakapacker"
238
+ if value.is_a?(Hash)
239
+ return value["version"] if key.start_with?("shakapacker") && value.key?("version")
240
+ return value["version"] if value["name"] == "shakapacker"
241
+ end
234
242
 
235
243
  # v9+ uses the same key format just without the leading slash, so we just add one in
236
244
  key = "/#{key}" unless key.start_with?("/")
@@ -248,6 +256,10 @@ module Shakapacker
248
256
 
249
257
  nil
250
258
  end
259
+
260
+ def safe_load_pnpm_lock(lockfile)
261
+ YAML.safe_load(lockfile, permitted_classes: [Time, Date])
262
+ end
251
263
  end
252
264
  end
253
265
  end
@@ -1,21 +1,6 @@
1
- namespace :shakapacker do
2
- def shakapacker_config_binstub_command(bin_path)
3
- # Read in binary mode so Windows CRLF line endings do not leak \r into the shebang.
4
- shebang = File.open(bin_path, "rb", &:gets).to_s
5
- command = shebang.delete_prefix("#!").strip.split(/\s+/)
6
- executable = File.basename(command.first.to_s)
7
-
8
- if executable == "env"
9
- executable = File.basename(command.drop(1).find { |part| !part.start_with?("-") }.to_s)
10
- end
11
-
12
- # Legacy JS binstubs are dispatched via PATH lookup; Kernel#exec resolves
13
- # "node" through the shell's PATH. The Ruby binstubs perform their own
14
- # explicit lookup via shakapacker_find_executable, which matters more on
15
- # Windows where PATHEXT is involved.
16
- executable == "node" ? ["node", bin_path.to_s] : [RbConfig.ruby, bin_path.to_s]
17
- end
1
+ require "shakapacker/utils/misc"
18
2
 
3
+ namespace :shakapacker do
19
4
  desc <<~DESC
20
5
  Export webpack or rspack configuration for debugging and analysis
21
6
 
@@ -62,7 +47,7 @@ namespace :shakapacker do
62
47
  # Try to use the binstub if it exists, otherwise use the gem's version
63
48
  bin_path = Rails.root.join("bin/shakapacker-config")
64
49
 
65
- unless File.exist?(bin_path)
50
+ unless File.file?(bin_path)
66
51
  # Binstub not installed, use the gem's version directly
67
52
  gem_bin_path = File.expand_path("../../install/bin/shakapacker-config", __dir__)
68
53
 
@@ -75,10 +60,34 @@ namespace :shakapacker do
75
60
  end
76
61
  else
77
62
  # Pass through command-line arguments after the task name.
78
- # Ruby binstubs run under the same Ruby as Rake; legacy JavaScript
79
- # binstubs from upgraded apps still need Node until users refresh them.
63
+ #
64
+ # Modern Ruby binstubs (the current default) are invoked with
65
+ # RbConfig.ruby so they run under the same Ruby as Rake — this avoids
66
+ # version-manager/shebang mismatches and works on Windows.
67
+ #
68
+ # Legacy JavaScript binstubs left over from earlier Shakapacker
69
+ # versions (`#!/usr/bin/env node`) are invoked through Node until
70
+ # users refresh them with `rake shakapacker:binstubs`.
71
+ js_binstub_executable = Shakapacker::Utils::Misc.js_binstub_executable(bin_path)
72
+
80
73
  Dir.chdir(Rails.root) do
81
- Kernel.exec(*shakapacker_config_binstub_command(bin_path), *ARGV[1..])
74
+ if js_binstub_executable
75
+ $stderr.puts "Note: bin/shakapacker-config is a legacy JavaScript binstub."
76
+ $stderr.puts "Run `bundle exec rake shakapacker:binstubs` to upgrade to the Ruby binstub."
77
+ $stderr.puts ""
78
+ begin
79
+ Kernel.exec(js_binstub_executable, bin_path.to_s, *ARGV[1..])
80
+ rescue Errno::ENOENT, Errno::EACCES
81
+ abort "Error: could not execute '#{js_binstub_executable}' because it was not found or is not executable.\n" \
82
+ "Run `bundle exec rake shakapacker:binstubs` to upgrade to the Ruby binstub."
83
+ end
84
+ else
85
+ begin
86
+ Kernel.exec(RbConfig.ruby, bin_path.to_s, *ARGV[1..])
87
+ rescue Errno::ENOENT, Errno::EACCES
88
+ abort "Error: Ruby interpreter '#{RbConfig.ruby}' was not found or is not executable."
89
+ end
90
+ end
82
91
  end
83
92
  end
84
93
  end
@@ -1,13 +1,15 @@
1
+ require "shakapacker/install/env"
2
+
1
3
  install_template_path = File.expand_path("../../install/template.rb", __dir__).freeze
2
4
  bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin")
3
5
 
4
6
  namespace :shakapacker do
5
- desc "Install Shakapacker in this application (use SHAKAPACKER_ASSETS_BUNDLER=rspack for Rspack, --typescript for TypeScript)"
7
+ desc "Install Shakapacker in this application (defaults to Rspack; pass webpack or SHAKAPACKER_ASSETS_BUNDLER=webpack for Webpack)"
6
8
  task :install, [:bundler, :typescript] => [:check_node] do |task, args|
7
9
  Shakapacker::Configuration.installing = true
8
10
 
9
- if args[:bundler] == "rspack" || ENV["SHAKAPACKER_ASSETS_BUNDLER"] == "rspack"
10
- ENV["SHAKAPACKER_ASSETS_BUNDLER"] = "rspack"
11
+ if (bundler_error = Shakapacker::Install::Env.apply_bundler_arg(args[:bundler]))
12
+ abort bundler_error
11
13
  end
12
14
 
13
15
  # Set typescript flag if passed as argument
@@ -1,6 +1,6 @@
1
1
  tasks = {
2
2
  "shakapacker:info" => "Provides information on Shakapacker's environment",
3
- "shakapacker:install" => "Installs and setup webpack",
3
+ "shakapacker:install" => "Installs and sets up Shakapacker",
4
4
  "shakapacker:compile" => "Compiles webpack bundles based on environment",
5
5
  "shakapacker:clean" => "Remove old compiled bundles",
6
6
  "shakapacker:clobber" => "Removes the webpack compiled output directory",