shakapacker 8.0.2 → 9.2.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/.eslintignore +1 -0
- data/.eslintrc.fast.js +40 -0
- data/.eslintrc.js +48 -0
- data/.github/STATUS.md +1 -0
- data/.github/workflows/claude-code-review.yml +54 -0
- data/.github/workflows/claude.yml +50 -0
- data/.github/workflows/dummy.yml +9 -4
- data/.github/workflows/generator.yml +32 -10
- data/.github/workflows/node.yml +23 -1
- data/.github/workflows/ruby.yml +33 -2
- data/.github/workflows/test-bundlers.yml +170 -0
- data/.gitignore +20 -0
- data/.husky/pre-commit +2 -0
- data/.npmignore +56 -0
- data/.prettierignore +3 -0
- data/.rubocop.yml +1 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +302 -16
- data/CLAUDE.md +29 -0
- data/CONTRIBUTING.md +138 -20
- data/Gemfile.lock +83 -89
- data/README.md +343 -105
- data/Rakefile +39 -4
- data/TODO.md +50 -0
- data/TODO_v9.md +87 -0
- data/bin/export-bundler-config +11 -0
- data/conductor-setup.sh +70 -0
- data/conductor.json +7 -0
- data/docs/cdn_setup.md +379 -0
- data/docs/common-upgrades.md +615 -0
- data/docs/css-modules-export-mode.md +512 -0
- data/docs/deployment.md +62 -9
- data/docs/optional-peer-dependencies.md +198 -0
- data/docs/peer-dependencies.md +60 -0
- data/docs/react.md +6 -14
- data/docs/releasing.md +197 -0
- data/docs/rspack.md +190 -0
- data/docs/rspack_migration_guide.md +305 -0
- data/docs/subresource_integrity.md +54 -0
- data/docs/transpiler-migration.md +209 -0
- data/docs/transpiler-performance.md +179 -0
- data/docs/troubleshooting.md +157 -22
- data/docs/typescript-migration.md +379 -0
- data/docs/typescript.md +99 -0
- data/docs/using_esbuild_loader.md +3 -3
- data/docs/using_swc_loader.md +112 -10
- data/docs/v6_upgrade.md +10 -0
- data/docs/v8_upgrade.md +3 -5
- data/docs/v9_upgrade.md +458 -0
- data/gemfiles/Gemfile-rails.6.0.x +2 -1
- data/gemfiles/Gemfile-rails.6.1.x +1 -1
- data/gemfiles/Gemfile-rails.7.0.x +2 -2
- data/gemfiles/Gemfile-rails.7.1.x +1 -2
- data/gemfiles/Gemfile-rails.7.2.x +11 -0
- data/gemfiles/Gemfile-rails.8.0.x +11 -0
- data/lib/install/bin/export-bundler-config +11 -0
- data/lib/install/bin/shakapacker +4 -6
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/install/config/rspack/rspack.config.js +6 -0
- data/lib/install/config/rspack/rspack.config.ts +7 -0
- data/lib/install/config/shakapacker.yml +25 -5
- data/lib/install/config/webpack/webpack.config.ts +7 -0
- data/lib/install/package.json +38 -0
- data/lib/install/template.rb +194 -44
- data/lib/shakapacker/bundler_switcher.rb +329 -0
- data/lib/shakapacker/compiler.rb +2 -1
- data/lib/shakapacker/compiler_strategy.rb +2 -2
- data/lib/shakapacker/configuration.rb +173 -2
- data/lib/shakapacker/dev_server_runner.rb +29 -8
- data/lib/shakapacker/digest_strategy.rb +2 -1
- data/lib/shakapacker/doctor.rb +905 -0
- data/lib/shakapacker/helper.rb +64 -16
- data/lib/shakapacker/manifest.rb +10 -3
- data/lib/shakapacker/mtime_strategy.rb +1 -1
- data/lib/shakapacker/railtie.rb +4 -4
- data/lib/shakapacker/rspack_runner.rb +19 -0
- data/lib/shakapacker/runner.rb +159 -10
- data/lib/shakapacker/swc_migrator.rb +384 -0
- data/lib/shakapacker/utils/manager.rb +15 -2
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/version_checker.rb +2 -2
- data/lib/shakapacker/webpack_runner.rb +6 -43
- data/lib/shakapacker.rb +22 -11
- data/lib/tasks/shakapacker/doctor.rake +8 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
- data/lib/tasks/shakapacker/install.rake +12 -2
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
- data/lib/tasks/shakapacker.rake +2 -0
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +56 -0
- data/package/config.ts +175 -0
- data/package/configExporter/cli.ts +683 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/fileWriter.ts +92 -0
- data/package/configExporter/index.ts +5 -0
- data/package/configExporter/types.ts +36 -0
- data/package/configExporter/yamlSerializer.ts +266 -0
- data/package/{dev_server.js → dev_server.ts} +8 -5
- data/package/env.ts +92 -0
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +30 -0
- data/package/environments/{base.js → base.ts} +56 -60
- data/package/environments/development.ts +90 -0
- data/package/environments/production.ts +80 -0
- data/package/environments/test.ts +53 -0
- data/package/environments/types.ts +98 -0
- data/package/esbuild/index.ts +42 -0
- data/package/index.d.ts +3 -60
- data/package/index.ts +55 -0
- data/package/loaders.d.ts +28 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/webpack.ts +57 -0
- data/package/plugins/rspack.ts +103 -0
- data/package/plugins/webpack.ts +62 -0
- data/package/rspack/index.ts +64 -0
- data/package/rules/{babel.js → babel.ts} +2 -2
- data/package/rules/{coffee.js → coffee.ts} +1 -1
- data/package/rules/css.ts +3 -0
- data/package/rules/{erb.js → erb.ts} +1 -1
- data/package/rules/esbuild.ts +10 -0
- data/package/rules/file.ts +40 -0
- data/package/rules/{jscommon.js → jscommon.ts} +4 -4
- data/package/rules/{less.js → less.ts} +4 -4
- data/package/rules/raw.ts +25 -0
- data/package/rules/rspack.ts +176 -0
- data/package/rules/{sass.js → sass.ts} +7 -3
- data/package/rules/{stylus.js → stylus.ts} +4 -8
- data/package/rules/swc.ts +10 -0
- data/package/rules/webpack.ts +16 -0
- data/package/swc/index.ts +56 -0
- data/package/types/README.md +88 -0
- data/package/types/index.ts +61 -0
- data/package/types.ts +108 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/debug.ts +49 -0
- data/package/utils/defaultConfigPath.ts +4 -0
- data/package/utils/errorCodes.ts +219 -0
- data/package/utils/errorHelpers.ts +143 -0
- data/package/utils/getStyleRule.ts +64 -0
- data/package/utils/helpers.ts +85 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/pathValidation.ts +139 -0
- data/package/utils/requireOrError.ts +15 -0
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +342 -0
- data/package/utils/validateDependencies.ts +61 -0
- data/package/webpack-types.d.ts +33 -0
- data/package/webpackDevServerConfig.ts +117 -0
- data/package-lock.json +13047 -0
- data/package.json +154 -18
- data/scripts/remove-use-strict.js +45 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/test/helpers.js +1 -1
- data/test/package/config.test.js +43 -0
- data/test/package/env.test.js +42 -7
- data/test/package/environments/base.test.js +5 -1
- data/test/package/rules/babel.test.js +16 -0
- data/test/package/rules/esbuild.test.js +1 -1
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/swc.test.js +1 -1
- data/test/package/rules/webpack.test.js +35 -0
- data/test/package/staging.test.js +4 -3
- data/test/package/transpiler-defaults.test.js +127 -0
- data/test/peer-dependencies.sh +85 -0
- data/test/scripts/remove-use-strict.test.js +125 -0
- data/test/typescript/build.test.js +118 -0
- data/test/typescript/environments.test.js +107 -0
- data/test/typescript/pathValidation.test.js +142 -0
- data/test/typescript/securityValidation.test.js +182 -0
- data/tools/README.md +124 -0
- data/tools/css-modules-v9-codemod.js +179 -0
- data/tsconfig.eslint.json +16 -0
- data/tsconfig.json +38 -0
- data/yarn.lock +4165 -2706
- metadata +129 -41
- data/package/babel/preset.js +0 -37
- data/package/config.js +0 -54
- data/package/env.js +0 -48
- data/package/environments/development.js +0 -13
- data/package/environments/production.js +0 -88
- data/package/environments/test.js +0 -3
- data/package/esbuild/index.js +0 -40
- data/package/index.js +0 -40
- data/package/rules/css.js +0 -3
- data/package/rules/esbuild.js +0 -10
- data/package/rules/file.js +0 -29
- data/package/rules/index.js +0 -20
- data/package/rules/raw.js +0 -5
- data/package/rules/swc.js +0 -10
- data/package/swc/index.js +0 -50
- data/package/utils/configPath.js +0 -4
- data/package/utils/defaultConfigPath.js +0 -2
- data/package/utils/getStyleRule.js +0 -40
- data/package/utils/helpers.js +0 -58
- data/package/utils/snakeToCamelCase.js +0 -5
- data/package/webpackDevServerConfig.js +0 -71
- data/test/package/rules/index.test.js +0 -16
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Tests for path validation and security utilities
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const {
|
|
4
|
+
isPathTraversalSafe,
|
|
5
|
+
safeResolvePath,
|
|
6
|
+
validatePaths,
|
|
7
|
+
sanitizeEnvValue,
|
|
8
|
+
validatePort
|
|
9
|
+
} = require("../../package/utils/pathValidation")
|
|
10
|
+
|
|
11
|
+
describe("Path Validation Security", () => {
|
|
12
|
+
describe("isPathTraversalSafe", () => {
|
|
13
|
+
it("detects directory traversal patterns", () => {
|
|
14
|
+
const unsafePaths = [
|
|
15
|
+
"../etc/passwd",
|
|
16
|
+
"../../secrets",
|
|
17
|
+
"/etc/passwd",
|
|
18
|
+
"~/ssh/keys",
|
|
19
|
+
"C:\\Windows\\System32",
|
|
20
|
+
"C:/Windows/System32", // Windows with forward slash
|
|
21
|
+
"D:\\Program Files", // Different drive letter
|
|
22
|
+
"\\\\server\\share\\file", // Windows UNC path
|
|
23
|
+
"\\\\192.168.1.1\\share", // UNC with IP
|
|
24
|
+
"%2e%2e%2fsecrets",
|
|
25
|
+
"%2E%2E%2Fsecrets", // URL encoded uppercase
|
|
26
|
+
"path\x00with\x00null" // Null bytes
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
unsafePaths.forEach((unsafePath) => {
|
|
30
|
+
expect(isPathTraversalSafe(unsafePath)).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("allows safe relative paths", () => {
|
|
35
|
+
const safePaths = [
|
|
36
|
+
path.join("src", "index.js"),
|
|
37
|
+
path.join(".", "components", "App.tsx"),
|
|
38
|
+
path.join("node_modules", "package", "index.js"),
|
|
39
|
+
path.join("dist", "bundle.js")
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
safePaths.forEach((safePath) => {
|
|
43
|
+
expect(isPathTraversalSafe(safePath)).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("safeResolvePath", () => {
|
|
49
|
+
it("resolves paths within base directory", () => {
|
|
50
|
+
const basePath = path.join(path.sep, "app")
|
|
51
|
+
const userPath = path.join("src", "index.js")
|
|
52
|
+
const result = safeResolvePath(basePath, userPath)
|
|
53
|
+
|
|
54
|
+
expect(result).toContain(basePath)
|
|
55
|
+
expect(result).toContain(userPath.replace(/\\/g, path.sep))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("throws on traversal attempts", () => {
|
|
59
|
+
const basePath = path.join(path.sep, "app")
|
|
60
|
+
const maliciousPath = path.join("..", "etc", "passwd")
|
|
61
|
+
|
|
62
|
+
expect(() => {
|
|
63
|
+
safeResolvePath(basePath, maliciousPath)
|
|
64
|
+
}).toThrow("Path traversal attempt detected")
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("validatePaths", () => {
|
|
69
|
+
it("filters out unsafe paths with warnings", () => {
|
|
70
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation()
|
|
71
|
+
|
|
72
|
+
const paths = [
|
|
73
|
+
path.join("src", "index.js"),
|
|
74
|
+
path.join("..", "etc", "passwd"),
|
|
75
|
+
path.join("components", "App.tsx")
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const result = validatePaths(paths, path.join(path.sep, "app"))
|
|
79
|
+
|
|
80
|
+
expect(result).toHaveLength(2)
|
|
81
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
82
|
+
expect.stringContaining("potentially unsafe path")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
consoleSpy.mockRestore()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe("sanitizeEnvValue", () => {
|
|
90
|
+
it("removes control characters", () => {
|
|
91
|
+
const dirty = "normal\x00text\x1Fwith\x7Fcontrol"
|
|
92
|
+
const clean = sanitizeEnvValue(dirty)
|
|
93
|
+
|
|
94
|
+
expect(clean).toBe("normaltextwithcontrol")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("warns when sanitization occurs", () => {
|
|
98
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation()
|
|
99
|
+
|
|
100
|
+
sanitizeEnvValue("text\x00with\x00nulls")
|
|
101
|
+
|
|
102
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
103
|
+
expect.stringContaining("control characters")
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
consoleSpy.mockRestore()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("returns undefined for undefined input", () => {
|
|
110
|
+
expect(sanitizeEnvValue(undefined)).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe("validatePort", () => {
|
|
115
|
+
it("accepts valid port numbers", () => {
|
|
116
|
+
expect(validatePort(3000)).toBe(true)
|
|
117
|
+
expect(validatePort(80)).toBe(true)
|
|
118
|
+
expect(validatePort(65535)).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("accepts valid port strings", () => {
|
|
122
|
+
expect(validatePort("3000")).toBe(true)
|
|
123
|
+
expect(validatePort("auto")).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("rejects invalid ports", () => {
|
|
127
|
+
expect(validatePort(0)).toBe(false)
|
|
128
|
+
expect(validatePort(65536)).toBe(false)
|
|
129
|
+
expect(validatePort(-1)).toBe(false)
|
|
130
|
+
expect(validatePort(3000.5)).toBe(false)
|
|
131
|
+
expect(validatePort("invalid")).toBe(false)
|
|
132
|
+
expect(validatePort("3000abc")).toBe(false) // Should reject strings with non-digits
|
|
133
|
+
expect(validatePort("abc3000")).toBe(false) // Should reject strings with non-digits
|
|
134
|
+
expect(validatePort("30.00")).toBe(false) // Should reject decimal strings
|
|
135
|
+
expect(validatePort("3000 ")).toBe(false) // Should reject strings with spaces
|
|
136
|
+
expect(validatePort(" 3000")).toBe(false) // Should reject strings with spaces
|
|
137
|
+
expect(validatePort("0x1234")).toBe(false) // Should reject hex notation
|
|
138
|
+
expect(validatePort(null)).toBe(false)
|
|
139
|
+
expect(validatePort(undefined)).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isValidConfig,
|
|
3
|
+
clearValidationCache
|
|
4
|
+
} = require("../../package/utils/typeGuards")
|
|
5
|
+
|
|
6
|
+
describe("security validation", () => {
|
|
7
|
+
const originalNodeEnv = process.env.NODE_ENV
|
|
8
|
+
const originalStrictValidation = process.env.SHAKAPACKER_STRICT_VALIDATION
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
process.env.NODE_ENV = originalNodeEnv
|
|
12
|
+
process.env.SHAKAPACKER_STRICT_VALIDATION = originalStrictValidation
|
|
13
|
+
clearValidationCache()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("path traversal security checks", () => {
|
|
17
|
+
const baseConfig = {
|
|
18
|
+
source_path: "./app/javascript",
|
|
19
|
+
source_entry_path: "./packs",
|
|
20
|
+
public_root_path: "./public",
|
|
21
|
+
public_output_path: "packs",
|
|
22
|
+
cache_path: "tmp/shakapacker",
|
|
23
|
+
javascript_transpiler: "babel",
|
|
24
|
+
nested_entries: false,
|
|
25
|
+
css_extract_ignore_order_warnings: false,
|
|
26
|
+
webpack_compile_output: true,
|
|
27
|
+
shakapacker_precompile: true,
|
|
28
|
+
cache_manifest: false,
|
|
29
|
+
ensure_consistent_versioning: false,
|
|
30
|
+
useContentHash: true,
|
|
31
|
+
compile: true,
|
|
32
|
+
additional_paths: []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it("always validates path traversal in required path fields in production", () => {
|
|
36
|
+
process.env.NODE_ENV = "production"
|
|
37
|
+
delete process.env.SHAKAPACKER_STRICT_VALIDATION
|
|
38
|
+
|
|
39
|
+
const unsafeConfig = {
|
|
40
|
+
...baseConfig,
|
|
41
|
+
source_path: "../../../etc/passwd"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(isValidConfig(unsafeConfig)).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("always validates path traversal in required path fields in development", () => {
|
|
48
|
+
process.env.NODE_ENV = "development"
|
|
49
|
+
|
|
50
|
+
const unsafeConfig = {
|
|
51
|
+
...baseConfig,
|
|
52
|
+
public_output_path: "../../sensitive/data"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(isValidConfig(unsafeConfig)).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("always validates path traversal in additional_paths in production", () => {
|
|
59
|
+
process.env.NODE_ENV = "production"
|
|
60
|
+
delete process.env.SHAKAPACKER_STRICT_VALIDATION
|
|
61
|
+
|
|
62
|
+
const unsafeConfig = {
|
|
63
|
+
...baseConfig,
|
|
64
|
+
additional_paths: ["./safe/path", "../../../etc/passwd"]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(isValidConfig(unsafeConfig)).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("always validates path traversal in additional_paths in development", () => {
|
|
71
|
+
process.env.NODE_ENV = "development"
|
|
72
|
+
|
|
73
|
+
const unsafeConfig = {
|
|
74
|
+
...baseConfig,
|
|
75
|
+
additional_paths: ["./safe/path", "../../../../root/.ssh"]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
expect(isValidConfig(unsafeConfig)).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("allows safe paths in production", () => {
|
|
82
|
+
process.env.NODE_ENV = "production"
|
|
83
|
+
delete process.env.SHAKAPACKER_STRICT_VALIDATION
|
|
84
|
+
|
|
85
|
+
const safeConfig = {
|
|
86
|
+
...baseConfig,
|
|
87
|
+
additional_paths: ["./app/assets", "./vendor/assets", "node_modules"]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(isValidConfig(safeConfig)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("allows safe paths in development", () => {
|
|
94
|
+
process.env.NODE_ENV = "development"
|
|
95
|
+
|
|
96
|
+
const safeConfig = {
|
|
97
|
+
...baseConfig,
|
|
98
|
+
additional_paths: ["./app/components", "./lib/assets"]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
expect(isValidConfig(safeConfig)).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("optional field validation", () => {
|
|
106
|
+
const validConfig = {
|
|
107
|
+
source_path: "./app/javascript",
|
|
108
|
+
source_entry_path: "./packs",
|
|
109
|
+
public_root_path: "./public",
|
|
110
|
+
public_output_path: "packs",
|
|
111
|
+
cache_path: "tmp/shakapacker",
|
|
112
|
+
javascript_transpiler: "babel",
|
|
113
|
+
nested_entries: false,
|
|
114
|
+
css_extract_ignore_order_warnings: false,
|
|
115
|
+
webpack_compile_output: true,
|
|
116
|
+
shakapacker_precompile: true,
|
|
117
|
+
cache_manifest: false,
|
|
118
|
+
ensure_consistent_versioning: false,
|
|
119
|
+
useContentHash: true,
|
|
120
|
+
compile: true,
|
|
121
|
+
additional_paths: [],
|
|
122
|
+
dev_server: {
|
|
123
|
+
hmr: true,
|
|
124
|
+
port: 3035
|
|
125
|
+
},
|
|
126
|
+
integrity: {
|
|
127
|
+
enabled: true,
|
|
128
|
+
cross_origin: "anonymous"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
it("skips deep validation of optional fields in production without strict mode", () => {
|
|
133
|
+
process.env.NODE_ENV = "production"
|
|
134
|
+
delete process.env.SHAKAPACKER_STRICT_VALIDATION
|
|
135
|
+
|
|
136
|
+
// Invalid integrity config that would fail deep validation
|
|
137
|
+
const configWithInvalidOptional = {
|
|
138
|
+
...validConfig,
|
|
139
|
+
integrity: {
|
|
140
|
+
enabled: "not-a-boolean", // Invalid type
|
|
141
|
+
cross_origin: "anonymous"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Should pass because deep validation is skipped in production
|
|
146
|
+
expect(isValidConfig(configWithInvalidOptional)).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("performs deep validation of optional fields in development", () => {
|
|
150
|
+
process.env.NODE_ENV = "development"
|
|
151
|
+
|
|
152
|
+
// Invalid integrity config
|
|
153
|
+
const configWithInvalidOptional = {
|
|
154
|
+
...validConfig,
|
|
155
|
+
integrity: {
|
|
156
|
+
enabled: "not-a-boolean", // Invalid type
|
|
157
|
+
cross_origin: "anonymous"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Should fail because deep validation runs in development
|
|
162
|
+
expect(isValidConfig(configWithInvalidOptional)).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("performs deep validation in production with strict mode", () => {
|
|
166
|
+
process.env.NODE_ENV = "production"
|
|
167
|
+
process.env.SHAKAPACKER_STRICT_VALIDATION = "true"
|
|
168
|
+
|
|
169
|
+
// Invalid integrity config
|
|
170
|
+
const configWithInvalidOptional = {
|
|
171
|
+
...validConfig,
|
|
172
|
+
integrity: {
|
|
173
|
+
enabled: "not-a-boolean", // Invalid type
|
|
174
|
+
cross_origin: "anonymous"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Should fail because strict validation is enabled
|
|
179
|
+
expect(isValidConfig(configWithInvalidOptional)).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
data/tools/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Shakapacker v9 Migration Tools
|
|
2
|
+
|
|
3
|
+
## CSS Modules Codemod
|
|
4
|
+
|
|
5
|
+
A jscodeshift codemod to help migrate CSS module imports from v8 to v9 format.
|
|
6
|
+
|
|
7
|
+
### What it does
|
|
8
|
+
|
|
9
|
+
#### For JavaScript files (.js, .jsx):
|
|
10
|
+
- Converts `import styles from './styles.module.css'` to `import { className1, className2 } from './styles.module.css'`
|
|
11
|
+
- Automatically detects which CSS classes are used in the file
|
|
12
|
+
- Handles kebab-case to camelCase conversion (e.g., `my-button` → `myButton`)
|
|
13
|
+
- Updates all class references from `styles.className` to `className`
|
|
14
|
+
|
|
15
|
+
#### For TypeScript files (.ts, .tsx):
|
|
16
|
+
- Converts `import styles from './styles.module.css'` to `import * as styles from './styles.module.css'`
|
|
17
|
+
- Preserves the same usage pattern (`styles.className`)
|
|
18
|
+
- Works around TypeScript's limitation with dynamic named exports
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g jscodeshift
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Usage
|
|
27
|
+
|
|
28
|
+
#### Dry run (see what would change):
|
|
29
|
+
```bash
|
|
30
|
+
npx jscodeshift -t tools/css-modules-v9-codemod.js src/ --dry
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### Apply to JavaScript files:
|
|
34
|
+
```bash
|
|
35
|
+
npx jscodeshift -t tools/css-modules-v9-codemod.js src/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Apply to TypeScript files:
|
|
39
|
+
```bash
|
|
40
|
+
npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Apply to specific file patterns:
|
|
44
|
+
```bash
|
|
45
|
+
# Only .jsx files
|
|
46
|
+
npx jscodeshift -t tools/css-modules-v9-codemod.js src/**/*.jsx
|
|
47
|
+
|
|
48
|
+
# Only .tsx files
|
|
49
|
+
npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/**/*.tsx
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Options
|
|
53
|
+
|
|
54
|
+
- `--dry` - Run without modifying files
|
|
55
|
+
- `--print` - Print the transformed output
|
|
56
|
+
- `--parser tsx` - Use TypeScript parser
|
|
57
|
+
- `--verbose` - Show detailed progress
|
|
58
|
+
|
|
59
|
+
### Examples
|
|
60
|
+
|
|
61
|
+
#### Before (JavaScript):
|
|
62
|
+
```javascript
|
|
63
|
+
import styles from './Button.module.css';
|
|
64
|
+
|
|
65
|
+
function Button() {
|
|
66
|
+
return (
|
|
67
|
+
<button className={styles.button}>
|
|
68
|
+
<span className={styles['button-text']}>Click me</span>
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### After (JavaScript):
|
|
75
|
+
```javascript
|
|
76
|
+
import { button, buttonText } from './Button.module.css';
|
|
77
|
+
|
|
78
|
+
function Button() {
|
|
79
|
+
return (
|
|
80
|
+
<button className={button}>
|
|
81
|
+
<span className={buttonText}>Click me</span>
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Before (TypeScript):
|
|
88
|
+
```typescript
|
|
89
|
+
import styles from './Button.module.css';
|
|
90
|
+
|
|
91
|
+
const Button: React.FC = () => {
|
|
92
|
+
return <button className={styles.button}>Click</button>;
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### After (TypeScript):
|
|
97
|
+
```typescript
|
|
98
|
+
import * as styles from './Button.module.css';
|
|
99
|
+
|
|
100
|
+
const Button: React.FC = () => {
|
|
101
|
+
return <button className={styles.button}>Click</button>;
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Notes
|
|
106
|
+
|
|
107
|
+
1. **Kebab-case conversion**: CSS classes with kebab-case (e.g., `my-button`) are automatically converted to camelCase (`myButton`) for JavaScript files, matching css-loader's `exportLocalsConvention: 'camelCaseOnly'` setting.
|
|
108
|
+
|
|
109
|
+
2. **Unused imports**: The codemod only imports CSS classes that are actually used in JavaScript files. If you pass the entire styles object to a component, it will convert to namespace import for safety.
|
|
110
|
+
|
|
111
|
+
3. **Manual review recommended**: Always review the changes, especially for complex usage patterns or dynamic class name construction.
|
|
112
|
+
|
|
113
|
+
4. **Backup your code**: Run the codemod on version-controlled code or create a backup first.
|
|
114
|
+
|
|
115
|
+
### Troubleshooting
|
|
116
|
+
|
|
117
|
+
**Issue**: Codemod doesn't detect all CSS class usages
|
|
118
|
+
**Solution**: For dynamic class names or complex patterns, manual migration may be needed.
|
|
119
|
+
|
|
120
|
+
**Issue**: TypeScript errors after transformation
|
|
121
|
+
**Solution**: Ensure your TypeScript definitions are updated as shown in the [v9 Upgrade Guide](../docs/v9_upgrade.md).
|
|
122
|
+
|
|
123
|
+
**Issue**: Runtime errors about missing CSS classes
|
|
124
|
+
**Solution**: Check if you have kebab-case class names that need camelCase conversion.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shakapacker v9 CSS Modules Codemod
|
|
5
|
+
*
|
|
6
|
+
* This codemod helps migrate CSS module imports from v8 (default exports)
|
|
7
|
+
* to v9 (named exports for JS, namespace imports for TS).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx jscodeshift -t tools/css-modules-v9-codemod.js src/
|
|
11
|
+
* npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/ (for TypeScript)
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --dry Run in dry mode (no files modified)
|
|
15
|
+
* --print Print transformed files to stdout
|
|
16
|
+
* --parser tsx Use TypeScript parser
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
module.exports = function transformer(fileInfo, api) {
|
|
20
|
+
const j = api.jscodeshift
|
|
21
|
+
const root = j(fileInfo.source)
|
|
22
|
+
let hasChanges = false
|
|
23
|
+
|
|
24
|
+
// Detect if this is a TypeScript file
|
|
25
|
+
const isTypeScript = fileInfo.path.match(/\.tsx?$/)
|
|
26
|
+
|
|
27
|
+
// Find all CSS module imports
|
|
28
|
+
root
|
|
29
|
+
.find(j.ImportDeclaration, {
|
|
30
|
+
source: {
|
|
31
|
+
value: (value) =>
|
|
32
|
+
value && value.match(/\.module\.(css|scss|sass|less)$/)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.forEach((path) => {
|
|
36
|
+
const importDecl = path.node
|
|
37
|
+
|
|
38
|
+
// Check if it's a default import (v8 style)
|
|
39
|
+
const defaultSpecifier = importDecl.specifiers.find(
|
|
40
|
+
(spec) => spec.type === "ImportDefaultSpecifier"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if (!defaultSpecifier) {
|
|
44
|
+
// Already using named or namespace imports, skip
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const defaultImportName = defaultSpecifier.local.name
|
|
49
|
+
|
|
50
|
+
if (isTypeScript) {
|
|
51
|
+
// For TypeScript: Convert to namespace import (import * as styles)
|
|
52
|
+
const namespaceSpecifier = j.importNamespaceSpecifier(
|
|
53
|
+
j.identifier(defaultImportName)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Replace the import specifiers
|
|
57
|
+
importDecl.specifiers = [namespaceSpecifier]
|
|
58
|
+
hasChanges = true
|
|
59
|
+
} else {
|
|
60
|
+
// For JavaScript: Convert to named imports
|
|
61
|
+
// First, we need to find all usages of the imported object
|
|
62
|
+
const usages = new Set()
|
|
63
|
+
|
|
64
|
+
// Find all member expressions using the imported default
|
|
65
|
+
root
|
|
66
|
+
.find(j.MemberExpression, {
|
|
67
|
+
object: {
|
|
68
|
+
type: "Identifier",
|
|
69
|
+
name: defaultImportName
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.forEach((memberPath) => {
|
|
73
|
+
// Handle both dot notation (styles.className) and bracket notation (styles['class-name'])
|
|
74
|
+
if (
|
|
75
|
+
memberPath.node.computed &&
|
|
76
|
+
memberPath.node.property.type === "Literal"
|
|
77
|
+
) {
|
|
78
|
+
// Computed property access: styles['active-button']
|
|
79
|
+
const propertyValue = memberPath.node.property.value
|
|
80
|
+
if (typeof propertyValue === "string") {
|
|
81
|
+
usages.add(propertyValue)
|
|
82
|
+
}
|
|
83
|
+
} else if (
|
|
84
|
+
!memberPath.node.computed &&
|
|
85
|
+
memberPath.node.property.type === "Identifier"
|
|
86
|
+
) {
|
|
87
|
+
// Dot notation: styles.activeButton
|
|
88
|
+
const propertyName = memberPath.node.property.name
|
|
89
|
+
if (propertyName) {
|
|
90
|
+
usages.add(propertyName)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (usages.size > 0) {
|
|
96
|
+
// Create named import specifiers
|
|
97
|
+
const namedSpecifiers = Array.from(usages)
|
|
98
|
+
.sort()
|
|
99
|
+
.map((name) => {
|
|
100
|
+
// Handle kebab-case to camelCase conversion
|
|
101
|
+
const camelCaseName = name.replace(/-([a-z])/g, (g) =>
|
|
102
|
+
g[1].toUpperCase()
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (camelCaseName !== name) {
|
|
106
|
+
// If conversion happened, we need to alias it
|
|
107
|
+
return j.importSpecifier(
|
|
108
|
+
j.identifier(camelCaseName),
|
|
109
|
+
j.identifier(camelCaseName) // css-loader exports it as camelCase
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return j.importSpecifier(j.identifier(name))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Replace the import specifiers
|
|
117
|
+
importDecl.specifiers = namedSpecifiers
|
|
118
|
+
|
|
119
|
+
// Update all usages in the file
|
|
120
|
+
root
|
|
121
|
+
.find(j.MemberExpression, {
|
|
122
|
+
object: {
|
|
123
|
+
type: "Identifier",
|
|
124
|
+
name: defaultImportName
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.forEach((memberPath) => {
|
|
128
|
+
let propertyName
|
|
129
|
+
|
|
130
|
+
// Extract property name from both computed and dot notation
|
|
131
|
+
if (
|
|
132
|
+
memberPath.node.computed &&
|
|
133
|
+
memberPath.node.property.type === "Literal"
|
|
134
|
+
) {
|
|
135
|
+
propertyName = memberPath.node.property.value
|
|
136
|
+
} else if (
|
|
137
|
+
!memberPath.node.computed &&
|
|
138
|
+
memberPath.node.property.type === "Identifier"
|
|
139
|
+
) {
|
|
140
|
+
propertyName = memberPath.node.property.name
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (propertyName && typeof propertyName === "string") {
|
|
144
|
+
// Convert kebab-case to camelCase
|
|
145
|
+
const camelCaseName = propertyName.replace(/-([a-z])/g, (g) =>
|
|
146
|
+
g[1].toUpperCase()
|
|
147
|
+
)
|
|
148
|
+
// Replace with direct identifier
|
|
149
|
+
j(memberPath).replaceWith(j.identifier(camelCaseName))
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
hasChanges = true
|
|
154
|
+
} else if (usages.size === 0) {
|
|
155
|
+
// No usages found, might be passed as a whole object
|
|
156
|
+
// In this case, convert to namespace import for safety
|
|
157
|
+
const namespaceSpecifier = j.importNamespaceSpecifier(
|
|
158
|
+
j.identifier(defaultImportName)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
importDecl.specifiers = [namespaceSpecifier]
|
|
162
|
+
hasChanges = true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if (!hasChanges) {
|
|
168
|
+
return null // No changes made
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return root.toSource({
|
|
172
|
+
quote: "single",
|
|
173
|
+
trailingComma: true,
|
|
174
|
+
tabWidth: 2
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Export the parser to use
|
|
179
|
+
module.exports.parser = "tsx"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"rootDir": "."
|
|
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"]
|
|
16
|
+
}
|
data/tsconfig.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"importHelpers": false,
|
|
7
|
+
"typeRoots": [
|
|
8
|
+
"./node_modules/@types",
|
|
9
|
+
"node_modules/@types",
|
|
10
|
+
"../node_modules/@types"
|
|
11
|
+
],
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"outDir": "./package",
|
|
15
|
+
"rootDir": "./package",
|
|
16
|
+
"strict": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"moduleResolution": "node",
|
|
22
|
+
"allowJs": true,
|
|
23
|
+
"checkJs": false,
|
|
24
|
+
"noImplicitAny": true,
|
|
25
|
+
"strictNullChecks": true,
|
|
26
|
+
"strictFunctionTypes": true,
|
|
27
|
+
"strictBindCallApply": true,
|
|
28
|
+
"strictPropertyInitialization": false,
|
|
29
|
+
"noImplicitThis": true,
|
|
30
|
+
"alwaysStrict": true,
|
|
31
|
+
"allowSyntheticDefaultImports": true,
|
|
32
|
+
"preserveConstEnums": true,
|
|
33
|
+
"isolatedModules": true,
|
|
34
|
+
"removeComments": false
|
|
35
|
+
},
|
|
36
|
+
"include": ["package/**/*.ts"],
|
|
37
|
+
"exclude": ["node_modules", "package/**/*.test.ts", "package/**/*.spec.ts"]
|
|
38
|
+
}
|