shakapacker 9.2.0 → 9.3.0.beta.1

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