shakapacker 8.4.0 → 9.7.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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/address-review.md +206 -0
  3. data/.claude/commands/update-changelog.md +354 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
  6. data/.github/STATUS.md +1 -0
  7. data/.github/actionlint-matcher.json +17 -0
  8. data/.github/workflows/claude-code-review.yml +45 -0
  9. data/.github/workflows/claude.yml +55 -0
  10. data/.github/workflows/dummy.yml +18 -5
  11. data/.github/workflows/eslint-validation.yml +46 -0
  12. data/.github/workflows/generator.yml +38 -22
  13. data/.github/workflows/node.yml +116 -2
  14. data/.github/workflows/ruby.yml +57 -15
  15. data/.github/workflows/test-bundlers.yml +180 -0
  16. data/.gitignore +27 -0
  17. data/.husky/pre-commit +2 -0
  18. data/.npmignore +56 -0
  19. data/.prettierignore +7 -0
  20. data/.rubocop.yml +2 -0
  21. data/.yalcignore +26 -0
  22. data/CHANGELOG.md +487 -19
  23. data/CLAUDE.md +63 -0
  24. data/CONTRIBUTING.md +268 -21
  25. data/ESLINT_TECHNICAL_DEBT.md +165 -0
  26. data/README.md +497 -137
  27. data/Rakefile +44 -4
  28. data/TODO.md +58 -0
  29. data/TODO_v9.md +97 -0
  30. data/bin/conductor-exec +24 -0
  31. data/bin/shakapacker-config +11 -0
  32. data/conductor-setup.sh +147 -0
  33. data/conductor.json +9 -0
  34. data/docs/api-reference.md +519 -0
  35. data/docs/cdn_setup.md +384 -0
  36. data/docs/common-upgrades.md +695 -0
  37. data/docs/configuration.md +845 -0
  38. data/docs/css-modules-export-mode.md +566 -0
  39. data/docs/customizing_babel_config.md +16 -16
  40. data/docs/deployment.md +78 -7
  41. data/docs/developing_shakapacker.md +6 -0
  42. data/docs/early_hints.md +433 -0
  43. data/docs/early_hints_manual_api.md +454 -0
  44. data/docs/feature_testing.md +492 -0
  45. data/docs/node_package_api.md +70 -0
  46. data/docs/optional-peer-dependencies.md +203 -0
  47. data/docs/peer-dependencies.md +71 -0
  48. data/docs/precompile_hook.md +486 -0
  49. data/docs/preventing_fouc.md +132 -0
  50. data/docs/react.md +58 -48
  51. data/docs/releasing.md +288 -0
  52. data/docs/rspack.md +218 -0
  53. data/docs/rspack_migration_guide.md +862 -0
  54. data/docs/sprockets.md +1 -0
  55. data/docs/style_loader_vs_mini_css.md +12 -12
  56. data/docs/subresource_integrity.md +13 -7
  57. data/docs/transpiler-migration.md +212 -0
  58. data/docs/transpiler-performance.md +200 -0
  59. data/docs/troubleshooting.md +272 -24
  60. data/docs/typescript-migration.md +388 -0
  61. data/docs/typescript.md +103 -0
  62. data/docs/using_esbuild_loader.md +12 -12
  63. data/docs/using_swc_loader.md +121 -16
  64. data/docs/v6_upgrade.md +42 -19
  65. data/docs/v7_upgrade.md +8 -6
  66. data/docs/v8_upgrade.md +13 -12
  67. data/docs/v9_upgrade.md +616 -0
  68. data/eslint.config.fast.js +254 -0
  69. data/eslint.config.js +309 -0
  70. data/jest.config.js +8 -1
  71. data/knip.ts +61 -0
  72. data/lib/install/bin/shakapacker +4 -6
  73. data/lib/install/bin/shakapacker-config +11 -0
  74. data/lib/install/bin/shakapacker-dev-server +1 -1
  75. data/lib/install/binstubs.rb +6 -2
  76. data/lib/install/config/rspack/rspack.config.js +6 -0
  77. data/lib/install/config/rspack/rspack.config.ts +7 -0
  78. data/lib/install/config/shakapacker.yml +75 -12
  79. data/lib/install/config/webpack/webpack.config.ts +7 -0
  80. data/lib/install/package.json +38 -0
  81. data/lib/install/template.rb +207 -45
  82. data/lib/shakapacker/build_config_loader.rb +147 -0
  83. data/lib/shakapacker/bundler_switcher.rb +415 -0
  84. data/lib/shakapacker/compiler.rb +87 -0
  85. data/lib/shakapacker/configuration.rb +475 -6
  86. data/lib/shakapacker/dev_server.rb +88 -1
  87. data/lib/shakapacker/dev_server_runner.rb +240 -6
  88. data/lib/shakapacker/doctor.rb +1191 -0
  89. data/lib/shakapacker/env.rb +19 -3
  90. data/lib/shakapacker/helper.rb +411 -14
  91. data/lib/shakapacker/install/env.rb +33 -0
  92. data/lib/shakapacker/instance.rb +93 -4
  93. data/lib/shakapacker/manifest.rb +167 -30
  94. data/lib/shakapacker/railtie.rb +4 -0
  95. data/lib/shakapacker/rspack_runner.rb +19 -0
  96. data/lib/shakapacker/runner.rb +668 -9
  97. data/lib/shakapacker/swc_migrator.rb +384 -0
  98. data/lib/shakapacker/utils/manager.rb +2 -0
  99. data/lib/shakapacker/utils/version_syntax_converter.rb +1 -1
  100. data/lib/shakapacker/version.rb +1 -1
  101. data/lib/shakapacker/version_checker.rb +1 -1
  102. data/lib/shakapacker/webpack_runner.rb +4 -42
  103. data/lib/shakapacker.rb +159 -1
  104. data/lib/tasks/shakapacker/binstubs.rake +4 -2
  105. data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
  106. data/lib/tasks/shakapacker/doctor.rake +48 -0
  107. data/lib/tasks/shakapacker/export_bundler_config.rake +68 -0
  108. data/lib/tasks/shakapacker/install.rake +16 -4
  109. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  110. data/lib/tasks/shakapacker/switch_bundler.rake +72 -0
  111. data/lib/tasks/shakapacker.rake +2 -0
  112. data/package/.npmignore +4 -0
  113. data/package/babel/preset.ts +59 -0
  114. data/package/config.ts +189 -0
  115. data/package/configExporter/buildValidator.ts +906 -0
  116. data/package/configExporter/cli.ts +1748 -0
  117. data/package/configExporter/configDocs.ts +102 -0
  118. data/package/configExporter/configFile.ts +663 -0
  119. data/package/configExporter/fileWriter.ts +112 -0
  120. data/package/configExporter/index.ts +15 -0
  121. data/package/configExporter/types.ts +159 -0
  122. data/package/configExporter/yamlSerializer.ts +391 -0
  123. data/package/dev_server.ts +27 -0
  124. data/package/env.ts +92 -0
  125. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +36 -0
  126. data/package/environments/base.ts +147 -0
  127. data/package/environments/development.ts +88 -0
  128. data/package/environments/production.ts +82 -0
  129. data/package/environments/test.ts +55 -0
  130. data/package/environments/types.ts +98 -0
  131. data/package/esbuild/index.ts +40 -0
  132. data/package/index.d.ts +68 -93
  133. data/package/index.d.ts.template +72 -0
  134. data/package/index.ts +104 -0
  135. data/package/loaders.d.ts +28 -0
  136. data/package/optimization/rspack.ts +36 -0
  137. data/package/optimization/webpack.ts +55 -0
  138. data/package/plugins/envFilter.ts +82 -0
  139. data/package/plugins/rspack.ts +119 -0
  140. data/package/plugins/webpack.ts +82 -0
  141. data/package/rspack/index.ts +91 -0
  142. data/package/rules/{babel.js → babel.ts} +2 -2
  143. data/package/rules/{coffee.js → coffee.ts} +1 -1
  144. data/package/rules/css.ts +3 -0
  145. data/package/rules/{erb.js → erb.ts} +1 -1
  146. data/package/rules/esbuild.ts +10 -0
  147. data/package/rules/file.ts +41 -0
  148. data/package/rules/{jscommon.js → jscommon.ts} +5 -4
  149. data/package/rules/{less.js → less.ts} +4 -4
  150. data/package/rules/raw.ts +28 -0
  151. data/package/rules/rspack.ts +174 -0
  152. data/package/rules/sass.ts +21 -0
  153. data/package/rules/{stylus.js → stylus.ts} +4 -8
  154. data/package/rules/swc.ts +10 -0
  155. data/package/rules/{index.js → webpack.ts} +1 -2
  156. data/package/swc/index.ts +54 -0
  157. data/package/types/README.md +90 -0
  158. data/package/types/index.ts +69 -0
  159. data/package/types.ts +105 -0
  160. data/package/utils/bundlerUtils.ts +232 -0
  161. data/package/utils/configPath.ts +6 -0
  162. data/package/utils/debug.ts +45 -0
  163. data/package/utils/defaultConfigPath.ts +7 -0
  164. data/package/utils/ensureManifestExists.ts +17 -0
  165. data/package/utils/errorCodes.ts +249 -0
  166. data/package/utils/errorHelpers.ts +152 -0
  167. data/package/utils/getStyleRule.ts +75 -0
  168. data/package/utils/helpers.ts +99 -0
  169. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  170. data/package/utils/pathValidation.ts +207 -0
  171. data/package/utils/requireOrError.ts +24 -0
  172. data/package/utils/snakeToCamelCase.ts +5 -0
  173. data/package/utils/typeGuards.ts +388 -0
  174. data/package/utils/validateDependencies.ts +61 -0
  175. data/package/webpack-types.d.ts +33 -0
  176. data/package/webpackDevServerConfig.ts +130 -0
  177. data/package.json +157 -18
  178. data/scripts/remove-use-strict.js +44 -0
  179. data/scripts/type-check-no-emit.js +27 -0
  180. data/shakapacker.gemspec +4 -2
  181. data/sig/shakapacker/commands.rbs +35 -0
  182. data/sig/shakapacker/compiler.rbs +65 -0
  183. data/sig/shakapacker/compiler_strategy.rbs +41 -0
  184. data/sig/shakapacker/configuration.rbs +140 -0
  185. data/sig/shakapacker/dev_server.rbs +56 -0
  186. data/sig/shakapacker/env.rbs +25 -0
  187. data/sig/shakapacker/helper.rbs +98 -0
  188. data/sig/shakapacker/instance.rbs +46 -0
  189. data/sig/shakapacker/manifest.rbs +69 -0
  190. data/sig/shakapacker/version.rbs +4 -0
  191. data/sig/shakapacker.rbs +66 -0
  192. data/test/configExporter/buildValidator.test.js +1295 -0
  193. data/test/configExporter/configFile.test.js +393 -0
  194. data/test/configExporter/integration.test.js +262 -0
  195. data/test/helpers.js +1 -1
  196. data/test/package/bundlerUtils.rspack.test.js +145 -0
  197. data/test/package/bundlerUtils.test.js +97 -0
  198. data/test/package/config.test.js +14 -0
  199. data/test/package/configExporter/cli.test.js +440 -0
  200. data/test/package/configExporter/types.test.js +163 -0
  201. data/test/package/configExporter.test.js +491 -0
  202. data/test/package/env.test.js +42 -7
  203. data/test/package/environments/base.test.js +14 -4
  204. data/test/package/helpers.test.js +2 -2
  205. data/test/package/plugins/envFiltering.test.js +453 -0
  206. data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
  207. data/test/package/rspack/index.test.js +293 -0
  208. data/test/package/rspack/optimization.test.js +86 -0
  209. data/test/package/rspack/plugins.test.js +185 -0
  210. data/test/package/rspack/rules.test.js +229 -0
  211. data/test/package/rules/babel.test.js +65 -38
  212. data/test/package/rules/esbuild.test.js +13 -4
  213. data/test/package/rules/file.test.js +7 -1
  214. data/test/package/rules/raw.test.js +40 -7
  215. data/test/package/rules/sass-version-parsing.test.js +71 -0
  216. data/test/package/rules/sass.test.js +11 -6
  217. data/test/package/rules/sass1.test.js +8 -5
  218. data/test/package/rules/sass16.test.js +24 -0
  219. data/test/package/rules/swc.test.js +50 -39
  220. data/test/package/rules/webpack.test.js +35 -0
  221. data/test/package/staging.test.js +4 -3
  222. data/test/package/transpiler-defaults.test.js +169 -0
  223. data/test/package/utils/ensureManifestExists.test.js +51 -0
  224. data/test/package/yamlSerializer.test.js +204 -0
  225. data/test/peer-dependencies.sh +85 -0
  226. data/test/resolver.js +34 -3
  227. data/test/scripts/remove-use-strict.test.js +125 -0
  228. data/test/typescript/build.test.js +118 -0
  229. data/test/typescript/environments.test.js +107 -0
  230. data/test/typescript/pathValidation.test.js +186 -0
  231. data/test/typescript/requireOrError.test.js +49 -0
  232. data/test/typescript/securityValidation.test.js +182 -0
  233. data/tools/README.md +134 -0
  234. data/tools/css-modules-v9-codemod.js +179 -0
  235. data/tsconfig.eslint.json +9 -0
  236. data/tsconfig.json +38 -0
  237. data/yarn.lock +3202 -1097
  238. metadata +212 -44
  239. data/.eslintignore +0 -4
  240. data/.eslintrc.js +0 -36
  241. data/Gemfile.lock +0 -251
  242. data/package/babel/preset.js +0 -48
  243. data/package/config.js +0 -56
  244. data/package/dev_server.js +0 -23
  245. data/package/env.js +0 -48
  246. data/package/environments/base.js +0 -171
  247. data/package/environments/development.js +0 -13
  248. data/package/environments/production.js +0 -88
  249. data/package/environments/test.js +0 -3
  250. data/package/esbuild/index.js +0 -40
  251. data/package/index.js +0 -40
  252. data/package/rules/css.js +0 -3
  253. data/package/rules/esbuild.js +0 -10
  254. data/package/rules/file.js +0 -29
  255. data/package/rules/raw.js +0 -5
  256. data/package/rules/sass.js +0 -18
  257. data/package/rules/swc.js +0 -10
  258. data/package/swc/index.js +0 -50
  259. data/package/utils/configPath.js +0 -4
  260. data/package/utils/defaultConfigPath.js +0 -2
  261. data/package/utils/getStyleRule.js +0 -40
  262. data/package/utils/helpers.js +0 -62
  263. data/package/utils/snakeToCamelCase.js +0 -5
  264. data/package/webpackDevServerConfig.js +0 -71
  265. data/test/package/rules/index.test.js +0 -16
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Security tests for environment variable filtering in EnvironmentPlugin.
3
+ *
4
+ * These tests verify that only allowlisted environment variables are exposed
5
+ * to client-side JavaScript bundles, preventing accidental leakage of secrets.
6
+ *
7
+ * CVE: Environment variables leak via EnvironmentPlugin(process.env)
8
+ * See: https://github.com/shakacode/shakapacker/security/advisories
9
+ */
10
+
11
+ const fs = require("fs")
12
+ const path = require("path")
13
+
14
+ const pluginsDir = path.resolve(__dirname, "../../../package/plugins")
15
+
16
+ describe("environment variable filtering security", () => {
17
+ const originalEnv = { ...process.env }
18
+
19
+ beforeEach(() => {
20
+ // Set up test environment with sensitive variables
21
+ process.env.NODE_ENV = "production"
22
+ process.env.RAILS_ENV = "production"
23
+ process.env.WEBPACK_SERVE = "false"
24
+
25
+ // Simulate sensitive build environment variables
26
+ process.env.DATABASE_URL = "postgres://user:password@host/db"
27
+ process.env.AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
28
+ process.env.AWS_SECRET_ACCESS_KEY =
29
+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
30
+ process.env.RAILS_MASTER_KEY = "abc123secretmasterkey456"
31
+ process.env.STRIPE_SECRET_KEY = "sk_live_secretkey123"
32
+ process.env.SESSION_SECRET = "supersecrettoken"
33
+
34
+ // Clear any cached modules
35
+ jest.resetModules()
36
+ })
37
+
38
+ afterEach(() => {
39
+ // Restore original environment
40
+ Object.keys(process.env).forEach((key) => {
41
+ if (!(key in originalEnv)) {
42
+ delete process.env[key]
43
+ }
44
+ })
45
+ Object.assign(process.env, originalEnv)
46
+ delete process.env.SHAKAPACKER_ENV_VARS
47
+ })
48
+
49
+ describe("shared envFilter module", () => {
50
+ it("exists and exports the filtering functions", () => {
51
+ const envFilterSource = fs.readFileSync(
52
+ path.join(pluginsDir, "envFilter.ts"),
53
+ "utf8"
54
+ )
55
+
56
+ // Verify exports
57
+ expect(envFilterSource).toContain("export const DEFAULT_ALLOWED_ENV_VARS")
58
+ expect(envFilterSource).toContain("export const getAllowedEnvVars")
59
+ expect(envFilterSource).toContain("export const getFilteredEnv")
60
+ })
61
+
62
+ it("has the default allowlist with only safe variables", () => {
63
+ const envFilterSource = fs.readFileSync(
64
+ path.join(pluginsDir, "envFilter.ts"),
65
+ "utf8"
66
+ )
67
+
68
+ // Extract the DEFAULT_ALLOWED_ENV_VARS array from source
69
+ const allowlistMatch = envFilterSource.match(
70
+ /DEFAULT_ALLOWED_ENV_VARS\s*=\s*\[([\s\S]*?)\]\s*as const/
71
+ )
72
+ expect(allowlistMatch).toBeTruthy()
73
+
74
+ const allowlistContent = allowlistMatch[1]
75
+
76
+ // These patterns should NEVER appear in the allowlist
77
+ const sensitivePatterns = [
78
+ "DATABASE",
79
+ "SECRET",
80
+ "PASSWORD",
81
+ "CREDENTIAL",
82
+ "AWS_",
83
+ "STRIPE",
84
+ "MASTER"
85
+ ]
86
+
87
+ sensitivePatterns.forEach((pattern) => {
88
+ expect(allowlistContent.toUpperCase()).not.toContain(pattern)
89
+ })
90
+
91
+ // Verify expected safe vars are present
92
+ expect(allowlistContent).toContain("NODE_ENV")
93
+ expect(allowlistContent).toContain("RAILS_ENV")
94
+ expect(allowlistContent).toContain("WEBPACK_SERVE")
95
+ })
96
+
97
+ it("includes SHAKAPACKER_ENV_VARS extension support", () => {
98
+ const envFilterSource = fs.readFileSync(
99
+ path.join(pluginsDir, "envFilter.ts"),
100
+ "utf8"
101
+ )
102
+
103
+ expect(envFilterSource).toContain("SHAKAPACKER_ENV_VARS")
104
+ expect(envFilterSource).toContain('split(",")')
105
+ })
106
+
107
+ it("exports PUBLIC_ENV_PREFIX constant", () => {
108
+ const envFilterSource = fs.readFileSync(
109
+ path.join(pluginsDir, "envFilter.ts"),
110
+ "utf8"
111
+ )
112
+
113
+ expect(envFilterSource).toContain("export const PUBLIC_ENV_PREFIX")
114
+ expect(envFilterSource).toContain('SHAKAPACKER_PUBLIC_"')
115
+ })
116
+
117
+ it("auto-exposes SHAKAPACKER_PUBLIC_* variables", () => {
118
+ const envFilterSource = fs.readFileSync(
119
+ path.join(pluginsDir, "envFilter.ts"),
120
+ "utf8"
121
+ )
122
+
123
+ // Verify the prefix check is present
124
+ expect(envFilterSource).toContain("startsWith(PUBLIC_ENV_PREFIX)")
125
+ expect(envFilterSource).toContain("Object.keys(process.env)")
126
+ })
127
+
128
+ it("uses SHAKAPACKER_PUBLIC_ prefix (with trailing underscore) to prevent system var exposure", () => {
129
+ const envFilterSource = fs.readFileSync(
130
+ path.join(pluginsDir, "envFilter.ts"),
131
+ "utf8"
132
+ )
133
+
134
+ // SECURITY: The prefix MUST include the trailing underscore to ensure
135
+ // Shakapacker system variables like SHAKAPACKER_CONFIG, SHAKAPACKER_ENV_VARS,
136
+ // SHAKAPACKER_PRECOMPILE, etc. are NOT accidentally exposed.
137
+ // Only SHAKAPACKER_PUBLIC_* variables should be auto-exposed.
138
+ const prefixMatch = envFilterSource.match(
139
+ /PUBLIC_ENV_PREFIX\s*=\s*["']([^"']+)["']/
140
+ )
141
+ expect(prefixMatch).toBeTruthy()
142
+ expect(prefixMatch[1]).toBe("SHAKAPACKER_PUBLIC_")
143
+
144
+ // Verify the trailing underscore is present - this is critical for security
145
+ expect(prefixMatch[1]).toMatch(/_$/)
146
+ })
147
+
148
+ it("handles whitespace and empty values in CSV", () => {
149
+ const envFilterSource = fs.readFileSync(
150
+ path.join(pluginsDir, "envFilter.ts"),
151
+ "utf8"
152
+ )
153
+
154
+ // Verify trim() is called on each value
155
+ expect(envFilterSource).toMatch(/\.map\(\s*\(?v\)?\s*=>\s*v\.trim\(\)/)
156
+ // Verify filter(Boolean) is called to remove empty strings
157
+ expect(envFilterSource).toMatch(/\.filter\(Boolean\)/)
158
+ })
159
+ })
160
+
161
+ describe("webpack plugin", () => {
162
+ it("imports from shared envFilter module", () => {
163
+ const webpackPluginSource = fs.readFileSync(
164
+ path.join(pluginsDir, "webpack.ts"),
165
+ "utf8"
166
+ )
167
+
168
+ expect(webpackPluginSource).toContain(
169
+ 'import { getFilteredEnv } from "./envFilter"'
170
+ )
171
+ })
172
+
173
+ it("uses getFilteredEnv() not process.env", () => {
174
+ const webpackPluginSource = fs.readFileSync(
175
+ path.join(pluginsDir, "webpack.ts"),
176
+ "utf8"
177
+ )
178
+
179
+ // SECURITY: Verify the dangerous pattern is NOT present
180
+ expect(webpackPluginSource).not.toMatch(
181
+ /new webpack\.EnvironmentPlugin\(process\.env\)/
182
+ )
183
+
184
+ // Verify the safe pattern IS present
185
+ expect(webpackPluginSource).toMatch(/getFilteredEnv\(\)/)
186
+ })
187
+
188
+ it("does not duplicate the filtering logic", () => {
189
+ const webpackPluginSource = fs.readFileSync(
190
+ path.join(pluginsDir, "webpack.ts"),
191
+ "utf8"
192
+ )
193
+
194
+ // Should NOT have its own copy of these
195
+ expect(webpackPluginSource).not.toContain("DEFAULT_ALLOWED_ENV_VARS")
196
+ expect(webpackPluginSource).not.toContain("PUBLIC_ENV_PREFIX")
197
+ expect(webpackPluginSource).not.toContain("getAllowedEnvVars")
198
+ })
199
+ })
200
+
201
+ describe("rspack plugin", () => {
202
+ it("imports from shared envFilter module", () => {
203
+ const rspackPluginSource = fs.readFileSync(
204
+ path.join(pluginsDir, "rspack.ts"),
205
+ "utf8"
206
+ )
207
+
208
+ expect(rspackPluginSource).toContain(
209
+ 'import { getFilteredEnv } from "./envFilter"'
210
+ )
211
+ })
212
+
213
+ it("uses getFilteredEnv() not process.env", () => {
214
+ const rspackPluginSource = fs.readFileSync(
215
+ path.join(pluginsDir, "rspack.ts"),
216
+ "utf8"
217
+ )
218
+
219
+ // SECURITY: Verify the dangerous pattern is NOT present
220
+ expect(rspackPluginSource).not.toMatch(
221
+ /new rspack\.EnvironmentPlugin\(process\.env\)/
222
+ )
223
+
224
+ // Verify the safe pattern IS present
225
+ expect(rspackPluginSource).toMatch(/getFilteredEnv\(\)/)
226
+ })
227
+
228
+ it("does not duplicate the filtering logic", () => {
229
+ const rspackPluginSource = fs.readFileSync(
230
+ path.join(pluginsDir, "rspack.ts"),
231
+ "utf8"
232
+ )
233
+
234
+ // Should NOT have its own copy of these
235
+ expect(rspackPluginSource).not.toContain("DEFAULT_ALLOWED_ENV_VARS")
236
+ expect(rspackPluginSource).not.toContain("PUBLIC_ENV_PREFIX")
237
+ expect(rspackPluginSource).not.toContain("getAllowedEnvVars")
238
+ })
239
+ })
240
+
241
+ describe("consistency", () => {
242
+ it("both plugins use the same shared module", () => {
243
+ const webpackPluginSource = fs.readFileSync(
244
+ path.join(pluginsDir, "webpack.ts"),
245
+ "utf8"
246
+ )
247
+ const rspackPluginSource = fs.readFileSync(
248
+ path.join(pluginsDir, "rspack.ts"),
249
+ "utf8"
250
+ )
251
+
252
+ // Both should import from the same source
253
+ const webpackImport = webpackPluginSource.match(
254
+ /import\s*{[^}]*getFilteredEnv[^}]*}\s*from\s*["']([^"']+)["']/
255
+ )
256
+ const rspackImport = rspackPluginSource.match(
257
+ /import\s*{[^}]*getFilteredEnv[^}]*}\s*from\s*["']([^"']+)["']/
258
+ )
259
+
260
+ expect(webpackImport).toBeTruthy()
261
+ expect(rspackImport).toBeTruthy()
262
+ expect(webpackImport[1]).toBe(rspackImport[1])
263
+ })
264
+ })
265
+
266
+ /**
267
+ * Runtime behavioral tests that actually call the filtering functions.
268
+ * These complement the static source analysis tests above.
269
+ */
270
+ describe("runtime behavior", () => {
271
+ // Helper to get fresh module instance (avoiding caching issues)
272
+ const getEnvFilter = () => {
273
+ jest.resetModules()
274
+ return require("../../../package/plugins/envFilter")
275
+ }
276
+
277
+ describe("getAllowedEnvVars", () => {
278
+ it("returns default allowed vars when SHAKAPACKER_ENV_VARS is unset", () => {
279
+ delete process.env.SHAKAPACKER_ENV_VARS
280
+ // Remove any SHAKAPACKER_PUBLIC_* vars from test setup
281
+ const publicVars = Object.keys(process.env).filter((key) =>
282
+ key.startsWith("SHAKAPACKER_PUBLIC_")
283
+ )
284
+ publicVars.forEach((key) => {
285
+ delete process.env[key]
286
+ })
287
+
288
+ const { getAllowedEnvVars } = getEnvFilter()
289
+ const allowed = getAllowedEnvVars()
290
+
291
+ expect(allowed).toContain("NODE_ENV")
292
+ expect(allowed).toContain("RAILS_ENV")
293
+ expect(allowed).toContain("WEBPACK_SERVE")
294
+ expect(allowed).toHaveLength(3)
295
+ })
296
+
297
+ it("includes SHAKAPACKER_PUBLIC_* variables when present", () => {
298
+ delete process.env.SHAKAPACKER_ENV_VARS
299
+ process.env.SHAKAPACKER_PUBLIC_API_URL = "https://api.example.com"
300
+ process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID = "UA-12345"
301
+
302
+ const { getAllowedEnvVars } = getEnvFilter()
303
+ const allowed = getAllowedEnvVars()
304
+
305
+ expect(allowed).toContain("NODE_ENV")
306
+ expect(allowed).toContain("RAILS_ENV")
307
+ expect(allowed).toContain("WEBPACK_SERVE")
308
+ expect(allowed).toContain("SHAKAPACKER_PUBLIC_API_URL")
309
+ expect(allowed).toContain("SHAKAPACKER_PUBLIC_ANALYTICS_ID")
310
+
311
+ // Cleanup
312
+ delete process.env.SHAKAPACKER_PUBLIC_API_URL
313
+ delete process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID
314
+ })
315
+
316
+ it("does NOT include SHAKAPACKER_* system variables (without PUBLIC_)", () => {
317
+ process.env.SHAKAPACKER_CONFIG = "/custom/path"
318
+ process.env.SHAKAPACKER_PRECOMPILE = "true"
319
+ delete process.env.SHAKAPACKER_ENV_VARS
320
+
321
+ const { getAllowedEnvVars } = getEnvFilter()
322
+ const allowed = getAllowedEnvVars()
323
+
324
+ expect(allowed).not.toContain("SHAKAPACKER_CONFIG")
325
+ expect(allowed).not.toContain("SHAKAPACKER_PRECOMPILE")
326
+
327
+ // Cleanup
328
+ delete process.env.SHAKAPACKER_CONFIG
329
+ delete process.env.SHAKAPACKER_PRECOMPILE
330
+ })
331
+
332
+ it("parses SHAKAPACKER_ENV_VARS CSV and includes those variables", () => {
333
+ process.env.SHAKAPACKER_ENV_VARS = "CUSTOM_VAR1,CUSTOM_VAR2,ANOTHER_VAR"
334
+
335
+ const { getAllowedEnvVars } = getEnvFilter()
336
+ const allowed = getAllowedEnvVars()
337
+
338
+ expect(allowed).toContain("CUSTOM_VAR1")
339
+ expect(allowed).toContain("CUSTOM_VAR2")
340
+ expect(allowed).toContain("ANOTHER_VAR")
341
+ })
342
+
343
+ it("handles whitespace in SHAKAPACKER_ENV_VARS CSV", () => {
344
+ process.env.SHAKAPACKER_ENV_VARS = " VAR1 , VAR2 , VAR3 "
345
+
346
+ const { getAllowedEnvVars } = getEnvFilter()
347
+ const allowed = getAllowedEnvVars()
348
+
349
+ expect(allowed).toContain("VAR1")
350
+ expect(allowed).toContain("VAR2")
351
+ expect(allowed).toContain("VAR3")
352
+ })
353
+
354
+ it("ignores empty entries in SHAKAPACKER_ENV_VARS CSV", () => {
355
+ process.env.SHAKAPACKER_ENV_VARS = "VAR1,,VAR2,,,VAR3,"
356
+
357
+ const { getAllowedEnvVars } = getEnvFilter()
358
+ const allowed = getAllowedEnvVars()
359
+
360
+ expect(allowed).toContain("VAR1")
361
+ expect(allowed).toContain("VAR2")
362
+ expect(allowed).toContain("VAR3")
363
+ // Should not contain empty strings
364
+ expect(allowed.filter((v) => v === "")).toHaveLength(0)
365
+ })
366
+
367
+ it("deduplicates variables from multiple sources", () => {
368
+ process.env.SHAKAPACKER_ENV_VARS = "NODE_ENV,CUSTOM_VAR"
369
+
370
+ const { getAllowedEnvVars } = getEnvFilter()
371
+ const allowed = getAllowedEnvVars()
372
+
373
+ // NODE_ENV should only appear once (from defaults, not duplicated from CSV)
374
+ const nodeEnvCount = allowed.filter((v) => v === "NODE_ENV").length
375
+ expect(nodeEnvCount).toBe(1)
376
+ })
377
+ })
378
+
379
+ describe("getFilteredEnv", () => {
380
+ it("exposes allowed variables with their values", () => {
381
+ delete process.env.SHAKAPACKER_ENV_VARS
382
+ process.env.NODE_ENV = "production"
383
+ process.env.RAILS_ENV = "staging"
384
+
385
+ const { getFilteredEnv } = getEnvFilter()
386
+ const filtered = getFilteredEnv()
387
+
388
+ expect(filtered).toHaveProperty("NODE_ENV", "production")
389
+ expect(filtered).toHaveProperty("RAILS_ENV", "staging")
390
+ })
391
+
392
+ it("omits sensitive variables that are not in allowlist", () => {
393
+ delete process.env.SHAKAPACKER_ENV_VARS
394
+ // Sensitive vars are set in beforeEach
395
+
396
+ const { getFilteredEnv } = getEnvFilter()
397
+ const filtered = getFilteredEnv()
398
+
399
+ // SECURITY: These must NOT be present
400
+ expect(filtered).not.toHaveProperty("DATABASE_URL")
401
+ expect(filtered).not.toHaveProperty("AWS_ACCESS_KEY_ID")
402
+ expect(filtered).not.toHaveProperty("AWS_SECRET_ACCESS_KEY")
403
+ expect(filtered).not.toHaveProperty("RAILS_MASTER_KEY")
404
+ expect(filtered).not.toHaveProperty("STRIPE_SECRET_KEY")
405
+ expect(filtered).not.toHaveProperty("SESSION_SECRET")
406
+ })
407
+
408
+ it("exposes SHAKAPACKER_PUBLIC_* variables with their values", () => {
409
+ delete process.env.SHAKAPACKER_ENV_VARS
410
+ process.env.SHAKAPACKER_PUBLIC_API_URL = "https://api.example.com"
411
+
412
+ const { getFilteredEnv } = getEnvFilter()
413
+ const filtered = getFilteredEnv()
414
+
415
+ expect(filtered).toHaveProperty(
416
+ "SHAKAPACKER_PUBLIC_API_URL",
417
+ "https://api.example.com"
418
+ )
419
+
420
+ // Cleanup
421
+ delete process.env.SHAKAPACKER_PUBLIC_API_URL
422
+ })
423
+
424
+ it("uses null for missing variables from SHAKAPACKER_ENV_VARS", () => {
425
+ process.env.SHAKAPACKER_ENV_VARS = "MISSING_VAR,ANOTHER_MISSING"
426
+ delete process.env.MISSING_VAR
427
+ delete process.env.ANOTHER_MISSING
428
+
429
+ const { getFilteredEnv } = getEnvFilter()
430
+ const filtered = getFilteredEnv()
431
+
432
+ expect(filtered).toHaveProperty("MISSING_VAR", null)
433
+ expect(filtered).toHaveProperty("ANOTHER_MISSING", null)
434
+ })
435
+
436
+ it("includes variables from SHAKAPACKER_ENV_VARS with their values", () => {
437
+ process.env.SHAKAPACKER_ENV_VARS = "CUSTOM_API_URL"
438
+ process.env.CUSTOM_API_URL = "https://custom.example.com"
439
+
440
+ const { getFilteredEnv } = getEnvFilter()
441
+ const filtered = getFilteredEnv()
442
+
443
+ expect(filtered).toHaveProperty(
444
+ "CUSTOM_API_URL",
445
+ "https://custom.example.com"
446
+ )
447
+
448
+ // Cleanup
449
+ delete process.env.CUSTOM_API_URL
450
+ })
451
+ })
452
+ })
453
+ })
@@ -0,0 +1,89 @@
1
+ const loadPluginsWithSriModule = (sriModule) => {
2
+ let getPlugins
3
+
4
+ jest.isolateModules(() => {
5
+ jest.doMock("../../../package/config", () => ({
6
+ manifestPath: "public/packs/manifest.json",
7
+ publicPathWithoutCDN: "/packs/",
8
+ integrity: {
9
+ enabled: true,
10
+ hash_functions: ["sha256"]
11
+ },
12
+ css_extract_ignore_order_warnings: false,
13
+ useContentHash: false
14
+ }))
15
+
16
+ jest.doMock("../../../package/env", () => ({
17
+ isProduction: true
18
+ }))
19
+
20
+ jest.doMock("../../../package/utils/helpers", () => ({
21
+ moduleExists: (moduleName) =>
22
+ moduleName === "webpack-subresource-integrity"
23
+ }))
24
+
25
+ jest.doMock("../../../package/utils/ensureManifestExists", () => ({
26
+ __esModule: true,
27
+ default: jest.fn()
28
+ }))
29
+
30
+ jest.doMock("../../../package/utils/requireOrError", () => ({
31
+ requireOrError: (moduleName) => {
32
+ if (moduleName === "webpack-assets-manifest") {
33
+ return function WebpackAssetsManifest() {}
34
+ }
35
+ if (moduleName === "webpack") {
36
+ return {
37
+ EnvironmentPlugin: function EnvironmentPlugin() {}
38
+ }
39
+ }
40
+ if (moduleName === "webpack-subresource-integrity") {
41
+ return sriModule
42
+ }
43
+
44
+ throw new Error(`Unexpected module request: ${moduleName}`)
45
+ }
46
+ }))
47
+ ;({ getPlugins } = require("../../../package/plugins/webpack"))
48
+ })
49
+
50
+ return getPlugins
51
+ }
52
+
53
+ describe("webpack plugins - webpack-subresource-integrity compatibility", () => {
54
+ afterEach(() => {
55
+ jest.clearAllMocks()
56
+ })
57
+
58
+ test("supports webpack-subresource-integrity v5 named export", () => {
59
+ const SubresourceIntegrityPlugin = jest.fn(
60
+ function SubresourceIntegrityPluginMock(options) {
61
+ this.options = options
62
+ }
63
+ )
64
+ const getPlugins = loadPluginsWithSriModule({ SubresourceIntegrityPlugin })
65
+
66
+ getPlugins()
67
+
68
+ expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
69
+ hashFuncNames: ["sha256"],
70
+ enabled: true
71
+ })
72
+ })
73
+
74
+ test("supports webpack-subresource-integrity default export", () => {
75
+ const SubresourceIntegrityPlugin = jest.fn(
76
+ function SubresourceIntegrityPluginMock(options) {
77
+ this.options = options
78
+ }
79
+ )
80
+ const getPlugins = loadPluginsWithSriModule(SubresourceIntegrityPlugin)
81
+
82
+ getPlugins()
83
+
84
+ expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
85
+ hashFuncNames: ["sha256"],
86
+ enabled: true
87
+ })
88
+ })
89
+ })