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
@@ -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
@@ -0,0 +1,154 @@
|
|
1
|
+
const { resetEnv } = require("../helpers")
|
2
|
+
|
3
|
+
// Helper function that mimics the env var restore logic from cli.ts lines 267-282
|
4
|
+
function restoreEnvVars(saved) {
|
5
|
+
Object.keys(saved).forEach((key) => {
|
6
|
+
if (saved[key] === undefined) {
|
7
|
+
delete process.env[key]
|
8
|
+
} else {
|
9
|
+
process.env[key] = saved[key]
|
10
|
+
}
|
11
|
+
})
|
12
|
+
}
|
13
|
+
|
14
|
+
describe("configExporter", () => {
|
15
|
+
beforeEach(() => jest.resetModules() && resetEnv())
|
16
|
+
|
17
|
+
describe("fileWriter", () => {
|
18
|
+
test("generates correct filename for client config", () => {
|
19
|
+
const { FileWriter } = require("../../package/configExporter/fileWriter")
|
20
|
+
const writer = new FileWriter()
|
21
|
+
const filename = writer.generateFilename(
|
22
|
+
"webpack",
|
23
|
+
"development",
|
24
|
+
"client",
|
25
|
+
"yaml"
|
26
|
+
)
|
27
|
+
expect(filename).toBe("webpack-development-client.yaml")
|
28
|
+
})
|
29
|
+
|
30
|
+
test("generates correct filename for server config", () => {
|
31
|
+
const { FileWriter } = require("../../package/configExporter/fileWriter")
|
32
|
+
const writer = new FileWriter()
|
33
|
+
const filename = writer.generateFilename(
|
34
|
+
"webpack",
|
35
|
+
"production",
|
36
|
+
"server",
|
37
|
+
"yaml"
|
38
|
+
)
|
39
|
+
expect(filename).toBe("webpack-production-server.yaml")
|
40
|
+
})
|
41
|
+
|
42
|
+
test("generates correct filename for client-hmr config", () => {
|
43
|
+
const { FileWriter } = require("../../package/configExporter/fileWriter")
|
44
|
+
const writer = new FileWriter()
|
45
|
+
const filename = writer.generateFilename(
|
46
|
+
"webpack",
|
47
|
+
"development",
|
48
|
+
"client-hmr",
|
49
|
+
"yaml"
|
50
|
+
)
|
51
|
+
expect(filename).toBe("webpack-development-client-hmr.yaml")
|
52
|
+
})
|
53
|
+
|
54
|
+
test("generates correct filename for json format", () => {
|
55
|
+
const { FileWriter } = require("../../package/configExporter/fileWriter")
|
56
|
+
const writer = new FileWriter()
|
57
|
+
const filename = writer.generateFilename(
|
58
|
+
"rspack",
|
59
|
+
"production",
|
60
|
+
"client",
|
61
|
+
"json"
|
62
|
+
)
|
63
|
+
expect(filename).toBe("rspack-production-client.json")
|
64
|
+
})
|
65
|
+
})
|
66
|
+
|
67
|
+
describe("environment variable preservation in runDoctorMode", () => {
|
68
|
+
let originalEnv
|
69
|
+
|
70
|
+
beforeEach(() => {
|
71
|
+
// Save original environment
|
72
|
+
originalEnv = {
|
73
|
+
NODE_ENV: process.env.NODE_ENV,
|
74
|
+
RAILS_ENV: process.env.RAILS_ENV,
|
75
|
+
CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
|
76
|
+
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY,
|
77
|
+
WEBPACK_SERVE: process.env.WEBPACK_SERVE
|
78
|
+
}
|
79
|
+
|
80
|
+
// Set up known initial state for development mode
|
81
|
+
process.env.NODE_ENV = "development"
|
82
|
+
process.env.RAILS_ENV = "development"
|
83
|
+
delete process.env.WEBPACK_SERVE
|
84
|
+
delete process.env.SERVER_BUNDLE_ONLY
|
85
|
+
})
|
86
|
+
|
87
|
+
afterEach(() => {
|
88
|
+
// Restore original environment
|
89
|
+
Object.keys(originalEnv).forEach((key) => {
|
90
|
+
if (originalEnv[key] === undefined) {
|
91
|
+
delete process.env[key]
|
92
|
+
} else {
|
93
|
+
process.env[key] = originalEnv[key]
|
94
|
+
}
|
95
|
+
})
|
96
|
+
})
|
97
|
+
|
98
|
+
test("preserves CLIENT_BUNDLE_ONLY when set before doctor mode", async () => {
|
99
|
+
// Set a custom value that should be preserved
|
100
|
+
process.env.CLIENT_BUNDLE_ONLY = "custom_value"
|
101
|
+
|
102
|
+
// The doctor mode code internally does:
|
103
|
+
// 1. Save original
|
104
|
+
const saved = {
|
105
|
+
CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
|
106
|
+
WEBPACK_SERVE: process.env.WEBPACK_SERVE,
|
107
|
+
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY
|
108
|
+
}
|
109
|
+
|
110
|
+
// 2. Set HMR env vars
|
111
|
+
process.env.WEBPACK_SERVE = "true"
|
112
|
+
process.env.CLIENT_BUNDLE_ONLY = "yes"
|
113
|
+
delete process.env.SERVER_BUNDLE_ONLY
|
114
|
+
|
115
|
+
// 3. Restore using helper
|
116
|
+
restoreEnvVars(saved)
|
117
|
+
|
118
|
+
// Assert the original value is preserved
|
119
|
+
expect(process.env.CLIENT_BUNDLE_ONLY).toBe("custom_value")
|
120
|
+
expect(process.env.WEBPACK_SERVE).toBeUndefined()
|
121
|
+
expect(process.env.SERVER_BUNDLE_ONLY).toBeUndefined()
|
122
|
+
})
|
123
|
+
|
124
|
+
test("deletes CLIENT_BUNDLE_ONLY when not set before doctor mode", async () => {
|
125
|
+
// Ensure CLIENT_BUNDLE_ONLY is not set
|
126
|
+
delete process.env.CLIENT_BUNDLE_ONLY
|
127
|
+
|
128
|
+
// The doctor mode code internally does:
|
129
|
+
// 1. Save original
|
130
|
+
const saved = {
|
131
|
+
CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
|
132
|
+
WEBPACK_SERVE: process.env.WEBPACK_SERVE,
|
133
|
+
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY
|
134
|
+
}
|
135
|
+
|
136
|
+
// 2. Set HMR env vars
|
137
|
+
process.env.WEBPACK_SERVE = "true"
|
138
|
+
process.env.CLIENT_BUNDLE_ONLY = "yes"
|
139
|
+
delete process.env.SERVER_BUNDLE_ONLY
|
140
|
+
|
141
|
+
// Verify they were set
|
142
|
+
expect(process.env.CLIENT_BUNDLE_ONLY).toBe("yes")
|
143
|
+
expect(process.env.WEBPACK_SERVE).toBe("true")
|
144
|
+
|
145
|
+
// 3. Restore using helper
|
146
|
+
restoreEnvVars(saved)
|
147
|
+
|
148
|
+
// Assert the variables are deleted since they were not set originally
|
149
|
+
expect(process.env.CLIENT_BUNDLE_ONLY).toBeUndefined()
|
150
|
+
expect(process.env.WEBPACK_SERVE).toBeUndefined()
|
151
|
+
expect(process.env.SERVER_BUNDLE_ONLY).toBeUndefined()
|
152
|
+
})
|
153
|
+
})
|
154
|
+
})
|
@@ -2,10 +2,10 @@ const { packageMajorVersion } = require("../../package/utils/helpers")
|
|
2
2
|
|
3
3
|
describe("packageMajorVersion", () => {
|
4
4
|
test("should find that sass-loader is v16", () => {
|
5
|
-
expect(packageMajorVersion("sass-loader")).toBe(
|
5
|
+
expect(packageMajorVersion("sass-loader")).toBe(16)
|
6
6
|
})
|
7
7
|
|
8
8
|
test("should find that nonexistent is v12", () => {
|
9
|
-
expect(packageMajorVersion("nonexistent")).toBe(
|
9
|
+
expect(packageMajorVersion("nonexistent")).toBe(12)
|
10
10
|
})
|
11
11
|
})
|
@@ -0,0 +1,71 @@
|
|
1
|
+
/**
|
2
|
+
* Tests demonstrating why parseInt is needed for sass-loader version comparison
|
3
|
+
*/
|
4
|
+
|
5
|
+
describe("sass-loader version comparison", () => {
|
6
|
+
describe("string comparison issues (without parseInt)", () => {
|
7
|
+
test("string '2' incorrectly compares as greater than number 15", () => {
|
8
|
+
// This demonstrates the bug: lexicographic string comparison
|
9
|
+
const stringVersion = "2"
|
10
|
+
// String comparison would be: "2" > 15
|
11
|
+
// JavaScript coerces to: 2 > 15 = false (correct)
|
12
|
+
expect(stringVersion > 15).toBe(false)
|
13
|
+
|
14
|
+
// But with >= 16 (the boundary we care about):
|
15
|
+
expect(stringVersion >= 16).toBe(false) // Correct behavior
|
16
|
+
})
|
17
|
+
|
18
|
+
test("string '16' correctly converts in numeric comparison", () => {
|
19
|
+
const stringVersion = "16"
|
20
|
+
// Coercion works: "16" >= 16 -> 16 >= 16 = true
|
21
|
+
expect(stringVersion >= 16).toBe(true)
|
22
|
+
})
|
23
|
+
|
24
|
+
test("demonstrates why > 15 is less clear than >= 16", () => {
|
25
|
+
// Version 15 should use includePaths
|
26
|
+
expect("15" > 15).toBe(false) // 15 > 15 = false ✓
|
27
|
+
expect(parseInt("15", 10) >= 16).toBe(false) // 15 >= 16 = false ✓
|
28
|
+
|
29
|
+
// Version 16 should use loadPaths
|
30
|
+
expect("16" > 15).toBe(true) // 16 > 15 = true ✓
|
31
|
+
expect(parseInt("16", 10) >= 16).toBe(true) // 16 >= 16 = true ✓
|
32
|
+
|
33
|
+
// But >= 16 is more semantically accurate:
|
34
|
+
// "Use loadPaths if version is 16 or greater"
|
35
|
+
})
|
36
|
+
})
|
37
|
+
|
38
|
+
describe("parseInt ensures numeric comparison", () => {
|
39
|
+
test("handles numeric string correctly", () => {
|
40
|
+
expect(parseInt("16", 10) >= 16).toBe(true)
|
41
|
+
expect(parseInt("15", 10) >= 16).toBe(false)
|
42
|
+
expect(parseInt("2", 10) >= 16).toBe(false)
|
43
|
+
})
|
44
|
+
|
45
|
+
test("handles edge cases safely", () => {
|
46
|
+
// If version can't be determined, parseInt returns NaN
|
47
|
+
// NaN >= 16 is false, so it falls back to includePaths (safe default)
|
48
|
+
expect(parseInt("invalid", 10) >= 16).toBe(false)
|
49
|
+
expect(parseInt(undefined, 10) >= 16).toBe(false)
|
50
|
+
expect(parseInt("", 10) >= 16).toBe(false)
|
51
|
+
})
|
52
|
+
})
|
53
|
+
|
54
|
+
describe("version 16 is the boundary", () => {
|
55
|
+
// Helper function to determine option key (same logic as production code)
|
56
|
+
const getOptionKey = (version) =>
|
57
|
+
version >= 16 ? "loadPaths" : "includePaths"
|
58
|
+
|
59
|
+
test("sass-loader v15 uses includePaths", () => {
|
60
|
+
expect(getOptionKey(15)).toBe("includePaths")
|
61
|
+
})
|
62
|
+
|
63
|
+
test("sass-loader v16 uses loadPaths", () => {
|
64
|
+
expect(getOptionKey(16)).toBe("loadPaths")
|
65
|
+
})
|
66
|
+
|
67
|
+
test("sass-loader v17+ uses loadPaths", () => {
|
68
|
+
expect(getOptionKey(17)).toBe("loadPaths")
|
69
|
+
})
|
70
|
+
})
|
71
|
+
})
|
@@ -2,10 +2,8 @@ const sass = require("../../../package/rules/sass")
|
|
2
2
|
|
3
3
|
jest.mock("../../../package/utils/helpers", () => {
|
4
4
|
const original = jest.requireActual("../../../package/utils/helpers")
|
5
|
-
const canProcess = (rule, fn) =>
|
6
|
-
|
7
|
-
}
|
8
|
-
const packageMajorVersion = () => "15"
|
5
|
+
const canProcess = (rule, fn) => fn("This path was mocked")
|
6
|
+
const packageMajorVersion = () => 15
|
9
7
|
return {
|
10
8
|
...original,
|
11
9
|
canProcess,
|
@@ -2,9 +2,7 @@ const sass = require("../../../package/rules/sass")
|
|
2
2
|
|
3
3
|
jest.mock("../../../package/utils/helpers", () => {
|
4
4
|
const original = jest.requireActual("../../../package/utils/helpers")
|
5
|
-
const canProcess = (rule, fn) =>
|
6
|
-
return fn("This path was mocked")
|
7
|
-
}
|
5
|
+
const canProcess = (rule, fn) => fn("This path was mocked")
|
8
6
|
return {
|
9
7
|
...original,
|
10
8
|
canProcess
|
@@ -0,0 +1,23 @@
|
|
1
|
+
const sass = require("../../../package/rules/sass")
|
2
|
+
|
3
|
+
jest.mock("../../../package/utils/helpers", () => {
|
4
|
+
const original = jest.requireActual("../../../package/utils/helpers")
|
5
|
+
const canProcess = (rule, fn) => fn("This path was mocked")
|
6
|
+
const packageMajorVersion = () => 16
|
7
|
+
return {
|
8
|
+
...original,
|
9
|
+
canProcess,
|
10
|
+
packageMajorVersion
|
11
|
+
}
|
12
|
+
})
|
13
|
+
|
14
|
+
jest.mock("../../../package/utils/inliningCss", () => true)
|
15
|
+
|
16
|
+
describe("sass rule", () => {
|
17
|
+
test("contains loadPaths as the sassOptions key if sass-loader is v16 or later", () => {
|
18
|
+
expect(typeof sass.use[3].options.sassOptions.includePaths).toBe(
|
19
|
+
"undefined"
|
20
|
+
)
|
21
|
+
expect(typeof sass.use[3].options.sassOptions.loadPaths).toBe("object")
|
22
|
+
})
|
23
|
+
})
|
data/tools/README.md
CHANGED
@@ -7,12 +7,14 @@ A jscodeshift codemod to help migrate CSS module imports from v8 to v9 format.
|
|
7
7
|
### What it does
|
8
8
|
|
9
9
|
#### For JavaScript files (.js, .jsx):
|
10
|
+
|
10
11
|
- Converts `import styles from './styles.module.css'` to `import { className1, className2 } from './styles.module.css'`
|
11
12
|
- Automatically detects which CSS classes are used in the file
|
12
13
|
- Handles kebab-case to camelCase conversion (e.g., `my-button` → `myButton`)
|
13
14
|
- Updates all class references from `styles.className` to `className`
|
14
15
|
|
15
16
|
#### For TypeScript files (.ts, .tsx):
|
17
|
+
|
16
18
|
- Converts `import styles from './styles.module.css'` to `import * as styles from './styles.module.css'`
|
17
19
|
- Preserves the same usage pattern (`styles.className`)
|
18
20
|
- Works around TypeScript's limitation with dynamic named exports
|
@@ -26,21 +28,25 @@ npm install -g jscodeshift
|
|
26
28
|
### Usage
|
27
29
|
|
28
30
|
#### Dry run (see what would change):
|
31
|
+
|
29
32
|
```bash
|
30
33
|
npx jscodeshift -t tools/css-modules-v9-codemod.js src/ --dry
|
31
34
|
```
|
32
35
|
|
33
36
|
#### Apply to JavaScript files:
|
37
|
+
|
34
38
|
```bash
|
35
39
|
npx jscodeshift -t tools/css-modules-v9-codemod.js src/
|
36
40
|
```
|
37
41
|
|
38
42
|
#### Apply to TypeScript files:
|
43
|
+
|
39
44
|
```bash
|
40
45
|
npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/
|
41
46
|
```
|
42
47
|
|
43
48
|
#### Apply to specific file patterns:
|
49
|
+
|
44
50
|
```bash
|
45
51
|
# Only .jsx files
|
46
52
|
npx jscodeshift -t tools/css-modules-v9-codemod.js src/**/*.jsx
|
@@ -59,32 +65,35 @@ npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/**/*.tsx
|
|
59
65
|
### Examples
|
60
66
|
|
61
67
|
#### Before (JavaScript):
|
68
|
+
|
62
69
|
```javascript
|
63
|
-
import styles from
|
70
|
+
import styles from "./Button.module.css"
|
64
71
|
|
65
72
|
function Button() {
|
66
73
|
return (
|
67
74
|
<button className={styles.button}>
|
68
|
-
<span className={styles[
|
75
|
+
<span className={styles["button-text"]}>Click me</span>
|
69
76
|
</button>
|
70
|
-
)
|
77
|
+
)
|
71
78
|
}
|
72
79
|
```
|
73
80
|
|
74
81
|
#### After (JavaScript):
|
82
|
+
|
75
83
|
```javascript
|
76
|
-
import { button, buttonText } from
|
84
|
+
import { button, buttonText } from "./Button.module.css"
|
77
85
|
|
78
86
|
function Button() {
|
79
87
|
return (
|
80
88
|
<button className={button}>
|
81
89
|
<span className={buttonText}>Click me</span>
|
82
90
|
</button>
|
83
|
-
)
|
91
|
+
)
|
84
92
|
}
|
85
93
|
```
|
86
94
|
|
87
95
|
#### Before (TypeScript):
|
96
|
+
|
88
97
|
```typescript
|
89
98
|
import styles from './Button.module.css';
|
90
99
|
|
@@ -94,6 +103,7 @@ const Button: React.FC = () => {
|
|
94
103
|
```
|
95
104
|
|
96
105
|
#### After (TypeScript):
|
106
|
+
|
97
107
|
```typescript
|
98
108
|
import * as styles from './Button.module.css';
|
99
109
|
|
data/tsconfig.eslint.json
CHANGED
@@ -4,13 +4,6 @@
|
|
4
4
|
"noEmit": true,
|
5
5
|
"rootDir": "."
|
6
6
|
},
|
7
|
-
"include": [
|
8
|
-
|
9
|
-
"package/**/*.tsx",
|
10
|
-
"package/**/*.test.ts",
|
11
|
-
"package/**/*.spec.ts",
|
12
|
-
"test/**/*.ts",
|
13
|
-
"test/**/*.tsx"
|
14
|
-
],
|
15
|
-
"exclude": ["node_modules"]
|
7
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
8
|
+
"exclude": ["node_modules", "vendor"]
|
16
9
|
}
|