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.
@@ -19,12 +19,13 @@ module Shakapacker
19
19
  prod: %w[webpack-merge]
20
20
  }.freeze
21
21
 
22
- # Default dependencies for each bundler (package names only, no versions)
22
+ # Default dependencies for each bundler. Rspack entries include install-time
23
+ # version ranges; package_names strips them before removal and display.
23
24
  # Note: Excludes independent/optional dependencies like @swc/core, swc-loader (user-configured
24
25
  # transpilers)
25
26
  DEFAULT_RSPACK_DEPS = {
26
- dev: %w[@rspack/cli @rspack/plugin-react-refresh],
27
- prod: %w[@rspack/core rspack-manifest-plugin]
27
+ dev: %w[@rspack/cli@^2.0.0 @rspack/dev-server@^2.0.0 @rspack/plugin-react-refresh@^2.0.0],
28
+ prod: %w[@rspack/core@^2.0.0 rspack-manifest-plugin@^5.2.2]
28
29
  }.freeze
29
30
 
30
31
  DEFAULT_WEBPACK_DEPS = {
@@ -267,8 +268,8 @@ module Shakapacker
267
268
  # Show what will be removed (only when switching and not no_uninstall)
268
269
  if switching && !no_uninstall && (!deps_to_remove[:dev].empty? || !deps_to_remove[:prod].empty?)
269
270
  puts " 🗑️ Removing:"
270
- deps_to_remove[:dev].each { |dep| puts " - #{dep} (dev)" }
271
- deps_to_remove[:prod].each { |dep| puts " - #{dep} (prod)" }
271
+ package_names(deps_to_remove[:dev]).each { |dep| puts " - #{dep} (dev)" }
272
+ package_names(deps_to_remove[:prod]).each { |dep| puts " - #{dep} (prod)" }
272
273
  puts ""
273
274
  elsif switching && no_uninstall
274
275
  puts " ⏭️ Skipping uninstall (--no-uninstall)"
@@ -302,7 +303,7 @@ module Shakapacker
302
303
 
303
304
  # Combine dev and prod dependencies into a single list for removal
304
305
  # Package managers remove packages from both dependencies and devDependencies sections if present
305
- all_deps = deps[:dev] + deps[:prod]
306
+ all_deps = package_names(deps[:dev] + deps[:prod])
306
307
 
307
308
  unless all_deps.empty?
308
309
  unless package_json.manager.remove(all_deps)
@@ -381,6 +382,11 @@ module Shakapacker
381
382
  end
382
383
 
383
384
  def print_uninstall_commands(package_manager, deps)
385
+ deps = {
386
+ dev: package_names(deps[:dev]),
387
+ prod: package_names(deps[:prod])
388
+ }
389
+
384
390
  case package_manager
385
391
  when "yarn"
386
392
  puts " yarn remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
@@ -397,6 +403,16 @@ module Shakapacker
397
403
  end
398
404
  end
399
405
 
406
+ def package_names(dependencies)
407
+ dependencies.map do |dependency|
408
+ if dependency.start_with?("@")
409
+ dependency.count("@") > 1 ? dependency.sub(/@[^@]+\z/, "") : dependency
410
+ else
411
+ dependency.sub(/@[^@]+\z/, "")
412
+ end
413
+ end
414
+ end
415
+
400
416
  # Load YAML config file with Ruby version compatibility
401
417
  # Ruby 3.1+ supports aliases: keyword, older versions need YAML.safe_load
402
418
  def load_yaml_config(path)
@@ -5,6 +5,9 @@ require "shellwords"
5
5
  require_relative "compiler_strategy"
6
6
 
7
7
  class Shakapacker::Compiler
8
+ SpawnFailure = Class.new(StandardError)
9
+ private_constant :SpawnFailure
10
+
8
11
  # Additional environment variables that the compiler is being run with
9
12
  # Shakapacker::Compiler.env['FRONTEND_API_KEY'] = 'your_secret_key'
10
13
  cattr_accessor(:env) { {} }
@@ -36,9 +39,16 @@ class Shakapacker::Compiler
36
39
  else
37
40
  acquire_ipc_lock do
38
41
  run_precompile_hook if should_run_precompile_hook?
39
- run_webpack.tap do |success|
40
- after_compile_hook
42
+ spawn_failed = false
43
+ begin
44
+ success = run_webpack
45
+ rescue SpawnFailure
46
+ spawn_failed = true
47
+ success = false
41
48
  end
49
+
50
+ after_compile_hook unless spawn_failed
51
+ success
42
52
  end
43
53
  end
44
54
  end
@@ -83,8 +93,9 @@ class Shakapacker::Compiler
83
93
  config.root_path.join("tmp/shakapacker.lock")
84
94
  end
85
95
 
96
+ # Returns one executable path for array-form Open3, not a shell command snippet.
86
97
  def optional_ruby_runner
87
- first_line = File.readlines(bin_shakapacker_path).first.chomp
98
+ first_line = File.readlines(bin_shakapacker_path).first&.chomp || ""
88
99
  /ruby/.match?(first_line) ? RbConfig.ruby : ""
89
100
  end
90
101
 
@@ -176,11 +187,21 @@ class Shakapacker::Compiler
176
187
  def run_webpack
177
188
  logger.info "Compiling..."
178
189
 
179
- stdout, stderr, status = Open3.capture3(
180
- webpack_env,
181
- "#{optional_ruby_runner} '#{bin_shakapacker_path}'",
182
- chdir: File.expand_path(config.root_path)
183
- )
190
+ # Fetch compile flags before the spawn rescue so configuration errors stay explicit.
191
+ compile_flags = config.webpack_compile_flags
192
+
193
+ begin
194
+ command = shakapacker_command(compile_flags)
195
+ stdout, stderr, status = Open3.capture3(
196
+ webpack_env,
197
+ *command,
198
+ chdir: File.expand_path(config.root_path)
199
+ )
200
+ rescue Errno::EACCES, Errno::ENOENT, Errno::ENOEXEC, Errno::EPERM, Errno::ENOTDIR => e
201
+ logger.error "\nCOMPILATION FAILED:\n#{e.class}: #{e.message}"
202
+ show_doctor_hint_once
203
+ raise SpawnFailure
204
+ end
184
205
 
185
206
  if status.success?
186
207
  logger.info "Compiled all packs in #{config.public_output_path}"
@@ -211,6 +232,15 @@ class Shakapacker::Compiler
211
232
  config.root_path.join("bin/shakapacker")
212
233
  end
213
234
 
235
+ def shakapacker_command(compile_flags)
236
+ runner = optional_ruby_runner
237
+ bin_path = bin_shakapacker_path.to_s
238
+ flags_part = compile_flags.any? ? ["--", *compile_flags] : []
239
+
240
+ # Use the [cmd, argv0] form so Open3 never routes a lone binstub path through the shell.
241
+ runner.empty? ? [[bin_path, bin_path], *flags_part] : [runner, bin_path, *flags_part]
242
+ end
243
+
214
244
  # Fires only after a failed compile, so users in a healthy loop never see the tip.
215
245
  def show_doctor_hint_once
216
246
  return if self.class.doctor_hint_shown
@@ -3,14 +3,14 @@ require_relative "digest_strategy"
3
3
 
4
4
  module Shakapacker
5
5
  class CompilerStrategy
6
- def self.from_config
7
- strategy_from_config = Shakapacker.config.compiler_strategy
6
+ def self.from_config(instance = Shakapacker.instance)
7
+ strategy_from_config = instance.config.compiler_strategy
8
8
 
9
9
  case strategy_from_config
10
10
  when "mtime"
11
- Shakapacker::MtimeStrategy.new
11
+ Shakapacker::MtimeStrategy.new(instance)
12
12
  when "digest"
13
- Shakapacker::DigestStrategy.new
13
+ Shakapacker::DigestStrategy.new(instance)
14
14
  else
15
15
  raise "Unknown strategy '#{strategy_from_config}'. " \
16
16
  "Available options are 'mtime' and 'digest'."
@@ -27,6 +27,41 @@ require "active_support/core_ext/hash/indifferent_access"
27
27
  #
28
28
  # @see https://github.com/shakacode/shakapacker/blob/main/docs/shakapacker.yml.md
29
29
  class Shakapacker::Configuration
30
+ # Shared Ruby source of truth for Shakapacker-owned Node flags; Runner aliases this constant.
31
+ SHAKAPACKER_NODE_FLAGS = %w[--debug-shakapacker --trace-deprecation --no-deprecation].freeze
32
+
33
+ SHAKAPACKER_RUNNER_COMMANDS = %w[help h --help -h --help=verbose version v --version -v info i].freeze
34
+ private_constant :SHAKAPACKER_RUNNER_COMMANDS
35
+
36
+ SHAKAPACKER_HELP_FLAG_PATTERN = /\A(?:--help|-h)(?:=.*)?\z/
37
+ private_constant :SHAKAPACKER_HELP_FLAG_PATTERN
38
+
39
+ SHAKAPACKER_WATCH_FLAGS = %w[--watch -w].freeze
40
+ private_constant :SHAKAPACKER_WATCH_FLAGS
41
+
42
+ SHAKAPACKER_WATCH_FLAG_PATTERN = /\A(?:--watch|-w)(?:=.*)?\z/
43
+ private_constant :SHAKAPACKER_WATCH_FLAG_PATTERN
44
+
45
+ SHAKAPACKER_MANAGED_COMPILE_FLAGS =
46
+ %w[--config -c --node-env --nodeEnv --bundler --build --init --list-builds].freeze
47
+ private_constant :SHAKAPACKER_MANAGED_COMPILE_FLAGS
48
+
49
+ SHAKAPACKER_MANAGED_COMPILE_FLAG_PATTERN =
50
+ /\A(?:--config|-c|--node-env|--nodeEnv|--bundler|--build|--init|--list-builds)(?:=.*)?\z/
51
+ private_constant :SHAKAPACKER_MANAGED_COMPILE_FLAG_PATTERN
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
+
60
+ DISALLOWED_WEBPACK_COMPILE_FLAGS =
61
+ (SHAKAPACKER_NODE_FLAGS + SHAKAPACKER_RUNNER_COMMANDS + SHAKAPACKER_WATCH_FLAGS +
62
+ SHAKAPACKER_MANAGED_COMPILE_FLAGS).freeze
63
+ private_constant :DISALLOWED_WEBPACK_COMPILE_FLAGS
64
+
30
65
  class << self
31
66
  # Flag indicating whether Shakapacker is currently being installed
32
67
  # Used to suppress certain validations during installation
@@ -233,6 +268,25 @@ class Shakapacker::Configuration
233
268
  fetch(:webpack_compile_output)
234
269
  end
235
270
 
271
+ # Returns extra command-line flags passed to the webpack/rspack compile command
272
+ #
273
+ # @return [Array<String>] bundler CLI flags
274
+ def webpack_compile_flags
275
+ flags = fetch(:webpack_compile_flags)
276
+ return [] if flags.nil?
277
+
278
+ valid_flags = flags.is_a?(Array) && flags.all? { |flag| valid_webpack_compile_flag?(flag) }
279
+
280
+ unless valid_flags
281
+ disallowed_flags = DISALLOWED_WEBPACK_COMPILE_FLAGS.join(", ")
282
+ raise "Shakapacker configuration error: compile flags (webpack_compile_flags) must be an array of " \
283
+ "non-empty strings and must not include \"--\" or Shakapacker-specific " \
284
+ "wrapper/short-circuit/watch/managed flags (#{disallowed_flags})"
285
+ end
286
+
287
+ flags
288
+ end
289
+
236
290
  # Returns the compiler strategy for determining staleness
237
291
  #
238
292
  # Options:
@@ -310,24 +364,54 @@ class Shakapacker::Configuration
310
364
  # Resolution order:
311
365
  # 1. javascript_transpiler setting in config file
312
366
  # 2. webpack_loader setting in config file (deprecated)
313
- # 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
314
369
  #
315
370
  # Validates that the configured transpiler matches installed packages.
316
371
  #
317
372
  # @return [String] "babel", "swc", or "esbuild"
318
373
  def javascript_transpiler
319
- # Show deprecation warning if using old 'webpack_loader' key
320
- if data.has_key?(:webpack_loader) && !data.has_key?(:javascript_transpiler)
321
- $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."
322
- 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)
323
380
 
324
- # Use explicit config if set, otherwise default based on bundler
325
- transpiler = fetch(:javascript_transpiler) || fetch(:webpack_loader) || default_javascript_transpiler
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
326
385
 
327
- # Validate transpiler configuration
328
- validate_transpiler_configuration(transpiler) unless self.class.installing
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
329
402
 
330
- 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
409
+
410
+ # Validate transpiler configuration
411
+ validate_transpiler_configuration(transpiler) unless self.class.installing
412
+
413
+ transpiler
414
+ end
331
415
  end
332
416
 
333
417
  # Deprecated alias for {#javascript_transpiler}
@@ -399,47 +483,139 @@ class Shakapacker::Configuration
399
483
 
400
484
  private
401
485
 
486
+ def valid_webpack_compile_flag?(flag)
487
+ flag.is_a?(String) &&
488
+ !flag.empty? &&
489
+ flag != "--" &&
490
+ !DISALLOWED_WEBPACK_COMPILE_FLAGS.include?(flag) &&
491
+ !SHAKAPACKER_HELP_FLAG_PATTERN.match?(flag) &&
492
+ !SHAKAPACKER_WATCH_FLAG_PATTERN.match?(flag) &&
493
+ !SHAKAPACKER_MANAGED_COMPILE_FLAG_PATTERN.match?(flag)
494
+ end
495
+
402
496
  def default_javascript_transpiler
403
497
  # RSpack has built-in SWC support, use it by default
404
498
  rspack? ? "swc" : "babel"
405
499
  end
406
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
+
407
591
  def validate_transpiler_configuration(transpiler)
408
592
  return unless ENV["NODE_ENV"] != "test" # Skip validation in test environment
409
593
 
410
594
  # Skip validation if transpiler is set to 'none' (custom webpack config)
411
595
  return if transpiler == "none"
412
596
 
413
- # Check if package.json exists
414
- package_json_path = root_path.join("package.json")
415
- return unless package_json_path.exist?
597
+ all_deps = declared_package_dependencies
598
+ return if all_deps.empty?
416
599
 
417
- begin
418
- package_json = JSON.parse(File.read(package_json_path))
419
- all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
420
-
421
- # Check for transpiler mismatch
422
- has_babel = all_deps.keys.any? { |pkg| pkg.start_with?("@babel/", "babel-") }
423
- has_swc = all_deps.keys.any? { |pkg| pkg.include?("swc") }
424
- has_esbuild = all_deps.keys.any? { |pkg| pkg.include?("esbuild") }
425
-
426
- case transpiler
427
- when "babel"
428
- if !has_babel && has_swc
429
- warn_transpiler_mismatch("Babel", "SWC packages found but Babel is configured")
430
- end
431
- when "swc"
432
- if !has_swc && has_babel
433
- warn_transpiler_mismatch("SWC", "Babel packages found but SWC is configured")
434
- end
435
- when "esbuild"
436
- if !has_esbuild && (has_babel || has_swc)
437
- other = has_babel ? "Babel" : "SWC"
438
- warn_transpiler_mismatch("esbuild", "#{other} packages found but esbuild is configured")
439
- 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")
440
618
  end
441
- rescue JSON::ParserError
442
- # Ignore if package.json is malformed
443
619
  end
444
620
  end
445
621
 
@@ -9,11 +9,13 @@ require_relative "version"
9
9
  module Shakapacker
10
10
  class DevServerRunner < Shakapacker::Runner
11
11
  def self.run(argv)
12
+ runner_argv, passthrough_argv = split_passthrough_argv(argv)
13
+
12
14
  # Show Shakapacker help and exit (don't call bundler)
13
- if argv.include?("--help") || argv.include?("-h")
15
+ if runner_argv.include?("--help") || runner_argv.include?("-h")
14
16
  print_help
15
17
  exit(0)
16
- elsif argv.include?("--version") || argv.include?("-v")
18
+ elsif runner_argv.include?("--version") || runner_argv.include?("-v")
17
19
  print_version
18
20
  exit(0)
19
21
  end
@@ -21,9 +23,9 @@ module Shakapacker
21
23
  Shakapacker.ensure_node_env!
22
24
 
23
25
  # Check for --build flag
24
- build_index = argv.index("--build")
26
+ build_index = runner_argv.index("--build")
25
27
  if build_index
26
- build_name = argv[build_index + 1]
28
+ build_name = runner_argv[build_index + 1]
27
29
 
28
30
  unless build_name
29
31
  $stderr.puts "[Shakapacker] Error: --build requires a build name"
@@ -51,11 +53,11 @@ module Shakapacker
51
53
  end
52
54
 
53
55
  # Remove --build and build name from argv
54
- remaining_argv = argv.dup
56
+ remaining_argv = runner_argv.dup
55
57
  remaining_argv.delete_at(build_index + 1)
56
58
  remaining_argv.delete_at(build_index)
57
59
 
58
- run_with_build_config(remaining_argv, build_config)
60
+ run_with_build_config(remaining_argv, build_config, passthrough_argv)
59
61
  return
60
62
  rescue ArgumentError => e
61
63
  $stderr.puts "[Shakapacker] #{e.message}"
@@ -63,10 +65,10 @@ module Shakapacker
63
65
  end
64
66
  end
65
67
 
66
- new(argv).run
68
+ new(runner_argv, nil, nil, passthrough_argv).run
67
69
  end
68
70
 
69
- def self.run_with_build_config(argv, build_config)
71
+ def self.run_with_build_config(argv, build_config, passthrough_argv = [])
70
72
  Shakapacker.ensure_node_env!
71
73
 
72
74
  # Apply build config environment variables
@@ -84,7 +86,7 @@ module Shakapacker
84
86
  puts "[Shakapacker] Config file: #{build_config[:config_file]}" if build_config[:config_file]
85
87
 
86
88
  # Pass bundler override so Configuration.assets_bundler reflects the build
87
- new(argv, build_config, build_config[:bundler]).run
89
+ new(argv, build_config, build_config[:bundler], passthrough_argv).run
88
90
  end
89
91
 
90
92
  def self.print_help
@@ -99,8 +101,14 @@ module Shakapacker
99
101
  -h, --help Show this help message
100
102
  -v, --version Show Shakapacker version
101
103
  --debug-shakapacker Enable Node.js debugging (--inspect-brk)
104
+ --trace-deprecation Show stack traces for Node.js deprecations
105
+ --no-deprecation Silence Node.js deprecation warnings
102
106
  --build <name> Run a specific build configuration
103
107
 
108
+ Put Shakapacker-specific options before --. Arguments after -- are
109
+ passed directly to the selected bundler, except --host and --port,
110
+ which remain managed by config/shakapacker.yml.
111
+
104
112
  Build configurations (config/shakapacker-builds.yml):
105
113
  bin/shakapacker-dev-server --build dev-hmr # Run the 'dev-hmr' build
106
114
 
@@ -116,6 +124,7 @@ module Shakapacker
116
124
  bin/shakapacker-dev-server --no-hot # Disable HMR
117
125
  bin/shakapacker-dev-server --open # Open browser automatically
118
126
  bin/shakapacker-dev-server --debug-shakapacker # Debug with Node inspector
127
+ bin/shakapacker-dev-server --trace-deprecation # Show Node deprecation traces
119
128
 
120
129
  HELP
121
130
 
@@ -126,12 +135,13 @@ module Shakapacker
126
135
  Options managed by Shakapacker (configured in config/shakapacker.yml):
127
136
  --host Set from dev_server.host (default: localhost)
128
137
  --port Set from dev_server.port (default: 3035)
129
- --https Set from dev_server.server (http or https)
138
+ --https Requires dev_server.server: https before forwarding
130
139
  --config Set automatically to config/webpack/webpack.config.js
131
140
  or config/rspack/rspack.config.js
132
141
 
133
- Note: CLI flags for --host, --port, and --https are NOT supported.
134
- Configure these in config/shakapacker.yml instead.
142
+ Note: CLI flags for --host and --port are NOT supported.
143
+ Configure these in config/shakapacker.yml instead. --https is accepted
144
+ only when dev_server.server is already set to https.
135
145
  HELP
136
146
  end
137
147
 
@@ -260,15 +270,19 @@ module Shakapacker
260
270
  UNSUPPORTED_SWITCHES = %w[--host --port]
261
271
  private_constant :UNSUPPORTED_SWITCHES
262
272
  def detect_unsupported_switches!
263
- unsupported_switches = UNSUPPORTED_SWITCHES & @argv
273
+ # Host/port stay config-owned even after -- because Shakapacker assembles the dev-server command.
274
+ unsupported_switches = UNSUPPORTED_SWITCHES & bundler_argv
264
275
  if unsupported_switches.any?
265
- $stdout.puts "The following CLI switches are not supported by Shakapacker: #{unsupported_switches.join(' ')}. Please edit your command and try again."
266
- exit!
276
+ log_output.puts(
277
+ "The following CLI switches are not supported by Shakapacker: #{unsupported_switches.join(' ')}. " \
278
+ "Please set dev_server.host or dev_server.port in shakapacker.yml instead."
279
+ )
280
+ exit_after_shakapacker_flag_error(1)
267
281
  end
268
282
 
269
- if @argv.include?("--https") && !@https
270
- $stdout.puts "--https requires that 'server' in shakapacker.yml is set to 'https'"
271
- exit!
283
+ if bundler_argv.include?("--https") && !@https
284
+ log_output.puts "--https requires that 'server' in shakapacker.yml is set to 'https'"
285
+ exit_after_shakapacker_flag_error(1)
272
286
  end
273
287
  end
274
288
 
@@ -289,10 +303,21 @@ module Shakapacker
289
303
 
290
304
  cmd = build_cmd
291
305
 
306
+ detect_shakapacker_flags_in_passthrough!
307
+
308
+ # Shakapacker-owned Node flags must appear before --; passthrough args belong to the bundler.
292
309
  if @argv.delete("--debug-shakapacker")
293
310
  env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --inspect-brk --trace-warnings"
294
311
  end
295
312
 
313
+ if @argv.delete("--trace-deprecation")
314
+ env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --trace-deprecation"
315
+ end
316
+
317
+ if @argv.delete("--no-deprecation")
318
+ env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --no-deprecation"
319
+ end
320
+
296
321
  # Add config file
297
322
  cmd += ["--config", @webpack_config]
298
323
 
@@ -307,7 +332,7 @@ module Shakapacker
307
332
  cmd += ["--hot"] if @hot && @hot != false
308
333
  end
309
334
 
310
- cmd += @argv
335
+ cmd += bundler_argv
311
336
 
312
337
  Dir.chdir(@app_path) do
313
338
  exec(env, *cmd)
@@ -30,7 +30,7 @@ module Shakapacker
30
30
  end
31
31
 
32
32
  def watched_files_digest
33
- if Rails.env.development?
33
+ if env.development?
34
34
  warn <<~MSG.strip
35
35
  Shakapacker::Compiler - Slow setup for development
36
36
  Prepare JS assets with either:
@@ -46,7 +46,7 @@ module Shakapacker
46
46
  files = Dir[*expanded_paths].reject { |f| File.directory?(f) }
47
47
  file_ids = files.sort.map { |f| "#{File.basename(f)}/#{Digest::SHA1.file(f).hexdigest}" }
48
48
 
49
- asset_host = Shakapacker.config.asset_host.to_s
49
+ asset_host = config.asset_host.to_s
50
50
  Digest::SHA1.hexdigest(file_ids.join("/").concat(asset_host))
51
51
  end
52
52
 
@@ -56,7 +56,7 @@ module Shakapacker
56
56
  end
57
57
 
58
58
  def compilation_digest_path
59
- config.cache_path.join("last-compilation-digest-#{Shakapacker.env}")
59
+ config.cache_path.join("last-compilation-digest-#{env}")
60
60
  end
61
61
  end
62
62
  end