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
@@ -2,6 +2,8 @@
|
|
2
2
|
* Error handling utilities for consistent error management
|
3
3
|
*/
|
4
4
|
|
5
|
+
import { ErrorCode, ShakapackerError } from './errorCodes'
|
6
|
+
|
5
7
|
/**
|
6
8
|
* Checks if an error is a file not found error (ENOENT)
|
7
9
|
*/
|
@@ -33,10 +35,31 @@ export function createFileOperationError(
|
|
33
35
|
operation: 'read' | 'write' | 'delete',
|
34
36
|
filePath: string,
|
35
37
|
details?: string
|
38
|
+
): ShakapackerError {
|
39
|
+
const errorCode = operation === 'read'
|
40
|
+
? ErrorCode.FILE_READ_ERROR
|
41
|
+
: operation === 'write'
|
42
|
+
? ErrorCode.FILE_WRITE_ERROR
|
43
|
+
: ErrorCode.FILE_NOT_FOUND
|
44
|
+
|
45
|
+
return new ShakapackerError(errorCode, {
|
46
|
+
path: filePath,
|
47
|
+
operation,
|
48
|
+
details
|
49
|
+
})
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Creates a consistent error message for file operations (backward compatibility)
|
54
|
+
*/
|
55
|
+
export function createFileOperationErrorLegacy(
|
56
|
+
operation: 'read' | 'write' | 'delete',
|
57
|
+
filePath: string,
|
58
|
+
details?: string
|
36
59
|
): Error {
|
37
60
|
const baseMessage = `Failed to ${operation} file at path '${filePath}'`
|
38
61
|
const errorDetails = details ? ` - ${details}` : ''
|
39
|
-
const suggestion = operation === 'read'
|
62
|
+
const suggestion = operation === 'read'
|
40
63
|
? ' (check if file exists and permissions are correct)'
|
41
64
|
: operation === 'write'
|
42
65
|
? ' (check write permissions and disk space)'
|
@@ -70,8 +93,51 @@ export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
70
93
|
return (
|
71
94
|
error instanceof Error &&
|
72
95
|
'code' in error &&
|
73
|
-
typeof (error as
|
96
|
+
typeof (error as NodeJS.ErrnoException).code === 'string'
|
74
97
|
)
|
75
98
|
}
|
76
99
|
|
100
|
+
/**
|
101
|
+
* Creates a configuration validation error
|
102
|
+
*/
|
103
|
+
export function createConfigValidationErrorWithCode(
|
104
|
+
configPath: string,
|
105
|
+
environment: string,
|
106
|
+
reason: string
|
107
|
+
): ShakapackerError {
|
108
|
+
return new ShakapackerError(ErrorCode.CONFIG_VALIDATION_FAILED, {
|
109
|
+
path: configPath,
|
110
|
+
environment,
|
111
|
+
reason
|
112
|
+
})
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Creates a module not found error
|
117
|
+
*/
|
118
|
+
export function createModuleNotFoundError(moduleName: string, details?: string): ShakapackerError {
|
119
|
+
return new ShakapackerError(ErrorCode.MODULE_NOT_FOUND, {
|
120
|
+
module: moduleName,
|
121
|
+
details
|
122
|
+
})
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Creates a path traversal security error
|
127
|
+
*/
|
128
|
+
export function createPathTraversalError(path: string): ShakapackerError {
|
129
|
+
return new ShakapackerError(ErrorCode.SECURITY_PATH_TRAVERSAL, {
|
130
|
+
path
|
131
|
+
})
|
132
|
+
}
|
133
|
+
|
134
|
+
/**
|
135
|
+
* Creates a port validation error
|
136
|
+
*/
|
137
|
+
export function createPortValidationError(port: unknown): ShakapackerError {
|
138
|
+
return new ShakapackerError(ErrorCode.DEVSERVER_PORT_INVALID, {
|
139
|
+
port: String(port)
|
140
|
+
})
|
141
|
+
}
|
142
|
+
|
77
143
|
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import * as path from "path"
|
2
|
+
import * as fs from "fs"
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Security utilities for validating and sanitizing file paths
|
6
|
+
*/
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Validates a path doesn't contain traversal patterns
|
10
|
+
*/
|
11
|
+
export function isPathTraversalSafe(inputPath: string): boolean {
|
12
|
+
// Check for common traversal patterns
|
13
|
+
// Null byte short-circuit (avoid regex with control chars)
|
14
|
+
if (inputPath.includes("\0")) return false
|
15
|
+
|
16
|
+
const dangerousPatterns = [
|
17
|
+
/\.\.[\/\\]/, // ../ or ..\
|
18
|
+
/^\//, // POSIX absolute
|
19
|
+
/^[A-Za-z]:[\/\\]/, // Windows absolute (C:\ or C:/)
|
20
|
+
/^\\\\/, // Windows UNC (\\server\share)
|
21
|
+
/~[\/\\]/, // Home directory expansion
|
22
|
+
/%2e%2e/i, // URL encoded traversal
|
23
|
+
]
|
24
|
+
|
25
|
+
return !dangerousPatterns.some(pattern => pattern.test(inputPath))
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Resolves and validates a path within a base directory
|
30
|
+
* Prevents directory traversal attacks by ensuring the resolved path
|
31
|
+
* stays within the base directory
|
32
|
+
*/
|
33
|
+
export function safeResolvePath(basePath: string, userPath: string): string {
|
34
|
+
// Normalize the base path
|
35
|
+
const normalizedBase = path.resolve(basePath)
|
36
|
+
|
37
|
+
// Resolve the user path relative to base
|
38
|
+
const resolved = path.resolve(normalizedBase, userPath)
|
39
|
+
|
40
|
+
// Ensure the resolved path is within the base directory
|
41
|
+
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
|
42
|
+
throw new Error(
|
43
|
+
`[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
|
44
|
+
`Requested path would resolve outside of allowed directory.\n` +
|
45
|
+
`Base: ${normalizedBase}\n` +
|
46
|
+
`Attempted: ${userPath}\n` +
|
47
|
+
`Resolved to: ${resolved}`
|
48
|
+
)
|
49
|
+
}
|
50
|
+
|
51
|
+
return resolved
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Validates that a path exists and is accessible
|
56
|
+
*/
|
57
|
+
export function validatePathExists(filePath: string): boolean {
|
58
|
+
try {
|
59
|
+
fs.accessSync(filePath, fs.constants.R_OK)
|
60
|
+
return true
|
61
|
+
} catch {
|
62
|
+
return false
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Validates an array of paths for security issues
|
68
|
+
*/
|
69
|
+
export function validatePaths(paths: string[], basePath: string): string[] {
|
70
|
+
const validatedPaths: string[] = []
|
71
|
+
|
72
|
+
for (const userPath of paths) {
|
73
|
+
if (!isPathTraversalSafe(userPath)) {
|
74
|
+
console.warn(
|
75
|
+
`[SHAKAPACKER WARNING] Skipping potentially unsafe path: ${userPath}`
|
76
|
+
)
|
77
|
+
continue
|
78
|
+
}
|
79
|
+
|
80
|
+
try {
|
81
|
+
const safePath = safeResolvePath(basePath, userPath)
|
82
|
+
validatedPaths.push(safePath)
|
83
|
+
} catch (error) {
|
84
|
+
console.warn(
|
85
|
+
`[SHAKAPACKER WARNING] Invalid path configuration: ${userPath}\n` +
|
86
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
87
|
+
)
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
return validatedPaths
|
92
|
+
}
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Sanitizes environment variable values to prevent injection
|
96
|
+
*/
|
97
|
+
export function sanitizeEnvValue(value: string | undefined): string | undefined {
|
98
|
+
if (!value) return value
|
99
|
+
|
100
|
+
// Remove control characters and null bytes
|
101
|
+
// Filter by character code to avoid control character regex (Biome compliance)
|
102
|
+
const sanitized = value.split('').filter(char => {
|
103
|
+
const code = char.charCodeAt(0)
|
104
|
+
// Keep chars with code > 31 (after control chars) and not 127 (DEL)
|
105
|
+
return code > 31 && code !== 127
|
106
|
+
}).join('')
|
107
|
+
|
108
|
+
// Warn if sanitization changed the value
|
109
|
+
if (sanitized !== value) {
|
110
|
+
console.warn(
|
111
|
+
`[SHAKAPACKER SECURITY] Environment variable value contained control characters that were removed`
|
112
|
+
)
|
113
|
+
}
|
114
|
+
|
115
|
+
return sanitized
|
116
|
+
}
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Validates a port number or string
|
120
|
+
*/
|
121
|
+
export function validatePort(port: unknown): boolean {
|
122
|
+
if (port === 'auto') return true
|
123
|
+
|
124
|
+
if (typeof port === 'number') {
|
125
|
+
return port > 0 && port <= 65535 && Number.isInteger(port)
|
126
|
+
}
|
127
|
+
|
128
|
+
if (typeof port === 'string') {
|
129
|
+
// First check if the string contains only digits
|
130
|
+
if (!/^\d+$/.test(port)) {
|
131
|
+
return false
|
132
|
+
}
|
133
|
+
// Only then parse and validate range
|
134
|
+
const num = parseInt(port, 10)
|
135
|
+
return num > 0 && num <= 65535
|
136
|
+
}
|
137
|
+
|
138
|
+
return false
|
139
|
+
}
|
data/package/utils/typeGuards.ts
CHANGED
@@ -1,43 +1,111 @@
|
|
1
1
|
import { Config, DevServerConfig, YamlConfig } from "../types"
|
2
|
+
import { isPathTraversalSafe, validatePort } from "./pathValidation"
|
2
3
|
|
3
|
-
// Cache for validated configs
|
4
|
-
|
4
|
+
// Cache for validated configs with TTL
|
5
|
+
interface CacheEntry {
|
6
|
+
result: boolean
|
7
|
+
timestamp: number
|
8
|
+
configHash?: string
|
9
|
+
}
|
10
|
+
|
11
|
+
let validatedConfigs = new WeakMap<object, CacheEntry>()
|
12
|
+
|
13
|
+
// Cache computed values to avoid repeated checks
|
14
|
+
let cachedIsWatchMode: boolean | null = null
|
15
|
+
let cachedCacheTTL: number | null = null
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Detect if running in watch mode (cached)
|
19
|
+
*/
|
20
|
+
function isWatchMode(): boolean {
|
21
|
+
if (cachedIsWatchMode === null) {
|
22
|
+
cachedIsWatchMode = process.argv.includes('--watch') || process.env.WEBPACK_WATCH === 'true'
|
23
|
+
}
|
24
|
+
return cachedIsWatchMode
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Get cache TTL based on environment (cached)
|
29
|
+
*/
|
30
|
+
function getCacheTTL(): number {
|
31
|
+
if (cachedCacheTTL === null) {
|
32
|
+
if (process.env.SHAKAPACKER_CACHE_TTL) {
|
33
|
+
cachedCacheTTL = parseInt(process.env.SHAKAPACKER_CACHE_TTL, 10)
|
34
|
+
} else if (process.env.NODE_ENV === 'production' && !isWatchMode()) {
|
35
|
+
cachedCacheTTL = Infinity
|
36
|
+
} else if (isWatchMode()) {
|
37
|
+
cachedCacheTTL = 5000 // 5 seconds in watch mode
|
38
|
+
} else {
|
39
|
+
cachedCacheTTL = 60000 // 1 minute in dev
|
40
|
+
}
|
41
|
+
}
|
42
|
+
return cachedCacheTTL
|
43
|
+
}
|
5
44
|
|
6
45
|
// Only validate in development or when explicitly enabled
|
7
|
-
|
46
|
+
function shouldValidate(): boolean {
|
47
|
+
return process.env.NODE_ENV !== 'production' || process.env.SHAKAPACKER_STRICT_VALIDATION === 'true'
|
48
|
+
}
|
49
|
+
|
50
|
+
// Debug logging for cache operations
|
51
|
+
const debugCache = process.env.SHAKAPACKER_DEBUG_CACHE === 'true'
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Clear the validation cache
|
55
|
+
* Useful for testing or when config files change
|
56
|
+
*/
|
57
|
+
export function clearValidationCache(): void {
|
58
|
+
// Reassign to a new WeakMap to clear all entries
|
59
|
+
validatedConfigs = new WeakMap<object, CacheEntry>()
|
60
|
+
if (debugCache) {
|
61
|
+
console.log('[SHAKAPACKER DEBUG] Validation cache cleared')
|
62
|
+
}
|
63
|
+
}
|
8
64
|
|
9
65
|
/**
|
10
66
|
* Type guard to validate Config object at runtime
|
11
67
|
* In production, caches results for performance unless SHAKAPACKER_STRICT_VALIDATION is set
|
68
|
+
*
|
69
|
+
* IMPORTANT: Path traversal security checks ALWAYS run regardless of environment or validation mode.
|
70
|
+
* This ensures application security is never compromised for performance.
|
12
71
|
*/
|
13
72
|
export function isValidConfig(obj: unknown): obj is Config {
|
14
73
|
if (typeof obj !== 'object' || obj === null) {
|
15
74
|
return false
|
16
75
|
}
|
17
76
|
|
18
|
-
//
|
19
|
-
|
20
|
-
|
77
|
+
// Check cache with TTL
|
78
|
+
const cached = validatedConfigs.get(obj as object)
|
79
|
+
if (cached && (Date.now() - cached.timestamp) < getCacheTTL()) {
|
80
|
+
if (debugCache) {
|
81
|
+
console.log(`[SHAKAPACKER DEBUG] Config validation cache hit (result: ${cached.result})`)
|
82
|
+
}
|
83
|
+
return cached.result
|
21
84
|
}
|
22
85
|
|
23
86
|
const config = obj as Record<string, unknown>
|
24
|
-
|
87
|
+
|
25
88
|
// Check required string fields
|
26
89
|
const requiredStringFields = [
|
27
90
|
'source_path',
|
28
|
-
'source_entry_path',
|
91
|
+
'source_entry_path',
|
29
92
|
'public_root_path',
|
30
93
|
'public_output_path',
|
31
94
|
'cache_path',
|
32
95
|
'javascript_transpiler'
|
33
96
|
]
|
34
|
-
|
97
|
+
|
35
98
|
for (const field of requiredStringFields) {
|
36
99
|
if (typeof config[field] !== 'string') {
|
37
|
-
// Cache negative result
|
38
|
-
|
39
|
-
|
40
|
-
|
100
|
+
// Cache negative result
|
101
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
102
|
+
return false
|
103
|
+
}
|
104
|
+
// SECURITY: Path traversal validation ALWAYS runs (not subject to shouldValidate)
|
105
|
+
// This ensures paths are safe regardless of environment or validation mode
|
106
|
+
if (field.includes('path') && !isPathTraversalSafe(config[field] as string)) {
|
107
|
+
console.warn(`[SHAKAPACKER SECURITY] Invalid path in ${field}: ${config[field]}`)
|
108
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
41
109
|
return false
|
42
110
|
}
|
43
111
|
}
|
@@ -56,52 +124,58 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
56
124
|
|
57
125
|
for (const field of requiredBooleanFields) {
|
58
126
|
if (typeof config[field] !== 'boolean') {
|
59
|
-
// Cache negative result
|
60
|
-
|
61
|
-
validatedConfigs.set(obj as object, false)
|
62
|
-
}
|
127
|
+
// Cache negative result
|
128
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
63
129
|
return false
|
64
130
|
}
|
65
131
|
}
|
66
132
|
|
67
133
|
// Check arrays
|
68
134
|
if (!Array.isArray(config.additional_paths)) {
|
69
|
-
// Cache negative result
|
70
|
-
|
71
|
-
validatedConfigs.set(obj as object, false)
|
72
|
-
}
|
135
|
+
// Cache negative result
|
136
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
73
137
|
return false
|
74
138
|
}
|
75
|
-
|
76
|
-
//
|
77
|
-
|
78
|
-
|
79
|
-
if (!
|
80
|
-
|
139
|
+
|
140
|
+
// SECURITY: Path traversal validation for additional_paths ALWAYS runs (not subject to shouldValidate)
|
141
|
+
// This critical security check ensures user-provided paths cannot escape the project directory
|
142
|
+
for (const additionalPath of config.additional_paths as string[]) {
|
143
|
+
if (!isPathTraversalSafe(additionalPath)) {
|
144
|
+
console.warn(`[SHAKAPACKER SECURITY] Invalid additional_path: ${additionalPath}`)
|
145
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
146
|
+
return false
|
81
147
|
}
|
148
|
+
}
|
149
|
+
|
150
|
+
// In production, skip deep validation of optional fields unless explicitly enabled
|
151
|
+
// Security checks above still run regardless of this flag
|
152
|
+
if (!shouldValidate()) {
|
153
|
+
// Cache positive result - basic structure and security validated
|
154
|
+
validatedConfigs.set(obj as object, { result: true, timestamp: Date.now() })
|
155
|
+
return true
|
156
|
+
}
|
157
|
+
|
158
|
+
// Deep validation of optional fields (only in development or with SHAKAPACKER_STRICT_VALIDATION=true)
|
159
|
+
if (config.dev_server !== undefined && !isValidDevServerConfig(config.dev_server)) {
|
160
|
+
// Cache negative result
|
161
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
82
162
|
return false
|
83
163
|
}
|
84
|
-
|
164
|
+
|
85
165
|
if (config.integrity !== undefined) {
|
86
166
|
const integrity = config.integrity as Record<string, unknown>
|
87
|
-
if (typeof integrity.enabled !== 'boolean' ||
|
167
|
+
if (typeof integrity.enabled !== 'boolean' ||
|
88
168
|
typeof integrity.cross_origin !== 'string') {
|
89
|
-
// Cache negative result
|
90
|
-
|
91
|
-
validatedConfigs.set(obj as object, false)
|
92
|
-
}
|
169
|
+
// Cache negative result
|
170
|
+
validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
|
93
171
|
return false
|
94
172
|
}
|
95
173
|
}
|
96
174
|
|
97
|
-
|
98
|
-
|
99
|
-
// Cache result in production
|
100
|
-
if (!shouldValidate) {
|
101
|
-
validatedConfigs.set(obj as object, result)
|
102
|
-
}
|
175
|
+
// Cache positive result
|
176
|
+
validatedConfigs.set(obj as object, { result: true, timestamp: Date.now() })
|
103
177
|
|
104
|
-
return
|
178
|
+
return true
|
105
179
|
}
|
106
180
|
|
107
181
|
/**
|
@@ -114,7 +188,7 @@ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
|
|
114
188
|
}
|
115
189
|
|
116
190
|
// In production, skip deep validation unless explicitly enabled
|
117
|
-
if (!shouldValidate) {
|
191
|
+
if (!shouldValidate()) {
|
118
192
|
return true
|
119
193
|
}
|
120
194
|
|
@@ -127,16 +201,56 @@ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
|
|
127
201
|
return false
|
128
202
|
}
|
129
203
|
|
130
|
-
if (config.port !== undefined &&
|
131
|
-
typeof config.port !== 'number' &&
|
132
|
-
typeof config.port !== 'string' &&
|
133
|
-
config.port !== 'auto') {
|
204
|
+
if (config.port !== undefined && !validatePort(config.port)) {
|
134
205
|
return false
|
135
206
|
}
|
136
207
|
|
137
208
|
return true
|
138
209
|
}
|
139
210
|
|
211
|
+
/**
|
212
|
+
* Type guard to validate Rspack plugin instance
|
213
|
+
* Checks if an object looks like a valid Rspack plugin
|
214
|
+
*/
|
215
|
+
export function isValidRspackPlugin(obj: unknown): boolean {
|
216
|
+
if (typeof obj !== 'object' || obj === null) {
|
217
|
+
return false
|
218
|
+
}
|
219
|
+
|
220
|
+
const plugin = obj as Record<string, unknown>
|
221
|
+
|
222
|
+
// Check for common plugin patterns
|
223
|
+
// Most rspack plugins should have an apply method
|
224
|
+
if (typeof plugin.apply === 'function') {
|
225
|
+
return true
|
226
|
+
}
|
227
|
+
|
228
|
+
// Check for constructor name pattern (e.g., HtmlRspackPlugin)
|
229
|
+
const constructorName = plugin.constructor?.name || ''
|
230
|
+
if (constructorName.includes('Plugin') || constructorName.includes('Rspack')) {
|
231
|
+
return true
|
232
|
+
}
|
233
|
+
|
234
|
+
// Check for common plugin properties
|
235
|
+
if ('name' in plugin && typeof plugin.name === 'string') {
|
236
|
+
return true
|
237
|
+
}
|
238
|
+
|
239
|
+
return false
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Type guard to validate array of Rspack plugins
|
244
|
+
* Ensures all items in the array are valid plugin instances
|
245
|
+
*/
|
246
|
+
export function isValidRspackPluginArray(arr: unknown): boolean {
|
247
|
+
if (!Array.isArray(arr)) {
|
248
|
+
return false
|
249
|
+
}
|
250
|
+
|
251
|
+
return arr.every(item => isValidRspackPlugin(item))
|
252
|
+
}
|
253
|
+
|
140
254
|
/**
|
141
255
|
* Type guard to validate YamlConfig structure
|
142
256
|
* In production, performs minimal validation for performance
|
@@ -147,7 +261,7 @@ export function isValidYamlConfig(obj: unknown): obj is YamlConfig {
|
|
147
261
|
}
|
148
262
|
|
149
263
|
// In production, skip deep validation unless explicitly enabled
|
150
|
-
if (!shouldValidate) {
|
264
|
+
if (!shouldValidate()) {
|
151
265
|
return true
|
152
266
|
}
|
153
267
|
|
@@ -174,7 +288,7 @@ export function isPartialConfig(obj: unknown): obj is Partial<Config> {
|
|
174
288
|
}
|
175
289
|
|
176
290
|
// In production, skip deep validation unless explicitly enabled
|
177
|
-
if (!shouldValidate) {
|
291
|
+
if (!shouldValidate()) {
|
178
292
|
return true
|
179
293
|
}
|
180
294
|
|
data/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "shakapacker",
|
3
|
-
"version": "9.0.0-beta.
|
3
|
+
"version": "9.0.0-beta.8",
|
4
4
|
"description": "Use webpack to manage app-like JavaScript modules in Rails",
|
5
5
|
"homepage": "https://github.com/shakacode/shakapacker",
|
6
6
|
"bugs": {
|
@@ -16,6 +16,7 @@
|
|
16
16
|
"types": "package/index.d.ts",
|
17
17
|
"exports": {
|
18
18
|
".": "./package/index.js",
|
19
|
+
"./types": "./package/types/index.js",
|
19
20
|
"./webpack": "./package/webpack/index.js",
|
20
21
|
"./rspack": "./package/rspack/index.js",
|
21
22
|
"./swc": "./package/swc/index.js",
|
@@ -30,11 +31,13 @@
|
|
30
31
|
],
|
31
32
|
"scripts": {
|
32
33
|
"clean:ts": "find package -name '*.ts' -not -name '*.d.ts' | sed 's/\\.ts$//' | xargs -I {} rm -f {}.js {}.d.ts {}.d.ts.map {}.js.map",
|
33
|
-
"build": "tsc",
|
34
|
+
"build": "tsc && node scripts/remove-use-strict.js && yarn prettier --write 'package/**/*.js'",
|
34
35
|
"build:types": "tsc",
|
35
36
|
"lint": "eslint .",
|
37
|
+
"lint:fast": "eslint . --ext .js,.jsx,.ts,.tsx --config .eslintrc.fast.js",
|
36
38
|
"test": "jest",
|
37
|
-
"type-check": "tsc --noEmit"
|
39
|
+
"type-check": "tsc --noEmit",
|
40
|
+
"prepublishOnly": "yarn build && yarn type-check"
|
38
41
|
},
|
39
42
|
"dependencies": {
|
40
43
|
"js-yaml": "^4.1.0",
|
@@ -44,12 +47,15 @@
|
|
44
47
|
"devDependencies": {
|
45
48
|
"@rspack/cli": "^1.4.11",
|
46
49
|
"@rspack/core": "^1.4.11",
|
50
|
+
"@types/babel__core": "^7.20.5",
|
47
51
|
"@types/js-yaml": "^4.0.9",
|
48
52
|
"@types/node": "^24.5.2",
|
49
53
|
"@types/path-complete-extname": "^1.0.3",
|
50
54
|
"@types/webpack": "^5.28.5",
|
51
55
|
"@types/webpack-dev-server": "^4.7.2",
|
52
56
|
"@types/webpack-merge": "^5.0.0",
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
58
|
+
"@typescript-eslint/parser": "^8.45.0",
|
53
59
|
"babel-loader": "^8.2.4",
|
54
60
|
"compression-webpack-plugin": "^9.0.0",
|
55
61
|
"css-loader": "^7.1.2",
|
@@ -63,7 +69,9 @@
|
|
63
69
|
"eslint-plugin-prettier": "^5.2.6",
|
64
70
|
"eslint-plugin-react": "^7.37.5",
|
65
71
|
"eslint-plugin-react-hooks": "^4.6.0",
|
72
|
+
"husky": "^9.1.7",
|
66
73
|
"jest": "^29.7.0",
|
74
|
+
"lint-staged": "^15.2.10",
|
67
75
|
"memory-fs": "^0.5.0",
|
68
76
|
"mini-css-extract-plugin": "^2.9.4",
|
69
77
|
"prettier": "^3.2.5",
|
@@ -81,8 +89,8 @@
|
|
81
89
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
82
90
|
"@babel/preset-env": "^7.16.11",
|
83
91
|
"@babel/runtime": "^7.17.9",
|
84
|
-
"@rspack/core": "^1.0.0",
|
85
92
|
"@rspack/cli": "^1.0.0",
|
93
|
+
"@rspack/core": "^1.0.0",
|
86
94
|
"@rspack/plugin-react-refresh": "^1.0.0",
|
87
95
|
"@types/babel__core": "^7.0.0",
|
88
96
|
"@types/webpack": "^5.0.0",
|
@@ -181,6 +189,20 @@
|
|
181
189
|
}
|
182
190
|
},
|
183
191
|
"packageManager": "yarn@1.22.22",
|
192
|
+
"lint-staged": {
|
193
|
+
"*.{js,jsx}": [
|
194
|
+
"eslint --fix",
|
195
|
+
"prettier --write"
|
196
|
+
],
|
197
|
+
"*.{ts,tsx}": [
|
198
|
+
"eslint --fix",
|
199
|
+
"prettier --write",
|
200
|
+
"node scripts/type-check-no-emit.js"
|
201
|
+
],
|
202
|
+
"*.{json,yml,yaml,md}": [
|
203
|
+
"prettier --write"
|
204
|
+
]
|
205
|
+
},
|
184
206
|
"engines": {
|
185
207
|
"node": ">= 14",
|
186
208
|
"yarn": ">=1 <5"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
const fs = require("fs")
|
3
|
+
const path = require("path")
|
4
|
+
|
5
|
+
// Recursively find all .js files in a directory
|
6
|
+
function findJsFiles(dir) {
|
7
|
+
const files = []
|
8
|
+
const items = fs.readdirSync(dir, { withFileTypes: true })
|
9
|
+
|
10
|
+
items.forEach((item) => {
|
11
|
+
const fullPath = path.join(dir, item.name)
|
12
|
+
if (item.isDirectory()) {
|
13
|
+
files.push(...findJsFiles(fullPath))
|
14
|
+
} else if (item.isFile() && item.name.endsWith(".js")) {
|
15
|
+
files.push(fullPath)
|
16
|
+
}
|
17
|
+
})
|
18
|
+
|
19
|
+
return files
|
20
|
+
}
|
21
|
+
|
22
|
+
// Find all .js files in package directory
|
23
|
+
const files = findJsFiles("package")
|
24
|
+
|
25
|
+
files.forEach((file) => {
|
26
|
+
let content = fs.readFileSync(file, "utf8")
|
27
|
+
|
28
|
+
// Remove "use strict" directive with various quote styles and formatting
|
29
|
+
// Handles: optional whitespace, single/double/unicode quotes, optional semicolon,
|
30
|
+
// and any trailing whitespace/newline sequences
|
31
|
+
content = content.replace(
|
32
|
+
/^\s*["'\u2018\u2019\u201C\u201D]use\s+strict["'\u2018\u2019\u201C\u201D]\s*;?\s*[\r\n]*/,
|
33
|
+
""
|
34
|
+
)
|
35
|
+
|
36
|
+
// Ensure file ends with exactly one newline
|
37
|
+
if (!content.endsWith("\n")) {
|
38
|
+
content += "\n"
|
39
|
+
}
|
40
|
+
|
41
|
+
fs.writeFileSync(file, content, "utf8")
|
42
|
+
})
|
43
|
+
|
44
|
+
// eslint-disable-next-line no-console
|
45
|
+
console.log(`Removed "use strict" from ${files.length} files`)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Type-check script for lint-staged
|
5
|
+
*
|
6
|
+
* This script runs TypeScript type checking without emitting files.
|
7
|
+
* It ignores any arguments passed by lint-staged to ensure tsc uses
|
8
|
+
* the project's tsconfig.json rather than trying to compile individual files.
|
9
|
+
*
|
10
|
+
* Without this wrapper, lint-staged would pass staged file paths as arguments
|
11
|
+
* to tsc, causing it to ignore tsconfig.json and fail type checking.
|
12
|
+
*/
|
13
|
+
|
14
|
+
const { execSync } = require("child_process")
|
15
|
+
|
16
|
+
try {
|
17
|
+
// Run tsc with no arguments (ignoring any passed by lint-staged)
|
18
|
+
// This ensures it uses tsconfig.json properly
|
19
|
+
execSync("npx tsc --noEmit", {
|
20
|
+
stdio: "inherit",
|
21
|
+
cwd: process.cwd()
|
22
|
+
})
|
23
|
+
process.exit(0)
|
24
|
+
} catch (error) {
|
25
|
+
// Type checking failed
|
26
|
+
process.exit(1)
|
27
|
+
}
|