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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/generator.yml +6 -0
  5. data/.github/workflows/test-bundlers.yml +9 -9
  6. data/.gitignore +0 -1
  7. data/.npmignore +55 -0
  8. data/CONTRIBUTING.md +64 -0
  9. data/Gemfile.lock +1 -1
  10. data/README.md +81 -0
  11. data/docs/optional-peer-dependencies.md +198 -0
  12. data/docs/transpiler-migration.md +191 -0
  13. data/docs/typescript-migration.md +378 -0
  14. data/docs/v9_upgrade.md +65 -1
  15. data/lib/install/template.rb +54 -7
  16. data/lib/shakapacker/doctor.rb +1 -2
  17. data/lib/shakapacker/version.rb +1 -1
  18. data/package/.npmignore +4 -0
  19. data/package/config.ts +23 -10
  20. data/package/env.ts +15 -2
  21. data/package/environments/base.ts +2 -1
  22. data/package/environments/{development.js → development.ts} +30 -8
  23. data/package/environments/{production.js → production.ts} +18 -4
  24. data/package/environments/test.ts +53 -0
  25. data/package/environments/types.ts +90 -0
  26. data/package/index.ts +2 -1
  27. data/package/loaders.d.ts +1 -0
  28. data/package/types/README.md +87 -0
  29. data/package/types/index.ts +60 -0
  30. data/package/utils/errorCodes.ts +219 -0
  31. data/package/utils/errorHelpers.ts +68 -2
  32. data/package/utils/pathValidation.ts +139 -0
  33. data/package/utils/typeGuards.ts +161 -47
  34. data/package/webpack-types.d.ts +1 -0
  35. data/package.json +111 -5
  36. data/scripts/remove-use-strict.js +45 -0
  37. data/test/package/transpiler-defaults.test.js +127 -0
  38. data/test/peer-dependencies.sh +85 -0
  39. data/test/scripts/remove-use-strict.test.js +125 -0
  40. data/test/typescript/build.test.js +3 -2
  41. data/test/typescript/environments.test.js +107 -0
  42. data/test/typescript/pathValidation.test.js +142 -0
  43. data/test/typescript/securityValidation.test.js +182 -0
  44. metadata +28 -6
  45. data/package/environments/base.js +0 -116
  46. 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 is newer than TS file (has been compiled)
27
+ // Verify JS file contains CommonJS exports (has been compiled)
28
28
  const jsContent = readFileSync(jsPath, "utf8")
29
- expect(jsContent).toContain("use strict")
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
+ })