shakapacker 9.2.0 → 9.3.0.beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +19 -4
- 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 +0 -2
- 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/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/cli.ts +799 -208
- data/package/configExporter/configFile.ts +520 -0
- data/package/configExporter/fileWriter.ts +12 -8
- data/package/configExporter/index.ts +9 -1
- data/package/configExporter/types.ts +36 -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 +36 -28
- data/scripts/type-check-no-emit.js +1 -1
- 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/helpers.test.js +2 -2
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +2 -4
- data/test/package/rules/sass1.test.js +1 -3
- data/test/package/rules/sass16.test.js +23 -0
- data/tools/README.md +15 -5
- data/tsconfig.eslint.json +2 -9
- data/yarn.lock +1894 -1492
- metadata +19 -3
- data/.eslintignore +0 -5
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "shakapacker",
|
3
|
-
"version": "9.
|
3
|
+
"version": "9.3.0-beta.0",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "shakapacker",
|
9
|
-
"version": "9.
|
9
|
+
"version": "9.3.0-beta.0",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"js-yaml": "^4.1.0",
|
data/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "shakapacker",
|
3
|
-
"version": "9.
|
3
|
+
"version": "9.3.0-beta.0",
|
4
4
|
"description": "Use webpack to manage app-like JavaScript modules in Rails",
|
5
5
|
"homepage": "https://github.com/shakacode/shakapacker",
|
6
6
|
"bugs": {
|
@@ -32,10 +32,12 @@
|
|
32
32
|
],
|
33
33
|
"scripts": {
|
34
34
|
"clean:ts": "find package -name '*.ts' -not -name '*.d.ts' | sed 's/\\.ts$//' | xargs -I {} rm -f {}.js {}.d.ts {}.d.ts.map {}.js.map",
|
35
|
-
"build": "tsc && node scripts/remove-use-strict.js && yarn prettier --write 'package/**/*.js'",
|
35
|
+
"build": "tsc && cp package/index.d.ts.template package/index.d.ts && node scripts/remove-use-strict.js && yarn prettier --write 'package/**/*.js'",
|
36
36
|
"build:types": "tsc",
|
37
|
-
"
|
38
|
-
"
|
37
|
+
"knip": "knip",
|
38
|
+
"knip:production": "knip --production",
|
39
|
+
"lint": "eslint . --cache",
|
40
|
+
"lint:fast": "eslint . --config eslint.config.fast.js --cache",
|
39
41
|
"test": "jest",
|
40
42
|
"type-check": "tsc --noEmit",
|
41
43
|
"prepublishOnly": "yarn build && yarn type-check"
|
@@ -43,12 +45,27 @@
|
|
43
45
|
"dependencies": {
|
44
46
|
"js-yaml": "^4.1.0",
|
45
47
|
"path-complete-extname": "^1.0.0",
|
46
|
-
"webpack-merge": "^5.8.0"
|
48
|
+
"webpack-merge": "^5.8.0",
|
49
|
+
"yargs": "^17.7.2"
|
47
50
|
},
|
48
51
|
"devDependencies": {
|
49
|
-
"@rspack/cli": "^1.
|
50
|
-
"@rspack/core": "^1.
|
52
|
+
"@rspack/cli": "^1.5.8",
|
53
|
+
"@rspack/core": "^1.5.8",
|
51
54
|
"@swc/core": "^1.3.0",
|
55
|
+
"babel-loader": "^8.2.4",
|
56
|
+
"compression-webpack-plugin": "^9.0.0",
|
57
|
+
"css-loader": "^7.1.2",
|
58
|
+
"esbuild-loader": "^2.18.0",
|
59
|
+
"mini-css-extract-plugin": "^2.9.4",
|
60
|
+
"rspack-manifest-plugin": "^5.0.3",
|
61
|
+
"sass-loader": "^16.0.5",
|
62
|
+
"swc-loader": "^0.1.15",
|
63
|
+
"webpack": "5.93.0",
|
64
|
+
"webpack-assets-manifest": "^5.0.6",
|
65
|
+
"webpack-cli": "^6.0.0",
|
66
|
+
"webpack-subresource-integrity": "^5.1.0",
|
67
|
+
"@eslint/eslintrc": "^3.2.0",
|
68
|
+
"@eslint/js": "^9.37.0",
|
52
69
|
"@types/babel__core": "^7.20.5",
|
53
70
|
"@types/js-yaml": "^4.0.9",
|
54
71
|
"@types/node": "^24.5.2",
|
@@ -56,35 +73,26 @@
|
|
56
73
|
"@types/webpack": "^5.28.5",
|
57
74
|
"@types/webpack-dev-server": "^4.7.2",
|
58
75
|
"@types/webpack-merge": "^5.0.0",
|
59
|
-
"@
|
60
|
-
"@typescript-eslint/
|
61
|
-
"
|
62
|
-
"
|
63
|
-
"css-loader": "^7.1.2",
|
64
|
-
"esbuild-loader": "^2.18.0",
|
65
|
-
"eslint": "^8.0.0",
|
76
|
+
"@types/yargs": "^17.0.33",
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
78
|
+
"@typescript-eslint/parser": "^8.46.0",
|
79
|
+
"eslint": "^9.37.0",
|
66
80
|
"eslint-config-airbnb": "^19.0.4",
|
67
|
-
"eslint-config-prettier": "^
|
68
|
-
"eslint-plugin-import": "^2.
|
69
|
-
"eslint-plugin-jest": "^
|
81
|
+
"eslint-config-prettier": "^10.1.8",
|
82
|
+
"eslint-plugin-import": "^2.32.0",
|
83
|
+
"eslint-plugin-jest": "^29.0.1",
|
70
84
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
71
|
-
"eslint-plugin-prettier": "^5.
|
85
|
+
"eslint-plugin-prettier": "^5.5.4",
|
72
86
|
"eslint-plugin-react": "^7.37.5",
|
73
|
-
"eslint-plugin-react-hooks": "^
|
87
|
+
"eslint-plugin-react-hooks": "^7.0.0",
|
74
88
|
"husky": "^9.1.7",
|
75
89
|
"jest": "^29.7.0",
|
90
|
+
"knip": "^5.64.2",
|
76
91
|
"lint-staged": "^15.2.10",
|
77
92
|
"memory-fs": "^0.5.0",
|
78
|
-
"mini-css-extract-plugin": "^2.9.4",
|
79
93
|
"prettier": "^3.2.5",
|
80
|
-
"rspack-manifest-plugin": "^5.0.3",
|
81
|
-
"sass-loader": "^16.0.5",
|
82
|
-
"swc-loader": "^0.1.15",
|
83
94
|
"thenify": "^3.3.1",
|
84
|
-
"typescript": "^5.9.2"
|
85
|
-
"webpack": "5.93.0",
|
86
|
-
"webpack-assets-manifest": "^5.0.6",
|
87
|
-
"webpack-subresource-integrity": "^5.1.0"
|
95
|
+
"typescript": "^5.9.2"
|
88
96
|
},
|
89
97
|
"peerDependencies": {
|
90
98
|
"@babel/core": "^7.17.9",
|
@@ -210,7 +218,7 @@
|
|
210
218
|
]
|
211
219
|
},
|
212
220
|
"engines": {
|
213
|
-
"node": ">=
|
221
|
+
"node": ">= 20",
|
214
222
|
"yarn": ">=1 <5"
|
215
223
|
},
|
216
224
|
"publishConfig": {
|
@@ -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
|
+
})
|