shakapacker 9.0.0.beta.5 → 9.0.0.beta.7
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/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/generator.yml +6 -0
- data/.github/workflows/test-bundlers.yml +9 -9
- data/.gitignore +0 -1
- data/.npmignore +55 -0
- data/CONTRIBUTING.md +64 -0
- data/Gemfile.lock +1 -1
- data/README.md +81 -0
- data/docs/optional-peer-dependencies.md +198 -0
- data/docs/transpiler-migration.md +191 -0
- data/docs/typescript-migration.md +378 -0
- data/docs/v9_upgrade.md +65 -1
- data/lib/install/template.rb +54 -7
- data/lib/shakapacker/doctor.rb +1 -2
- data/lib/shakapacker/version.rb +1 -1
- data/package/.npmignore +4 -0
- data/package/config.ts +23 -10
- data/package/env.ts +15 -2
- data/package/environments/base.ts +2 -1
- 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/index.ts +2 -1
- data/package/loaders.d.ts +1 -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/webpack-types.d.ts +1 -0
- data/package.json +111 -5
- data/scripts/remove-use-strict.js +45 -0
- 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 +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
- metadata +28 -6
- data/package/environments/base.js +0 -116
- data/package/environments/test.js +0 -19
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Test script for verifying optional peer dependencies work correctly
|
4
|
+
# This ensures no warnings are shown during installation with different package managers
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
echo "Testing optional peer dependencies installation..."
|
9
|
+
|
10
|
+
# Colors for output
|
11
|
+
RED='\033[0;31m'
|
12
|
+
GREEN='\033[0;32m'
|
13
|
+
NC='\033[0m' # No Color
|
14
|
+
|
15
|
+
# Get the current directory (shakapacker root)
|
16
|
+
SHAKAPACKER_PATH=$(pwd)
|
17
|
+
|
18
|
+
# Create a temporary directory for tests
|
19
|
+
TEST_DIR=$(mktemp -d)
|
20
|
+
echo "Testing in: $TEST_DIR"
|
21
|
+
|
22
|
+
# Function to check for peer dependency warnings
|
23
|
+
check_warnings() {
|
24
|
+
local output=$1
|
25
|
+
local pkg_manager=$2
|
26
|
+
|
27
|
+
# Check for common peer dependency warning patterns
|
28
|
+
if echo "$output" | grep -i "peer" | grep -i "warn" > /dev/null 2>&1; then
|
29
|
+
echo -e "${RED}✗ $pkg_manager shows peer dependency warnings${NC}"
|
30
|
+
return 1
|
31
|
+
else
|
32
|
+
echo -e "${GREEN}✓ $pkg_manager installation clean (no warnings)${NC}"
|
33
|
+
return 0
|
34
|
+
fi
|
35
|
+
}
|
36
|
+
|
37
|
+
# Test with npm
|
38
|
+
echo ""
|
39
|
+
echo "Testing with npm..."
|
40
|
+
mkdir -p "$TEST_DIR/npm-test"
|
41
|
+
cd "$TEST_DIR/npm-test"
|
42
|
+
npm init -y > /dev/null 2>&1
|
43
|
+
NPM_OUTPUT=$(npm install "$SHAKAPACKER_PATH" 2>&1)
|
44
|
+
check_warnings "$NPM_OUTPUT" "npm"
|
45
|
+
NPM_RESULT=$?
|
46
|
+
|
47
|
+
# Test with yarn
|
48
|
+
echo ""
|
49
|
+
echo "Testing with yarn..."
|
50
|
+
mkdir -p "$TEST_DIR/yarn-test"
|
51
|
+
cd "$TEST_DIR/yarn-test"
|
52
|
+
yarn init -y > /dev/null 2>&1
|
53
|
+
YARN_OUTPUT=$(yarn add "$SHAKAPACKER_PATH" 2>&1)
|
54
|
+
check_warnings "$YARN_OUTPUT" "yarn"
|
55
|
+
YARN_RESULT=$?
|
56
|
+
|
57
|
+
# Test with pnpm (if available)
|
58
|
+
if command -v pnpm &> /dev/null; then
|
59
|
+
echo ""
|
60
|
+
echo "Testing with pnpm..."
|
61
|
+
mkdir -p "$TEST_DIR/pnpm-test"
|
62
|
+
cd "$TEST_DIR/pnpm-test"
|
63
|
+
pnpm init > /dev/null 2>&1
|
64
|
+
PNPM_OUTPUT=$(pnpm add "$SHAKAPACKER_PATH" 2>&1)
|
65
|
+
check_warnings "$PNPM_OUTPUT" "pnpm"
|
66
|
+
PNPM_RESULT=$?
|
67
|
+
else
|
68
|
+
echo ""
|
69
|
+
echo "Skipping pnpm test (not installed)"
|
70
|
+
PNPM_RESULT=0
|
71
|
+
fi
|
72
|
+
|
73
|
+
# Cleanup
|
74
|
+
rm -rf "$TEST_DIR"
|
75
|
+
|
76
|
+
# Summary
|
77
|
+
echo ""
|
78
|
+
echo "===== Test Summary ====="
|
79
|
+
if [ $NPM_RESULT -eq 0 ] && [ $YARN_RESULT -eq 0 ] && [ $PNPM_RESULT -eq 0 ]; then
|
80
|
+
echo -e "${GREEN}All tests passed! No peer dependency warnings detected.${NC}"
|
81
|
+
exit 0
|
82
|
+
else
|
83
|
+
echo -e "${RED}Some tests failed. Peer dependency warnings were detected.${NC}"
|
84
|
+
exit 1
|
85
|
+
fi
|
@@ -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
|
+
})
|
@@ -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
|
+
})
|