shakapacker 9.3.0.beta.6 → 9.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -105
  3. data/ESLINT_TECHNICAL_DEBT.md +8 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +53 -2
  6. data/docs/configuration.md +28 -0
  7. data/docs/rspack_migration_guide.md +238 -2
  8. data/docs/troubleshooting.md +21 -21
  9. data/eslint.config.fast.js +8 -0
  10. data/eslint.config.js +47 -10
  11. data/knip.ts +8 -1
  12. data/lib/install/config/shakapacker.yml +6 -6
  13. data/lib/shakapacker/configuration.rb +227 -4
  14. data/lib/shakapacker/dev_server.rb +88 -1
  15. data/lib/shakapacker/doctor.rb +129 -72
  16. data/lib/shakapacker/instance.rb +85 -1
  17. data/lib/shakapacker/manifest.rb +85 -11
  18. data/lib/shakapacker/runner.rb +12 -8
  19. data/lib/shakapacker/swc_migrator.rb +7 -7
  20. data/lib/shakapacker/version.rb +1 -1
  21. data/lib/shakapacker.rb +143 -3
  22. data/lib/tasks/shakapacker/doctor.rake +1 -1
  23. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  24. data/package/config.ts +0 -1
  25. data/package/configExporter/buildValidator.ts +53 -29
  26. data/package/configExporter/cli.ts +152 -118
  27. data/package/configExporter/configFile.ts +33 -26
  28. data/package/configExporter/fileWriter.ts +3 -3
  29. data/package/configExporter/types.ts +64 -0
  30. data/package/configExporter/yamlSerializer.ts +147 -36
  31. data/package/dev_server.ts +2 -1
  32. data/package/env.ts +1 -1
  33. data/package/environments/base.ts +4 -4
  34. data/package/environments/development.ts +7 -6
  35. data/package/environments/production.ts +6 -7
  36. data/package/environments/test.ts +2 -1
  37. data/package/index.ts +28 -4
  38. data/package/loaders.d.ts +2 -2
  39. data/package/optimization/webpack.ts +29 -31
  40. data/package/plugins/webpack.ts +2 -1
  41. data/package/rspack/index.ts +2 -1
  42. data/package/rules/file.ts +1 -0
  43. data/package/rules/jscommon.ts +1 -0
  44. data/package/utils/helpers.ts +0 -1
  45. data/package/utils/pathValidation.ts +68 -7
  46. data/package/utils/requireOrError.ts +10 -2
  47. data/package/utils/typeGuards.ts +43 -46
  48. data/package/webpack-types.d.ts +2 -2
  49. data/package/webpackDevServerConfig.ts +1 -0
  50. data/package.json +2 -3
  51. data/test/configExporter/integration.test.js +8 -8
  52. data/test/package/configExporter/cli.test.js +440 -0
  53. data/test/package/configExporter/types.test.js +163 -0
  54. data/test/package/configExporter.test.js +271 -7
  55. data/test/package/yamlSerializer.test.js +204 -0
  56. data/test/typescript/pathValidation.test.js +44 -0
  57. data/test/typescript/requireOrError.test.js +49 -0
  58. data/yarn.lock +0 -32
  59. metadata +11 -6
  60. data/.eslintrc.fast.js +0 -40
  61. data/.eslintrc.js +0 -84
  62. data/package-lock.json +0 -13047
data/lib/shakapacker.rb CHANGED
@@ -3,22 +3,86 @@ require "active_support/core_ext/string/inquiry"
3
3
  require "active_support/logger"
4
4
  require "active_support/tagged_logging"
5
5
 
6
+ # = Shakapacker
7
+ #
8
+ # Shakapacker is a Ruby gem that integrates webpack and rspack with Rails applications,
9
+ # providing a modern asset pipeline for JavaScript, CSS, and other web assets.
10
+ #
11
+ # The main Shakapacker module provides singleton-style access to configuration,
12
+ # compilation, and asset manifest functionality. Most methods delegate to a shared
13
+ # {Shakapacker::Instance} object.
14
+ #
15
+ # == Basic Usage
16
+ #
17
+ # # Access configuration
18
+ # Shakapacker.config.source_path
19
+ # #=> Pathname("/path/to/app/packs")
20
+ #
21
+ # # Check if dev server is running
22
+ # Shakapacker.dev_server.running?
23
+ # #=> true
24
+ #
25
+ # # Look up compiled assets
26
+ # Shakapacker.manifest.lookup("application.js")
27
+ # #=> "/packs/application-abc123.js"
28
+ #
29
+ # # Compile assets
30
+ # Shakapacker.compile
31
+ #
32
+ # == Configuration
33
+ #
34
+ # Configuration is loaded from +config/shakapacker.yml+ and can be accessed via
35
+ # {Shakapacker.config}. The configuration determines the source paths, output paths,
36
+ # compilation settings, and dev server options.
37
+ #
38
+ # @see Shakapacker::Configuration
39
+ # @see Shakapacker::Instance
6
40
  module Shakapacker
7
41
  extend self
8
42
 
43
+ # Default environment when RAILS_ENV is not set
9
44
  DEFAULT_ENV = "development".freeze
10
45
  # Environments that use their RAILS_ENV value for NODE_ENV
11
46
  # All other environments (production, staging, etc.) use "production" for webpack optimizations
12
47
  DEV_TEST_ENVS = %w[development test].freeze
13
48
 
49
+ # Sets the shared Shakapacker instance
50
+ #
51
+ # This is primarily used for testing or advanced customization scenarios.
52
+ # In most applications, the default instance is sufficient.
53
+ #
54
+ # @param instance [Shakapacker::Instance] the instance to use
55
+ # @return [Shakapacker::Instance] the instance that was set
56
+ # @api public
14
57
  def instance=(instance)
15
58
  @instance = instance
16
59
  end
17
60
 
61
+ # Returns the shared Shakapacker instance
62
+ #
63
+ # This instance is used by all module-level delegate methods. It provides
64
+ # access to configuration, compilation, manifest lookup, and more.
65
+ #
66
+ # @return [Shakapacker::Instance] the shared instance
67
+ # @api public
18
68
  def instance
19
69
  @instance ||= Shakapacker::Instance.new
20
70
  end
21
71
 
72
+ # Temporarily overrides NODE_ENV for the duration of the block
73
+ #
74
+ # This is useful when you need to perform operations with a specific NODE_ENV
75
+ # value without permanently changing the environment.
76
+ #
77
+ # @param env [String] the NODE_ENV value to use temporarily
78
+ # @yield the block to execute with the temporary NODE_ENV
79
+ # @return [Object] the return value of the block
80
+ # @example
81
+ # Shakapacker.with_node_env("production") do
82
+ # # This code runs with NODE_ENV=production
83
+ # Shakapacker.compile
84
+ # end
85
+ # @api public
22
86
  def with_node_env(env)
23
87
  original = ENV["NODE_ENV"]
24
88
  ENV["NODE_ENV"] = env
@@ -27,13 +91,32 @@ module Shakapacker
27
91
  ENV["NODE_ENV"] = original
28
92
  end
29
93
 
30
- # Set NODE_ENV based on RAILS_ENV if not already set
31
- # - development/test environments use their RAILS_ENV value
32
- # - all other environments (production, staging, etc.) use "production" for webpack optimizations
94
+ # Sets NODE_ENV based on RAILS_ENV if not already set
95
+ #
96
+ # Environment mapping:
97
+ # - +development+ and +test+ environments use their RAILS_ENV value for NODE_ENV
98
+ # - All other environments (+production+, +staging+, etc.) use "production" for webpack optimizations
99
+ #
100
+ # This method is typically called automatically during Rails initialization.
101
+ #
102
+ # @return [String] the NODE_ENV value that was set
103
+ # @api private
33
104
  def ensure_node_env!
34
105
  ENV["NODE_ENV"] ||= DEV_TEST_ENVS.include?(ENV["RAILS_ENV"]) ? ENV["RAILS_ENV"] : "production"
35
106
  end
36
107
 
108
+ # Temporarily redirects Shakapacker logging to STDOUT
109
+ #
110
+ # This is useful for debugging or when you want to see compilation output
111
+ # in the console instead of the Rails log.
112
+ #
113
+ # @yield the block to execute with STDOUT logging
114
+ # @return [Object] the return value of the block
115
+ # @example
116
+ # Shakapacker.ensure_log_goes_to_stdout do
117
+ # Shakapacker.compile
118
+ # end
119
+ # @api public
37
120
  def ensure_log_goes_to_stdout
38
121
  old_logger = Shakapacker.logger
39
122
  Shakapacker.logger = Logger.new(STDOUT)
@@ -42,8 +125,65 @@ module Shakapacker
42
125
  Shakapacker.logger = old_logger
43
126
  end
44
127
 
128
+ # @!method logger
129
+ # Returns the logger instance used by Shakapacker
130
+ # @return [Logger] the logger instance
131
+ # @see Shakapacker::Instance#logger
132
+ # @!method logger=(logger)
133
+ # Sets the logger instance used by Shakapacker
134
+ # @param logger [Logger] the logger to use
135
+ # @return [Logger] the logger that was set
136
+ # @see Shakapacker::Instance#logger=
137
+ # @!method env
138
+ # Returns the current Rails environment as an ActiveSupport::StringInquirer
139
+ # @return [ActiveSupport::StringInquirer] the environment
140
+ # @see Shakapacker::Instance#env
141
+ # @!method inlining_css?
142
+ # Returns whether CSS inlining is enabled
143
+ # @return [Boolean] true if CSS should be inlined
144
+ # @see Shakapacker::Instance#inlining_css?
45
145
  delegate :logger, :logger=, :env, :inlining_css?, to: :instance
146
+
147
+ # @!method config
148
+ # Returns the Shakapacker configuration object
149
+ # @return [Shakapacker::Configuration] the configuration
150
+ # @see Shakapacker::Instance#config
151
+ # @!method compiler
152
+ # Returns the compiler instance for compiling assets
153
+ # @return [Shakapacker::Compiler] the compiler
154
+ # @see Shakapacker::Instance#compiler
155
+ # @!method manifest
156
+ # Returns the manifest instance for looking up compiled assets
157
+ # @return [Shakapacker::Manifest] the manifest
158
+ # @see Shakapacker::Instance#manifest
159
+ # @!method commands
160
+ # Returns the commands instance for build operations
161
+ # @return [Shakapacker::Commands] the commands object
162
+ # @see Shakapacker::Instance#commands
163
+ # @!method dev_server
164
+ # Returns the dev server instance for querying server status
165
+ # @return [Shakapacker::DevServer] the dev server
166
+ # @see Shakapacker::Instance#dev_server
46
167
  delegate :config, :compiler, :manifest, :commands, :dev_server, to: :instance
168
+
169
+ # @!method bootstrap
170
+ # Creates the default configuration files and directory structure
171
+ # @return [void]
172
+ # @see Shakapacker::Commands#bootstrap
173
+ # @!method clean(count = nil, age = nil)
174
+ # Removes old compiled packs, keeping the most recent versions
175
+ # @param count [Integer, nil] number of versions to keep per entry
176
+ # @param age [Integer, nil] maximum age in seconds for packs to keep
177
+ # @return [void]
178
+ # @see Shakapacker::Commands#clean
179
+ # @!method clobber
180
+ # Removes all compiled packs
181
+ # @return [void]
182
+ # @see Shakapacker::Commands#clobber
183
+ # @!method compile
184
+ # Compiles all webpack/rspack packs
185
+ # @return [Boolean] true if compilation succeeded
186
+ # @see Shakapacker::Commands#compile
47
187
  delegate :bootstrap, :clean, :clobber, :compile, to: :commands
48
188
  end
49
189
 
@@ -11,7 +11,7 @@ namespace :shakapacker do
11
11
  • Required and optional npm dependencies
12
12
  • JavaScript transpiler (Babel, SWC, esbuild) configuration
13
13
  • CSS, CSS Modules, and stylesheet preprocessor setup
14
- • Binstubs presence (shakapacker, shakapacker-dev-server, export-bundler-config)
14
+ • Binstubs presence (shakapacker, shakapacker-dev-server, shakapacker-config)
15
15
  • Version consistency between gem and npm package
16
16
  • Legacy Webpacker file detection
17
17
 
@@ -42,18 +42,18 @@ namespace :shakapacker do
42
42
  Note: When using 'rake', you must use '--' to separate rake options from task arguments.
43
43
  Example: rake shakapacker:export_bundler_config -- --doctor
44
44
 
45
- The task automatically falls back to the gem version if bin/export-bundler-config
45
+ The task automatically falls back to the gem version if bin/shakapacker-config
46
46
  binstub is not installed. To install all binstubs, run: rails shakapacker:binstubs
47
47
  DESC
48
48
  task :export_bundler_config do
49
49
  # Try to use the binstub if it exists, otherwise use the gem's version
50
- bin_path = Rails.root.join("bin/export-bundler-config")
50
+ bin_path = Rails.root.join("bin/shakapacker-config")
51
51
 
52
52
  unless File.exist?(bin_path)
53
53
  # Binstub not installed, use the gem's version directly
54
- gem_bin_path = File.expand_path("../../install/bin/export-bundler-config", __dir__)
54
+ gem_bin_path = File.expand_path("../../install/bin/shakapacker-config", __dir__)
55
55
 
56
- $stderr.puts "Note: bin/export-bundler-config binstub not found."
56
+ $stderr.puts "Note: bin/shakapacker-config binstub not found."
57
57
  $stderr.puts "Using gem version directly. To install the binstub, run: rake shakapacker:binstubs"
58
58
  $stderr.puts ""
59
59
 
data/package/config.ts CHANGED
@@ -55,7 +55,6 @@ if (existsSync(configPath)) {
55
55
  const envAppConfig = appYmlObject[railsEnv]
56
56
 
57
57
  if (!envAppConfig) {
58
- /* eslint no-console:0 */
59
58
  console.warn(
60
59
  `[SHAKAPACKER WARNING] Environment '${railsEnv}' not found in ${configPath}\n` +
61
60
  `Available environments: ${Object.keys(appYmlObject).join(", ")}\n` +
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process"
2
2
  import { existsSync } from "fs"
3
- import { resolve, relative, sep } from "path"
3
+ import { resolve, relative } from "path"
4
4
  import { ResolvedBuildConfig, BuildValidationResult } from "./types"
5
5
 
6
6
  export interface ValidatorOptions {
@@ -173,7 +173,7 @@ export class BuildValidator {
173
173
  * @returns The resolved absolute path to the config file
174
174
  * @throws Error if the config file does not exist or is outside appRoot
175
175
  */
176
- private validateConfigPath(
176
+ private static validateConfigPath(
177
177
  configFile: string,
178
178
  appRoot: string,
179
179
  buildName: string
@@ -222,7 +222,7 @@ export class BuildValidator {
222
222
  const isHMR =
223
223
  build.environment.WEBPACK_SERVE === "true" ||
224
224
  build.environment.HMR === "true"
225
- const bundler = build.bundler
225
+ const { bundler } = build
226
226
 
227
227
  if (isHMR) {
228
228
  return this.validateHMRBuild(build, appRoot, bundler)
@@ -273,7 +273,7 @@ export class BuildValidator {
273
273
  // Add config file if specified
274
274
  if (build.configFile) {
275
275
  try {
276
- const configPath = this.validateConfigPath(
276
+ const configPath = BuildValidator.validateConfigPath(
277
277
  build.configFile,
278
278
  appRoot,
279
279
  build.name
@@ -301,7 +301,7 @@ export class BuildValidator {
301
301
  args.push(...build.bundlerEnvArgs)
302
302
  }
303
303
 
304
- return new Promise((resolve) => {
304
+ return new Promise((resolvePromise) => {
305
305
  const child = spawn(devServerBin, args, {
306
306
  cwd: appRoot,
307
307
  env: this.filterEnvironment(build.environment),
@@ -316,7 +316,7 @@ export class BuildValidator {
316
316
  const resolveOnce = (res: BuildValidationResult) => {
317
317
  if (!resolved) {
318
318
  resolved = true
319
- resolve(res)
319
+ resolvePromise(res)
320
320
  }
321
321
  }
322
322
 
@@ -397,8 +397,8 @@ export class BuildValidator {
397
397
  })
398
398
  }
399
399
 
400
- child.stdout?.on("data", (data) => processOutput(data))
401
- child.stderr?.on("data", (data) => processOutput(data))
400
+ child.stdout?.on("data", (data: Buffer) => processOutput(data))
401
+ child.stderr?.on("data", (data: Buffer) => processOutput(data))
402
402
 
403
403
  child.on("exit", (code) => {
404
404
  clearTimeout(timeoutId)
@@ -428,7 +428,7 @@ export class BuildValidator {
428
428
 
429
429
  // Check for specific error codes and provide actionable guidance
430
430
  if ("code" in err) {
431
- const code = (err as NodeJS.ErrnoException).code
431
+ const { code } = err as NodeJS.ErrnoException
432
432
  if (code === "ENOENT") {
433
433
  errorMessage += `. Binary not found. Install with: npm install -D ${devServerCmd}`
434
434
  } else if (code === "EMFILE" || code === "ENFILE") {
@@ -484,7 +484,7 @@ export class BuildValidator {
484
484
  // Add config file if specified
485
485
  if (build.configFile) {
486
486
  try {
487
- const configPath = this.validateConfigPath(
487
+ const configPath = BuildValidator.validateConfigPath(
488
488
  build.configFile,
489
489
  appRoot,
490
490
  build.name
@@ -515,7 +515,7 @@ export class BuildValidator {
515
515
  // Add --json for structured output (helps parse errors)
516
516
  args.push("--json")
517
517
 
518
- return new Promise((resolve) => {
518
+ return new Promise((resolvePromise) => {
519
519
  const child = spawn(bundlerBin, args, {
520
520
  cwd: appRoot,
521
521
  env: this.filterEnvironment(build.environment),
@@ -534,7 +534,7 @@ export class BuildValidator {
534
534
  `Timeout: ${bundler} did not complete within ${this.options.timeout}ms.`
535
535
  )
536
536
  child.kill("SIGTERM")
537
- resolve(result)
537
+ resolvePromise(result)
538
538
  }, this.options.timeout)
539
539
 
540
540
  child.stdout?.on("data", (data: Buffer) => {
@@ -603,7 +603,7 @@ export class BuildValidator {
603
603
 
604
604
  // Parse JSON output
605
605
  try {
606
- const jsonOutput: WebpackJsonOutput = JSON.parse(stdoutData)
606
+ const jsonOutput = JSON.parse(stdoutData) as WebpackJsonOutput
607
607
 
608
608
  // Extract output path if available
609
609
  if (jsonOutput.outputPath) {
@@ -613,10 +613,22 @@ export class BuildValidator {
613
613
  // Check for errors in webpack/rspack JSON output
614
614
  if (jsonOutput.errors && jsonOutput.errors.length > 0) {
615
615
  jsonOutput.errors.forEach((error) => {
616
- const errorMsg =
617
- typeof error === "string"
618
- ? error
619
- : error.message || String(error)
616
+ let errorMsg: string
617
+ if (typeof error === "string") {
618
+ errorMsg = error
619
+ } else if (error.message) {
620
+ errorMsg = error.message
621
+ } else {
622
+ // Attempt to extract useful info from malformed error using all enumerable props
623
+ try {
624
+ errorMsg = JSON.stringify(
625
+ error,
626
+ Object.getOwnPropertyNames(error)
627
+ )
628
+ } catch {
629
+ errorMsg = "[Error object with no message]"
630
+ }
631
+ }
620
632
  result.errors.push(errorMsg)
621
633
  // Also add to output for visibility
622
634
  if (!this.options.verbose) {
@@ -628,10 +640,22 @@ export class BuildValidator {
628
640
  // Check for warnings
629
641
  if (jsonOutput.warnings && jsonOutput.warnings.length > 0) {
630
642
  jsonOutput.warnings.forEach((warning) => {
631
- const warningMsg =
632
- typeof warning === "string"
633
- ? warning
634
- : warning.message || String(warning)
643
+ let warningMsg: string
644
+ if (typeof warning === "string") {
645
+ warningMsg = warning
646
+ } else if (warning.message) {
647
+ warningMsg = warning.message
648
+ } else {
649
+ // Attempt to extract useful info from malformed warning using all enumerable props
650
+ try {
651
+ warningMsg = JSON.stringify(
652
+ warning,
653
+ Object.getOwnPropertyNames(warning)
654
+ )
655
+ } catch {
656
+ warningMsg = "[Warning object with no message]"
657
+ }
658
+ }
635
659
  result.warnings.push(warningMsg)
636
660
  })
637
661
  }
@@ -683,7 +707,7 @@ export class BuildValidator {
683
707
  result.output.push(stderrData)
684
708
  }
685
709
 
686
- resolve(result)
710
+ resolvePromise(result)
687
711
  })
688
712
 
689
713
  child.on("error", (err) => {
@@ -693,7 +717,7 @@ export class BuildValidator {
693
717
 
694
718
  // Check for specific error codes and provide actionable guidance
695
719
  if ("code" in err) {
696
- const code = (err as NodeJS.ErrnoException).code
720
+ const { code } = err as NodeJS.ErrnoException
697
721
  if (code === "ENOENT") {
698
722
  errorMessage += `. Binary not found. Install with: npm install -D ${bundler}`
699
723
  } else if (code === "EMFILE" || code === "ENFILE") {
@@ -704,7 +728,7 @@ export class BuildValidator {
704
728
  }
705
729
 
706
730
  result.errors.push(errorMessage)
707
- resolve(result)
731
+ resolvePromise(result)
708
732
  })
709
733
  })
710
734
  }
@@ -776,19 +800,19 @@ export class BuildValidator {
776
800
  formatResults(results: BuildValidationResult[]): string {
777
801
  const lines: string[] = []
778
802
 
779
- lines.push("\n" + "=".repeat(80))
803
+ lines.push(`\n${"=".repeat(80)}`)
780
804
  lines.push("🔍 Build Validation Results")
781
- lines.push("=".repeat(80) + "\n")
805
+ lines.push(`${"=".repeat(80)}\n`)
782
806
 
783
- let totalBuilds = results.length
807
+ const totalBuilds = results.length
784
808
  let successCount = 0
785
809
  let failureCount = 0
786
810
 
787
811
  results.forEach((result) => {
788
812
  if (result.success) {
789
- successCount++
813
+ successCount += 1
790
814
  } else {
791
- failureCount++
815
+ failureCount += 1
792
816
  }
793
817
 
794
818
  const icon = result.success ? "✅" : "❌"