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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
- data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
- data/.github/workflows/claude-code-review.yml +4 -5
- data/.github/workflows/claude.yml +1 -2
- data/.github/workflows/dummy.yml +4 -4
- data/.github/workflows/generator.yml +9 -9
- data/.github/workflows/node.yml +11 -2
- data/.github/workflows/ruby.yml +16 -16
- data/.github/workflows/test-bundlers.yml +9 -9
- data/.gitignore +4 -0
- data/CHANGELOG.md +74 -5
- data/CLAUDE.md +6 -1
- data/CONTRIBUTING.md +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +14 -14
- data/TODO.md +10 -2
- data/TODO_v9.md +13 -3
- data/bin/export-bundler-config +1 -1
- data/conductor-setup.sh +1 -1
- data/conductor.json +1 -1
- data/docs/cdn_setup.md +13 -8
- data/docs/common-upgrades.md +2 -1
- data/docs/configuration.md +630 -0
- data/docs/css-modules-export-mode.md +120 -100
- data/docs/customizing_babel_config.md +16 -16
- data/docs/deployment.md +18 -0
- data/docs/developing_shakapacker.md +6 -0
- data/docs/optional-peer-dependencies.md +9 -4
- data/docs/peer-dependencies.md +17 -6
- data/docs/precompile_hook.md +342 -0
- data/docs/react.md +57 -47
- data/docs/releasing.md +0 -2
- data/docs/rspack.md +25 -21
- data/docs/rspack_migration_guide.md +335 -8
- data/docs/sprockets.md +1 -0
- data/docs/style_loader_vs_mini_css.md +12 -12
- data/docs/subresource_integrity.md +13 -7
- data/docs/transpiler-performance.md +40 -19
- data/docs/troubleshooting.md +141 -3
- data/docs/typescript-migration.md +48 -39
- data/docs/typescript.md +12 -8
- data/docs/using_esbuild_loader.md +10 -10
- data/docs/v6_upgrade.md +33 -20
- data/docs/v7_upgrade.md +8 -6
- data/docs/v8_upgrade.md +13 -12
- data/docs/v9_upgrade.md +2 -1
- data/eslint.config.fast.js +134 -0
- data/eslint.config.js +140 -0
- data/jest.config.js +8 -1
- data/knip.ts +54 -0
- data/lib/install/bin/export-bundler-config +1 -1
- data/lib/install/config/shakapacker.yml +16 -5
- data/lib/shakapacker/compiler.rb +80 -0
- data/lib/shakapacker/configuration.rb +33 -5
- data/lib/shakapacker/dev_server_runner.rb +140 -1
- data/lib/shakapacker/doctor.rb +294 -65
- data/lib/shakapacker/instance.rb +8 -3
- data/lib/shakapacker/runner.rb +244 -8
- data/lib/shakapacker/version.rb +1 -1
- data/lib/tasks/shakapacker/doctor.rake +42 -2
- data/package/babel/preset.ts +7 -4
- data/package/config.ts +42 -30
- data/package/configExporter/buildValidator.ts +883 -0
- data/package/configExporter/cli.ts +972 -210
- data/package/configExporter/configFile.ts +520 -0
- data/package/configExporter/fileWriter.ts +12 -8
- data/package/configExporter/index.ts +11 -1
- data/package/configExporter/types.ts +54 -2
- data/package/configExporter/yamlSerializer.ts +22 -8
- data/package/dev_server.ts +1 -1
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
- data/package/environments/base.ts +18 -13
- data/package/environments/development.ts +1 -1
- data/package/environments/production.ts +4 -1
- data/package/index.d.ts +50 -3
- data/package/index.d.ts.template +50 -0
- data/package/index.ts +7 -7
- data/package/loaders.d.ts +2 -2
- data/package/optimization/rspack.ts +1 -1
- data/package/plugins/rspack.ts +15 -4
- data/package/plugins/webpack.ts +7 -3
- data/package/rspack/index.ts +10 -2
- data/package/rules/raw.ts +3 -2
- data/package/rules/sass.ts +1 -1
- data/package/types/README.md +15 -13
- data/package/types/index.ts +5 -5
- data/package/types.ts +0 -1
- data/package/utils/defaultConfigPath.ts +4 -1
- data/package/utils/errorCodes.ts +129 -100
- data/package/utils/errorHelpers.ts +34 -29
- data/package/utils/getStyleRule.ts +5 -2
- data/package/utils/helpers.ts +21 -11
- data/package/utils/pathValidation.ts +43 -35
- data/package/utils/requireOrError.ts +1 -1
- data/package/utils/snakeToCamelCase.ts +1 -1
- data/package/utils/typeGuards.ts +132 -83
- data/package/utils/validateDependencies.ts +1 -1
- data/package/webpack-types.d.ts +3 -3
- data/package/webpackDevServerConfig.ts +22 -10
- data/package-lock.json +2 -2
- data/package.json +25 -16
- data/scripts/type-check-no-emit.js +1 -1
- data/test/configExporter/buildValidator.test.js +1292 -0
- data/test/configExporter/configFile.test.js +392 -0
- data/test/configExporter/integration.test.js +275 -0
- data/test/helpers.js +1 -1
- data/test/package/configExporter.test.js +154 -0
- data/test/package/environments/base.test.js +6 -3
- data/test/package/helpers.test.js +2 -2
- data/test/package/rules/babel.test.js +61 -51
- data/test/package/rules/esbuild.test.js +12 -3
- data/test/package/rules/file.test.js +3 -1
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +11 -6
- data/test/package/rules/sass1.test.js +4 -5
- data/test/package/rules/sass16.test.js +24 -0
- data/test/package/rules/swc.test.js +48 -38
- data/tools/README.md +15 -5
- data/tsconfig.eslint.json +2 -9
- data/yarn.lock +1954 -1493
- metadata +22 -3
- 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
|
+
})
|