shakapacker 9.3.0.beta.7 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -109
- data/Gemfile.lock +1 -1
- data/README.md +53 -2
- data/docs/configuration.md +28 -0
- data/docs/rspack_migration_guide.md +238 -2
- data/docs/troubleshooting.md +21 -21
- data/eslint.config.fast.js +8 -0
- data/eslint.config.js +47 -10
- data/knip.ts +8 -1
- data/lib/install/config/shakapacker.yml +6 -6
- data/lib/shakapacker/configuration.rb +227 -4
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/doctor.rb +4 -4
- data/lib/shakapacker/instance.rb +85 -1
- data/lib/shakapacker/manifest.rb +85 -11
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker.rb +143 -3
- data/lib/tasks/shakapacker/doctor.rake +1 -1
- data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
- data/package/config.ts +0 -1
- data/package/configExporter/buildValidator.ts +53 -29
- data/package/configExporter/cli.ts +81 -56
- data/package/configExporter/configFile.ts +33 -26
- data/package/configExporter/types.ts +64 -0
- data/package/configExporter/yamlSerializer.ts +118 -43
- data/package/dev_server.ts +2 -1
- data/package/env.ts +1 -1
- data/package/environments/base.ts +4 -4
- data/package/environments/development.ts +7 -6
- data/package/environments/production.ts +6 -7
- data/package/environments/test.ts +2 -1
- data/package/index.ts +28 -4
- data/package/loaders.d.ts +2 -2
- data/package/optimization/webpack.ts +29 -31
- data/package/rspack/index.ts +2 -1
- data/package/rules/file.ts +1 -0
- data/package/rules/jscommon.ts +1 -0
- data/package/utils/helpers.ts +0 -1
- data/package/utils/pathValidation.ts +68 -7
- data/package/utils/requireOrError.ts +10 -2
- data/package/utils/typeGuards.ts +43 -46
- data/package/webpack-types.d.ts +2 -2
- data/package/webpackDevServerConfig.ts +1 -0
- data/package.json +2 -3
- data/test/package/configExporter/cli.test.js +440 -0
- data/test/package/configExporter/types.test.js +163 -0
- data/test/package/configExporter.test.js +264 -0
- data/test/package/yamlSerializer.test.js +204 -0
- data/test/typescript/pathValidation.test.js +44 -0
- data/test/typescript/requireOrError.test.js +49 -0
- data/yarn.lock +0 -32
- metadata +11 -5
- data/.eslintrc.fast.js +0 -40
- data/.eslintrc.js +0 -84
data/package/rules/file.ts
CHANGED
data/package/rules/jscommon.ts
CHANGED
data/package/utils/helpers.ts
CHANGED
|
@@ -59,7 +59,6 @@ const loaderMatches = <T = unknown>(
|
|
|
59
59
|
|
|
60
60
|
const packageFullVersion = (packageName: string): string => {
|
|
61
61
|
try {
|
|
62
|
-
// eslint-disable-next-line import/no-dynamic-require
|
|
63
62
|
const packageJsonPath = require.resolve(`${packageName}/package.json`)
|
|
64
63
|
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
65
64
|
const packageJson = require(packageJsonPath) as { version: string }
|
|
@@ -28,26 +28,87 @@ export function isPathTraversalSafe(inputPath: string): boolean {
|
|
|
28
28
|
/**
|
|
29
29
|
* Resolves and validates a path within a base directory
|
|
30
30
|
* Prevents directory traversal attacks by ensuring the resolved path
|
|
31
|
-
* stays within the base directory
|
|
31
|
+
* stays within the base directory.
|
|
32
|
+
* Also resolves symlinks to prevent symlink-based path traversal attacks.
|
|
33
|
+
*
|
|
34
|
+
* @param basePath - The base directory to validate against
|
|
35
|
+
* @param userPath - The user-provided path to validate
|
|
36
|
+
* @param resolveSymlinks - Whether to resolve symlinks (default: true for security)
|
|
37
|
+
* @returns The validated absolute path
|
|
38
|
+
* @throws Error if path is outside base directory
|
|
32
39
|
*/
|
|
33
|
-
export function safeResolvePath(
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
export function safeResolvePath(
|
|
41
|
+
basePath: string,
|
|
42
|
+
userPath: string,
|
|
43
|
+
resolveSymlinks = true
|
|
44
|
+
): string {
|
|
45
|
+
// Resolve the base path through symlinks if enabled
|
|
46
|
+
let normalizedBase: string
|
|
47
|
+
try {
|
|
48
|
+
normalizedBase = resolveSymlinks
|
|
49
|
+
? fs.realpathSync(basePath)
|
|
50
|
+
: path.resolve(basePath)
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
// If basePath doesn't exist (ENOENT), fall back to path.resolve
|
|
53
|
+
// Rethrow other errors (e.g., permission issues) as they indicate real problems
|
|
54
|
+
const nodeError = error as NodeJS.ErrnoException
|
|
55
|
+
if (nodeError?.code === "ENOENT") {
|
|
56
|
+
normalizedBase = path.resolve(basePath)
|
|
57
|
+
} else {
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For paths that may not exist yet, validate the parent directory
|
|
63
|
+
const absolutePath = path.resolve(basePath, userPath)
|
|
64
|
+
const parentDir = path.dirname(absolutePath)
|
|
65
|
+
const fileName = path.basename(absolutePath)
|
|
66
|
+
|
|
67
|
+
// Resolve parent directory through symlinks if it exists and symlink resolution is enabled
|
|
68
|
+
let resolvedParent: string
|
|
69
|
+
try {
|
|
70
|
+
resolvedParent = resolveSymlinks
|
|
71
|
+
? fs.realpathSync(parentDir)
|
|
72
|
+
: path.resolve(parentDir)
|
|
73
|
+
} catch (error: unknown) {
|
|
74
|
+
// If parent doesn't exist (ENOENT), validate the absolute path as-is
|
|
75
|
+
// Rethrow other errors (e.g., permission issues) as they indicate real problems
|
|
76
|
+
const nodeError = error as NodeJS.ErrnoException
|
|
77
|
+
if (nodeError?.code === "ENOENT") {
|
|
78
|
+
if (
|
|
79
|
+
!absolutePath.startsWith(normalizedBase + path.sep) &&
|
|
80
|
+
absolutePath !== normalizedBase
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
|
|
84
|
+
`Requested path would resolve outside of allowed directory.\n` +
|
|
85
|
+
`Base: ${normalizedBase}\n` +
|
|
86
|
+
`Attempted: ${userPath}\n` +
|
|
87
|
+
`Resolved to: ${absolutePath}`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return absolutePath
|
|
91
|
+
}
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
36
94
|
|
|
37
|
-
//
|
|
38
|
-
const resolved = path.resolve(
|
|
95
|
+
// Reconstruct the full path with the resolved (symlink-free) parent
|
|
96
|
+
const resolved = path.resolve(resolvedParent, fileName)
|
|
39
97
|
|
|
40
98
|
// Ensure the resolved path is within the base directory
|
|
41
99
|
if (
|
|
42
100
|
!resolved.startsWith(normalizedBase + path.sep) &&
|
|
43
101
|
resolved !== normalizedBase
|
|
44
102
|
) {
|
|
103
|
+
const symlinkNote = resolveSymlinks
|
|
104
|
+
? ` (symlink-resolved from ${userPath})`
|
|
105
|
+
: ""
|
|
45
106
|
throw new Error(
|
|
46
107
|
`[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
|
|
47
108
|
`Requested path would resolve outside of allowed directory.\n` +
|
|
48
109
|
`Base: ${normalizedBase}\n` +
|
|
49
110
|
`Attempted: ${userPath}\n` +
|
|
50
|
-
`Resolved to: ${resolved}`
|
|
111
|
+
`Resolved to: ${resolved}${symlinkNote}`
|
|
51
112
|
)
|
|
52
113
|
}
|
|
53
114
|
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
/* eslint import/no-dynamic-require: 0 */
|
|
3
3
|
const config = require("../config")
|
|
4
4
|
|
|
5
|
+
interface ErrorWithCause extends Error {
|
|
6
|
+
cause?: unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
const requireOrError = (moduleName: string): any => {
|
|
6
10
|
try {
|
|
7
11
|
return require(moduleName)
|
|
8
|
-
} catch (
|
|
9
|
-
|
|
12
|
+
} catch (originalError: unknown) {
|
|
13
|
+
const error: ErrorWithCause = new Error(
|
|
10
14
|
`[SHAKAPACKER]: ${moduleName} is required for ${config.assets_bundler} but is not installed. View Shakapacker's documented dependencies at https://github.com/shakacode/shakapacker/tree/main/docs/peer-dependencies.md`
|
|
11
15
|
)
|
|
16
|
+
// Add the original error as the cause for better debugging (ES2022+)
|
|
17
|
+
// Using custom interface since target is ES2020 but runtime supports it
|
|
18
|
+
error.cause = originalError
|
|
19
|
+
throw error
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
|
data/package/utils/typeGuards.ts
CHANGED
|
@@ -66,6 +66,38 @@ export function clearValidationCache(): void {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Type guard to validate DevServerConfig object at runtime
|
|
71
|
+
* In production, performs minimal validation for performance
|
|
72
|
+
*/
|
|
73
|
+
export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
|
|
74
|
+
if (typeof obj !== "object" || obj === null) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// In production, skip deep validation unless explicitly enabled
|
|
79
|
+
if (!shouldValidate()) {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const config = obj as Record<string, unknown>
|
|
84
|
+
|
|
85
|
+
// All fields are optional, just check types if present
|
|
86
|
+
if (
|
|
87
|
+
config.hmr !== undefined &&
|
|
88
|
+
typeof config.hmr !== "boolean" &&
|
|
89
|
+
config.hmr !== "only"
|
|
90
|
+
) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.port !== undefined && !validatePort(config.port)) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
/**
|
|
70
102
|
* Type guard to validate Config object at runtime
|
|
71
103
|
* In production, caches results for performance unless SHAKAPACKER_STRICT_VALIDATION is set
|
|
@@ -79,7 +111,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
// Check cache with TTL
|
|
82
|
-
const cached = validatedConfigs.get(obj
|
|
114
|
+
const cached = validatedConfigs.get(obj)
|
|
83
115
|
if (cached && Date.now() - cached.timestamp < getCacheTTL()) {
|
|
84
116
|
if (debugCache) {
|
|
85
117
|
console.log(
|
|
@@ -104,7 +136,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
104
136
|
for (const field of requiredStringFields) {
|
|
105
137
|
if (typeof config[field] !== "string") {
|
|
106
138
|
// Cache negative result
|
|
107
|
-
validatedConfigs.set(obj
|
|
139
|
+
validatedConfigs.set(obj, {
|
|
108
140
|
result: false,
|
|
109
141
|
timestamp: Date.now()
|
|
110
142
|
})
|
|
@@ -112,14 +144,11 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
112
144
|
}
|
|
113
145
|
// SECURITY: Path traversal validation ALWAYS runs (not subject to shouldValidate)
|
|
114
146
|
// This ensures paths are safe regardless of environment or validation mode
|
|
115
|
-
if (
|
|
116
|
-
field.includes("path") &&
|
|
117
|
-
!isPathTraversalSafe(config[field] as string)
|
|
118
|
-
) {
|
|
147
|
+
if (field.includes("path") && !isPathTraversalSafe(config[field])) {
|
|
119
148
|
console.warn(
|
|
120
149
|
`[SHAKAPACKER SECURITY] Invalid path in ${field}: ${config[field]}`
|
|
121
150
|
)
|
|
122
|
-
validatedConfigs.set(obj
|
|
151
|
+
validatedConfigs.set(obj, {
|
|
123
152
|
result: false,
|
|
124
153
|
timestamp: Date.now()
|
|
125
154
|
})
|
|
@@ -142,7 +171,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
142
171
|
for (const field of requiredBooleanFields) {
|
|
143
172
|
if (typeof config[field] !== "boolean") {
|
|
144
173
|
// Cache negative result
|
|
145
|
-
validatedConfigs.set(obj
|
|
174
|
+
validatedConfigs.set(obj, {
|
|
146
175
|
result: false,
|
|
147
176
|
timestamp: Date.now()
|
|
148
177
|
})
|
|
@@ -153,7 +182,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
153
182
|
// Check arrays
|
|
154
183
|
if (!Array.isArray(config.additional_paths)) {
|
|
155
184
|
// Cache negative result
|
|
156
|
-
validatedConfigs.set(obj
|
|
185
|
+
validatedConfigs.set(obj, {
|
|
157
186
|
result: false,
|
|
158
187
|
timestamp: Date.now()
|
|
159
188
|
})
|
|
@@ -167,7 +196,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
167
196
|
console.warn(
|
|
168
197
|
`[SHAKAPACKER SECURITY] Invalid additional_path: ${additionalPath}`
|
|
169
198
|
)
|
|
170
|
-
validatedConfigs.set(obj
|
|
199
|
+
validatedConfigs.set(obj, {
|
|
171
200
|
result: false,
|
|
172
201
|
timestamp: Date.now()
|
|
173
202
|
})
|
|
@@ -179,7 +208,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
179
208
|
// Security checks above still run regardless of this flag
|
|
180
209
|
if (!shouldValidate()) {
|
|
181
210
|
// Cache positive result - basic structure and security validated
|
|
182
|
-
validatedConfigs.set(obj
|
|
211
|
+
validatedConfigs.set(obj, { result: true, timestamp: Date.now() })
|
|
183
212
|
return true
|
|
184
213
|
}
|
|
185
214
|
|
|
@@ -189,7 +218,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
189
218
|
!isValidDevServerConfig(config.dev_server)
|
|
190
219
|
) {
|
|
191
220
|
// Cache negative result
|
|
192
|
-
validatedConfigs.set(obj
|
|
221
|
+
validatedConfigs.set(obj, {
|
|
193
222
|
result: false,
|
|
194
223
|
timestamp: Date.now()
|
|
195
224
|
})
|
|
@@ -203,7 +232,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
203
232
|
typeof integrity.cross_origin !== "string"
|
|
204
233
|
) {
|
|
205
234
|
// Cache negative result
|
|
206
|
-
validatedConfigs.set(obj
|
|
235
|
+
validatedConfigs.set(obj, {
|
|
207
236
|
result: false,
|
|
208
237
|
timestamp: Date.now()
|
|
209
238
|
})
|
|
@@ -212,39 +241,7 @@ export function isValidConfig(obj: unknown): obj is Config {
|
|
|
212
241
|
}
|
|
213
242
|
|
|
214
243
|
// Cache positive result
|
|
215
|
-
validatedConfigs.set(obj
|
|
216
|
-
|
|
217
|
-
return true
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Type guard to validate DevServerConfig object at runtime
|
|
222
|
-
* In production, performs minimal validation for performance
|
|
223
|
-
*/
|
|
224
|
-
export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
|
|
225
|
-
if (typeof obj !== "object" || obj === null) {
|
|
226
|
-
return false
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// In production, skip deep validation unless explicitly enabled
|
|
230
|
-
if (!shouldValidate()) {
|
|
231
|
-
return true
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const config = obj as Record<string, unknown>
|
|
235
|
-
|
|
236
|
-
// All fields are optional, just check types if present
|
|
237
|
-
if (
|
|
238
|
-
config.hmr !== undefined &&
|
|
239
|
-
typeof config.hmr !== "boolean" &&
|
|
240
|
-
config.hmr !== "only"
|
|
241
|
-
) {
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (config.port !== undefined && !validatePort(config.port)) {
|
|
246
|
-
return false
|
|
247
|
-
}
|
|
244
|
+
validatedConfigs.set(obj, { result: true, timestamp: Date.now() })
|
|
248
245
|
|
|
249
246
|
return true
|
|
250
247
|
}
|
data/package/webpack-types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ts-
|
|
1
|
+
// @ts-expect-error: webpack is an optional peer dependency (using type-only import)
|
|
2
2
|
import type { Configuration, RuleSetRule, RuleSetUseItem } from "webpack"
|
|
3
3
|
|
|
4
4
|
export interface ShakapackerWebpackConfig extends Configuration {
|
|
@@ -13,7 +13,7 @@ export interface ShakapackerRule extends RuleSetRule {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface ShakapackerLoaderOptions {
|
|
16
|
-
[key: string]:
|
|
16
|
+
[key: string]: unknown
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface ShakapackerLoader {
|
data/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakapacker",
|
|
3
|
-
"version": "9.3.0
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "Use webpack to manage app-like JavaScript modules in Rails",
|
|
5
5
|
"homepage": "https://github.com/shakacode/shakapacker",
|
|
6
6
|
"bugs": {
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"eslint-plugin-import": "^2.32.0",
|
|
75
75
|
"eslint-plugin-jest": "^29.0.1",
|
|
76
76
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
77
|
-
"eslint-plugin-prettier": "^5.5.4",
|
|
78
77
|
"eslint-plugin-react": "^7.37.5",
|
|
79
78
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
80
79
|
"husky": "^9.1.7",
|
|
@@ -109,7 +108,7 @@
|
|
|
109
108
|
"babel-loader": "^8.2.4 || ^9.0.0 || ^10.0.0",
|
|
110
109
|
"compression-webpack-plugin": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
111
110
|
"css-loader": "^6.8.1 || ^7.0.0",
|
|
112
|
-
"esbuild": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0",
|
|
111
|
+
"esbuild": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0",
|
|
113
112
|
"esbuild-loader": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
114
113
|
"mini-css-extract-plugin": "^2.0.0",
|
|
115
114
|
"rspack-manifest-plugin": "^5.0.0",
|