shakapacker 9.0.0.beta.6 → 9.0.0.beta.8
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/.eslintrc.fast.js +40 -0
- data/.eslintrc.js +48 -0
- data/.github/workflows/generator.yml +6 -0
- data/.gitignore +1 -4
- data/.npmignore +56 -0
- data/CHANGELOG.md +64 -1
- data/CONTRIBUTING.md +75 -21
- data/Gemfile.lock +1 -1
- data/README.md +4 -0
- data/TODO.md +15 -16
- data/docs/transpiler-migration.md +191 -0
- data/docs/typescript-migration.md +378 -0
- data/lib/install/template.rb +54 -7
- data/lib/shakapacker/version.rb +1 -1
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +56 -0
- data/package/config.ts +23 -10
- data/package/env.ts +15 -2
- data/package/environments/{development.js → development.ts} +30 -8
- data/package/environments/{production.js → production.ts} +18 -4
- data/package/environments/test.ts +53 -0
- data/package/environments/types.ts +90 -0
- data/package/esbuild/index.ts +42 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/{webpack.js → webpack.ts} +12 -4
- data/package/plugins/{rspack.js → rspack.ts} +20 -5
- data/package/plugins/{webpack.js → webpack.ts} +2 -2
- data/package/rspack/{index.js → index.ts} +17 -10
- data/package/rules/{babel.js → babel.ts} +1 -1
- data/package/rules/{coffee.js → coffee.ts} +1 -1
- data/package/rules/{css.js → css.ts} +1 -1
- data/package/rules/{erb.js → erb.ts} +1 -1
- data/package/rules/{esbuild.js → esbuild.ts} +2 -2
- data/package/rules/{file.js → file.ts} +11 -6
- data/package/rules/{jscommon.js → jscommon.ts} +4 -4
- data/package/rules/{less.js → less.ts} +3 -3
- data/package/rules/raw.ts +25 -0
- data/package/rules/{rspack.js → rspack.ts} +21 -11
- data/package/rules/{sass.js → sass.ts} +1 -1
- data/package/rules/{stylus.js → stylus.ts} +3 -7
- data/package/rules/{swc.js → swc.ts} +2 -2
- data/package/rules/{webpack.js → webpack.ts} +1 -1
- data/package/swc/index.ts +54 -0
- data/package/types/README.md +87 -0
- data/package/types/index.ts +60 -0
- data/package/utils/errorCodes.ts +219 -0
- data/package/utils/errorHelpers.ts +68 -2
- data/package/utils/pathValidation.ts +139 -0
- data/package/utils/typeGuards.ts +161 -47
- data/package.json +26 -4
- data/scripts/remove-use-strict.js +45 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/webpack.test.js +21 -2
- data/test/package/transpiler-defaults.test.js +127 -0
- data/test/scripts/remove-use-strict.test.js +125 -0
- data/test/typescript/build.test.js +3 -2
- 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/tsconfig.eslint.json +16 -0
- data/tsconfig.json +9 -10
- data/yarn.lock +415 -6
- metadata +50 -28
- data/package/babel/preset.js +0 -48
- data/package/environments/base.js +0 -103
- data/package/environments/test.js +0 -19
- data/package/esbuild/index.js +0 -40
- data/package/optimization/rspack.js +0 -29
- data/package/rules/raw.js +0 -15
- data/package/swc/index.js +0 -50
@@ -1,12 +1,45 @@
|
|
1
|
-
const raw = require("../../../package/rules/raw")
|
2
|
-
|
3
1
|
describe("raw", () => {
|
4
|
-
|
5
|
-
|
2
|
+
describe("rspack bundler", () => {
|
3
|
+
beforeEach(() => {
|
4
|
+
jest.resetModules()
|
5
|
+
jest.doMock("../../../package/config", () => ({
|
6
|
+
assets_bundler: "rspack"
|
7
|
+
}))
|
8
|
+
})
|
9
|
+
|
10
|
+
afterEach(() => {
|
11
|
+
jest.dontMock("../../../package/config")
|
12
|
+
})
|
13
|
+
|
14
|
+
test("uses resourceQuery for any file with ?raw", () => {
|
15
|
+
const raw = require("../../../package/rules/raw")
|
16
|
+
expect(raw.resourceQuery).toStrictEqual(/raw/)
|
17
|
+
expect(raw.type).toBe("asset/source")
|
18
|
+
})
|
6
19
|
})
|
7
20
|
|
8
|
-
|
9
|
-
|
10
|
-
|
21
|
+
describe("webpack bundler", () => {
|
22
|
+
beforeEach(() => {
|
23
|
+
jest.resetModules()
|
24
|
+
jest.doMock("../../../package/config", () => ({
|
25
|
+
assets_bundler: "webpack"
|
26
|
+
}))
|
27
|
+
})
|
28
|
+
|
29
|
+
afterEach(() => {
|
30
|
+
jest.dontMock("../../../package/config")
|
31
|
+
})
|
32
|
+
|
33
|
+
test("supports ?raw query and .html fallback with oneOf", () => {
|
34
|
+
const raw = require("../../../package/rules/raw")
|
35
|
+
expect(raw.oneOf).toHaveLength(2)
|
36
|
+
// First rule: any file with ?raw
|
37
|
+
expect(raw.oneOf[0].resourceQuery).toStrictEqual(/raw/)
|
38
|
+
expect(raw.oneOf[0].type).toBe("asset/source")
|
39
|
+
// Second rule: .html files without query
|
40
|
+
expect(raw.oneOf[1].test.test(".html")).toBe(true)
|
41
|
+
expect(raw.oneOf[1].exclude.test(".js")).toBe(true)
|
42
|
+
expect(raw.oneOf[1].type).toBe("asset/source")
|
43
|
+
})
|
11
44
|
})
|
12
45
|
})
|
@@ -10,7 +10,26 @@ jest.mock("../../../package/utils/helpers", () => {
|
|
10
10
|
})
|
11
11
|
|
12
12
|
describe("index", () => {
|
13
|
-
test("rule tests are regexes", () => {
|
14
|
-
rules.
|
13
|
+
test("rule tests are regexes or oneOf arrays", () => {
|
14
|
+
const rulesWithTest = rules.filter((rule) => !rule.oneOf)
|
15
|
+
const rulesWithOneOf = rules.filter((rule) => rule.oneOf)
|
16
|
+
|
17
|
+
// Verify all non-oneOf rules have test property
|
18
|
+
rulesWithTest.forEach((rule) => {
|
19
|
+
expect(rule.test).toBeInstanceOf(RegExp)
|
20
|
+
})
|
21
|
+
|
22
|
+
// Verify all oneOf rules are properly structured
|
23
|
+
rulesWithOneOf.forEach((rule) => {
|
24
|
+
expect(Array.isArray(rule.oneOf)).toBe(true)
|
25
|
+
rule.oneOf.forEach((subRule) => {
|
26
|
+
// Each subRule must have either a test or resourceQuery property (RegExp)
|
27
|
+
const matchers = [
|
28
|
+
subRule.test instanceof RegExp,
|
29
|
+
subRule.resourceQuery instanceof RegExp
|
30
|
+
]
|
31
|
+
expect(matchers.some(Boolean)).toBe(true)
|
32
|
+
})
|
33
|
+
})
|
15
34
|
})
|
16
35
|
})
|
@@ -0,0 +1,127 @@
|
|
1
|
+
// Test transpiler defaults for backward compatibility
|
2
|
+
|
3
|
+
describe("JavaScript Transpiler Defaults", () => {
|
4
|
+
let originalEnv
|
5
|
+
|
6
|
+
beforeEach(() => {
|
7
|
+
// Save original environment
|
8
|
+
originalEnv = { ...process.env }
|
9
|
+
|
10
|
+
// Clear module cache to test different configurations
|
11
|
+
jest.resetModules()
|
12
|
+
})
|
13
|
+
|
14
|
+
afterEach(() => {
|
15
|
+
// Restore original environment
|
16
|
+
process.env = originalEnv
|
17
|
+
})
|
18
|
+
|
19
|
+
describe("webpack bundler", () => {
|
20
|
+
it("respects config file transpiler setting (swc in this project)", () => {
|
21
|
+
// Set up webpack environment
|
22
|
+
delete process.env.SHAKAPACKER_ASSETS_BUNDLER
|
23
|
+
delete process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
|
24
|
+
|
25
|
+
// Load config fresh
|
26
|
+
const config = require("../../package/config")
|
27
|
+
|
28
|
+
// This project's shakapacker.yml has javascript_transpiler: 'swc'
|
29
|
+
// which overrides the default babel for webpack
|
30
|
+
expect(config.javascript_transpiler).toBe("swc")
|
31
|
+
expect(config.webpack_loader).toBe("swc") // Legacy property
|
32
|
+
})
|
33
|
+
|
34
|
+
it("respects explicit javascript_transpiler setting", () => {
|
35
|
+
delete process.env.SHAKAPACKER_ASSETS_BUNDLER
|
36
|
+
process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER = "swc"
|
37
|
+
|
38
|
+
jest.resetModules()
|
39
|
+
const config = require("../../package/config")
|
40
|
+
|
41
|
+
expect(config.javascript_transpiler).toBe("swc")
|
42
|
+
})
|
43
|
+
})
|
44
|
+
|
45
|
+
describe("rspack bundler", () => {
|
46
|
+
it("uses swc as default transpiler for modern performance", () => {
|
47
|
+
process.env.SHAKAPACKER_ASSETS_BUNDLER = "rspack"
|
48
|
+
delete process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
|
49
|
+
|
50
|
+
jest.resetModules()
|
51
|
+
const config = require("../../package/config")
|
52
|
+
|
53
|
+
expect(config.javascript_transpiler).toBe("swc")
|
54
|
+
expect(config.webpack_loader).toBe("swc") // Legacy property
|
55
|
+
})
|
56
|
+
|
57
|
+
it("allows environment override to babel if needed", () => {
|
58
|
+
process.env.SHAKAPACKER_ASSETS_BUNDLER = "rspack"
|
59
|
+
process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER = "babel"
|
60
|
+
|
61
|
+
jest.resetModules()
|
62
|
+
const config = require("../../package/config")
|
63
|
+
|
64
|
+
expect(config.javascript_transpiler).toBe("babel")
|
65
|
+
})
|
66
|
+
})
|
67
|
+
|
68
|
+
describe("backward compatibility", () => {
|
69
|
+
it("supports deprecated webpack_loader property", () => {
|
70
|
+
delete process.env.SHAKAPACKER_ASSETS_BUNDLER
|
71
|
+
delete process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
|
72
|
+
|
73
|
+
jest.resetModules()
|
74
|
+
const config = require("../../package/config")
|
75
|
+
|
76
|
+
// Both properties should exist and match
|
77
|
+
expect(config.webpack_loader).toBeDefined()
|
78
|
+
expect(config.javascript_transpiler).toBeDefined()
|
79
|
+
expect(config.webpack_loader).toBe(config.javascript_transpiler)
|
80
|
+
})
|
81
|
+
|
82
|
+
it("warns when using deprecated webpack_loader in config", () => {
|
83
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation()
|
84
|
+
|
85
|
+
// Simulate config with webpack_loader set
|
86
|
+
// This would normally come from YAML config
|
87
|
+
delete process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
|
88
|
+
|
89
|
+
jest.resetModules()
|
90
|
+
require("../../package/config")
|
91
|
+
|
92
|
+
// The warning is shown during config loading if webpack_loader is detected
|
93
|
+
// Since we can't easily mock the YAML config here, we check the mechanism exists
|
94
|
+
expect(consoleSpy.mock.calls.length >= 0).toBe(true)
|
95
|
+
|
96
|
+
consoleSpy.mockRestore()
|
97
|
+
})
|
98
|
+
})
|
99
|
+
|
100
|
+
describe("environment variable precedence", () => {
|
101
|
+
it("environment variable overrides default", () => {
|
102
|
+
process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER = "esbuild"
|
103
|
+
|
104
|
+
jest.resetModules()
|
105
|
+
const config = require("../../package/config")
|
106
|
+
|
107
|
+
expect(config.javascript_transpiler).toBe("esbuild")
|
108
|
+
})
|
109
|
+
|
110
|
+
it("bundler change doesn't affect transpiler when explicitly set in config", () => {
|
111
|
+
// With this project's config, transpiler is always 'swc'
|
112
|
+
// regardless of bundler because it's explicitly set in shakapacker.yml
|
113
|
+
|
114
|
+
// Test webpack
|
115
|
+
delete process.env.SHAKAPACKER_ASSETS_BUNDLER
|
116
|
+
jest.resetModules()
|
117
|
+
let config = require("../../package/config")
|
118
|
+
expect(config.javascript_transpiler).toBe("swc") // Config file overrides default
|
119
|
+
|
120
|
+
// Test rspack - should still be swc
|
121
|
+
process.env.SHAKAPACKER_ASSETS_BUNDLER = "rspack"
|
122
|
+
jest.resetModules()
|
123
|
+
config = require("../../package/config")
|
124
|
+
expect(config.javascript_transpiler).toBe("swc")
|
125
|
+
})
|
126
|
+
})
|
127
|
+
})
|
@@ -0,0 +1,125 @@
|
|
1
|
+
const fs = require("fs")
|
2
|
+
const path = require("path")
|
3
|
+
const { execSync } = require("child_process")
|
4
|
+
const os = require("os")
|
5
|
+
|
6
|
+
describe("remove-use-strict script", () => {
|
7
|
+
let tempDir
|
8
|
+
|
9
|
+
beforeEach(() => {
|
10
|
+
// Create a temporary directory for test files
|
11
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "remove-use-strict-test-"))
|
12
|
+
})
|
13
|
+
|
14
|
+
afterEach(() => {
|
15
|
+
// Clean up the temporary directory
|
16
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
17
|
+
})
|
18
|
+
|
19
|
+
function createTestFile(filename, content) {
|
20
|
+
const filePath = path.join(tempDir, filename)
|
21
|
+
fs.writeFileSync(filePath, content, "utf8")
|
22
|
+
return filePath
|
23
|
+
}
|
24
|
+
|
25
|
+
function runScript(directory) {
|
26
|
+
// Run the script with a custom directory
|
27
|
+
const scriptContent = fs.readFileSync(
|
28
|
+
"scripts/remove-use-strict.js",
|
29
|
+
"utf8"
|
30
|
+
)
|
31
|
+
// Replace 'package' with our test directory
|
32
|
+
const modifiedScript = scriptContent.replace(
|
33
|
+
'findJsFiles("package")',
|
34
|
+
`findJsFiles("${directory}")`
|
35
|
+
)
|
36
|
+
const tempScript = path.join(tempDir, "test-script.js")
|
37
|
+
fs.writeFileSync(tempScript, modifiedScript, "utf8")
|
38
|
+
execSync(`node "${tempScript}"`, { stdio: "pipe" })
|
39
|
+
}
|
40
|
+
|
41
|
+
it("removes standard double-quoted use strict with semicolon", () => {
|
42
|
+
const filePath = createTestFile("test1.js", '"use strict";\nconst x = 1;')
|
43
|
+
runScript(tempDir)
|
44
|
+
const result = fs.readFileSync(filePath, "utf8")
|
45
|
+
expect(result).toBe("const x = 1;\n")
|
46
|
+
})
|
47
|
+
|
48
|
+
it("removes single-quoted use strict without semicolon", () => {
|
49
|
+
const filePath = createTestFile("test2.js", "'use strict'\nconst x = 1;")
|
50
|
+
runScript(tempDir)
|
51
|
+
const result = fs.readFileSync(filePath, "utf8")
|
52
|
+
expect(result).toBe("const x = 1;\n")
|
53
|
+
})
|
54
|
+
|
55
|
+
it("removes use strict with leading whitespace", () => {
|
56
|
+
const filePath = createTestFile(
|
57
|
+
"test3.js",
|
58
|
+
' \t"use strict";\nconst x = 1;'
|
59
|
+
)
|
60
|
+
runScript(tempDir)
|
61
|
+
const result = fs.readFileSync(filePath, "utf8")
|
62
|
+
expect(result).toBe("const x = 1;\n")
|
63
|
+
})
|
64
|
+
|
65
|
+
it("removes use strict with trailing whitespace and multiple newlines", () => {
|
66
|
+
const filePath = createTestFile(
|
67
|
+
"test4.js",
|
68
|
+
'"use strict"; \n\n\nconst x = 1;'
|
69
|
+
)
|
70
|
+
runScript(tempDir)
|
71
|
+
const result = fs.readFileSync(filePath, "utf8")
|
72
|
+
expect(result).toBe("const x = 1;\n")
|
73
|
+
})
|
74
|
+
|
75
|
+
it("removes use strict with unicode quotes", () => {
|
76
|
+
const filePath = createTestFile(
|
77
|
+
"test5.js",
|
78
|
+
"\u201Cuse strict\u201D;\nconst x = 1;"
|
79
|
+
)
|
80
|
+
runScript(tempDir)
|
81
|
+
const result = fs.readFileSync(filePath, "utf8")
|
82
|
+
expect(result).toBe("const x = 1;\n")
|
83
|
+
})
|
84
|
+
|
85
|
+
it("ensures trailing newline when missing", () => {
|
86
|
+
const filePath = createTestFile("test6.js", '"use strict";\nconst x = 1')
|
87
|
+
runScript(tempDir)
|
88
|
+
const result = fs.readFileSync(filePath, "utf8")
|
89
|
+
expect(result).toBe("const x = 1\n")
|
90
|
+
expect(result.endsWith("\n")).toBe(true)
|
91
|
+
})
|
92
|
+
|
93
|
+
it("preserves content that doesn't start with use strict", () => {
|
94
|
+
const filePath = createTestFile(
|
95
|
+
"test7.js",
|
96
|
+
'const y = 2;\n"use strict";\nconst x = 1;'
|
97
|
+
)
|
98
|
+
runScript(tempDir)
|
99
|
+
const result = fs.readFileSync(filePath, "utf8")
|
100
|
+
expect(result).toBe('const y = 2;\n"use strict";\nconst x = 1;\n')
|
101
|
+
})
|
102
|
+
|
103
|
+
it("handles files already ending with newline", () => {
|
104
|
+
const filePath = createTestFile("test8.js", '"use strict";\nconst x = 1;\n')
|
105
|
+
runScript(tempDir)
|
106
|
+
const result = fs.readFileSync(filePath, "utf8")
|
107
|
+
expect(result).toBe("const x = 1;\n")
|
108
|
+
// Should have exactly one trailing newline, not double
|
109
|
+
expect(result.match(/\n$/g)).toHaveLength(1)
|
110
|
+
})
|
111
|
+
|
112
|
+
it("handles Windows-style line endings", () => {
|
113
|
+
const filePath = createTestFile("test9.js", '"use strict";\r\nconst x = 1;')
|
114
|
+
runScript(tempDir)
|
115
|
+
const result = fs.readFileSync(filePath, "utf8")
|
116
|
+
expect(result).toBe("const x = 1;\n")
|
117
|
+
})
|
118
|
+
|
119
|
+
it("handles use strict with extra spaces", () => {
|
120
|
+
const filePath = createTestFile("test10.js", '"use strict";\nconst x = 1;')
|
121
|
+
runScript(tempDir)
|
122
|
+
const result = fs.readFileSync(filePath, "utf8")
|
123
|
+
expect(result).toBe("const x = 1;\n")
|
124
|
+
})
|
125
|
+
})
|
@@ -24,9 +24,10 @@ describe("typescript build", () => {
|
|
24
24
|
expect(existsSync(tsPath)).toBe(true)
|
25
25
|
expect(existsSync(jsPath)).toBe(true)
|
26
26
|
|
27
|
-
// Verify JS file
|
27
|
+
// Verify JS file contains CommonJS exports (has been compiled)
|
28
28
|
const jsContent = readFileSync(jsPath, "utf8")
|
29
|
-
expect(jsContent).toContain("
|
29
|
+
expect(jsContent).toContain("require(")
|
30
|
+
expect(jsContent).toContain("module.exports")
|
30
31
|
})
|
31
32
|
})
|
32
33
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
// Type-specific tests for environment modules
|
2
|
+
// Test imports to ensure TypeScript modules compile correctly
|
3
|
+
const developmentConfig = require("../../package/environments/development")
|
4
|
+
const productionConfig = require("../../package/environments/production")
|
5
|
+
const testConfig = require("../../package/environments/test")
|
6
|
+
|
7
|
+
describe("TypeScript Environment Modules", () => {
|
8
|
+
describe("development.ts", () => {
|
9
|
+
it("exports a valid webpack/rspack configuration", () => {
|
10
|
+
expect(developmentConfig).toBeDefined()
|
11
|
+
expect(typeof developmentConfig).toBe("object")
|
12
|
+
expect(developmentConfig.mode).toBe("development")
|
13
|
+
})
|
14
|
+
|
15
|
+
it("includes proper devtool configuration", () => {
|
16
|
+
expect(developmentConfig.devtool).toBe("cheap-module-source-map")
|
17
|
+
})
|
18
|
+
|
19
|
+
it("can be used as webpack configuration", () => {
|
20
|
+
// This test verifies the module exports valid config
|
21
|
+
const config = developmentConfig
|
22
|
+
expect(config).toBeDefined()
|
23
|
+
expect(typeof config).toBe("object")
|
24
|
+
})
|
25
|
+
})
|
26
|
+
|
27
|
+
describe("production.ts", () => {
|
28
|
+
it("exports a valid webpack/rspack configuration", () => {
|
29
|
+
expect(productionConfig).toBeDefined()
|
30
|
+
expect(typeof productionConfig).toBe("object")
|
31
|
+
expect(productionConfig.mode).toBe("production")
|
32
|
+
})
|
33
|
+
|
34
|
+
it("includes proper devtool configuration", () => {
|
35
|
+
expect(productionConfig.devtool).toBe("source-map")
|
36
|
+
})
|
37
|
+
|
38
|
+
it("includes optimization configuration", () => {
|
39
|
+
expect(productionConfig.optimization).toBeDefined()
|
40
|
+
})
|
41
|
+
|
42
|
+
it("includes plugins array", () => {
|
43
|
+
expect(Array.isArray(productionConfig.plugins)).toBe(true)
|
44
|
+
})
|
45
|
+
|
46
|
+
it("can be used as webpack configuration", () => {
|
47
|
+
const config = productionConfig
|
48
|
+
expect(config).toBeDefined()
|
49
|
+
expect(typeof config).toBe("object")
|
50
|
+
})
|
51
|
+
})
|
52
|
+
|
53
|
+
describe("test.ts", () => {
|
54
|
+
it("exports a valid webpack/rspack configuration", () => {
|
55
|
+
expect(testConfig).toBeDefined()
|
56
|
+
expect(typeof testConfig).toBe("object")
|
57
|
+
})
|
58
|
+
|
59
|
+
it("includes proper mode configuration", () => {
|
60
|
+
// Test environment should always have a mode defined
|
61
|
+
expect(testConfig.mode).toBeDefined()
|
62
|
+
expect(["development", "production", "test"]).toContain(testConfig.mode)
|
63
|
+
})
|
64
|
+
|
65
|
+
it("can be used as webpack configuration", () => {
|
66
|
+
const config = testConfig
|
67
|
+
expect(config).toBeDefined()
|
68
|
+
expect(typeof config).toBe("object")
|
69
|
+
})
|
70
|
+
})
|
71
|
+
|
72
|
+
describe("type safety", () => {
|
73
|
+
it("ensures all environment configs have consistent base structure", () => {
|
74
|
+
const configs = [developmentConfig, productionConfig, testConfig]
|
75
|
+
|
76
|
+
configs.forEach((config) => {
|
77
|
+
expect(config).toHaveProperty("module")
|
78
|
+
expect(config).toHaveProperty("entry")
|
79
|
+
expect(config).toHaveProperty("output")
|
80
|
+
expect(config).toHaveProperty("resolve")
|
81
|
+
})
|
82
|
+
})
|
83
|
+
|
84
|
+
it("validates dev server configuration when present", () => {
|
85
|
+
// Development config may or may not have devServer depending on environment
|
86
|
+
const { devServer = {} } = developmentConfig
|
87
|
+
|
88
|
+
// Validate devServer type (either undefined or object from config)
|
89
|
+
const actualDevServer = developmentConfig.devServer
|
90
|
+
expect(["undefined", "object"]).toContain(typeof actualDevServer)
|
91
|
+
|
92
|
+
// For port validation, we accept: undefined, "auto", number, or string
|
93
|
+
const { port } = devServer
|
94
|
+
|
95
|
+
// Map "auto" string to type "auto", everything else to its typeof
|
96
|
+
// Use array to avoid conditional operators - mappedType takes precedence
|
97
|
+
const portTypeMap = { auto: "auto" }
|
98
|
+
const mappedType = portTypeMap[port]
|
99
|
+
const actualType = typeof port
|
100
|
+
const possibleTypes = [mappedType, actualType]
|
101
|
+
const portType = possibleTypes.find((t) => t !== undefined)
|
102
|
+
|
103
|
+
// Port should be undefined, "auto", number, or string
|
104
|
+
expect(["undefined", "auto", "number", "string"]).toContain(portType)
|
105
|
+
})
|
106
|
+
})
|
107
|
+
})
|
@@ -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
|
+
})
|