shakapacker 9.3.0 → 9.3.2

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/update-changelog.md +232 -0
  3. data/.github/actionlint-matcher.json +17 -0
  4. data/.github/workflows/dummy.yml +9 -0
  5. data/.github/workflows/generator.yml +13 -0
  6. data/.github/workflows/node.yml +83 -0
  7. data/.github/workflows/ruby.yml +11 -0
  8. data/.github/workflows/test-bundlers.yml +10 -0
  9. data/CHANGELOG.md +24 -9
  10. data/CLAUDE.md +6 -10
  11. data/CONTRIBUTING.md +44 -0
  12. data/Gemfile.lock +1 -1
  13. data/README.md +74 -44
  14. data/docs/api-reference.md +519 -0
  15. data/docs/cdn_setup.md +3 -3
  16. data/docs/common-upgrades.md +6 -7
  17. data/docs/configuration.md +14 -8
  18. data/docs/css-modules-export-mode.md +40 -6
  19. data/docs/deployment.md +3 -3
  20. data/docs/early_hints_manual_api.md +1 -1
  21. data/docs/feature_testing.md +3 -3
  22. data/docs/optional-peer-dependencies.md +2 -2
  23. data/docs/precompile_hook.md +15 -15
  24. data/docs/react.md +1 -1
  25. data/docs/releasing.md +7 -7
  26. data/docs/rspack_migration_guide.md +8 -14
  27. data/docs/transpiler-migration.md +12 -9
  28. data/docs/troubleshooting.md +3 -3
  29. data/docs/using_swc_loader.md +13 -10
  30. data/docs/v6_upgrade.md +2 -2
  31. data/docs/v9_upgrade.md +78 -3
  32. data/eslint.config.fast.js +120 -8
  33. data/eslint.config.js +50 -31
  34. data/lib/install/config/shakapacker.yml +14 -1
  35. data/lib/shakapacker/bundler_switcher.rb +83 -18
  36. data/lib/shakapacker/configuration.rb +47 -4
  37. data/lib/shakapacker/dev_server_runner.rb +4 -0
  38. data/lib/shakapacker/doctor.rb +7 -7
  39. data/lib/shakapacker/runner.rb +1 -1
  40. data/lib/shakapacker/swc_migrator.rb +2 -2
  41. data/lib/shakapacker/version.rb +1 -1
  42. data/lib/tasks/shakapacker/binstubs.rake +4 -2
  43. data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
  44. data/lib/tasks/shakapacker/doctor.rake +3 -3
  45. data/lib/tasks/shakapacker/export_bundler_config.rake +5 -9
  46. data/lib/tasks/shakapacker/install.rake +4 -2
  47. data/lib/tasks/shakapacker/switch_bundler.rake +30 -40
  48. data/package/config.ts +2 -3
  49. data/package/configExporter/cli.ts +29 -24
  50. data/package/dev_server.ts +2 -2
  51. data/package/env.ts +1 -1
  52. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +6 -6
  53. data/package/environments/base.ts +2 -2
  54. data/package/environments/development.ts +3 -6
  55. data/package/environments/production.ts +2 -2
  56. data/package/environments/test.ts +2 -1
  57. data/package/esbuild/index.ts +0 -2
  58. data/package/index.d.ts +1 -0
  59. data/package/index.d.ts.template +1 -0
  60. data/package/index.ts +0 -1
  61. data/package/loaders.d.ts +1 -1
  62. data/package/plugins/rspack.ts +3 -1
  63. data/package/plugins/webpack.ts +5 -3
  64. data/package/rspack/index.ts +3 -3
  65. data/package/rules/file.ts +1 -1
  66. data/package/rules/raw.ts +3 -1
  67. data/package/rules/rspack.ts +0 -2
  68. data/package/rules/sass.ts +0 -2
  69. data/package/rules/webpack.ts +0 -1
  70. data/package/swc/index.ts +0 -2
  71. data/package/types.ts +8 -11
  72. data/package/utils/debug.ts +0 -4
  73. data/package/utils/getStyleRule.ts +17 -9
  74. data/package/utils/helpers.ts +8 -3
  75. data/package/utils/pathValidation.ts +10 -11
  76. data/package/utils/requireOrError.ts +4 -3
  77. data/package/webpack-types.d.ts +1 -1
  78. data/package/webpackDevServerConfig.ts +4 -4
  79. data/package.json +3 -3
  80. data/test/package/transpiler-defaults.test.js +42 -0
  81. data/yarn.lock +1 -1
  82. metadata +5 -2
@@ -17,11 +17,12 @@ module.exports = [
17
17
  // Global ignores (replaces .eslintignore)
18
18
  {
19
19
  ignores: [
20
- "lib/**",
21
- "**/node_modules/**",
22
- "vendor/**",
23
- "spec/**",
24
- "package/**" // TODO: Remove after issue #644 is resolved (lints package/ TS source files)
20
+ "lib/**", // Ruby files, not JavaScript
21
+ "**/node_modules/**", // Third-party dependencies
22
+ "vendor/**", // Vendored dependencies
23
+ "spec/**", // Ruby specs, not JavaScript
24
+ "package/**/*.js", // Generated/compiled JavaScript from TypeScript
25
+ "package/**/*.d.ts" // Generated TypeScript declaration files
25
26
  ]
26
27
  },
27
28
 
@@ -63,7 +64,11 @@ module.exports = [
63
64
  "import/no-extraneous-dependencies": "off",
64
65
  // TypeScript handles extensions, not needed for JS imports
65
66
  "import/extensions": "off",
66
- indent: ["error", 2]
67
+ indent: ["error", 2],
68
+ // Allow for...of loops - modern JS syntax
69
+ "no-restricted-syntax": "off",
70
+ // Allow console statements - used for debugging/logging throughout
71
+ "no-console": "off"
67
72
  },
68
73
  settings: {
69
74
  react: {
@@ -130,13 +135,120 @@ module.exports = [
130
135
  "@typescript-eslint/no-use-before-define": ["error"],
131
136
  "@typescript-eslint/no-unused-vars": [
132
137
  "error",
133
- { argsIgnorePattern: "^_" }
138
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
134
139
  ],
135
140
  "@typescript-eslint/no-explicit-any": "error",
136
- "@typescript-eslint/explicit-module-boundary-types": "off"
141
+ "@typescript-eslint/explicit-module-boundary-types": "off",
142
+ "no-undef": "off"
137
143
  }
138
144
  },
139
145
 
146
+ // Global rule for all TypeScript files in package/
147
+ // Suppress require() imports - these are intentional for CommonJS compatibility
148
+ // Will be addressed in Phase 3 (breaking changes) - see #708
149
+ {
150
+ files: ["package/**/*.ts"],
151
+ rules: {
152
+ "@typescript-eslint/no-require-imports": "off",
153
+ "global-require": "off",
154
+ "import/no-import-module-exports": "off"
155
+ }
156
+ },
157
+
158
+ // Consolidated override for package/config.ts and package/babel/preset.ts
159
+ {
160
+ files: ["package/babel/preset.ts", "package/config.ts"],
161
+ rules: {
162
+ "@typescript-eslint/no-unused-vars": "off",
163
+ "import/order": "off",
164
+ "import/newline-after-import": "off",
165
+ "import/first": "off",
166
+ "@typescript-eslint/no-explicit-any": "off",
167
+ "no-useless-escape": "off"
168
+ }
169
+ },
170
+
171
+ // configExporter module overrides
172
+ {
173
+ files: ["package/configExporter/**/*.ts"],
174
+ rules: {
175
+ "@typescript-eslint/no-use-before-define": "off",
176
+ "import/no-dynamic-require": "off",
177
+ "class-methods-use-this": "off",
178
+ "import/prefer-default-export": "off",
179
+ "no-underscore-dangle": "off",
180
+ "no-restricted-globals": "off",
181
+ "@typescript-eslint/no-unused-vars": "off",
182
+ "@typescript-eslint/no-explicit-any": "off"
183
+ }
184
+ },
185
+
186
+ // Utils module overrides
187
+ {
188
+ files: [
189
+ "package/utils/inliningCss.ts",
190
+ "package/utils/errorCodes.ts",
191
+ "package/utils/errorHelpers.ts",
192
+ "package/utils/pathValidation.ts",
193
+ "package/utils/getStyleRule.ts",
194
+ "package/utils/helpers.ts",
195
+ "package/utils/validateDependencies.ts",
196
+ "package/webpackDevServerConfig.ts"
197
+ ],
198
+ rules: {
199
+ "@typescript-eslint/no-explicit-any": "off",
200
+ "no-useless-escape": "off"
201
+ }
202
+ },
203
+
204
+ // Plugins and optimization overrides
205
+ {
206
+ files: ["package/plugins/**/*.ts", "package/optimization/**/*.ts"],
207
+ rules: {
208
+ "import/prefer-default-export": "off"
209
+ }
210
+ },
211
+
212
+ // Rules, rspack, swc, esbuild, and other modules
213
+ {
214
+ files: [
215
+ "package/index.ts",
216
+ "package/rspack/index.ts",
217
+ "package/rules/**/*.ts",
218
+ "package/swc/index.ts",
219
+ "package/esbuild/index.ts",
220
+ "package/dev_server.ts",
221
+ "package/env.ts"
222
+ ],
223
+ rules: {
224
+ "@typescript-eslint/no-unused-vars": "off",
225
+ "import/prefer-default-export": "off",
226
+ "no-underscore-dangle": "off"
227
+ }
228
+ },
229
+
230
+ // Environments module overrides
231
+ {
232
+ files: ["package/environments/**/*.ts"],
233
+ rules: {
234
+ "import/prefer-default-export": "off",
235
+ "no-underscore-dangle": "off"
236
+ }
237
+ },
238
+
239
+ // Type tests are intentionally unused - they test type compatibility
240
+ {
241
+ files: ["package/**/__type-tests__/**/*.ts"],
242
+ rules: {
243
+ "@typescript-eslint/no-unused-vars": "off"
244
+ }
245
+ },
246
+
247
+ // Note: Type-aware rule overrides from main config (e.g., @typescript-eslint/no-unsafe-*,
248
+ // @typescript-eslint/restrict-template-expressions) are intentionally omitted here since
249
+ // fast mode doesn't enable type-aware linting (no parserOptions.project specified).
250
+ // This keeps fast mode performant while maintaining consistency for non-type-aware rules.
251
+
140
252
  // Prettier config must be last to override other configs
141
253
  prettierConfig
142
254
  ]
data/eslint.config.js CHANGED
@@ -19,13 +19,7 @@ module.exports = [
19
19
  "vendor/**", // Vendored dependencies
20
20
  "spec/**", // Ruby specs, not JavaScript
21
21
  "package/**/*.js", // Generated/compiled JavaScript from TypeScript
22
- "package/**/*.d.ts", // Generated TypeScript declaration files
23
- // Temporarily ignore TypeScript files until technical debt is resolved
24
- // See ESLINT_TECHNICAL_DEBT.md for tracking
25
- // TODO: Remove this once ESLint issues are fixed (tracked in #723)
26
- // Exception: configExporter is being fixed in #707
27
- "package/**/*.ts",
28
- "!package/configExporter/**/*.ts" // Enable linting for configExporter (issue #707)
22
+ "package/**/*.d.ts" // Generated TypeScript declaration files
29
23
  ]
30
24
  },
31
25
 
@@ -142,10 +136,10 @@ module.exports = [
142
136
  // Disable base rule in favor of TypeScript version
143
137
  "no-use-before-define": "off",
144
138
  "@typescript-eslint/no-use-before-define": ["error"],
145
- // Allow unused vars if they start with underscore (convention for ignored params)
139
+ // Allow unused vars if they start with underscore (convention for ignored params and type tests)
146
140
  "@typescript-eslint/no-unused-vars": [
147
141
  "error",
148
- { argsIgnorePattern: "^_" }
142
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
149
143
  ],
150
144
  // Strict: no 'any' types allowed - use 'unknown' or specific types instead
151
145
  "@typescript-eslint/no-explicit-any": "error",
@@ -157,37 +151,42 @@ module.exports = [
157
151
  }
158
152
  },
159
153
 
154
+ // Global rule for all TypeScript files in package/
155
+ // Suppress require() imports - these are intentional for CommonJS compatibility
156
+ // Will be addressed in Phase 3 (breaking changes) - see #708
157
+ {
158
+ files: ["package/**/*.ts"],
159
+ rules: {
160
+ "@typescript-eslint/no-require-imports": "off",
161
+ "global-require": "off",
162
+ "import/no-import-module-exports": "off"
163
+ }
164
+ },
165
+
160
166
  // Temporary overrides for files with remaining errors
161
167
  // See ESLINT_TECHNICAL_DEBT.md for detailed documentation
162
168
  //
163
- // These overrides suppress ~172 errors that require either:
169
+ // These overrides suppress ~94 type safety errors that require:
164
170
  // 1. Major type refactoring (any/unsafe-* rules)
165
- // 2. Potential breaking changes (module system)
166
- // 3. Significant code restructuring
171
+ // 2. Proper type definitions for config objects
167
172
  //
168
- // GitHub Issues tracking this technical debt:
169
- // - #707: TypeScript: Refactor configExporter module for type safety
170
- // - #708: Module System: Modernize to ES6 modules with codemod
171
- // - #709: Code Style: Fix remaining ESLint style issues
173
+ // GitHub Issue tracking this technical debt:
174
+ // - #790: TypeScript ESLint Phase 2b: Type Safety Improvements (~94 errors)
172
175
  {
173
176
  // Consolidated override for package/config.ts and package/babel/preset.ts
174
177
  // Combines rules from both previous override blocks to avoid duplication
175
178
  files: ["package/babel/preset.ts", "package/config.ts"],
176
179
  rules: {
177
- // From first override block
178
- "@typescript-eslint/no-require-imports": "off",
179
180
  "@typescript-eslint/no-unused-vars": "off",
180
181
  "@typescript-eslint/no-unsafe-call": "off",
181
182
  "import/order": "off",
182
183
  "import/newline-after-import": "off",
183
184
  "import/first": "off",
184
- // Additional rules that were in the second override for config.ts
185
185
  "@typescript-eslint/no-unsafe-assignment": "off",
186
186
  "@typescript-eslint/no-unsafe-member-access": "off",
187
187
  "@typescript-eslint/no-unsafe-argument": "off",
188
188
  "@typescript-eslint/no-explicit-any": "off",
189
- "no-useless-escape": "off",
190
- "no-continue": "off"
189
+ "no-useless-escape": "off"
191
190
  }
192
191
  },
193
192
  {
@@ -205,19 +204,14 @@ module.exports = [
205
204
  // Code organization (functions before use due to large file)
206
205
  "@typescript-eslint/no-use-before-define": "off",
207
206
  // Import style (CommonJS require for dynamic imports)
208
- "@typescript-eslint/no-require-imports": "off",
209
207
  "import/no-dynamic-require": "off",
210
- "global-require": "off",
211
208
  // Class methods that are part of public API
212
209
  "class-methods-use-this": "off",
213
210
  // Template expressions (valid use cases with union types)
214
211
  "@typescript-eslint/restrict-template-expressions": "off",
215
212
  // Style preferences
216
- "no-continue": "off",
217
213
  "import/prefer-default-export": "off",
218
- "no-await-in-loop": "off",
219
214
  "no-underscore-dangle": "off",
220
- "no-shadow": "off",
221
215
  "no-restricted-globals": "off",
222
216
  "@typescript-eslint/no-unused-vars": "off",
223
217
  "@typescript-eslint/require-await": "off"
@@ -236,20 +230,26 @@ module.exports = [
236
230
  }
237
231
  },
238
232
  {
239
- // Remaining utils files (removed package/config.ts from this block)
233
+ // Remaining utils files that need type safety improvements
234
+ // These use dynamic requires and helper functions that return `any`
240
235
  files: [
241
236
  "package/utils/inliningCss.ts",
242
237
  "package/utils/errorCodes.ts",
243
238
  "package/utils/errorHelpers.ts",
244
- "package/utils/pathValidation.ts"
239
+ "package/utils/pathValidation.ts",
240
+ "package/utils/getStyleRule.ts",
241
+ "package/utils/helpers.ts",
242
+ "package/utils/validateDependencies.ts",
243
+ "package/webpackDevServerConfig.ts"
245
244
  ],
246
245
  rules: {
247
246
  "@typescript-eslint/no-unsafe-assignment": "off",
248
247
  "@typescript-eslint/no-unsafe-member-access": "off",
249
248
  "@typescript-eslint/no-unsafe-argument": "off",
249
+ "@typescript-eslint/no-unsafe-call": "off",
250
+ "@typescript-eslint/no-unsafe-return": "off",
250
251
  "@typescript-eslint/no-explicit-any": "off",
251
- "no-useless-escape": "off",
252
- "no-continue": "off"
252
+ "no-useless-escape": "off"
253
253
  }
254
254
  },
255
255
  {
@@ -257,13 +257,13 @@ module.exports = [
257
257
  rules: {
258
258
  "@typescript-eslint/no-unsafe-assignment": "off",
259
259
  "@typescript-eslint/no-unsafe-call": "off",
260
+ "@typescript-eslint/no-unsafe-member-access": "off",
260
261
  "@typescript-eslint/no-redundant-type-constituents": "off",
261
262
  "import/prefer-default-export": "off"
262
263
  }
263
264
  },
264
265
  {
265
266
  files: [
266
- "package/environments/**/*.ts",
267
267
  "package/index.ts",
268
268
  "package/rspack/index.ts",
269
269
  "package/rules/**/*.ts",
@@ -276,6 +276,8 @@ module.exports = [
276
276
  "@typescript-eslint/no-unsafe-assignment": "off",
277
277
  "@typescript-eslint/no-unsafe-call": "off",
278
278
  "@typescript-eslint/no-unsafe-return": "off",
279
+ "@typescript-eslint/no-unsafe-member-access": "off",
280
+ "@typescript-eslint/no-unsafe-argument": "off",
279
281
  "@typescript-eslint/no-redundant-type-constituents": "off",
280
282
  "@typescript-eslint/no-unused-vars": "off",
281
283
  "@typescript-eslint/no-unsafe-function-type": "off",
@@ -283,6 +285,23 @@ module.exports = [
283
285
  "no-underscore-dangle": "off"
284
286
  }
285
287
  },
288
+ {
289
+ // package/environments/**/*.ts now passes no-unused-vars rule
290
+ // Type test functions use underscore prefix (argsIgnorePattern: "^_")
291
+ // All other variables are used in the code
292
+ files: ["package/environments/**/*.ts"],
293
+ rules: {
294
+ "@typescript-eslint/no-unsafe-assignment": "off",
295
+ "@typescript-eslint/no-unsafe-call": "off",
296
+ "@typescript-eslint/no-unsafe-return": "off",
297
+ "@typescript-eslint/no-unsafe-member-access": "off",
298
+ "@typescript-eslint/no-unsafe-argument": "off",
299
+ "@typescript-eslint/no-redundant-type-constituents": "off",
300
+ "@typescript-eslint/no-unsafe-function-type": "off",
301
+ "import/prefer-default-export": "off",
302
+ "no-underscore-dangle": "off"
303
+ }
304
+ },
286
305
 
287
306
  // Prettier config must be last to override other configs
288
307
  prettierConfig
@@ -19,6 +19,18 @@ default: &default
19
19
  # css_extract_ignore_order_warnings to true
20
20
  css_extract_ignore_order_warnings: false
21
21
 
22
+ # CSS Modules export mode
23
+ # Controls how CSS Module class names are exported in JavaScript
24
+ # Defaults to 'named' if not specified. Uncomment and change to 'default' for v8 behavior.
25
+ # Options:
26
+ # - named (default): Use named exports with camelCase conversion (v9 default)
27
+ # Example: import { button } from './styles.module.css'
28
+ # - default: Use default export with both original and camelCase names (v8 behavior)
29
+ # Example: import styles from './styles.module.css'
30
+ # For gradual migration, you can set this to default to maintain v8 behavior
31
+ # See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md
32
+ # css_modules_export_mode: named
33
+
22
34
  public_root_path: public
23
35
  public_output_path: packs
24
36
  cache_path: tmp/shakapacker
@@ -41,7 +53,8 @@ default: &default
41
53
  cache_manifest: false
42
54
 
43
55
  # Select JavaScript transpiler to use
44
- # Available options: 'swc' (default, 20x faster), 'babel', or 'esbuild'
56
+ # Available options: 'swc' (default, 20x faster), 'babel', 'esbuild', or 'none'
57
+ # Use 'none' when providing a completely custom webpack configuration
45
58
  # Note: When using rspack, swc is used automatically regardless of this setting
46
59
  javascript_transpiler: "swc"
47
60
 
@@ -9,6 +9,9 @@ module Shakapacker
9
9
  SHAKAPACKER_CONFIG = "config/shakapacker.yml"
10
10
  CUSTOM_DEPS_CONFIG = ".shakapacker-switch-bundler-dependencies.yml"
11
11
 
12
+ # Regex pattern to detect assets_bundler key in config (only matches uncommented lines)
13
+ ASSETS_BUNDLER_PATTERN = /^[ \t]*assets_bundler:/
14
+
12
15
  # Default dependencies for each bundler (package names only, no versions)
13
16
  DEFAULT_RSPACK_DEPS = {
14
17
  dev: %w[@rspack/cli @rspack/plugin-react-refresh],
@@ -37,18 +40,33 @@ module Shakapacker
37
40
  end
38
41
 
39
42
  current = current_bundler
40
- if current == bundler && !install_deps
43
+ config_content = File.read(config_path)
44
+ has_assets_bundler = config_content =~ ASSETS_BUNDLER_PATTERN
45
+
46
+ # Early exit if already using the target bundler
47
+ # For webpack: if current is webpack, we're done (key optional due to default)
48
+ # For rspack: requires explicit key to be present
49
+ already_configured = if bundler == "webpack"
50
+ current == bundler
51
+ else
52
+ current == bundler && has_assets_bundler
53
+ end
54
+
55
+ if already_configured && !install_deps
41
56
  puts "✅ Already using #{bundler}"
42
57
  return
43
58
  end
44
59
 
45
- if current == bundler && install_deps
60
+ if already_configured && install_deps
46
61
  puts "✅ Already using #{bundler} - reinstalling dependencies as requested"
47
62
  manage_dependencies(bundler, install_deps, switching: false, no_uninstall: no_uninstall)
48
63
  return
49
64
  end
50
65
 
51
- update_config(bundler)
66
+ successfully_updated = update_config(bundler, config_content, has_assets_bundler)
67
+
68
+ # Verify the update was successful (only if update reported success)
69
+ verify_config_update(bundler) if successfully_updated
52
70
 
53
71
  puts "✅ Switched from #{current} to #{bundler}"
54
72
  puts ""
@@ -95,22 +113,15 @@ module Shakapacker
95
113
  puts "Current bundler: #{current}"
96
114
  puts ""
97
115
  puts "Usage:"
98
- puts " rails shakapacker:switch_bundler [webpack|rspack] [OPTIONS]"
99
116
  puts " rake shakapacker:switch_bundler [webpack|rspack] -- [OPTIONS]"
100
117
  puts ""
101
118
  puts "Options:"
102
119
  puts " --install-deps Automatically install/uninstall dependencies"
103
- puts " --no-uninstall Skip uninstalling old bundler packages (faster, keeps both bundlers)"
120
+ puts " --no-uninstall Skip uninstalling old bundler packages"
104
121
  puts " --init-config Create #{CUSTOM_DEPS_CONFIG} with default dependencies"
105
122
  puts " --help, -h Show this help message"
106
123
  puts ""
107
124
  puts "Examples:"
108
- puts " # Using rails command"
109
- puts " rails shakapacker:switch_bundler rspack --install-deps"
110
- puts " rails shakapacker:switch_bundler webpack --install-deps --no-uninstall"
111
- puts " rails shakapacker:switch_bundler --init-config"
112
- puts ""
113
- puts " # Using rake command (note the -- separator)"
114
125
  puts " rake shakapacker:switch_bundler rspack -- --install-deps"
115
126
  puts " rake shakapacker:switch_bundler webpack -- --install-deps --no-uninstall"
116
127
  puts " rake shakapacker:switch_bundler -- --init-config"
@@ -150,20 +161,74 @@ module Shakapacker
150
161
  end
151
162
  end
152
163
 
153
- def update_config(bundler)
154
- content = File.read(config_path)
164
+ def update_config(bundler, content, has_assets_bundler)
165
+ # Check if assets_bundler key exists (only uncommented lines)
166
+ unless has_assets_bundler
167
+ # Track whether we successfully added the key
168
+ added = false
169
+
170
+ # Add assets_bundler after javascript_transpiler if it exists (excluding commented lines)
171
+ if (match = content.match(/^[ \t]*(?![ \t]*#)javascript_transpiler:.*$/))
172
+ indent = match[0][/^[ \t]*/]
173
+ content.sub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
174
+ added = true
175
+ # Otherwise, add it after source_path if it exists (excluding commented lines)
176
+ elsif (match = content.match(/^[ \t]*(?![ \t]*#)source_path:.*$/))
177
+ indent = match[0][/^[ \t]*/]
178
+ content.sub!(/^([ \t]*(?![ \t]*#)source_path:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
179
+ added = true
180
+ # Add it after default: &default if it exists
181
+ elsif content.match?(/^default:[ \t]*&default[ \t]*$/)
182
+ # Use default 2-space indentation for this case
183
+ content.sub!(/^(default:[ \t]*&default[ \t]*)$/, "\\1\n#{assets_bundler_entry(bundler, ' ')}")
184
+ added = true
185
+ # Fallback: add after "default:" with proper indentation detection (handles blank lines)
186
+ elsif (match = content.match(/^default:\s*\n\s*([ \t]+)/m))
187
+ # Extract indentation from first indented line after "default:"
188
+ indent = match[1]
189
+ content.sub!(/^(default:\s*)$/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
190
+ added = true
191
+ end
155
192
 
156
- # Replace assets_bundler value (handles spaces, tabs, and various quote styles)
157
- # Only matches uncommented lines
158
- content.gsub!(/^([ \t]*assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1#{bundler}\\3")
193
+ unless added
194
+ puts "⚠️ Warning: Could not find appropriate location for assets_bundler in config"
195
+ puts " Please add 'assets_bundler: #{bundler}' to the default section manually"
196
+ end
197
+ else
198
+ # Replace existing assets_bundler value (handles spaces, tabs, and various quote styles)
199
+ # Only matches uncommented lines
200
+ content.gsub!(/^([ \t]*)(?![ \t]*#)(assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1\\2#{bundler}\\4")
201
+ added = true
202
+ end
159
203
 
160
204
  # Update javascript_transpiler recommendation for rspack
161
205
  # Only update if not already set to swc and only on uncommented lines
162
- if bundler == "rspack" && content !~ /^[ \t]*javascript_transpiler:[ \t]*['"]?swc['"]?/
163
- content.gsub!(/^([ \t]*javascript_transpiler:[ \t]*['"]?)\w+(['"]?)/, "\\1swc\\2")
206
+ if bundler == "rspack" && content !~ /^[ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?swc['"]?/
207
+ content.gsub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?)(\w+)(['"]?)/, '\1swc\3')
164
208
  end
165
209
 
166
210
  File.write(config_path, content)
211
+ added
212
+ end
213
+
214
+ # Verify that the config was updated successfully
215
+ def verify_config_update(bundler)
216
+ config = load_yaml_config(config_path)
217
+ actual_bundler = config.dig("default", "assets_bundler")
218
+
219
+ if actual_bundler != bundler
220
+ raise "Config update verification failed: expected assets_bundler to be '#{bundler}', but got '#{actual_bundler}'"
221
+ end
222
+ rescue Psych::SyntaxError => e
223
+ raise "Config update generated invalid YAML: #{e.message}"
224
+ end
225
+
226
+ # Generate the assets_bundler YAML entry with proper indentation
227
+ # @param bundler [String] The bundler name ('webpack' or 'rspack')
228
+ # @param indent [String] The indentation string to use (e.g., ' ' or '\t')
229
+ # @return [String] The formatted YAML entry
230
+ def assets_bundler_entry(bundler, indent)
231
+ "\n#{indent}# Select assets bundler to use\n#{indent}# Available options: 'webpack' (default) or 'rspack'\n#{indent}assets_bundler: \"#{bundler}\""
167
232
  end
168
233
 
169
234
  def manage_dependencies(bundler, install_deps, switching: true, no_uninstall: false)
@@ -339,6 +339,31 @@ class Shakapacker::Configuration
339
339
  javascript_transpiler
340
340
  end
341
341
 
342
+ # Returns the CSS Modules export mode configuration
343
+ #
344
+ # Controls how CSS Module class names are exported in JavaScript:
345
+ # - "named" (default): Use named exports with camelCase conversion (v9 behavior)
346
+ # - "default": Use default export with both original and camelCase names (v8 behavior)
347
+ #
348
+ # @return [String] "named" or "default"
349
+ # @raise [ArgumentError] if an invalid value is configured
350
+ def css_modules_export_mode
351
+ @css_modules_export_mode ||= begin
352
+ mode = fetch(:css_modules_export_mode) || "named"
353
+
354
+ # Validate the configuration value
355
+ valid_modes = ["named", "default"]
356
+ unless valid_modes.include?(mode)
357
+ raise ArgumentError,
358
+ "Invalid css_modules_export_mode: '#{mode}'. " \
359
+ "Valid values are: #{valid_modes.map { |m| "'#{m}'" }.join(', ')}. " \
360
+ "See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md"
361
+ end
362
+
363
+ mode
364
+ end
365
+ end
366
+
342
367
  # Returns the path to the bundler configuration directory
343
368
  #
344
369
  # This is where webpack.config.js or rspack.config.js should be located.
@@ -353,6 +378,25 @@ class Shakapacker::Configuration
353
378
  rspack? ? "config/rspack" : "config/webpack"
354
379
  end
355
380
 
381
+ # Returns the raw configuration data hash
382
+ #
383
+ # Returns the merged configuration from the shakapacker.yml file for the current environment.
384
+ # The hash has symbolized keys loaded from the config file. Individual config values can be
385
+ # accessed through specific accessor methods like {#source_path}, which apply defaults via {#fetch}.
386
+ #
387
+ # The returned hash is frozen to prevent accidental mutations. To access config values,
388
+ # use the provided accessor methods instead of modifying this hash directly.
389
+ #
390
+ # @return [Hash<Symbol, Object>] the raw configuration data with symbolized keys (frozen)
391
+ # @example
392
+ # config.data[:source_path] #=> "app/javascript"
393
+ # config.data[:compile] #=> true
394
+ # @note The hash is frozen to prevent mutations. Use accessor methods for safe config access.
395
+ # @api public
396
+ def data
397
+ @data ||= load.freeze
398
+ end
399
+
356
400
  private
357
401
 
358
402
  def default_javascript_transpiler
@@ -363,6 +407,9 @@ class Shakapacker::Configuration
363
407
  def validate_transpiler_configuration(transpiler)
364
408
  return unless ENV["NODE_ENV"] != "test" # Skip validation in test environment
365
409
 
410
+ # Skip validation if transpiler is set to 'none' (custom webpack config)
411
+ return if transpiler == "none"
412
+
366
413
  # Check if package.json exists
367
414
  package_json_path = root_path.join("package.json")
368
415
  return unless package_json_path.exist?
@@ -495,10 +542,6 @@ class Shakapacker::Configuration
495
542
  [private_full_path.cleanpath.to_s, public_full_path.cleanpath.to_s]
496
543
  end
497
544
 
498
- def data
499
- @data ||= load
500
- end
501
-
502
545
  def load
503
546
  config = begin
504
547
  YAML.load_file(config_path.to_s, aliases: true)
@@ -18,6 +18,8 @@ module Shakapacker
18
18
  exit(0)
19
19
  end
20
20
 
21
+ Shakapacker.ensure_node_env!
22
+
21
23
  # Check for --build flag
22
24
  build_index = argv.index("--build")
23
25
  if build_index
@@ -65,6 +67,8 @@ module Shakapacker
65
67
  end
66
68
 
67
69
  def self.run_with_build_config(argv, build_config)
70
+ Shakapacker.ensure_node_env!
71
+
68
72
  # Apply build config environment variables
69
73
  build_config[:environment].each do |key, value|
70
74
  ENV[key] = value.to_s
@@ -54,7 +54,7 @@ module Shakapacker
54
54
  Shakapacker Doctor - Diagnostic tool for Shakapacker configuration
55
55
 
56
56
  Usage:
57
- bin/rails shakapacker:doctor [options]
57
+ bundle exec rake shakapacker:doctor [options]
58
58
 
59
59
  Options:
60
60
  --help Show this help message
@@ -163,7 +163,7 @@ module Shakapacker
163
163
  begin
164
164
  manifest_content = JSON.parse(File.read(manifest_path))
165
165
  if manifest_content.empty?
166
- add_warning("Manifest file is empty - you may need to run 'bin/rails assets:precompile'")
166
+ add_warning("Manifest file is empty - you may need to run 'bundle exec rake assets:precompile'")
167
167
  end
168
168
  rescue JSON::ParserError
169
169
  @issues << "Manifest file #{manifest_path} contains invalid JSON"
@@ -325,16 +325,16 @@ module Shakapacker
325
325
  if source_files.any?
326
326
  newest_source = source_files.map { |f| File.mtime(f) }.max
327
327
  if newest_source > File.mtime(manifest_path)
328
- add_warning("Source files have been modified after last asset compilation. Run 'bin/rails assets:precompile'")
328
+ add_warning("Source files have been modified after last asset compilation. Run 'bundle exec rake assets:precompile'")
329
329
  end
330
330
  end
331
331
  else
332
332
  rails_env = defined?(Rails) ? Rails.env : ENV["RAILS_ENV"]
333
333
  if rails_env == "production"
334
- @issues << "No compiled assets found (manifest.json missing). Run 'bin/rails assets:precompile'"
334
+ @issues << "No compiled assets found (manifest.json missing). Run 'bundle exec rake assets:precompile'"
335
335
  elsif options[:verbose]
336
336
  # Only show in verbose mode for non-production environments
337
- @info << "Assets not yet compiled. Run 'bin/rails assets:precompile' or start the dev server"
337
+ @info << "Assets not yet compiled. Run 'bundle exec rake assets:precompile' or start the dev server"
338
338
  end
339
339
  end
340
340
  end
@@ -402,7 +402,7 @@ module Shakapacker
402
402
 
403
403
  unless missing_binstubs.empty?
404
404
  add_action_required("Missing binstubs: #{missing_binstubs.join(', ')}.")
405
- add_action_required(" Fix: Run 'bin/rails shakapacker:binstubs' to create them.")
405
+ add_action_required(" Fix: Run 'bundle exec rake shakapacker:binstubs' to create them.")
406
406
  end
407
407
  end
408
408
 
@@ -914,7 +914,7 @@ module Shakapacker
914
914
  return unless doctor.config.config_path.exist?
915
915
 
916
916
  puts "\nConfiguration values for '#{doctor.config.env}' environment:"
917
- config_data = doctor.config.send(:data)
917
+ config_data = doctor.config.data
918
918
 
919
919
  if config_data.any?
920
920
  print_config_data(config_data)