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,393 @@
1
+ /* eslint-disable no-template-curly-in-string */
2
+ const {
3
+ writeFileSync,
4
+ mkdirSync,
5
+ rmSync,
6
+ existsSync,
7
+ symlinkSync
8
+ } = require("fs")
9
+ const { resolve, join } = require("path")
10
+ const { tmpdir } = require("os")
11
+ const {
12
+ ConfigFileLoader,
13
+ generateSampleConfigFile
14
+ } = require("../../package/configExporter")
15
+
16
+ describe("ConfigFileLoader", () => {
17
+ const testDir = resolve(__dirname, "../tmp/config-file-test")
18
+ let configPath
19
+
20
+ beforeEach(() => {
21
+ // Create test directory
22
+ if (!existsSync(testDir)) {
23
+ mkdirSync(testDir, { recursive: true })
24
+ }
25
+ mkdirSync(join(testDir, "config"), { recursive: true })
26
+ configPath = join(testDir, "config/shakapacker-builds.yml")
27
+ })
28
+
29
+ afterEach(() => {
30
+ // Clean up test directory
31
+ if (existsSync(testDir)) {
32
+ rmSync(testDir, { recursive: true, force: true })
33
+ }
34
+ })
35
+
36
+ describe("validateConfigPath", () => {
37
+ it("should reject path traversal attempts with ..", () => {
38
+ // Use a path that's definitely outside the project
39
+ const maliciousPath = "/etc/passwd"
40
+ expect(() => {
41
+ // eslint-disable-next-line no-new
42
+ new ConfigFileLoader(maliciousPath)
43
+ }).toThrow(/Config file must be within project directory/)
44
+ })
45
+
46
+ it("should reject symlink traversal to files outside project", async () => {
47
+ const outsideFile = join(tmpdir(), `test-outside-${Date.now()}.yml`)
48
+ const symlinkPath = join(testDir, "symlink-config.yml")
49
+
50
+ const cleanup = () => {
51
+ try {
52
+ rmSync(symlinkPath, { force: true })
53
+ // eslint-disable-next-line no-empty
54
+ } catch {}
55
+ try {
56
+ rmSync(outsideFile, { force: true })
57
+ // eslint-disable-next-line no-empty
58
+ } catch {}
59
+ }
60
+
61
+ try {
62
+ // Create a real file outside the project (in system temp dir)
63
+ writeFileSync(
64
+ outsideFile,
65
+ "builds:\n test:\n outputs:\n - client\n"
66
+ )
67
+
68
+ // Attempt to create symlink
69
+ try {
70
+ symlinkSync(outsideFile, symlinkPath)
71
+ } catch (error) {
72
+ // Skip test if symlinks aren't supported or require elevated permissions
73
+ const skipCodes = ["EPERM", "ENOSYS", "EACCES"]
74
+ cleanup()
75
+ // eslint-disable-next-line jest/no-conditional-expect
76
+ expect(skipCodes).toContain(error.code)
77
+ return
78
+ }
79
+
80
+ // Verify that loading via symlink is rejected
81
+ expect(() => {
82
+ // eslint-disable-next-line no-new
83
+ new ConfigFileLoader(symlinkPath)
84
+ }).toThrow(/Config file must be within project directory/)
85
+
86
+ cleanup()
87
+ } catch (error) {
88
+ cleanup()
89
+ throw error
90
+ }
91
+ })
92
+
93
+ it("should accept paths within the project directory", () => {
94
+ expect(() => {
95
+ // eslint-disable-next-line no-new
96
+ new ConfigFileLoader(configPath)
97
+ }).not.toThrow()
98
+ })
99
+ })
100
+
101
+ describe("exists", () => {
102
+ it("should return false when config file does not exist", () => {
103
+ const loader = new ConfigFileLoader(configPath)
104
+ expect(loader.exists()).toBe(false)
105
+ })
106
+
107
+ it("should return true when config file exists", () => {
108
+ writeFileSync(configPath, "default_bundler: webpack\nbuilds: {}")
109
+ const loader = new ConfigFileLoader(configPath)
110
+ expect(loader.exists()).toBe(true)
111
+ })
112
+ })
113
+
114
+ describe("load", () => {
115
+ it("should load valid YAML config", () => {
116
+ writeFileSync(
117
+ configPath,
118
+ `
119
+ default_bundler: rspack
120
+ builds:
121
+ dev:
122
+ description: Development build
123
+ environment:
124
+ NODE_ENV: development
125
+ outputs:
126
+ - client
127
+ - server
128
+ `
129
+ )
130
+ const loader = new ConfigFileLoader(configPath)
131
+ const loaded = loader.load()
132
+ expect(loaded.default_bundler).toBe("rspack")
133
+ expect(loaded.builds.dev).toBeDefined()
134
+ expect(loaded.builds.dev.description).toBe("Development build")
135
+ })
136
+
137
+ it("should throw error for malformed YAML", () => {
138
+ writeFileSync(configPath, "invalid: yaml: content:\n - broken")
139
+ const loader = new ConfigFileLoader(configPath)
140
+ expect(() => loader.load()).toThrow(Error)
141
+ })
142
+
143
+ it("should throw error if builds key is missing", () => {
144
+ writeFileSync(configPath, "default_bundler: webpack")
145
+ const loader = new ConfigFileLoader(configPath)
146
+ expect(() => loader.load()).toThrow(/must contain a 'builds'/)
147
+ })
148
+
149
+ it("should throw error if builds is not an object", () => {
150
+ writeFileSync(configPath, "builds: []")
151
+ const loader = new ConfigFileLoader(configPath)
152
+ expect(() => loader.load()).toThrow(/must contain at least one build/)
153
+ })
154
+ })
155
+
156
+ describe("resolveBuild", () => {
157
+ beforeEach(() => {
158
+ writeFileSync(
159
+ configPath,
160
+ `
161
+ default_bundler: rspack
162
+ builds:
163
+ dev:
164
+ description: Development build
165
+ environment:
166
+ NODE_ENV: development
167
+ RAILS_ENV: development
168
+ outputs:
169
+ - client
170
+ - server
171
+ prod:
172
+ description: Production build
173
+ bundler: webpack
174
+ environment:
175
+ NODE_ENV: production
176
+ outputs:
177
+ - client
178
+ `
179
+ )
180
+ })
181
+
182
+ it("should throw error for non-existent build", () => {
183
+ const loader = new ConfigFileLoader(configPath)
184
+ expect(() => {
185
+ loader.resolveBuild("nonexistent", {}, "webpack")
186
+ }).toThrow(/Build 'nonexistent' not found/)
187
+ })
188
+
189
+ it("should resolve build with environment variables", () => {
190
+ const loader = new ConfigFileLoader(configPath)
191
+ const resolved = loader.resolveBuild("dev", {}, "webpack")
192
+ expect(resolved.name).toBe("dev")
193
+ expect(resolved.environment.NODE_ENV).toBe("development")
194
+ expect(resolved.environment.RAILS_ENV).toBe("development")
195
+ expect(resolved.outputs).toStrictEqual(["client", "server"])
196
+ })
197
+
198
+ it("should use build-specific bundler over default", () => {
199
+ const loader = new ConfigFileLoader(configPath)
200
+ const resolved = loader.resolveBuild("prod", {}, "rspack")
201
+ expect(resolved.bundler).toBe("webpack")
202
+ })
203
+
204
+ it("should use CLI bundler option over everything", () => {
205
+ const loader = new ConfigFileLoader(configPath)
206
+ const resolved = loader.resolveBuild(
207
+ "prod",
208
+ { bundler: "rspack" },
209
+ "webpack"
210
+ )
211
+ expect(resolved.bundler).toBe("rspack")
212
+ })
213
+ })
214
+
215
+ describe("edge case validation", () => {
216
+ it("should throw error for empty outputs array", () => {
217
+ writeFileSync(
218
+ configPath,
219
+ `
220
+ builds:
221
+ bad:
222
+ environment:
223
+ NODE_ENV: development
224
+ outputs: []
225
+ `
226
+ )
227
+ const loader = new ConfigFileLoader(configPath)
228
+ expect(() => {
229
+ loader.resolveBuild("bad", {}, "webpack")
230
+ }).toThrow(/empty outputs array/)
231
+ })
232
+
233
+ it("should throw error for duplicate outputs", () => {
234
+ writeFileSync(
235
+ configPath,
236
+ `
237
+ builds:
238
+ bad:
239
+ environment:
240
+ NODE_ENV: development
241
+ outputs:
242
+ - client
243
+ - client
244
+ - server
245
+ `
246
+ )
247
+ const loader = new ConfigFileLoader(configPath)
248
+ expect(() => {
249
+ loader.resolveBuild("bad", {}, "webpack")
250
+ }).toThrow(/duplicate output types/)
251
+ })
252
+
253
+ it("should throw error for invalid config file path with path traversal", () => {
254
+ writeFileSync(
255
+ configPath,
256
+ `
257
+ builds:
258
+ bad:
259
+ environment:
260
+ NODE_ENV: development
261
+ config: ../../../malicious.js
262
+ outputs:
263
+ - client
264
+ `
265
+ )
266
+ const loader = new ConfigFileLoader(configPath)
267
+ expect(() => {
268
+ loader.resolveBuild("bad", {}, "webpack")
269
+ }).toThrow(/Invalid config file path/)
270
+ })
271
+ })
272
+
273
+ describe("environment variable expansion", () => {
274
+ beforeEach(() => {
275
+ process.env.TEST_VAR = "test-value"
276
+ process.env.BUNDLER_VAR = "should-not-be-used"
277
+ })
278
+
279
+ afterEach(() => {
280
+ delete process.env.TEST_VAR
281
+ delete process.env.BUNDLER_VAR
282
+ })
283
+
284
+ it("should expand ${BUNDLER} variable", () => {
285
+ writeFileSync(
286
+ configPath,
287
+ "builds:\n test:\n environment:\n CONFIG_PATH: config/${BUNDLER}/config.js\n outputs:\n - client\n"
288
+ )
289
+ const loader = new ConfigFileLoader(configPath)
290
+ const resolved = loader.resolveBuild("test", {}, "rspack")
291
+ expect(resolved.environment.CONFIG_PATH).toBe("config/rspack/config.js")
292
+ })
293
+
294
+ it("should expand ${VAR} from environment", () => {
295
+ writeFileSync(
296
+ configPath,
297
+ "builds:\n test:\n environment:\n CUSTOM: ${TEST_VAR}\n outputs:\n - client\n"
298
+ )
299
+ const loader = new ConfigFileLoader(configPath)
300
+ const resolved = loader.resolveBuild("test", {}, "webpack")
301
+ expect(resolved.environment.CUSTOM).toBe("test-value")
302
+ })
303
+
304
+ it("should expand ${VAR:-default} with default value", () => {
305
+ writeFileSync(
306
+ configPath,
307
+ "builds:\n test:\n environment:\n WITH_DEFAULT: ${NONEXISTENT:-fallback-value}\n outputs:\n - client\n"
308
+ )
309
+ const loader = new ConfigFileLoader(configPath)
310
+ const resolved = loader.resolveBuild("test", {}, "webpack")
311
+ expect(resolved.environment.WITH_DEFAULT).toBe("fallback-value")
312
+ })
313
+
314
+ it("should use environment value over default in ${VAR:-default}", () => {
315
+ writeFileSync(
316
+ configPath,
317
+ "builds:\n test:\n environment:\n WITH_DEFAULT: ${TEST_VAR:-fallback-value}\n outputs:\n - client\n"
318
+ )
319
+ const loader = new ConfigFileLoader(configPath)
320
+ const resolved = loader.resolveBuild("test", {}, "webpack")
321
+ expect(resolved.environment.WITH_DEFAULT).toBe("test-value")
322
+ })
323
+
324
+ it("should reject invalid environment variable names", () => {
325
+ writeFileSync(
326
+ configPath,
327
+ "builds:\n test:\n environment:\n BAD: ${Invalid-Var-Name}\n outputs:\n - client\n"
328
+ )
329
+ const loader = new ConfigFileLoader(configPath)
330
+ const resolved = loader.resolveBuild("test", {}, "webpack")
331
+ // Should not expand invalid var names (contains hyphen)
332
+ expect(resolved.environment.BAD).toBe("${Invalid-Var-Name}")
333
+ })
334
+ })
335
+
336
+ describe("bundler_env conversion", () => {
337
+ it("should convert bundler_env to CLI arguments", () => {
338
+ writeFileSync(
339
+ configPath,
340
+ `
341
+ builds:
342
+ test:
343
+ environment:
344
+ NODE_ENV: production
345
+ bundler_env:
346
+ target: modern
347
+ instrumented: true
348
+ disabled: false
349
+ outputs:
350
+ - client
351
+ `
352
+ )
353
+ const loader = new ConfigFileLoader(configPath)
354
+ const resolved = loader.resolveBuild("test", {}, "webpack")
355
+
356
+ // YAML parses booleans as true/false, or as strings "true"/"false"
357
+ // The code handles both cases: true or "true" becomes a flag, false/"false" is ignored
358
+ // Expected format: ['--env', 'target=modern', '--env', 'instrumented']
359
+ expect(resolved.bundlerEnvArgs).toContain("--env")
360
+ expect(resolved.bundlerEnvArgs).toContain("target=modern")
361
+
362
+ // Boolean true becomes a flag (--env key), false is ignored
363
+ const argsString = resolved.bundlerEnvArgs.join(" ")
364
+ expect(argsString).toContain("--env instrumented")
365
+ expect(argsString).not.toContain("disabled")
366
+ })
367
+ })
368
+ })
369
+
370
+ describe("generateSampleConfigFile", () => {
371
+ it("should generate valid YAML string", () => {
372
+ const content = generateSampleConfigFile()
373
+ expect(content).toContain("builds:")
374
+ expect(content).toContain("dev-hmr:")
375
+ expect(content).toContain("dev_server: true")
376
+ expect(content).toContain("dev:")
377
+ expect(content).toContain("prod:")
378
+ })
379
+
380
+ it("should include documentation comments", () => {
381
+ const content = generateSampleConfigFile()
382
+ expect(content).toContain("# Bundler Build Configurations")
383
+ expect(content).toContain("HMR")
384
+ expect(content).toContain("production")
385
+ })
386
+
387
+ it("should escape template literal variables correctly", () => {
388
+ const content = generateSampleConfigFile()
389
+ // Should have ${BUNDLER} not actual 'webpack' or 'rspack'
390
+ expect(content).toContain("${BUNDLER}")
391
+ expect(content).toContain("${RAILS_ENV:-staging}")
392
+ })
393
+ })
@@ -0,0 +1,262 @@
1
+ const {
2
+ writeFileSync,
3
+ mkdirSync,
4
+ rmSync,
5
+ existsSync,
6
+ readdirSync
7
+ } = require("fs")
8
+ const { resolve, join } = require("path")
9
+ const { execSync } = require("child_process")
10
+
11
+ describe("Config Exporter Integration Tests", () => {
12
+ const testDir = resolve(__dirname, "../tmp/integration-test")
13
+ const configPath = join(testDir, "config/shakapacker-builds.yml")
14
+ const outputDir = join(testDir, "output")
15
+ const binPath = resolve(__dirname, "../../bin/shakapacker-config")
16
+
17
+ beforeEach(() => {
18
+ // Create test directory
19
+ if (existsSync(testDir)) {
20
+ rmSync(testDir, { recursive: true, force: true })
21
+ }
22
+ mkdirSync(testDir, { recursive: true })
23
+ mkdirSync(join(testDir, "config"), { recursive: true })
24
+
25
+ // Create minimal package.json
26
+ writeFileSync(
27
+ join(testDir, "package.json"),
28
+ JSON.stringify({ name: "test-app", private: true })
29
+ )
30
+
31
+ // Create minimal shakapacker.yml
32
+ writeFileSync(
33
+ join(testDir, "shakapacker.yml"),
34
+ `default: &default
35
+ source_path: app/javascript
36
+ source_entry_path: /
37
+ public_root_path: public
38
+ public_output_path: packs
39
+
40
+ development:
41
+ <<: *default
42
+ compile: true
43
+
44
+ production:
45
+ <<: *default
46
+ compile: true
47
+ `
48
+ )
49
+
50
+ // Create minimal webpack config that doesn't require shakapacker
51
+ mkdirSync(join(testDir, "config", "webpack"), { recursive: true })
52
+ writeFileSync(
53
+ join(testDir, "config", "webpack", "webpack.config.js"),
54
+ `module.exports = {
55
+ mode: process.env.NODE_ENV || 'development',
56
+ entry: './app/javascript/application.js',
57
+ output: {
58
+ path: require('path').resolve(__dirname, '../../public/packs'),
59
+ filename: '[name].js'
60
+ }
61
+ }\n`
62
+ )
63
+
64
+ // Create minimal entry file
65
+ mkdirSync(join(testDir, "app", "javascript"), { recursive: true })
66
+ writeFileSync(
67
+ join(testDir, "app", "javascript", "application.js"),
68
+ "// Test entry file\nconsole.log('test');\n"
69
+ )
70
+ })
71
+
72
+ afterEach(() => {
73
+ if (existsSync(testDir)) {
74
+ rmSync(testDir, { recursive: true, force: true })
75
+ }
76
+ })
77
+
78
+ describe("--all-builds with environment variable isolation", () => {
79
+ it("should isolate environment variables between builds", () => {
80
+ // Create config with builds that have different env vars
81
+ const configContent = `
82
+ default_bundler: webpack
83
+
84
+ builds:
85
+ dev-hmr:
86
+ description: Development with HMR
87
+ environment:
88
+ NODE_ENV: development
89
+ RAILS_ENV: development
90
+ WEBPACK_SERVE: "true"
91
+ outputs:
92
+ - client
93
+
94
+ dev:
95
+ description: Development without HMR
96
+ environment:
97
+ NODE_ENV: development
98
+ RAILS_ENV: development
99
+ outputs:
100
+ - client
101
+
102
+ prod:
103
+ description: Production
104
+ environment:
105
+ NODE_ENV: production
106
+ RAILS_ENV: production
107
+ outputs:
108
+ - client
109
+ `
110
+ writeFileSync(configPath, configContent)
111
+
112
+ // Run --all-builds command
113
+ const result = execSync(
114
+ `cd "${testDir}" && node "${binPath}" --all-builds --save-dir="${outputDir}"`,
115
+ { encoding: "utf8" }
116
+ )
117
+
118
+ // Verify output
119
+ expect(result).toContain("Exporting 3 builds")
120
+ expect(result).toContain("dev-hmr")
121
+ expect(result).toContain("dev")
122
+ expect(result).toContain("prod")
123
+
124
+ // Verify files were created
125
+ expect(existsSync(outputDir)).toBe(true)
126
+ const files = readdirSync(outputDir)
127
+
128
+ // Should have 3 files (one per build)
129
+ expect(files).toHaveLength(3)
130
+ expect(files).toContain("webpack-dev-hmr-client.yml")
131
+ expect(files).toContain("webpack-dev-client.yml")
132
+ expect(files).toContain("webpack-prod-client.yml")
133
+
134
+ // Verify files have different content (proving environment isolation)
135
+ const devHmrContent = require("fs").readFileSync(
136
+ join(outputDir, "webpack-dev-hmr-client.yml"),
137
+ "utf8"
138
+ )
139
+ const devContent = require("fs").readFileSync(
140
+ join(outputDir, "webpack-dev-client.yml"),
141
+ "utf8"
142
+ )
143
+ const prodContent = require("fs").readFileSync(
144
+ join(outputDir, "webpack-prod-client.yml"),
145
+ "utf8"
146
+ )
147
+
148
+ // All three files should be different (proving isolation)
149
+ expect(devHmrContent).not.toBe(devContent)
150
+ expect(devContent).not.toBe(prodContent)
151
+ expect(devHmrContent).not.toBe(prodContent)
152
+
153
+ // Verify environment-specific values
154
+ expect(devContent).toContain("mode: development")
155
+ expect(prodContent).toContain("mode: production")
156
+ })
157
+ })
158
+
159
+ describe("--doctor mode with config file", () => {
160
+ it("should always use config file builds when config exists", () => {
161
+ // Create config with custom builds
162
+ const configContent = `
163
+ builds:
164
+ custom-dev:
165
+ description: Custom development
166
+ environment:
167
+ NODE_ENV: development
168
+ RAILS_ENV: development
169
+ outputs:
170
+ - client
171
+
172
+ custom-prod:
173
+ description: Custom production
174
+ environment:
175
+ NODE_ENV: production
176
+ RAILS_ENV: production
177
+ outputs:
178
+ - client
179
+ `
180
+ writeFileSync(configPath, configContent)
181
+
182
+ // Run --doctor command
183
+ const result = execSync(
184
+ `cd "${testDir}" && node "${binPath}" --doctor --save-dir="${outputDir}"`,
185
+ { encoding: "utf8" }
186
+ )
187
+
188
+ // Verify it used config builds
189
+ expect(result).toContain(
190
+ "Using builds from config/shakapacker-builds.yml"
191
+ )
192
+ expect(result).toContain("custom-dev")
193
+ expect(result).toContain("custom-prod")
194
+
195
+ // Verify files
196
+ expect(existsSync(outputDir)).toBe(true)
197
+ const files = readdirSync(outputDir)
198
+ expect(files).toContain("webpack-custom-dev-client.yml")
199
+ expect(files).toContain("webpack-custom-prod-client.yml")
200
+ })
201
+
202
+ it("should use fallback builds when no config file exists", () => {
203
+ // Don't create config file
204
+
205
+ // Run --doctor command
206
+ const result = execSync(
207
+ `cd "${testDir}" && node "${binPath}" --doctor --save-dir="${outputDir}"`,
208
+ { encoding: "utf8" }
209
+ )
210
+
211
+ // Verify it warns and uses hardcoded fallback builds
212
+ expect(result).toContain("No build config file found")
213
+ expect(result).toContain("bin/shakapacker-config --init")
214
+ expect(result).toContain("development (HMR)")
215
+ expect(result).toContain("development")
216
+ expect(result).toContain("production")
217
+ })
218
+ })
219
+
220
+ describe("hMR config generation", () => {
221
+ it("should generate HMR client config with correct metadata", () => {
222
+ const configContent = `
223
+ default_bundler: webpack
224
+
225
+ builds:
226
+ dev-hmr:
227
+ description: Development with HMR
228
+ environment:
229
+ NODE_ENV: development
230
+ RAILS_ENV: development
231
+ WEBPACK_SERVE: "true"
232
+ outputs:
233
+ - client
234
+ `
235
+ writeFileSync(configPath, configContent)
236
+
237
+ // Run command
238
+ execSync(
239
+ `cd "${testDir}" && node "${binPath}" --build=dev-hmr --save-dir="${outputDir}"`,
240
+ { encoding: "utf8" }
241
+ )
242
+
243
+ // Verify HMR file was created with correct naming
244
+ expect(existsSync(outputDir)).toBe(true)
245
+ const files = readdirSync(outputDir)
246
+
247
+ // Should create file with -hmr suffix or similar indicator
248
+ expect(files).toHaveLength(1)
249
+ const filename = files[0]
250
+
251
+ // Read content and verify it's a valid webpack config
252
+ const content = require("fs").readFileSync(
253
+ join(outputDir, filename),
254
+ "utf8"
255
+ )
256
+ // Verify it contains webpack config content
257
+ expect(content).toContain("mode: development")
258
+ expect(content).toContain("entry:")
259
+ expect(content).toContain("output:")
260
+ })
261
+ })
262
+ })
data/test/helpers.js CHANGED
@@ -43,7 +43,7 @@ const createTestCompiler = (config, fs = createInMemoryFs()) => {
43
43
  const chdirTestApp = () => {
44
44
  try {
45
45
  return process.chdir("spec/shakapacker/test_app")
46
- } catch (e) {
46
+ } catch {
47
47
  return null
48
48
  }
49
49
  }