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.
Files changed (115) 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 +19 -4
  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 +0 -2
  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/knip.ts +54 -0
  51. data/lib/install/bin/export-bundler-config +1 -1
  52. data/lib/install/config/shakapacker.yml +16 -5
  53. data/lib/shakapacker/compiler.rb +80 -0
  54. data/lib/shakapacker/configuration.rb +33 -5
  55. data/lib/shakapacker/dev_server_runner.rb +140 -1
  56. data/lib/shakapacker/doctor.rb +294 -65
  57. data/lib/shakapacker/instance.rb +8 -3
  58. data/lib/shakapacker/runner.rb +244 -8
  59. data/lib/shakapacker/version.rb +1 -1
  60. data/lib/tasks/shakapacker/doctor.rake +42 -2
  61. data/package/babel/preset.ts +7 -4
  62. data/package/config.ts +42 -30
  63. data/package/configExporter/cli.ts +799 -208
  64. data/package/configExporter/configFile.ts +520 -0
  65. data/package/configExporter/fileWriter.ts +12 -8
  66. data/package/configExporter/index.ts +9 -1
  67. data/package/configExporter/types.ts +36 -2
  68. data/package/configExporter/yamlSerializer.ts +22 -8
  69. data/package/dev_server.ts +1 -1
  70. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
  71. data/package/environments/base.ts +18 -13
  72. data/package/environments/development.ts +1 -1
  73. data/package/environments/production.ts +4 -1
  74. data/package/index.d.ts +50 -3
  75. data/package/index.d.ts.template +50 -0
  76. data/package/index.ts +7 -7
  77. data/package/loaders.d.ts +2 -2
  78. data/package/optimization/rspack.ts +1 -1
  79. data/package/plugins/rspack.ts +15 -4
  80. data/package/plugins/webpack.ts +7 -3
  81. data/package/rspack/index.ts +10 -2
  82. data/package/rules/raw.ts +3 -2
  83. data/package/rules/sass.ts +1 -1
  84. data/package/types/README.md +15 -13
  85. data/package/types/index.ts +5 -5
  86. data/package/types.ts +0 -1
  87. data/package/utils/defaultConfigPath.ts +4 -1
  88. data/package/utils/errorCodes.ts +129 -100
  89. data/package/utils/errorHelpers.ts +34 -29
  90. data/package/utils/getStyleRule.ts +5 -2
  91. data/package/utils/helpers.ts +21 -11
  92. data/package/utils/pathValidation.ts +43 -35
  93. data/package/utils/requireOrError.ts +1 -1
  94. data/package/utils/snakeToCamelCase.ts +1 -1
  95. data/package/utils/typeGuards.ts +132 -83
  96. data/package/utils/validateDependencies.ts +1 -1
  97. data/package/webpack-types.d.ts +3 -3
  98. data/package/webpackDevServerConfig.ts +22 -10
  99. data/package-lock.json +2 -2
  100. data/package.json +36 -28
  101. data/scripts/type-check-no-emit.js +1 -1
  102. data/test/configExporter/configFile.test.js +392 -0
  103. data/test/configExporter/integration.test.js +275 -0
  104. data/test/helpers.js +1 -1
  105. data/test/package/configExporter.test.js +154 -0
  106. data/test/package/helpers.test.js +2 -2
  107. data/test/package/rules/sass-version-parsing.test.js +71 -0
  108. data/test/package/rules/sass.test.js +2 -4
  109. data/test/package/rules/sass1.test.js +1 -3
  110. data/test/package/rules/sass16.test.js +23 -0
  111. data/tools/README.md +15 -5
  112. data/tsconfig.eslint.json +2 -9
  113. data/yarn.lock +1894 -1492
  114. metadata +19 -3
  115. 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
@@ -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
  }
@@ -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("16")
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("12")
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
- return fn("This path was mocked")
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 './Button.module.css';
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['button-text']}>Click me</span>
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 './Button.module.css';
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
- "package/**/*.ts",
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
  }