shakapacker 9.1.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 (123) 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 +7 -0
  12. data/CHANGELOG.md +50 -4
  13. data/CLAUDE.md +6 -1
  14. data/CONTRIBUTING.md +0 -1
  15. data/Gemfile.lock +1 -1
  16. data/README.md +35 -14
  17. data/TODO.md +10 -2
  18. data/TODO_v9.md +13 -3
  19. data/bin/export-bundler-config +11 -0
  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 +68 -6
  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 +195 -0
  34. data/docs/rspack.md +25 -21
  35. data/docs/rspack_migration_guide.md +363 -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 +122 -23
  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 +11 -0
  52. data/lib/install/bin/shakapacker +1 -1
  53. data/lib/install/bin/shakapacker-dev-server +1 -1
  54. data/lib/install/config/shakapacker.yml +16 -5
  55. data/lib/shakapacker/bundler_switcher.rb +7 -0
  56. data/lib/shakapacker/compiler.rb +80 -0
  57. data/lib/shakapacker/configuration.rb +56 -2
  58. data/lib/shakapacker/dev_server_runner.rb +140 -1
  59. data/lib/shakapacker/doctor.rb +302 -57
  60. data/lib/shakapacker/instance.rb +8 -3
  61. data/lib/shakapacker/rspack_runner.rb +1 -1
  62. data/lib/shakapacker/runner.rb +245 -9
  63. data/lib/shakapacker/version.rb +1 -1
  64. data/lib/shakapacker/webpack_runner.rb +1 -1
  65. data/lib/shakapacker.rb +10 -0
  66. data/lib/tasks/shakapacker/doctor.rake +42 -2
  67. data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
  68. data/package/babel/preset.ts +7 -4
  69. data/package/config.ts +42 -30
  70. data/package/configExporter/cli.ts +1274 -0
  71. data/package/configExporter/configDocs.ts +102 -0
  72. data/package/configExporter/configFile.ts +520 -0
  73. data/package/configExporter/fileWriter.ts +96 -0
  74. data/package/configExporter/index.ts +13 -0
  75. data/package/configExporter/types.ts +70 -0
  76. data/package/configExporter/yamlSerializer.ts +280 -0
  77. data/package/dev_server.ts +1 -1
  78. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
  79. data/package/environments/base.ts +18 -13
  80. data/package/environments/development.ts +1 -1
  81. data/package/environments/production.ts +4 -1
  82. data/package/index.d.ts +50 -3
  83. data/package/index.d.ts.template +50 -0
  84. data/package/index.ts +7 -7
  85. data/package/loaders.d.ts +2 -2
  86. data/package/optimization/rspack.ts +1 -1
  87. data/package/plugins/rspack.ts +15 -4
  88. data/package/plugins/webpack.ts +7 -3
  89. data/package/rspack/index.ts +10 -2
  90. data/package/rules/raw.ts +3 -2
  91. data/package/rules/sass.ts +1 -1
  92. data/package/types/README.md +15 -13
  93. data/package/types/index.ts +5 -5
  94. data/package/types.ts +0 -1
  95. data/package/utils/defaultConfigPath.ts +4 -1
  96. data/package/utils/errorCodes.ts +129 -100
  97. data/package/utils/errorHelpers.ts +34 -29
  98. data/package/utils/getStyleRule.ts +5 -2
  99. data/package/utils/helpers.ts +21 -11
  100. data/package/utils/pathValidation.ts +43 -35
  101. data/package/utils/requireOrError.ts +1 -1
  102. data/package/utils/snakeToCamelCase.ts +1 -1
  103. data/package/utils/typeGuards.ts +132 -83
  104. data/package/utils/validateDependencies.ts +1 -1
  105. data/package/webpack-types.d.ts +3 -3
  106. data/package/webpackDevServerConfig.ts +22 -10
  107. data/package-lock.json +2 -2
  108. data/package.json +37 -28
  109. data/scripts/type-check-no-emit.js +1 -1
  110. data/test/configExporter/configFile.test.js +392 -0
  111. data/test/configExporter/integration.test.js +275 -0
  112. data/test/helpers.js +1 -1
  113. data/test/package/configExporter.test.js +154 -0
  114. data/test/package/helpers.test.js +2 -2
  115. data/test/package/rules/sass-version-parsing.test.js +71 -0
  116. data/test/package/rules/sass.test.js +2 -4
  117. data/test/package/rules/sass1.test.js +1 -3
  118. data/test/package/rules/sass16.test.js +23 -0
  119. data/tools/README.md +15 -5
  120. data/tsconfig.eslint.json +2 -9
  121. data/yarn.lock +1635 -1442
  122. metadata +29 -3
  123. data/.eslintignore +0 -5
data/docs/v8_upgrade.md CHANGED
@@ -18,6 +18,7 @@ If you are not using CDN, then this change will have no effect on your setup.
18
18
  If you are using CDN and your CDN host is static, `config.asset_host` setting in Rails will be respected during compilation and when referencing assets through view helpers.
19
19
 
20
20
  If your host might differ, between various environments for example, you will either need to:
21
+
21
22
  - Ensure the assets are specifically rebuilt for each environment (Heroku pipeline promote feature for example does not do that by default).
22
23
  - Make sure the assets are compiled with `SHAKAPACKER_ASSET_HOST=''` ENV variable to avoid hardcording URLs in packs output.
23
24
 
@@ -131,41 +132,41 @@ The function will return the same object with less risk:
131
132
 
132
133
  ```js
133
134
  // before
134
- const { globalMutableWebpackConfig, merge } = require('shakapacker');
135
+ const { globalMutableWebpackConfig, merge } = require("shakapacker")
135
136
 
136
137
  const customConfig = {
137
138
  module: {
138
139
  rules: [
139
140
  {
140
- test: require.resolve('jquery'),
141
- loader: 'expose-loader',
142
- options: { exposes: ['$', 'jQuery'] }
141
+ test: require.resolve("jquery"),
142
+ loader: "expose-loader",
143
+ options: { exposes: ["$", "jQuery"] }
143
144
  }
144
145
  ]
145
146
  }
146
- };
147
+ }
147
148
 
148
- module.exports = merge(globalMutableWebpackConfig, customConfig);
149
+ module.exports = merge(globalMutableWebpackConfig, customConfig)
149
150
  ```
150
151
 
151
152
  ```js
152
153
  // after
153
- const { generateWebpackConfig, merge } = require('shakapacker');
154
+ const { generateWebpackConfig, merge } = require("shakapacker")
154
155
 
155
156
  const customConfig = {
156
157
  module: {
157
158
  rules: [
158
159
  {
159
- test: require.resolve('jquery'),
160
- loader: 'expose-loader',
161
- options: { exposes: ['$', 'jQuery'] }
160
+ test: require.resolve("jquery"),
161
+ loader: "expose-loader",
162
+ options: { exposes: ["$", "jQuery"] }
162
163
  }
163
164
  ]
164
165
  }
165
- };
166
+ }
166
167
 
167
168
  // you can also pass your config directly to the generator function to have it merged in!
168
- module.exports = merge(generateWebpackConfig(), customConfig);
169
+ module.exports = merge(generateWebpackConfig(), customConfig)
169
170
  ```
170
171
 
171
172
  ## `additional_paths` are now stripped just like with `source_path`
data/docs/v9_upgrade.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This guide outlines new features, breaking changes, and migration steps for upgrading from Shakapacker v8 to v9.
4
4
 
5
+ **📖 For detailed configuration options, see the [Configuration Guide](./configuration.md)**
6
+
5
7
  > **⚠️ Important for v9.1.0 Users:** If you're upgrading to v9.1.0 or later, please note the [SWC Configuration Breaking Change](#swc-loose-mode-breaking-change-v910) below. This affects users who previously configured SWC in v9.0.0.
6
8
 
7
9
  ## New Features
@@ -130,7 +132,6 @@ import * as styles from './Component.module.css';
130
132
  **Migration Options:**
131
133
 
132
134
  1. **Update your code** (Recommended):
133
-
134
135
  - JavaScript: Change to named imports (`import { className }`)
135
136
  - TypeScript: Change to namespace imports (`import * as styles`)
136
137
  - Kebab-case class names are automatically converted to camelCase
@@ -0,0 +1,134 @@
1
+ // Fast ESLint config for quick development feedback
2
+ // Skips type-aware rules that require TypeScript compilation
3
+
4
+ const { FlatCompat } = require("@eslint/eslintrc")
5
+ const js = require("@eslint/js")
6
+ const typescriptParser = require("@typescript-eslint/parser")
7
+ const typescriptPlugin = require("@typescript-eslint/eslint-plugin")
8
+ const jestPlugin = require("eslint-plugin-jest")
9
+ const prettierConfig = require("eslint-config-prettier")
10
+
11
+ const compat = new FlatCompat({
12
+ baseDirectory: __dirname,
13
+ recommendedConfig: js.configs.recommended
14
+ })
15
+
16
+ module.exports = [
17
+ // Global ignores (replaces .eslintignore)
18
+ {
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)
25
+ ]
26
+ },
27
+
28
+ // Base config for all JS files
29
+ ...compat.extends("airbnb"),
30
+ {
31
+ languageOptions: {
32
+ ecmaVersion: 2020,
33
+ sourceType: "module",
34
+ globals: {
35
+ // Browser globals
36
+ window: "readonly",
37
+ document: "readonly",
38
+ navigator: "readonly",
39
+ console: "readonly",
40
+ // Node globals
41
+ process: "readonly",
42
+ __dirname: "readonly",
43
+ __filename: "readonly",
44
+ module: "readonly",
45
+ require: "readonly",
46
+ exports: "readonly",
47
+ global: "readonly",
48
+ Buffer: "readonly"
49
+ }
50
+ },
51
+ rules: {
52
+ // Webpack handles module resolution, not ESLint
53
+ "import/no-unresolved": "off",
54
+ // Allow importing devDependencies in config/test files
55
+ "import/no-extraneous-dependencies": "off",
56
+ // TypeScript handles extensions, not needed for JS imports
57
+ "import/extensions": "off",
58
+ indent: ["error", 2]
59
+ },
60
+ settings: {
61
+ react: {
62
+ // Suppress "react package not installed" warning
63
+ // This project doesn't use React but airbnb config requires react-plugin
64
+ version: "999.999.999"
65
+ }
66
+ }
67
+ },
68
+
69
+ // Jest test files
70
+ {
71
+ files: ["test/**"],
72
+ plugins: {
73
+ jest: jestPlugin
74
+ },
75
+ languageOptions: {
76
+ globals: {
77
+ ...jestPlugin.environments.globals.globals
78
+ }
79
+ },
80
+ rules: {
81
+ ...jestPlugin.configs.recommended.rules,
82
+ ...jestPlugin.configs.style.rules,
83
+ "global-require": "off",
84
+ "jest/prefer-called-with": "error",
85
+ "jest/no-conditional-in-test": "error",
86
+ "jest/no-test-return-statement": "error",
87
+ "jest/prefer-expect-resolves": "error",
88
+ "jest/require-to-throw-message": "error",
89
+ "jest/require-top-level-describe": "error",
90
+ "jest/prefer-hooks-on-top": "error",
91
+ "jest/prefer-lowercase-title": [
92
+ "error",
93
+ { ignoreTopLevelDescribe: true }
94
+ ],
95
+ "jest/prefer-spy-on": "error",
96
+ "jest/prefer-strict-equal": "error",
97
+ "jest/prefer-todo": "error"
98
+ }
99
+ },
100
+
101
+ // TypeScript files - fast mode without type-aware linting
102
+ {
103
+ files: ["**/*.ts", "**/*.tsx"],
104
+ languageOptions: {
105
+ parser: typescriptParser,
106
+ parserOptions: {
107
+ // No project specified - disables type-aware linting
108
+ ecmaVersion: 2020,
109
+ sourceType: "module"
110
+ }
111
+ },
112
+ plugins: {
113
+ "@typescript-eslint": typescriptPlugin
114
+ },
115
+ rules: {
116
+ ...typescriptPlugin.configs.recommended.rules,
117
+ // Same rules as main config minus type-aware ones
118
+ "import/no-unresolved": "off",
119
+ "import/no-extraneous-dependencies": "off",
120
+ "import/extensions": "off",
121
+ "no-use-before-define": "off",
122
+ "@typescript-eslint/no-use-before-define": ["error"],
123
+ "@typescript-eslint/no-unused-vars": [
124
+ "error",
125
+ { argsIgnorePattern: "^_" }
126
+ ],
127
+ "@typescript-eslint/no-explicit-any": "error",
128
+ "@typescript-eslint/explicit-module-boundary-types": "off"
129
+ }
130
+ },
131
+
132
+ // Prettier config must be last to override other configs
133
+ prettierConfig
134
+ ]
data/eslint.config.js ADDED
@@ -0,0 +1,140 @@
1
+ const { FlatCompat } = require("@eslint/eslintrc")
2
+ const js = require("@eslint/js")
3
+ const typescriptParser = require("@typescript-eslint/parser")
4
+ const typescriptPlugin = require("@typescript-eslint/eslint-plugin")
5
+ const jestPlugin = require("eslint-plugin-jest")
6
+ const prettierConfig = require("eslint-config-prettier")
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ recommendedConfig: js.configs.recommended
11
+ })
12
+
13
+ module.exports = [
14
+ // Global ignores (replaces .eslintignore)
15
+ {
16
+ ignores: [
17
+ "lib/**",
18
+ "**/node_modules/**",
19
+ "vendor/**",
20
+ "spec/**",
21
+ "package/**" // TODO: Remove after issue #644 is resolved (lints package/ TS source files)
22
+ ]
23
+ },
24
+
25
+ // Base config for all JS files
26
+ ...compat.extends("airbnb"),
27
+ {
28
+ languageOptions: {
29
+ ecmaVersion: 2020,
30
+ sourceType: "module",
31
+ globals: {
32
+ // Browser globals
33
+ window: "readonly",
34
+ document: "readonly",
35
+ navigator: "readonly",
36
+ console: "readonly",
37
+ // Node globals
38
+ process: "readonly",
39
+ __dirname: "readonly",
40
+ __filename: "readonly",
41
+ module: "readonly",
42
+ require: "readonly",
43
+ exports: "readonly",
44
+ global: "readonly",
45
+ Buffer: "readonly"
46
+ }
47
+ },
48
+ rules: {
49
+ // Webpack handles module resolution, not ESLint
50
+ "import/no-unresolved": "off",
51
+ // Allow importing devDependencies in config/test files
52
+ "import/no-extraneous-dependencies": "off",
53
+ // TypeScript handles extensions, not needed for JS imports
54
+ "import/extensions": "off",
55
+ indent: ["error", 2]
56
+ },
57
+ settings: {
58
+ react: {
59
+ // Suppress "react package not installed" warning
60
+ // This project doesn't use React but airbnb config requires react-plugin
61
+ version: "999.999.999"
62
+ }
63
+ }
64
+ },
65
+
66
+ // Jest test files
67
+ {
68
+ files: ["test/**"],
69
+ plugins: {
70
+ jest: jestPlugin
71
+ },
72
+ languageOptions: {
73
+ globals: {
74
+ ...jestPlugin.environments.globals.globals
75
+ }
76
+ },
77
+ rules: {
78
+ ...jestPlugin.configs.recommended.rules,
79
+ ...jestPlugin.configs.style.rules,
80
+ "global-require": "off",
81
+ "jest/prefer-called-with": "error",
82
+ "jest/no-conditional-in-test": "error",
83
+ "jest/no-test-return-statement": "error",
84
+ "jest/prefer-expect-resolves": "error",
85
+ "jest/require-to-throw-message": "error",
86
+ "jest/require-top-level-describe": "error",
87
+ "jest/prefer-hooks-on-top": "error",
88
+ "jest/prefer-lowercase-title": [
89
+ "error",
90
+ { ignoreTopLevelDescribe: true }
91
+ ],
92
+ "jest/prefer-spy-on": "error",
93
+ "jest/prefer-strict-equal": "error",
94
+ "jest/prefer-todo": "error"
95
+ }
96
+ },
97
+
98
+ // TypeScript files
99
+ {
100
+ files: ["**/*.ts", "**/*.tsx"],
101
+ languageOptions: {
102
+ parser: typescriptParser,
103
+ parserOptions: {
104
+ // Enables type-aware linting for better type safety
105
+ // Note: This can slow down linting on large codebases
106
+ // Consider using --cache flag with ESLint if performance degrades
107
+ project: "./tsconfig.eslint.json",
108
+ tsconfigRootDir: __dirname
109
+ }
110
+ },
111
+ plugins: {
112
+ "@typescript-eslint": typescriptPlugin
113
+ },
114
+ rules: {
115
+ ...typescriptPlugin.configs.recommended.rules,
116
+ ...typescriptPlugin.configs["recommended-requiring-type-checking"].rules,
117
+ // TypeScript compiler handles module resolution
118
+ "import/no-unresolved": "off",
119
+ // Allow importing devDependencies in TypeScript files
120
+ "import/no-extraneous-dependencies": "off",
121
+ // TypeScript handles file extensions via moduleResolution
122
+ "import/extensions": "off",
123
+ // Disable base rule in favor of TypeScript version
124
+ "no-use-before-define": "off",
125
+ "@typescript-eslint/no-use-before-define": ["error"],
126
+ // Allow unused vars if they start with underscore (convention for ignored params)
127
+ "@typescript-eslint/no-unused-vars": [
128
+ "error",
129
+ { argsIgnorePattern: "^_" }
130
+ ],
131
+ // Strict: no 'any' types allowed - use 'unknown' or specific types instead
132
+ "@typescript-eslint/no-explicit-any": "error",
133
+ // Allow implicit return types - TypeScript can infer them
134
+ "@typescript-eslint/explicit-module-boundary-types": "off"
135
+ }
136
+ },
137
+
138
+ // Prettier config must be last to override other configs
139
+ prettierConfig
140
+ ]
data/knip.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { KnipConfig } from "knip"
2
+
3
+ const config: KnipConfig = {
4
+ project: ["package/**/*.{ts,js}", "test/**/*.{ts,js}", "scripts/**/*.js"],
5
+ ignore: [
6
+ "package/**/*.d.ts",
7
+ "package/**/*.js",
8
+ "package/**/*.js.map",
9
+ "package/**/*.d.ts.map",
10
+ "test/fixtures/**",
11
+ "test/helpers.js", // Test utility file used by jest
12
+ "spec/**",
13
+ "gemfiles/**"
14
+ ],
15
+ ignoreBinaries: ["sed"],
16
+ ignoreDependencies: [
17
+ // These are peer dependencies that may not be directly imported
18
+ "@babel/core",
19
+ "@types/babel__core",
20
+ "@types/webpack",
21
+ "webpack-dev-server",
22
+ // Test/build tooling
23
+ "memory-fs",
24
+ "thenify",
25
+ // Used in type tests but not directly imported
26
+ "@rspack/plugin-react-refresh",
27
+ // CLI tools used by developers
28
+ "@rspack/cli",
29
+ "webpack-cli",
30
+ "husky",
31
+ // Optional dependencies used in webpack/rspack configs
32
+ "mini-css-extract-plugin",
33
+ "webpack-assets-manifest",
34
+ "webpack-subresource-integrity",
35
+ "rspack-manifest-plugin",
36
+ "sass-loader",
37
+ // Package merger utility
38
+ "@types/webpack-merge",
39
+ // Optional runtime dependencies
40
+ "ts-node",
41
+ "@pmmmwh/react-refresh-webpack-plugin",
42
+ // Optional peer dependencies referenced in code
43
+ "@rspack/core",
44
+ "@swc/core",
45
+ "babel-loader",
46
+ "compression-webpack-plugin",
47
+ "css-loader",
48
+ "esbuild-loader",
49
+ "swc-loader",
50
+ "webpack"
51
+ ]
52
+ }
53
+
54
+ export default config
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Minimal shim - all logic is in the TypeScript module
4
+ const { run } = require("shakapacker/configExporter")
5
+
6
+ run(process.argv.slice(2))
7
+ .then((exitCode) => process.exit(exitCode))
8
+ .catch((error) => {
9
+ console.error(error.message)
10
+ process.exit(1)
11
+ })
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  ENV["RAILS_ENV"] ||= "development"
4
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
4
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
5
5
  ENV["APP_ROOT"] ||= File.expand_path("..", __dir__)
6
6
 
7
7
  require "bundler/setup"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  ENV["RAILS_ENV"] ||= "development"
4
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
4
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
5
5
 
6
6
  require "bundler/setup"
7
7
  require "shakapacker"
@@ -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
@@ -241,6 +241,13 @@ module Shakapacker
241
241
  raise "Failed to install prod dependencies"
242
242
  end
243
243
  end
244
+
245
+ # Run a full install to ensure optional dependencies (like native bindings) are properly resolved
246
+ # This is especially important for packages like @rspack/core that use platform-specific native modules
247
+ unless package_json.manager.install
248
+ puts "❌ Failed to run full install to resolve optional dependencies"
249
+ raise "Failed to run full install"
250
+ end
244
251
  end
245
252
 
246
253
  def get_package_json
@@ -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