shakapacker 9.2.0 → 9.3.0.beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
  4. data/.github/workflows/claude-code-review.yml +4 -5
  5. data/.github/workflows/claude.yml +1 -2
  6. data/.github/workflows/dummy.yml +4 -4
  7. data/.github/workflows/generator.yml +9 -9
  8. data/.github/workflows/node.yml +11 -2
  9. data/.github/workflows/ruby.yml +16 -16
  10. data/.github/workflows/test-bundlers.yml +9 -9
  11. data/.gitignore +4 -0
  12. data/CHANGELOG.md +19 -4
  13. data/CLAUDE.md +6 -1
  14. data/CONTRIBUTING.md +0 -1
  15. data/Gemfile.lock +1 -1
  16. data/README.md +14 -14
  17. data/TODO.md +10 -2
  18. data/TODO_v9.md +13 -3
  19. data/bin/export-bundler-config +1 -1
  20. data/conductor-setup.sh +1 -1
  21. data/conductor.json +1 -1
  22. data/docs/cdn_setup.md +13 -8
  23. data/docs/common-upgrades.md +2 -1
  24. data/docs/configuration.md +630 -0
  25. data/docs/css-modules-export-mode.md +120 -100
  26. data/docs/customizing_babel_config.md +16 -16
  27. data/docs/deployment.md +18 -0
  28. data/docs/developing_shakapacker.md +6 -0
  29. data/docs/optional-peer-dependencies.md +9 -4
  30. data/docs/peer-dependencies.md +17 -6
  31. data/docs/precompile_hook.md +342 -0
  32. data/docs/react.md +57 -47
  33. data/docs/releasing.md +0 -2
  34. data/docs/rspack.md +25 -21
  35. data/docs/rspack_migration_guide.md +335 -8
  36. data/docs/sprockets.md +1 -0
  37. data/docs/style_loader_vs_mini_css.md +12 -12
  38. data/docs/subresource_integrity.md +13 -7
  39. data/docs/transpiler-performance.md +40 -19
  40. data/docs/troubleshooting.md +0 -2
  41. data/docs/typescript-migration.md +48 -39
  42. data/docs/typescript.md +12 -8
  43. data/docs/using_esbuild_loader.md +10 -10
  44. data/docs/v6_upgrade.md +33 -20
  45. data/docs/v7_upgrade.md +8 -6
  46. data/docs/v8_upgrade.md +13 -12
  47. data/docs/v9_upgrade.md +2 -1
  48. data/eslint.config.fast.js +134 -0
  49. data/eslint.config.js +140 -0
  50. data/knip.ts +54 -0
  51. data/lib/install/bin/export-bundler-config +1 -1
  52. data/lib/install/config/shakapacker.yml +16 -5
  53. data/lib/shakapacker/compiler.rb +80 -0
  54. data/lib/shakapacker/configuration.rb +33 -5
  55. data/lib/shakapacker/dev_server_runner.rb +140 -1
  56. data/lib/shakapacker/doctor.rb +294 -65
  57. data/lib/shakapacker/instance.rb +8 -3
  58. data/lib/shakapacker/runner.rb +244 -8
  59. data/lib/shakapacker/version.rb +1 -1
  60. data/lib/tasks/shakapacker/doctor.rake +42 -2
  61. data/package/babel/preset.ts +7 -4
  62. data/package/config.ts +42 -30
  63. data/package/configExporter/cli.ts +799 -208
  64. data/package/configExporter/configFile.ts +520 -0
  65. data/package/configExporter/fileWriter.ts +12 -8
  66. data/package/configExporter/index.ts +9 -1
  67. data/package/configExporter/types.ts +36 -2
  68. data/package/configExporter/yamlSerializer.ts +22 -8
  69. data/package/dev_server.ts +1 -1
  70. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
  71. data/package/environments/base.ts +18 -13
  72. data/package/environments/development.ts +1 -1
  73. data/package/environments/production.ts +4 -1
  74. data/package/index.d.ts +50 -3
  75. data/package/index.d.ts.template +50 -0
  76. data/package/index.ts +7 -7
  77. data/package/loaders.d.ts +2 -2
  78. data/package/optimization/rspack.ts +1 -1
  79. data/package/plugins/rspack.ts +15 -4
  80. data/package/plugins/webpack.ts +7 -3
  81. data/package/rspack/index.ts +10 -2
  82. data/package/rules/raw.ts +3 -2
  83. data/package/rules/sass.ts +1 -1
  84. data/package/types/README.md +15 -13
  85. data/package/types/index.ts +5 -5
  86. data/package/types.ts +0 -1
  87. data/package/utils/defaultConfigPath.ts +4 -1
  88. data/package/utils/errorCodes.ts +129 -100
  89. data/package/utils/errorHelpers.ts +34 -29
  90. data/package/utils/getStyleRule.ts +5 -2
  91. data/package/utils/helpers.ts +21 -11
  92. data/package/utils/pathValidation.ts +43 -35
  93. data/package/utils/requireOrError.ts +1 -1
  94. data/package/utils/snakeToCamelCase.ts +1 -1
  95. data/package/utils/typeGuards.ts +132 -83
  96. data/package/utils/validateDependencies.ts +1 -1
  97. data/package/webpack-types.d.ts +3 -3
  98. data/package/webpackDevServerConfig.ts +22 -10
  99. data/package-lock.json +2 -2
  100. data/package.json +36 -28
  101. data/scripts/type-check-no-emit.js +1 -1
  102. data/test/configExporter/configFile.test.js +392 -0
  103. data/test/configExporter/integration.test.js +275 -0
  104. data/test/helpers.js +1 -1
  105. data/test/package/configExporter.test.js +154 -0
  106. data/test/package/helpers.test.js +2 -2
  107. data/test/package/rules/sass-version-parsing.test.js +71 -0
  108. data/test/package/rules/sass.test.js +2 -4
  109. data/test/package/rules/sass1.test.js +1 -3
  110. data/test/package/rules/sass16.test.js +23 -0
  111. data/tools/README.md +15 -5
  112. data/tsconfig.eslint.json +2 -9
  113. data/yarn.lock +1894 -1492
  114. metadata +19 -3
  115. data/.eslintignore +0 -5
@@ -43,11 +43,22 @@ default: &default
43
43
  # Select JavaScript transpiler to use
44
44
  # Available options: 'swc' (default, 20x faster), 'babel', or 'esbuild'
45
45
  # Note: When using rspack, swc is used automatically regardless of this setting
46
- javascript_transpiler: 'swc'
46
+ javascript_transpiler: "swc"
47
47
 
48
48
  # Select assets bundler to use
49
49
  # Available options: 'webpack' (default) or 'rspack'
50
- assets_bundler: 'webpack'
50
+ assets_bundler: "webpack"
51
+
52
+ # Path to the directory containing webpack/rspack config files
53
+ # Defaults to 'config/webpack' for webpack or 'config/rspack' for rspack
54
+ # Use '.' to specify the root directory of your project
55
+ # assets_bundler_config_path: config/webpack
56
+
57
+ # Hook to run before webpack compilation (e.g., for generating dynamic entry points)
58
+ # SECURITY: Only reference trusted scripts within your project. The hook command will be
59
+ # validated to ensure it points to a file within the project root.
60
+ # Example: precompile_hook: 'bin/shakapacker-precompile-hook'
61
+ # precompile_hook: ~
51
62
 
52
63
  # Raises an error if there is a mismatch in the shakapacker gem and npm package being used
53
64
  ensure_consistent_versioning: true
@@ -112,14 +123,14 @@ development:
112
123
  # Should we use gzip compression?
113
124
  compress: true
114
125
  # Note that apps that do not check the host are vulnerable to DNS rebinding attacks
115
- allowed_hosts: 'auto'
126
+ allowed_hosts: "auto"
116
127
  # Shows progress and colorizes output of bin/shakapacker[-dev-server]
117
128
  pretty: true
118
129
  headers:
119
- 'Access-Control-Allow-Origin': '*'
130
+ "Access-Control-Allow-Origin": "*"
120
131
  static:
121
132
  watch:
122
- ignored: '**/node_modules/**'
133
+ ignored: "**/node_modules/**"
123
134
 
124
135
  test:
125
136
  <<: *default
@@ -1,5 +1,6 @@
1
1
  require "open3"
2
2
  require "fileutils"
3
+ require "shellwords"
3
4
 
4
5
  require_relative "compiler_strategy"
5
6
 
@@ -26,6 +27,7 @@ class Shakapacker::Compiler
26
27
  true
27
28
  else
28
29
  acquire_ipc_lock do
30
+ run_precompile_hook if config.precompile_hook
29
31
  run_webpack.tap do |success|
30
32
  after_compile_hook
31
33
  end
@@ -78,6 +80,84 @@ class Shakapacker::Compiler
78
80
  /ruby/.match?(first_line) ? RbConfig.ruby : ""
79
81
  end
80
82
 
83
+ def run_precompile_hook
84
+ hook_command = config.precompile_hook
85
+ hook_spec = validate_precompile_hook(hook_command)
86
+
87
+ logger.info "Running precompile hook: #{hook_command}"
88
+
89
+ runtime_env = webpack_env.merge(hook_spec[:env])
90
+ stdout, stderr, status = Open3.capture3(
91
+ runtime_env,
92
+ hook_spec[:executable],
93
+ *hook_spec[:args],
94
+ chdir: File.expand_path(config.root_path)
95
+ )
96
+
97
+ if status.success?
98
+ logger.info "Precompile hook completed successfully"
99
+ logger.info stdout unless stdout.empty?
100
+ logger.warn stderr unless stderr.empty?
101
+ else
102
+ non_empty_streams = [stdout, stderr].delete_if(&:empty?)
103
+ logger.error "\nPRECOMPILE HOOK FAILED:\nEXIT STATUS: #{status.exitstatus}\nCOMMAND: #{hook_command}\nOUTPUTS:\n#{non_empty_streams.join("\n\n")}"
104
+ logger.error "\nTo fix this:"
105
+ logger.error " 1. Check that the hook script exists and is executable"
106
+ logger.error " 2. Test the hook command manually: #{hook_command}"
107
+ logger.error " 3. Review the error output above for details"
108
+ logger.error " 4. You can disable the hook temporarily by commenting out 'precompile_hook' in shakapacker.yml"
109
+ raise "Precompile hook '#{hook_command}' failed with exit status #{status.exitstatus}"
110
+ end
111
+ end
112
+
113
+ def validate_precompile_hook(hook_command)
114
+ hook_tokens = begin
115
+ Shellwords.shellsplit(hook_command)
116
+ rescue ArgumentError => e
117
+ raise "Shakapacker configuration error: Invalid precompile_hook command syntax: #{e.message}. Check for unmatched quotes in: #{hook_command}"
118
+ end
119
+
120
+ env_assignments = {}
121
+ while hook_tokens.first&.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/)
122
+ key, value = hook_tokens.shift.split("=", 2)
123
+ env_assignments[key] = value
124
+ end
125
+
126
+ executable = hook_tokens.shift
127
+ if executable.nil? || executable.empty?
128
+ raise "Shakapacker configuration error: precompile_hook must include an executable command. Got: #{hook_command}"
129
+ end
130
+
131
+ executable_path = config.root_path.join(executable)
132
+
133
+ # Security: Resolve symlinks and verify the hook points to a file within the project
134
+ # This prevents symlink bypass attacks and path traversal attacks
135
+ begin
136
+ resolved_path = executable_path.realpath
137
+ resolved_root = config.root_path.realpath
138
+ rescue Errno::ENOENT
139
+ # If file doesn't exist, use cleanpath for basic validation
140
+ resolved_path = executable_path.cleanpath
141
+ resolved_root = config.root_path.cleanpath
142
+ end
143
+
144
+ # Verify path is within project root with proper separator check
145
+ # Using File::SEPARATOR prevents partial path matches (e.g., /project vs /project-evil)
146
+ unless resolved_path.to_s.start_with?(resolved_root.to_s + File::SEPARATOR)
147
+ raise "Security Error: precompile_hook must reference a script within the project root. " \
148
+ "Got: #{hook_command} (resolved to: #{resolved_path})"
149
+ end
150
+
151
+ # Warn if the executable doesn't exist within the project
152
+ unless File.exist?(executable_path)
153
+ logger.warn "⚠️ Warning: precompile_hook executable not found: #{executable_path}"
154
+ logger.warn " The hook command is configured but the script does not exist within the project root."
155
+ logger.warn " Please ensure the script exists or remove 'precompile_hook' from your shakapacker.yml configuration."
156
+ end
157
+
158
+ { env: env_assignments, executable: executable, args: hook_tokens }
159
+ end
160
+
81
161
  def run_webpack
82
162
  logger.info "Compiling..."
83
163
 
@@ -117,6 +117,17 @@ class Shakapacker::Configuration
117
117
  assets_bundler == "webpack"
118
118
  end
119
119
 
120
+ def precompile_hook
121
+ hook = fetch(:precompile_hook)
122
+ return nil if hook.nil? || (hook.is_a?(String) && hook.strip.empty?)
123
+
124
+ unless hook.is_a?(String)
125
+ raise "Shakapacker configuration error: precompile_hook must be a string, got #{hook.class}"
126
+ end
127
+
128
+ hook.strip
129
+ end
130
+
120
131
  def javascript_transpiler
121
132
  # Show deprecation warning if using old 'webpack_loader' key
122
133
  if data.has_key?(:webpack_loader) && !data.has_key?(:javascript_transpiler)
@@ -137,6 +148,14 @@ class Shakapacker::Configuration
137
148
  javascript_transpiler
138
149
  end
139
150
 
151
+ def assets_bundler_config_path
152
+ custom_path = fetch(:assets_bundler_config_path)
153
+ return custom_path if custom_path
154
+
155
+ # Default paths based on bundler type
156
+ rspack? ? "config/rspack" : "config/webpack"
157
+ end
158
+
140
159
  private
141
160
 
142
161
  def default_javascript_transpiler
@@ -308,11 +327,20 @@ class Shakapacker::Configuration
308
327
  end
309
328
 
310
329
  def log_fallback(requested_env, fallback_env)
311
- return unless Shakapacker.logger
330
+ message = "Shakapacker environment '#{requested_env}' not found in #{config_path}, " \
331
+ "falling back to '#{fallback_env}'"
312
332
 
313
- Shakapacker.logger.info(
314
- "Shakapacker environment '#{requested_env}' not found in #{config_path}, " \
315
- "falling back to '#{fallback_env}'"
316
- )
333
+ # Try to use the logger if available, otherwise fall back to stdout
334
+ begin
335
+ if Shakapacker.respond_to?(:logger) && Shakapacker.logger
336
+ Shakapacker.logger.info(message)
337
+ else
338
+ puts message
339
+ end
340
+ rescue NameError, NoMethodError
341
+ # If logger access fails (e.g., circular dependency in standalone runner context),
342
+ # fall back to stdout so the message still gets displayed
343
+ puts message
344
+ end
317
345
  end
318
346
  end
@@ -4,13 +4,150 @@ require "socket"
4
4
  require_relative "configuration"
5
5
  require_relative "dev_server"
6
6
  require_relative "runner"
7
+ require_relative "version"
7
8
 
8
9
  module Shakapacker
9
10
  class DevServerRunner < Shakapacker::Runner
10
11
  def self.run(argv)
12
+ # Show Shakapacker help and exit (don't call bundler)
13
+ if argv.include?("--help") || argv.include?("-h")
14
+ print_help
15
+ exit(0)
16
+ elsif argv.include?("--version") || argv.include?("-v")
17
+ print_version
18
+ exit(0)
19
+ end
20
+
11
21
  new(argv).run
12
22
  end
13
23
 
24
+ def self.print_help
25
+ puts <<~HELP
26
+ ================================================================================
27
+ SHAKAPACKER DEV SERVER - Development Server with Hot Module Replacement
28
+ ================================================================================
29
+
30
+ Usage: bin/shakapacker-dev-server [options]
31
+
32
+ Shakapacker-specific options:
33
+ -h, --help Show this help message
34
+ -v, --version Show Shakapacker version
35
+ --debug-shakapacker Enable Node.js debugging (--inspect-brk)
36
+
37
+ Examples:
38
+ bin/shakapacker-dev-server # Start dev server
39
+ bin/shakapacker-dev-server --no-hot # Disable HMR
40
+ bin/shakapacker-dev-server --open # Open browser automatically
41
+ bin/shakapacker-dev-server --debug-shakapacker # Debug with Node inspector
42
+
43
+ HELP
44
+
45
+ print_dev_server_help
46
+
47
+ puts <<~HELP
48
+
49
+ Options managed by Shakapacker (configured in config/shakapacker.yml):
50
+ --host Set from dev_server.host (default: localhost)
51
+ --port Set from dev_server.port (default: 3035)
52
+ --https Set from dev_server.server (http or https)
53
+ --config Set automatically to config/webpack/webpack.config.js
54
+ or config/rspack/rspack.config.js
55
+
56
+ Note: CLI flags for --host, --port, and --https are NOT supported.
57
+ Configure these in config/shakapacker.yml instead.
58
+ HELP
59
+ end
60
+
61
+ def self.print_dev_server_help
62
+ bundler_type, bundler_help = get_dev_server_help
63
+
64
+ if bundler_help
65
+ bundler_name = bundler_type == :rspack ? "RSPACK" : "WEBPACK"
66
+ puts "=" * 80
67
+ puts "AVAILABLE #{bundler_name} DEV SERVER OPTIONS (Passed directly to #{bundler_name.downcase})"
68
+ puts "=" * 80
69
+ puts
70
+ puts filter_managed_options(bundler_help)
71
+ puts
72
+ puts "For complete documentation:"
73
+ if bundler_type == :rspack
74
+ puts " https://rspack.dev/config/dev-server"
75
+ else
76
+ puts " https://webpack.js.org/configuration/dev-server/"
77
+ end
78
+ else
79
+ puts "For complete documentation:"
80
+ puts " Webpack: https://webpack.js.org/configuration/dev-server/"
81
+ puts " Rspack: https://rspack.dev/config/dev-server"
82
+ end
83
+ end
84
+
85
+ def self.get_dev_server_help
86
+ Runner.execute_bundler_command("serve", "--help") { |stdout| stdout }
87
+ end
88
+
89
+ # Filter dev server help output to remove Shakapacker-managed options
90
+ #
91
+ # This method processes the raw help output from webpack-dev-server/rspack serve
92
+ # and removes options that Shakapacker manages automatically:
93
+ # - --config (set from config/webpack or config/rspack)
94
+ # - --host, --port (set from config/shakapacker.yml dev_server settings)
95
+ # - --help, --version (shown separately in Shakapacker's help)
96
+ #
97
+ # The filtering uses skip_until_blank to track multi-line option descriptions
98
+ # and skip them entirely when the option header matches a managed option.
99
+ #
100
+ # Note: This relies on dev server help format conventions. If webpack-dev-server
101
+ # or rspack significantly changes their help output format, this may need adjustment.
102
+ def self.filter_managed_options(help_text)
103
+ lines = help_text.lines
104
+ filtered_lines = []
105
+ skip_until_blank = false
106
+
107
+ lines.each do |line|
108
+ # Skip options that Shakapacker manages and their descriptions
109
+ # These options are shown in the "Options managed by Shakapacker" section
110
+ if line.match?(/^\s*(-c,\s*)?--config\b/) ||
111
+ line.match?(/^\s*--configName\b/) ||
112
+ line.match?(/^\s*--configLoader\b/) ||
113
+ line.match?(/^\s*--nodeEnv\b/) ||
114
+ line.match?(/^\s*--host\b/) ||
115
+ line.match?(/^\s*--port\b/) ||
116
+ line.match?(/^\s*--https\b/) ||
117
+ line.match?(/^\s*(-h,\s*)?--help\b/) ||
118
+ line.match?(/^\s*(-v,\s*)?--version\b/)
119
+ skip_until_blank = true
120
+ next
121
+ end
122
+
123
+ # Continue skipping lines that are part of a filtered option's description
124
+ # Reset when we hit a blank line or the start of a new option (starts with -)
125
+ if skip_until_blank
126
+ if line.strip.empty? || line.match?(/^\s*-/)
127
+ skip_until_blank = false
128
+ else
129
+ next
130
+ end
131
+ end
132
+
133
+ filtered_lines << line
134
+ end
135
+
136
+ filtered_lines.join
137
+ end
138
+
139
+ def self.print_version
140
+ puts "Shakapacker #{Shakapacker::VERSION}"
141
+ puts "Framework: Rails #{Rails.version}" if defined?(Rails)
142
+
143
+ # Try to get bundler version
144
+ bundler_type, bundler_version = Runner.get_bundler_version
145
+ if bundler_version
146
+ bundler_name = bundler_type == :rspack ? "Rspack" : "Webpack"
147
+ puts "Bundler: #{bundler_name} #{bundler_version}"
148
+ end
149
+ end
150
+
14
151
  def run
15
152
  load_config
16
153
  detect_unsupported_switches!
@@ -96,8 +233,10 @@ module Shakapacker
96
233
  cmd += @argv
97
234
 
98
235
  Dir.chdir(@app_path) do
99
- Kernel.exec env, *cmd
236
+ system(env, *cmd)
100
237
  end
238
+
239
+ exit($?.exitstatus || 1) unless $?.success?
101
240
  end
102
241
 
103
242
  def build_cmd