shakapacker 8.4.0 → 9.0.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.eslintrc.fast.js +40 -0
  4. data/.eslintrc.js +48 -0
  5. data/.github/STATUS.md +1 -0
  6. data/.github/workflows/claude-code-review.yml +54 -0
  7. data/.github/workflows/claude.yml +50 -0
  8. data/.github/workflows/dummy.yml +8 -4
  9. data/.github/workflows/generator.yml +17 -14
  10. data/.github/workflows/node.yml +23 -1
  11. data/.github/workflows/ruby.yml +11 -0
  12. data/.github/workflows/test-bundlers.yml +170 -0
  13. data/.gitignore +17 -0
  14. data/.husky/pre-commit +2 -0
  15. data/.npmignore +56 -0
  16. data/.prettierignore +3 -0
  17. data/.rubocop.yml +1 -0
  18. data/.yalcignore +26 -0
  19. data/CHANGELOG.md +156 -18
  20. data/CLAUDE.md +29 -0
  21. data/CONTRIBUTING.md +138 -20
  22. data/Gemfile.lock +3 -3
  23. data/README.md +130 -5
  24. data/Rakefile +39 -4
  25. data/TODO.md +50 -0
  26. data/TODO_v9.md +87 -0
  27. data/conductor-setup.sh +70 -0
  28. data/conductor.json +7 -0
  29. data/docs/cdn_setup.md +379 -0
  30. data/docs/css-modules-export-mode.md +512 -0
  31. data/docs/deployment.md +10 -1
  32. data/docs/optional-peer-dependencies.md +198 -0
  33. data/docs/peer-dependencies.md +60 -0
  34. data/docs/rspack.md +190 -0
  35. data/docs/rspack_migration_guide.md +202 -0
  36. data/docs/transpiler-migration.md +188 -0
  37. data/docs/transpiler-performance.md +179 -0
  38. data/docs/troubleshooting.md +5 -0
  39. data/docs/typescript-migration.md +378 -0
  40. data/docs/typescript.md +99 -0
  41. data/docs/using_esbuild_loader.md +3 -3
  42. data/docs/using_swc_loader.md +5 -3
  43. data/docs/v6_upgrade.md +10 -0
  44. data/docs/v9_upgrade.md +413 -0
  45. data/lib/install/bin/shakapacker +3 -5
  46. data/lib/install/config/rspack/rspack.config.js +6 -0
  47. data/lib/install/config/rspack/rspack.config.ts +7 -0
  48. data/lib/install/config/shakapacker.yml +12 -2
  49. data/lib/install/config/webpack/webpack.config.ts +7 -0
  50. data/lib/install/package.json +38 -0
  51. data/lib/install/template.rb +194 -44
  52. data/lib/shakapacker/configuration.rb +141 -0
  53. data/lib/shakapacker/dev_server_runner.rb +25 -5
  54. data/lib/shakapacker/doctor.rb +844 -0
  55. data/lib/shakapacker/manifest.rb +4 -2
  56. data/lib/shakapacker/rspack_runner.rb +19 -0
  57. data/lib/shakapacker/runner.rb +144 -4
  58. data/lib/shakapacker/swc_migrator.rb +376 -0
  59. data/lib/shakapacker/utils/manager.rb +2 -0
  60. data/lib/shakapacker/version.rb +1 -1
  61. data/lib/shakapacker/version_checker.rb +1 -1
  62. data/lib/shakapacker/webpack_runner.rb +4 -42
  63. data/lib/shakapacker.rb +2 -1
  64. data/lib/tasks/shakapacker/doctor.rake +8 -0
  65. data/lib/tasks/shakapacker/install.rake +12 -2
  66. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  67. data/lib/tasks/shakapacker.rake +1 -0
  68. data/package/.npmignore +4 -0
  69. data/package/babel/preset.ts +56 -0
  70. data/package/config.ts +175 -0
  71. data/package/{dev_server.js → dev_server.ts} +8 -5
  72. data/package/env.ts +92 -0
  73. data/package/environments/base.ts +138 -0
  74. data/package/environments/development.ts +90 -0
  75. data/package/environments/production.ts +80 -0
  76. data/package/environments/test.ts +53 -0
  77. data/package/environments/types.ts +90 -0
  78. data/package/esbuild/index.ts +42 -0
  79. data/package/index.d.ts +3 -97
  80. data/package/index.ts +52 -0
  81. data/package/loaders.d.ts +28 -0
  82. data/package/optimization/rspack.ts +36 -0
  83. data/package/optimization/webpack.ts +57 -0
  84. data/package/plugins/rspack.ts +103 -0
  85. data/package/plugins/webpack.ts +62 -0
  86. data/package/rspack/index.ts +64 -0
  87. data/package/rules/{babel.js → babel.ts} +2 -2
  88. data/package/rules/{coffee.js → coffee.ts} +1 -1
  89. data/package/rules/css.ts +3 -0
  90. data/package/rules/{erb.js → erb.ts} +1 -1
  91. data/package/rules/esbuild.ts +10 -0
  92. data/package/rules/file.ts +40 -0
  93. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  94. data/package/rules/{less.js → less.ts} +4 -4
  95. data/package/rules/raw.ts +25 -0
  96. data/package/rules/rspack.ts +176 -0
  97. data/package/rules/{sass.js → sass.ts} +7 -3
  98. data/package/rules/{stylus.js → stylus.ts} +4 -8
  99. data/package/rules/swc.ts +10 -0
  100. data/package/rules/{index.js → webpack.ts} +1 -1
  101. data/package/swc/index.ts +54 -0
  102. data/package/types/README.md +87 -0
  103. data/package/types/index.ts +60 -0
  104. data/package/types.ts +108 -0
  105. data/package/utils/configPath.ts +6 -0
  106. data/package/utils/debug.ts +49 -0
  107. data/package/utils/defaultConfigPath.ts +4 -0
  108. data/package/utils/errorCodes.ts +219 -0
  109. data/package/utils/errorHelpers.ts +143 -0
  110. data/package/utils/getStyleRule.ts +64 -0
  111. data/package/utils/helpers.ts +85 -0
  112. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  113. data/package/utils/pathValidation.ts +139 -0
  114. data/package/utils/requireOrError.ts +15 -0
  115. data/package/utils/snakeToCamelCase.ts +5 -0
  116. data/package/utils/typeGuards.ts +342 -0
  117. data/package/utils/validateDependencies.ts +61 -0
  118. data/package/webpack-types.d.ts +33 -0
  119. data/package/webpackDevServerConfig.ts +117 -0
  120. data/package.json +134 -9
  121. data/scripts/remove-use-strict.js +45 -0
  122. data/scripts/type-check-no-emit.js +27 -0
  123. data/test/package/config.test.js +3 -0
  124. data/test/package/env.test.js +42 -7
  125. data/test/package/environments/base.test.js +5 -1
  126. data/test/package/rules/babel.test.js +16 -0
  127. data/test/package/rules/esbuild.test.js +1 -1
  128. data/test/package/rules/raw.test.js +40 -7
  129. data/test/package/rules/swc.test.js +1 -1
  130. data/test/package/rules/webpack.test.js +35 -0
  131. data/test/package/staging.test.js +4 -3
  132. data/test/package/transpiler-defaults.test.js +127 -0
  133. data/test/peer-dependencies.sh +85 -0
  134. data/test/scripts/remove-use-strict.test.js +125 -0
  135. data/test/typescript/build.test.js +118 -0
  136. data/test/typescript/environments.test.js +107 -0
  137. data/test/typescript/pathValidation.test.js +142 -0
  138. data/test/typescript/securityValidation.test.js +182 -0
  139. data/tools/README.md +124 -0
  140. data/tools/css-modules-v9-codemod.js +179 -0
  141. data/tsconfig.eslint.json +16 -0
  142. data/tsconfig.json +38 -0
  143. data/yarn.lock +2704 -767
  144. metadata +111 -41
  145. data/package/babel/preset.js +0 -48
  146. data/package/config.js +0 -56
  147. data/package/env.js +0 -48
  148. data/package/environments/base.js +0 -171
  149. data/package/environments/development.js +0 -13
  150. data/package/environments/production.js +0 -88
  151. data/package/environments/test.js +0 -3
  152. data/package/esbuild/index.js +0 -40
  153. data/package/index.js +0 -40
  154. data/package/rules/css.js +0 -3
  155. data/package/rules/esbuild.js +0 -10
  156. data/package/rules/file.js +0 -29
  157. data/package/rules/raw.js +0 -5
  158. data/package/rules/swc.js +0 -10
  159. data/package/swc/index.js +0 -50
  160. data/package/utils/configPath.js +0 -4
  161. data/package/utils/defaultConfigPath.js +0 -2
  162. data/package/utils/getStyleRule.js +0 -40
  163. data/package/utils/helpers.js +0 -62
  164. data/package/utils/snakeToCamelCase.js +0 -5
  165. data/package/webpackDevServerConfig.js +0 -71
  166. data/test/package/rules/index.test.js +0 -16
data/lib/shakapacker.rb CHANGED
@@ -6,7 +6,7 @@ require "active_support/tagged_logging"
6
6
  module Shakapacker
7
7
  extend self
8
8
 
9
- DEFAULT_ENV = "production".freeze
9
+ DEFAULT_ENV = "development".freeze
10
10
 
11
11
  def instance=(instance)
12
12
  @instance = instance
@@ -44,6 +44,7 @@ require_relative "shakapacker/manifest"
44
44
  require_relative "shakapacker/compiler"
45
45
  require_relative "shakapacker/commands"
46
46
  require_relative "shakapacker/dev_server"
47
+ require_relative "shakapacker/doctor"
47
48
  require_relative "shakapacker/deprecation_helper"
48
49
 
49
50
  require_relative "shakapacker/railtie" if defined?(Rails)
@@ -0,0 +1,8 @@
1
+ require "shakapacker/doctor"
2
+
3
+ namespace :shakapacker do
4
+ desc "Checks for common Shakapacker configuration issues and missing dependencies"
5
+ task doctor: :environment do
6
+ Shakapacker::Doctor.new.run
7
+ end
8
+ end
@@ -2,10 +2,20 @@ install_template_path = File.expand_path("../../install/template.rb", __dir__).f
2
2
  bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin")
3
3
 
4
4
  namespace :shakapacker do
5
- desc "Install Shakapacker in this application"
6
- task install: [:check_node] do |task|
5
+ desc "Install Shakapacker in this application (use SHAKAPACKER_ASSETS_BUNDLER=rspack for Rspack, --typescript for TypeScript)"
6
+ task :install, [:bundler, :typescript] => [:check_node] do |task, args|
7
7
  Shakapacker::Configuration.installing = true
8
8
 
9
+ if args[:bundler] == "rspack" || ENV["SHAKAPACKER_ASSETS_BUNDLER"] == "rspack"
10
+ ENV["SHAKAPACKER_ASSETS_BUNDLER"] = "rspack"
11
+ end
12
+
13
+ # Set typescript flag if passed as argument
14
+ # Accepts: typescript, true, or any truthy value
15
+ if args[:typescript] && args[:typescript] != "false"
16
+ ENV["SHAKAPACKER_USE_TYPESCRIPT"] = "true"
17
+ end
18
+
9
19
  prefix = task.name.split(/#|shakapacker:install/).first
10
20
 
11
21
  if Rails::VERSION::MAJOR >= 5
@@ -0,0 +1,13 @@
1
+ require "shakapacker/swc_migrator"
2
+
3
+ namespace :shakapacker do
4
+ desc "Migrate from Babel to SWC transpiler"
5
+ task :migrate_to_swc do
6
+ Shakapacker::SwcMigrator.new(Rails.root).migrate_to_swc
7
+ end
8
+
9
+ desc "Remove Babel packages after migrating to SWC"
10
+ task :clean_babel_packages do
11
+ Shakapacker::SwcMigrator.new(Rails.root).clean_babel_packages
12
+ end
13
+ end
@@ -9,6 +9,7 @@ tasks = {
9
9
  "shakapacker:check_binstubs" => "Verifies that bin/shakapacker is present",
10
10
  "shakapacker:binstubs" => "Installs Shakapacker binstubs in this application",
11
11
  "shakapacker:verify_install" => "Verifies if Shakapacker is installed",
12
+ "shakapacker:doctor" => "Checks for configuration issues and missing dependencies"
12
13
  }.freeze
13
14
 
14
15
  desc "Lists all available tasks in Shakapacker"
@@ -0,0 +1,4 @@
1
+ # Exclude TypeScript source files from the package directory
2
+ *.ts
3
+ # But keep the TypeScript declaration files
4
+ !*.d.ts
@@ -0,0 +1,56 @@
1
+ import { moduleExists, packageFullVersion } from "../utils/helpers"
2
+ import type { ConfigAPI, PluginItem } from "@babel/core"
3
+
4
+ const CORE_JS_VERSION_REGEX = /^\d+\.\d+/
5
+
6
+ const coreJsVersion = (): string => {
7
+ try {
8
+ const version = packageFullVersion("core-js").match(CORE_JS_VERSION_REGEX)
9
+ return version?.[0] ?? "3.8"
10
+ } catch (e) {
11
+ const error = e as NodeJS.ErrnoException
12
+ if (error.code !== "MODULE_NOT_FOUND") {
13
+ throw e
14
+ }
15
+
16
+ return "3.8"
17
+ }
18
+ }
19
+
20
+ export = function config(api: ConfigAPI): { presets: PluginItem[]; plugins: PluginItem[] } {
21
+ const validEnv = ["development", "test", "production"]
22
+ const currentEnv = api.env()
23
+ const isDevelopmentEnv = api.env("development")
24
+ const isProductionEnv = api.env("production")
25
+ const isTestEnv = api.env("test")
26
+
27
+ if (!validEnv.includes(currentEnv)) {
28
+ throw new Error(
29
+ `Please specify a valid NODE_ENV or BABEL_ENV environment variable. Valid values are "development", "test", and "production". Instead, received: "${currentEnv}".`
30
+ )
31
+ }
32
+
33
+ const presets: PluginItem[] = [
34
+ isTestEnv && ["@babel/preset-env", { targets: { node: "current" } }],
35
+ (isProductionEnv || isDevelopmentEnv) && [
36
+ "@babel/preset-env",
37
+ {
38
+ useBuiltIns: "entry",
39
+ corejs: coreJsVersion(),
40
+ modules: "auto",
41
+ bugfixes: true,
42
+ exclude: ["transform-typeof-symbol"]
43
+ }
44
+ ],
45
+ moduleExists("@babel/preset-typescript") && "@babel/preset-typescript"
46
+ ].filter(Boolean) as PluginItem[]
47
+
48
+ const plugins: PluginItem[] = [["@babel/plugin-transform-runtime", { helpers: false }]].filter(
49
+ Boolean
50
+ ) as PluginItem[]
51
+
52
+ return {
53
+ presets,
54
+ plugins
55
+ }
56
+ }
data/package/config.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { resolve } from "path"
2
+ import { load } from "js-yaml"
3
+ import { existsSync, readFileSync } from "fs"
4
+ import { merge } from "webpack-merge"
5
+ const { ensureTrailingSlash } = require("./utils/helpers")
6
+ const { railsEnv } = require("./env")
7
+ const configPath = require("./utils/configPath")
8
+ const defaultConfigPath = require("./utils/defaultConfigPath")
9
+ import { Config, YamlConfig, LegacyConfig } from "./types"
10
+ const { isValidYamlConfig, createConfigValidationError, isPartialConfig, isValidConfig } = require("./utils/typeGuards")
11
+ const { isFileNotFoundError, createFileOperationError } = require("./utils/errorHelpers")
12
+
13
+ const loadAndValidateYaml = (path: string): YamlConfig => {
14
+ const fileContent = readFileSync(path, "utf8")
15
+ const yamlContent = load(fileContent)
16
+
17
+ if (!isValidYamlConfig(yamlContent)) {
18
+ throw createConfigValidationError(path, railsEnv, "Invalid YAML structure")
19
+ }
20
+
21
+ return yamlContent as YamlConfig
22
+ }
23
+
24
+ const getDefaultConfig = (): Partial<Config> => {
25
+ try {
26
+ const defaultConfig = loadAndValidateYaml(defaultConfigPath)
27
+ return defaultConfig[railsEnv] || defaultConfig.production || {}
28
+ } catch (error) {
29
+ if (isFileNotFoundError(error)) {
30
+ throw createFileOperationError(
31
+ 'read',
32
+ defaultConfigPath,
33
+ `Default configuration not found at ${defaultConfigPath}. Please ensure Shakapacker is properly installed. You may need to run 'yarn add shakapacker' or 'npm install shakapacker'.`
34
+ )
35
+ }
36
+ throw error
37
+ }
38
+ }
39
+
40
+ const defaults = getDefaultConfig()
41
+ let config: Config
42
+
43
+ if (existsSync(configPath)) {
44
+ try {
45
+ const appYmlObject = loadAndValidateYaml(configPath)
46
+
47
+ const envAppConfig = appYmlObject[railsEnv]
48
+
49
+ if (!envAppConfig) {
50
+ /* eslint no-console:0 */
51
+ console.warn(
52
+ `[SHAKAPACKER WARNING] Environment '${railsEnv}' not found in ${configPath}\n` +
53
+ `Available environments: ${Object.keys(appYmlObject).join(', ')}\n` +
54
+ `Using 'production' configuration as fallback.\n\n` +
55
+ `To fix this, either:\n` +
56
+ ` - Add a '${railsEnv}' section to your shakapacker.yml\n` +
57
+ ` - Set RAILS_ENV to one of the available environments\n` +
58
+ ` - Copy settings from another environment as a starting point`
59
+ )
60
+ }
61
+
62
+ // Merge returns the merged type
63
+ const mergedConfig = merge(defaults, envAppConfig || {})
64
+
65
+ // Validate merged config before type assertion
66
+ if (!isPartialConfig(mergedConfig)) {
67
+ throw createConfigValidationError(
68
+ configPath,
69
+ railsEnv,
70
+ `Invalid configuration structure in ${configPath}. Please check your shakapacker.yml syntax and ensure all required fields are properly defined.`
71
+ )
72
+ }
73
+
74
+ // After merging with defaults, config should be complete
75
+ // Use type assertion only after validation
76
+ config = mergedConfig as Config
77
+ } catch (error) {
78
+ if (isFileNotFoundError(error)) {
79
+ // File not found is OK, use defaults
80
+ if (!isPartialConfig(defaults)) {
81
+ throw createConfigValidationError(
82
+ defaultConfigPath,
83
+ railsEnv,
84
+ `Invalid default configuration. This may indicate a corrupted Shakapacker installation. Try reinstalling with 'yarn add shakapacker --force'.`
85
+ )
86
+ }
87
+ // Using defaults only, might be partial
88
+ config = defaults as Config
89
+ } else {
90
+ throw error
91
+ }
92
+ }
93
+ } else {
94
+ // No user config, use defaults
95
+ if (!isPartialConfig(defaults)) {
96
+ throw createConfigValidationError(
97
+ defaultConfigPath,
98
+ railsEnv,
99
+ `Invalid default configuration. This may indicate a corrupted Shakapacker installation. Try reinstalling with 'yarn add shakapacker --force'.`
100
+ )
101
+ }
102
+ // Using defaults only, might be partial
103
+ config = defaults as Config
104
+ }
105
+
106
+ config.outputPath = resolve(config.public_root_path, config.public_output_path)
107
+
108
+ // Ensure that the publicPath includes our asset host so dynamic imports
109
+ // (code-splitting chunks and static assets) load from the CDN instead of a relative path.
110
+ const getPublicPath = (): string => {
111
+ const rootUrl = ensureTrailingSlash(process.env.SHAKAPACKER_ASSET_HOST || "/")
112
+ return `${rootUrl}${config.public_output_path}/`
113
+ }
114
+
115
+ config.publicPath = getPublicPath()
116
+ config.publicPathWithoutCDN = `/${config.public_output_path}/`
117
+
118
+ if (config.manifest_path) {
119
+ config.manifestPath = resolve(config.manifest_path)
120
+ } else {
121
+ config.manifestPath = resolve(config.outputPath, "manifest.json")
122
+ }
123
+ // Ensure no duplicate hash functions exist in the returned config object
124
+ if (config.integrity?.hash_functions) {
125
+ config.integrity.hash_functions = [...new Set(config.integrity.hash_functions)]
126
+ }
127
+
128
+ // Ensure assets_bundler has a default value
129
+ if (!config.assets_bundler) {
130
+ config.assets_bundler = "webpack"
131
+ }
132
+
133
+ // Allow ENV variable to override assets_bundler
134
+ if (process.env.SHAKAPACKER_ASSETS_BUNDLER) {
135
+ config.assets_bundler = process.env.SHAKAPACKER_ASSETS_BUNDLER
136
+ }
137
+
138
+ // Define clear defaults
139
+ // Keep Babel as default for webpack to maintain backward compatibility
140
+ // Use SWC for rspack as it's a newer bundler where we can set modern defaults
141
+ const DEFAULT_JAVASCRIPT_TRANSPILER =
142
+ config.assets_bundler === "rspack" ? "swc" : "babel"
143
+
144
+ // Backward compatibility: Check for webpack_loader using proper type guard
145
+ function hasWebpackLoader(obj: unknown): obj is Config & { webpack_loader: string } {
146
+ return (
147
+ typeof obj === 'object' &&
148
+ obj !== null &&
149
+ 'webpack_loader' in obj &&
150
+ typeof (obj as Record<string, unknown>).webpack_loader === 'string'
151
+ )
152
+ }
153
+
154
+ // Allow environment variable to override javascript_transpiler
155
+ if (process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER) {
156
+ config.javascript_transpiler = process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
157
+ } else if (hasWebpackLoader(config) && !config.javascript_transpiler) {
158
+ console.warn(
159
+ "[SHAKAPACKER DEPRECATION] The 'webpack_loader' configuration option is deprecated.\n" +
160
+ "Please use 'javascript_transpiler' instead as it better reflects its purpose of configuring JavaScript transpilation regardless of the bundler used."
161
+ )
162
+ config.javascript_transpiler = config.webpack_loader
163
+ } else if (!config.javascript_transpiler) {
164
+ config.javascript_transpiler = DEFAULT_JAVASCRIPT_TRANSPILER
165
+ }
166
+
167
+ // Ensure webpack_loader is always available for backward compatibility
168
+ Object.defineProperty(config, 'webpack_loader', {
169
+ value: config.javascript_transpiler,
170
+ writable: true,
171
+ enumerable: true,
172
+ configurable: true
173
+ })
174
+
175
+ export = config
@@ -1,23 +1,26 @@
1
1
  // These are the raw shakapacker dev server config settings from the YML file with ENV overrides applied.
2
2
  const { isBoolean } = require("./utils/helpers")
3
3
  const config = require("./config")
4
+ import { DevServerConfig } from "./types"
4
5
 
5
- const envFetch = (key) => {
6
+ const envFetch = (key: string): string | boolean | undefined => {
6
7
  const value = process.env[key]
8
+ if (!value) return undefined
7
9
  return isBoolean(value) ? JSON.parse(value) : value
8
10
  }
9
11
 
10
- const devServerConfig = config.dev_server
12
+ const devServerConfig: DevServerConfig | undefined = config.dev_server
11
13
 
12
14
  if (devServerConfig) {
13
- const envPrefix = config.dev_server.env_prefix || "SHAKAPACKER_DEV_SERVER"
15
+ const envPrefix = devServerConfig.env_prefix || "SHAKAPACKER_DEV_SERVER"
14
16
 
15
17
  Object.keys(devServerConfig).forEach((key) => {
16
18
  const envValue = envFetch(`${envPrefix}_${key.toUpperCase()}`)
17
19
  if (envValue !== undefined) {
18
- devServerConfig[key] = envValue
20
+ // Use bracket notation to avoid ASI issues
21
+ (devServerConfig as Record<string, unknown>)[key] = envValue
19
22
  }
20
23
  })
21
24
  }
22
25
 
23
- module.exports = devServerConfig || {}
26
+ export = devServerConfig || {}
data/package/env.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { load } from "js-yaml"
2
+ import { readFileSync } from "fs"
3
+ const defaultConfigPath = require("./utils/defaultConfigPath")
4
+ const configPath = require("./utils/configPath")
5
+ const { isFileNotFoundError } = require("./utils/errorHelpers")
6
+ const { sanitizeEnvValue } = require("./utils/pathValidation")
7
+
8
+ const NODE_ENVIRONMENTS = ["development", "production", "test"] as const
9
+
10
+ // Sanitize environment variables to prevent injection
11
+ const initialRailsEnv = sanitizeEnvValue(process.env.RAILS_ENV)
12
+ const rawNodeEnv = sanitizeEnvValue(process.env.NODE_ENV)
13
+
14
+ // Default NODE_ENV based on RAILS_ENV to match bin/shakapacker behavior (see lib/shakapacker/runner.rb:27)
15
+ // - RAILS_ENV=production → DEFAULT="production" (safe for production builds)
16
+ // - RAILS_ENV=development, test, staging, or unset → DEFAULT="development" (good DX for dev server)
17
+ // This ensures the dev server works out of the box without requiring NODE_ENV to be set explicitly
18
+ const DEFAULT = initialRailsEnv === "production" ? "production" : "development"
19
+
20
+ // Validate NODE_ENV strictly
21
+ const nodeEnv =
22
+ rawNodeEnv &&
23
+ NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
24
+ ? rawNodeEnv
25
+ : DEFAULT
26
+
27
+ // Log warning if NODE_ENV was invalid
28
+ if (
29
+ rawNodeEnv &&
30
+ !NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
31
+ ) {
32
+ console.warn(
33
+ `[SHAKAPACKER WARNING] Invalid NODE_ENV value: ${rawNodeEnv}. ` +
34
+ `Valid values are: ${NODE_ENVIRONMENTS.join(", ")}. Using default: ${DEFAULT}`
35
+ )
36
+ }
37
+
38
+ const isProduction = nodeEnv === "production"
39
+ const isDevelopment = nodeEnv === "development"
40
+
41
+ interface ConfigFile {
42
+ [environment: string]: Record<string, unknown>
43
+ }
44
+
45
+ let config: ConfigFile
46
+ try {
47
+ config = load(readFileSync(configPath, "utf8")) as ConfigFile
48
+ } catch (error: unknown) {
49
+ if (isFileNotFoundError(error)) {
50
+ // File not found, use default configuration
51
+ try {
52
+ config = load(readFileSync(defaultConfigPath, "utf8")) as ConfigFile
53
+ } catch (defaultError) {
54
+ throw new Error(
55
+ `Failed to load Shakapacker configuration.\n` +
56
+ `Neither user config (${configPath}) nor default config (${defaultConfigPath}) could be loaded.\n\n` +
57
+ `To fix this issue:\n` +
58
+ `1. Create a config/shakapacker.yml file in your project\n` +
59
+ `2. Or set the SHAKAPACKER_CONFIG environment variable to point to your config file\n` +
60
+ `3. Or reinstall Shakapacker to restore the default configuration:\n` +
61
+ ` npm install shakapacker --force\n` +
62
+ ` yarn add shakapacker --force`
63
+ )
64
+ }
65
+ } else {
66
+ throw error
67
+ }
68
+ }
69
+
70
+ const availableEnvironments = Object.keys(config).join("|")
71
+ const regex = new RegExp(`^(${availableEnvironments})$`, "g")
72
+
73
+ const runningWebpackDevServer = process.env.WEBPACK_SERVE === "true"
74
+
75
+ const validatedRailsEnv =
76
+ initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
77
+
78
+ if (initialRailsEnv && validatedRailsEnv !== initialRailsEnv) {
79
+ /* eslint no-console:0 */
80
+ console.warn(
81
+ `[SHAKAPACKER WARNING] Environment '${initialRailsEnv}' not found in the configuration.\n` +
82
+ `Using '${DEFAULT}' configuration as a fallback.`
83
+ )
84
+ }
85
+
86
+ export = {
87
+ railsEnv: validatedRailsEnv,
88
+ nodeEnv,
89
+ isProduction,
90
+ isDevelopment,
91
+ runningWebpackDevServer
92
+ }
@@ -0,0 +1,138 @@
1
+ /* eslint global-require: 0 */
2
+ /* eslint import/no-dynamic-require: 0 */
3
+
4
+ const { basename, dirname, join, relative, resolve } = require("path")
5
+ const { existsSync, readdirSync } = require("fs")
6
+ import { Dirent } from "fs"
7
+ const extname = require("path-complete-extname")
8
+ // @ts-ignore: webpack is an optional peer dependency (using type-only import)
9
+ import type { Configuration, Entry } from "webpack"
10
+ const config = require("../config")
11
+ const { isProduction } = require("../env")
12
+
13
+ const pluginsPath = resolve(
14
+ __dirname,
15
+ "..",
16
+ "plugins",
17
+ `${config.assets_bundler}.js`
18
+ )
19
+ const { getPlugins } = require(pluginsPath)
20
+ const rulesPath = resolve(
21
+ __dirname,
22
+ "..",
23
+ "rules",
24
+ `${config.assets_bundler}.js`
25
+ )
26
+ const rules = require(rulesPath)
27
+
28
+ // Don't use contentHash except for production for performance
29
+ // https://webpack.js.org/guides/build-performance/#avoid-production-specific-tooling
30
+ const hash = isProduction || config.useContentHash ? "-[contenthash]" : ""
31
+
32
+ const getFilesInDirectory = (dir: string, includeNested: boolean): string[] => {
33
+ if (!existsSync(dir)) {
34
+ return []
35
+ }
36
+
37
+ return readdirSync(dir, { withFileTypes: true }).flatMap((dirent: Dirent) => {
38
+ const filePath = join(dir, dirent.name)
39
+
40
+ if (dirent.isDirectory() && includeNested) {
41
+ return getFilesInDirectory(filePath, includeNested)
42
+ }
43
+ if (dirent.isFile()) {
44
+ return filePath
45
+ }
46
+ return []
47
+ })
48
+ }
49
+
50
+ const getEntryObject = (): Entry => {
51
+ const entries: Entry = {}
52
+ const rootPath = join(config.source_path, config.source_entry_path)
53
+ if (config.source_entry_path === "/" && config.nested_entries) {
54
+ throw new Error(
55
+ `Invalid Shakapacker configuration detected!\n\n` +
56
+ `You have set source_entry_path to '/' with nested_entries enabled.\n` +
57
+ `This would create webpack entry points for EVERY file in your source directory,\n` +
58
+ `which would severely impact build performance.\n\n` +
59
+ `To fix this issue, either:\n` +
60
+ `1. Set 'nested_entries: false' in your shakapacker.yml\n` +
61
+ `2. Change 'source_entry_path' to a specific subdirectory (e.g., 'packs')\n` +
62
+ `3. Or use both options for better organization of your entry points`
63
+ )
64
+ }
65
+
66
+ getFilesInDirectory(rootPath, config.nested_entries).forEach((path) => {
67
+ const namespace = relative(join(rootPath), dirname(path))
68
+ const name = join(namespace, basename(path, extname(path)))
69
+ const assetPath: string = resolve(path)
70
+
71
+ // Allows for multiple filetypes per entry (https://webpack.js.org/guides/entry-advanced/)
72
+ // Transforms the config object value to an array with all values under the same name
73
+ const previousPaths = entries[name]
74
+ if (previousPaths) {
75
+ const pathArray = Array.isArray(previousPaths)
76
+ ? previousPaths as string[]
77
+ : [previousPaths as string]
78
+ pathArray.push(assetPath)
79
+ entries[name] = pathArray
80
+ } else {
81
+ entries[name] = assetPath
82
+ }
83
+ })
84
+
85
+ return entries
86
+ }
87
+
88
+ const getModulePaths = (): string[] => {
89
+ const result = [resolve(config.source_path)]
90
+
91
+ if (config.additional_paths) {
92
+ config.additional_paths.forEach((path: string) => result.push(resolve(path)))
93
+ }
94
+ result.push("node_modules")
95
+
96
+ return result
97
+ }
98
+
99
+ const baseConfig: Configuration = {
100
+ mode: "production",
101
+ output: {
102
+ filename: `js/[name]${hash}.js`,
103
+ chunkFilename: `js/[name]${hash}.chunk.js`,
104
+
105
+ // https://webpack.js.org/configuration/output/#outputhotupdatechunkfilename
106
+ hotUpdateChunkFilename: "js/[id].[fullhash].hot-update.js",
107
+ path: config.outputPath,
108
+ publicPath: config.publicPath,
109
+
110
+ // This is required for SRI to work.
111
+ crossOriginLoading: config.integrity && config.integrity.enabled
112
+ ? (config.integrity.cross_origin as "anonymous" | "use-credentials" | false)
113
+ : false
114
+ },
115
+ entry: getEntryObject(),
116
+ resolve: {
117
+ extensions: [".js", ".jsx", ".mjs", ".ts", ".tsx", ".coffee"],
118
+ modules: getModulePaths()
119
+ },
120
+
121
+ plugins: getPlugins(),
122
+
123
+ resolveLoader: {
124
+ modules: ["node_modules"]
125
+ },
126
+
127
+ optimization: {
128
+ splitChunks: { chunks: "all" },
129
+ runtimeChunk: "single"
130
+ },
131
+
132
+ module: {
133
+ rules
134
+ }
135
+ }
136
+
137
+ export = baseConfig
138
+
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Development environment configuration for webpack and rspack bundlers
3
+ * @module environments/development
4
+ */
5
+
6
+ const { merge } = require("webpack-merge")
7
+ const config = require("../config")
8
+ const baseConfig = require("./base")
9
+ const webpackDevServerConfig = require("../webpackDevServerConfig")
10
+ const { runningWebpackDevServer } = require("../env")
11
+ const { moduleExists } = require("../utils/helpers")
12
+ import type {
13
+ WebpackConfigWithDevServer,
14
+ RspackConfigWithDevServer,
15
+ ReactRefreshWebpackPlugin,
16
+ ReactRefreshRspackPlugin
17
+ } from "./types"
18
+
19
+ /**
20
+ * Base development configuration shared between webpack and rspack
21
+ */
22
+ const baseDevConfig = {
23
+ mode: "development" as const,
24
+ devtool: "cheap-module-source-map" as const
25
+ }
26
+
27
+ /**
28
+ * Generate webpack-specific development configuration
29
+ * @returns Webpack configuration with dev server settings
30
+ */
31
+ const webpackDevConfig = (): WebpackConfigWithDevServer => {
32
+ const webpackConfig: WebpackConfigWithDevServer = {
33
+ ...baseDevConfig,
34
+ ...(runningWebpackDevServer && { devServer: webpackDevServerConfig() })
35
+ }
36
+
37
+ const devServerConfig = webpackDevServerConfig()
38
+ if (
39
+ runningWebpackDevServer &&
40
+ devServerConfig.hot &&
41
+ moduleExists("@pmmmwh/react-refresh-webpack-plugin")
42
+ ) {
43
+ // eslint-disable-next-line global-require
44
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")
45
+ webpackConfig.plugins = [
46
+ ...(webpackConfig.plugins || []),
47
+ new ReactRefreshWebpackPlugin()
48
+ ]
49
+ }
50
+
51
+ return webpackConfig
52
+ }
53
+
54
+ /**
55
+ * Generate rspack-specific development configuration
56
+ * @returns Rspack configuration with dev server settings
57
+ */
58
+ const rspackDevConfig = (): RspackConfigWithDevServer => {
59
+ const devServerConfig = webpackDevServerConfig()
60
+ const rspackConfig: RspackConfigWithDevServer = {
61
+ ...baseDevConfig,
62
+ devServer: {
63
+ ...devServerConfig,
64
+ devMiddleware: {
65
+ ...(devServerConfig.devMiddleware || {}),
66
+ writeToDisk: (filePath: string) => !filePath.includes(".hot-update.")
67
+ }
68
+ }
69
+ }
70
+
71
+ if (
72
+ runningWebpackDevServer &&
73
+ devServerConfig.hot &&
74
+ moduleExists("@rspack/plugin-react-refresh")
75
+ ) {
76
+ // eslint-disable-next-line global-require
77
+ const ReactRefreshPlugin = require("@rspack/plugin-react-refresh")
78
+ rspackConfig.plugins = [
79
+ ...(rspackConfig.plugins || []),
80
+ new ReactRefreshPlugin()
81
+ ]
82
+ }
83
+
84
+ return rspackConfig
85
+ }
86
+
87
+ const bundlerConfig =
88
+ config.assets_bundler === "rspack" ? rspackDevConfig() : webpackDevConfig()
89
+
90
+ module.exports = merge(baseConfig, bundlerConfig)