shakapacker 8.4.0 → 9.7.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/.claude/commands/address-review.md +206 -0
- data/.claude/commands/update-changelog.md +354 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
- data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
- data/.github/STATUS.md +1 -0
- data/.github/actionlint-matcher.json +17 -0
- data/.github/workflows/claude-code-review.yml +45 -0
- data/.github/workflows/claude.yml +55 -0
- data/.github/workflows/dummy.yml +18 -5
- data/.github/workflows/eslint-validation.yml +46 -0
- data/.github/workflows/generator.yml +38 -22
- data/.github/workflows/node.yml +116 -2
- data/.github/workflows/ruby.yml +57 -15
- data/.github/workflows/test-bundlers.yml +180 -0
- data/.gitignore +27 -0
- data/.husky/pre-commit +2 -0
- data/.npmignore +56 -0
- data/.prettierignore +7 -0
- data/.rubocop.yml +2 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +487 -19
- data/CLAUDE.md +63 -0
- data/CONTRIBUTING.md +268 -21
- data/ESLINT_TECHNICAL_DEBT.md +165 -0
- data/README.md +497 -137
- data/Rakefile +44 -4
- data/TODO.md +58 -0
- data/TODO_v9.md +97 -0
- data/bin/conductor-exec +24 -0
- data/bin/shakapacker-config +11 -0
- data/conductor-setup.sh +147 -0
- data/conductor.json +9 -0
- data/docs/api-reference.md +519 -0
- data/docs/cdn_setup.md +384 -0
- data/docs/common-upgrades.md +695 -0
- data/docs/configuration.md +845 -0
- data/docs/css-modules-export-mode.md +566 -0
- data/docs/customizing_babel_config.md +16 -16
- data/docs/deployment.md +78 -7
- data/docs/developing_shakapacker.md +6 -0
- data/docs/early_hints.md +433 -0
- data/docs/early_hints_manual_api.md +454 -0
- data/docs/feature_testing.md +492 -0
- data/docs/node_package_api.md +70 -0
- data/docs/optional-peer-dependencies.md +203 -0
- data/docs/peer-dependencies.md +71 -0
- data/docs/precompile_hook.md +486 -0
- data/docs/preventing_fouc.md +132 -0
- data/docs/react.md +58 -48
- data/docs/releasing.md +288 -0
- data/docs/rspack.md +218 -0
- data/docs/rspack_migration_guide.md +862 -0
- data/docs/sprockets.md +1 -0
- data/docs/style_loader_vs_mini_css.md +12 -12
- data/docs/subresource_integrity.md +13 -7
- data/docs/transpiler-migration.md +212 -0
- data/docs/transpiler-performance.md +200 -0
- data/docs/troubleshooting.md +272 -24
- data/docs/typescript-migration.md +388 -0
- data/docs/typescript.md +103 -0
- data/docs/using_esbuild_loader.md +12 -12
- data/docs/using_swc_loader.md +121 -16
- data/docs/v6_upgrade.md +42 -19
- data/docs/v7_upgrade.md +8 -6
- data/docs/v8_upgrade.md +13 -12
- data/docs/v9_upgrade.md +616 -0
- data/eslint.config.fast.js +254 -0
- data/eslint.config.js +309 -0
- data/jest.config.js +8 -1
- data/knip.ts +61 -0
- data/lib/install/bin/shakapacker +4 -6
- data/lib/install/bin/shakapacker-config +11 -0
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/install/binstubs.rb +6 -2
- data/lib/install/config/rspack/rspack.config.js +6 -0
- data/lib/install/config/rspack/rspack.config.ts +7 -0
- data/lib/install/config/shakapacker.yml +75 -12
- data/lib/install/config/webpack/webpack.config.ts +7 -0
- data/lib/install/package.json +38 -0
- data/lib/install/template.rb +207 -45
- data/lib/shakapacker/build_config_loader.rb +147 -0
- data/lib/shakapacker/bundler_switcher.rb +415 -0
- data/lib/shakapacker/compiler.rb +87 -0
- data/lib/shakapacker/configuration.rb +475 -6
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/dev_server_runner.rb +240 -6
- data/lib/shakapacker/doctor.rb +1191 -0
- data/lib/shakapacker/env.rb +19 -3
- data/lib/shakapacker/helper.rb +411 -14
- data/lib/shakapacker/install/env.rb +33 -0
- data/lib/shakapacker/instance.rb +93 -4
- data/lib/shakapacker/manifest.rb +167 -30
- data/lib/shakapacker/railtie.rb +4 -0
- data/lib/shakapacker/rspack_runner.rb +19 -0
- data/lib/shakapacker/runner.rb +668 -9
- data/lib/shakapacker/swc_migrator.rb +384 -0
- data/lib/shakapacker/utils/manager.rb +2 -0
- data/lib/shakapacker/utils/version_syntax_converter.rb +1 -1
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/version_checker.rb +1 -1
- data/lib/shakapacker/webpack_runner.rb +4 -42
- data/lib/shakapacker.rb +159 -1
- data/lib/tasks/shakapacker/binstubs.rake +4 -2
- data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
- data/lib/tasks/shakapacker/doctor.rake +48 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +68 -0
- data/lib/tasks/shakapacker/install.rake +16 -4
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker/switch_bundler.rake +72 -0
- data/lib/tasks/shakapacker.rake +2 -0
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +59 -0
- data/package/config.ts +189 -0
- data/package/configExporter/buildValidator.ts +906 -0
- data/package/configExporter/cli.ts +1748 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/configFile.ts +663 -0
- data/package/configExporter/fileWriter.ts +112 -0
- data/package/configExporter/index.ts +15 -0
- data/package/configExporter/types.ts +159 -0
- data/package/configExporter/yamlSerializer.ts +391 -0
- data/package/dev_server.ts +27 -0
- data/package/env.ts +92 -0
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +36 -0
- data/package/environments/base.ts +147 -0
- data/package/environments/development.ts +88 -0
- data/package/environments/production.ts +82 -0
- data/package/environments/test.ts +55 -0
- data/package/environments/types.ts +98 -0
- data/package/esbuild/index.ts +40 -0
- data/package/index.d.ts +68 -93
- data/package/index.d.ts.template +72 -0
- data/package/index.ts +104 -0
- data/package/loaders.d.ts +28 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/webpack.ts +55 -0
- data/package/plugins/envFilter.ts +82 -0
- data/package/plugins/rspack.ts +119 -0
- data/package/plugins/webpack.ts +82 -0
- data/package/rspack/index.ts +91 -0
- data/package/rules/{babel.js → babel.ts} +2 -2
- data/package/rules/{coffee.js → coffee.ts} +1 -1
- data/package/rules/css.ts +3 -0
- data/package/rules/{erb.js → erb.ts} +1 -1
- data/package/rules/esbuild.ts +10 -0
- data/package/rules/file.ts +41 -0
- data/package/rules/{jscommon.js → jscommon.ts} +5 -4
- data/package/rules/{less.js → less.ts} +4 -4
- data/package/rules/raw.ts +28 -0
- data/package/rules/rspack.ts +174 -0
- data/package/rules/sass.ts +21 -0
- data/package/rules/{stylus.js → stylus.ts} +4 -8
- data/package/rules/swc.ts +10 -0
- data/package/rules/{index.js → webpack.ts} +1 -2
- data/package/swc/index.ts +54 -0
- data/package/types/README.md +90 -0
- data/package/types/index.ts +69 -0
- data/package/types.ts +105 -0
- data/package/utils/bundlerUtils.ts +232 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/debug.ts +45 -0
- data/package/utils/defaultConfigPath.ts +7 -0
- data/package/utils/ensureManifestExists.ts +17 -0
- data/package/utils/errorCodes.ts +249 -0
- data/package/utils/errorHelpers.ts +152 -0
- data/package/utils/getStyleRule.ts +75 -0
- data/package/utils/helpers.ts +99 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/pathValidation.ts +207 -0
- data/package/utils/requireOrError.ts +24 -0
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +388 -0
- data/package/utils/validateDependencies.ts +61 -0
- data/package/webpack-types.d.ts +33 -0
- data/package/webpackDevServerConfig.ts +130 -0
- data/package.json +157 -18
- data/scripts/remove-use-strict.js +44 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/shakapacker.gemspec +4 -2
- data/sig/shakapacker/commands.rbs +35 -0
- data/sig/shakapacker/compiler.rbs +65 -0
- data/sig/shakapacker/compiler_strategy.rbs +41 -0
- data/sig/shakapacker/configuration.rbs +140 -0
- data/sig/shakapacker/dev_server.rbs +56 -0
- data/sig/shakapacker/env.rbs +25 -0
- data/sig/shakapacker/helper.rbs +98 -0
- data/sig/shakapacker/instance.rbs +46 -0
- data/sig/shakapacker/manifest.rbs +69 -0
- data/sig/shakapacker/version.rbs +4 -0
- data/sig/shakapacker.rbs +66 -0
- data/test/configExporter/buildValidator.test.js +1295 -0
- data/test/configExporter/configFile.test.js +393 -0
- data/test/configExporter/integration.test.js +262 -0
- data/test/helpers.js +1 -1
- data/test/package/bundlerUtils.rspack.test.js +145 -0
- data/test/package/bundlerUtils.test.js +97 -0
- data/test/package/config.test.js +14 -0
- data/test/package/configExporter/cli.test.js +440 -0
- data/test/package/configExporter/types.test.js +163 -0
- data/test/package/configExporter.test.js +491 -0
- data/test/package/env.test.js +42 -7
- data/test/package/environments/base.test.js +14 -4
- data/test/package/helpers.test.js +2 -2
- data/test/package/plugins/envFiltering.test.js +453 -0
- data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
- data/test/package/rspack/index.test.js +293 -0
- data/test/package/rspack/optimization.test.js +86 -0
- data/test/package/rspack/plugins.test.js +185 -0
- data/test/package/rspack/rules.test.js +229 -0
- data/test/package/rules/babel.test.js +65 -38
- data/test/package/rules/esbuild.test.js +13 -4
- data/test/package/rules/file.test.js +7 -1
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +11 -6
- data/test/package/rules/sass1.test.js +8 -5
- data/test/package/rules/sass16.test.js +24 -0
- data/test/package/rules/swc.test.js +50 -39
- data/test/package/rules/webpack.test.js +35 -0
- data/test/package/staging.test.js +4 -3
- data/test/package/transpiler-defaults.test.js +169 -0
- data/test/package/utils/ensureManifestExists.test.js +51 -0
- data/test/package/yamlSerializer.test.js +204 -0
- data/test/peer-dependencies.sh +85 -0
- data/test/resolver.js +34 -3
- data/test/scripts/remove-use-strict.test.js +125 -0
- data/test/typescript/build.test.js +118 -0
- data/test/typescript/environments.test.js +107 -0
- data/test/typescript/pathValidation.test.js +186 -0
- data/test/typescript/requireOrError.test.js +49 -0
- data/test/typescript/securityValidation.test.js +182 -0
- data/tools/README.md +134 -0
- data/tools/css-modules-v9-codemod.js +179 -0
- data/tsconfig.eslint.json +9 -0
- data/tsconfig.json +38 -0
- data/yarn.lock +3202 -1097
- metadata +212 -44
- data/.eslintignore +0 -4
- data/.eslintrc.js +0 -36
- data/Gemfile.lock +0 -251
- data/package/babel/preset.js +0 -48
- data/package/config.js +0 -56
- data/package/dev_server.js +0 -23
- data/package/env.js +0 -48
- data/package/environments/base.js +0 -171
- data/package/environments/development.js +0 -13
- data/package/environments/production.js +0 -88
- data/package/environments/test.js +0 -3
- data/package/esbuild/index.js +0 -40
- data/package/index.js +0 -40
- data/package/rules/css.js +0 -3
- data/package/rules/esbuild.js +0 -10
- data/package/rules/file.js +0 -29
- data/package/rules/raw.js +0 -5
- data/package/rules/sass.js +0 -18
- data/package/rules/swc.js +0 -10
- data/package/swc/index.js +0 -50
- data/package/utils/configPath.js +0 -4
- data/package/utils/defaultConfigPath.js +0 -2
- data/package/utils/getStyleRule.js +0 -40
- data/package/utils/helpers.js +0 -62
- data/package/utils/snakeToCamelCase.js +0 -5
- data/package/webpackDevServerConfig.js +0 -71
- data/test/package/rules/index.test.js +0 -16
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
* 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
|
|
39
|
+
*/
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
// Reconstruct the full path with the resolved (symlink-free) parent
|
|
96
|
+
const resolved = path.resolve(resolvedParent, fileName)
|
|
97
|
+
|
|
98
|
+
// Ensure the resolved path is within the base directory
|
|
99
|
+
if (
|
|
100
|
+
!resolved.startsWith(normalizedBase + path.sep) &&
|
|
101
|
+
resolved !== normalizedBase
|
|
102
|
+
) {
|
|
103
|
+
const symlinkNote = resolveSymlinks
|
|
104
|
+
? ` (symlink-resolved from ${userPath})`
|
|
105
|
+
: ""
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
|
|
108
|
+
`Requested path would resolve outside of allowed directory.\n` +
|
|
109
|
+
`Base: ${normalizedBase}\n` +
|
|
110
|
+
`Attempted: ${userPath}\n` +
|
|
111
|
+
`Resolved to: ${resolved}${symlinkNote}`
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return resolved
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validates that a path exists and is accessible
|
|
120
|
+
*/
|
|
121
|
+
export function validatePathExists(filePath: string): boolean {
|
|
122
|
+
try {
|
|
123
|
+
fs.accessSync(filePath, fs.constants.R_OK)
|
|
124
|
+
return true
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates an array of paths for security issues
|
|
132
|
+
*/
|
|
133
|
+
export function validatePaths(paths: string[], basePath: string): string[] {
|
|
134
|
+
const validatedPaths: string[] = []
|
|
135
|
+
|
|
136
|
+
for (const userPath of paths) {
|
|
137
|
+
if (!isPathTraversalSafe(userPath)) {
|
|
138
|
+
console.warn(
|
|
139
|
+
`[SHAKAPACKER WARNING] Skipping potentially unsafe path: ${userPath}`
|
|
140
|
+
)
|
|
141
|
+
} else {
|
|
142
|
+
try {
|
|
143
|
+
const safePath = safeResolvePath(basePath, userPath)
|
|
144
|
+
validatedPaths.push(safePath)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`[SHAKAPACKER WARNING] Invalid path configuration: ${userPath}\n` +
|
|
148
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return validatedPaths
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sanitizes environment variable values to prevent injection
|
|
159
|
+
*/
|
|
160
|
+
export function sanitizeEnvValue(
|
|
161
|
+
value: string | undefined
|
|
162
|
+
): string | undefined {
|
|
163
|
+
if (!value) return value
|
|
164
|
+
|
|
165
|
+
// Remove control characters and null bytes
|
|
166
|
+
// Filter by character code to avoid control character regex (Biome compliance)
|
|
167
|
+
const sanitized = value
|
|
168
|
+
.split("")
|
|
169
|
+
.filter((char) => {
|
|
170
|
+
const code = char.charCodeAt(0)
|
|
171
|
+
// Keep chars with code > 31 (after control chars) and not 127 (DEL)
|
|
172
|
+
return code > 31 && code !== 127
|
|
173
|
+
})
|
|
174
|
+
.join("")
|
|
175
|
+
|
|
176
|
+
// Warn if sanitization changed the value
|
|
177
|
+
if (sanitized !== value) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[SHAKAPACKER SECURITY] Environment variable value contained control characters that were removed`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return sanitized
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validates a port number or string
|
|
188
|
+
*/
|
|
189
|
+
export function validatePort(port: unknown): boolean {
|
|
190
|
+
if (port === "auto") return true
|
|
191
|
+
|
|
192
|
+
if (typeof port === "number") {
|
|
193
|
+
return port > 0 && port <= 65535 && Number.isInteger(port)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof port === "string") {
|
|
197
|
+
// First check if the string contains only digits
|
|
198
|
+
if (!/^\d+$/.test(port)) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
// Only then parse and validate range
|
|
202
|
+
const num = parseInt(port, 10)
|
|
203
|
+
return num > 0 && num <= 65535
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint import/no-dynamic-require: 0 */
|
|
2
|
+
import type { Config } from "../types"
|
|
3
|
+
|
|
4
|
+
const config = require("../config") as Config
|
|
5
|
+
|
|
6
|
+
interface ErrorWithCause extends Error {
|
|
7
|
+
cause?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const requireOrError = (moduleName: string): unknown => {
|
|
11
|
+
try {
|
|
12
|
+
return require(moduleName)
|
|
13
|
+
} catch (originalError: unknown) {
|
|
14
|
+
const error: ErrorWithCause = new Error(
|
|
15
|
+
`[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`
|
|
16
|
+
)
|
|
17
|
+
// Add the original error as the cause for better debugging (ES2022+)
|
|
18
|
+
// Using custom interface since target is ES2020 but runtime supports it
|
|
19
|
+
error.cause = originalError
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export = { requireOrError }
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { Config, DevServerConfig, YamlConfig } from "../types"
|
|
2
|
+
import { isPathTraversalSafe, validatePort } from "./pathValidation"
|
|
3
|
+
|
|
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 =
|
|
23
|
+
process.argv.includes("--watch") || process.env.WEBPACK_WATCH === "true"
|
|
24
|
+
}
|
|
25
|
+
return cachedIsWatchMode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get cache TTL based on environment (cached)
|
|
30
|
+
*/
|
|
31
|
+
function getCacheTTL(): number {
|
|
32
|
+
if (cachedCacheTTL === null) {
|
|
33
|
+
if (process.env.SHAKAPACKER_CACHE_TTL) {
|
|
34
|
+
cachedCacheTTL = parseInt(process.env.SHAKAPACKER_CACHE_TTL, 10)
|
|
35
|
+
} else if (process.env.NODE_ENV === "production" && !isWatchMode()) {
|
|
36
|
+
cachedCacheTTL = Infinity
|
|
37
|
+
} else if (isWatchMode()) {
|
|
38
|
+
cachedCacheTTL = 5000 // 5 seconds in watch mode
|
|
39
|
+
} else {
|
|
40
|
+
cachedCacheTTL = 60000 // 1 minute in dev
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return cachedCacheTTL
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Only validate in development or when explicitly enabled
|
|
47
|
+
function shouldValidate(): boolean {
|
|
48
|
+
return (
|
|
49
|
+
process.env.NODE_ENV !== "production" ||
|
|
50
|
+
process.env.SHAKAPACKER_STRICT_VALIDATION === "true"
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Debug logging for cache operations
|
|
55
|
+
const debugCache = process.env.SHAKAPACKER_DEBUG_CACHE === "true"
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear the validation cache
|
|
59
|
+
* Useful for testing or when config files change
|
|
60
|
+
*/
|
|
61
|
+
export function clearValidationCache(): void {
|
|
62
|
+
// Reassign to a new WeakMap to clear all entries
|
|
63
|
+
validatedConfigs = new WeakMap<object, CacheEntry>()
|
|
64
|
+
if (debugCache) {
|
|
65
|
+
console.log("[SHAKAPACKER DEBUG] Validation cache cleared")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
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
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Type guard to validate Config object at runtime
|
|
103
|
+
* In production, caches results for performance unless SHAKAPACKER_STRICT_VALIDATION is set
|
|
104
|
+
*
|
|
105
|
+
* IMPORTANT: Path traversal security checks ALWAYS run regardless of environment or validation mode.
|
|
106
|
+
* This ensures application security is never compromised for performance.
|
|
107
|
+
*/
|
|
108
|
+
export function isValidConfig(obj: unknown): obj is Config {
|
|
109
|
+
if (typeof obj !== "object" || obj === null) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check cache with TTL
|
|
114
|
+
const cached = validatedConfigs.get(obj)
|
|
115
|
+
if (cached && Date.now() - cached.timestamp < getCacheTTL()) {
|
|
116
|
+
if (debugCache) {
|
|
117
|
+
console.log(
|
|
118
|
+
`[SHAKAPACKER DEBUG] Config validation cache hit (result: ${cached.result})`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
return cached.result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const config = obj as Record<string, unknown>
|
|
125
|
+
|
|
126
|
+
// Check required string fields
|
|
127
|
+
const requiredStringFields = [
|
|
128
|
+
"source_path",
|
|
129
|
+
"source_entry_path",
|
|
130
|
+
"public_root_path",
|
|
131
|
+
"public_output_path",
|
|
132
|
+
"cache_path",
|
|
133
|
+
"javascript_transpiler"
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
for (const field of requiredStringFields) {
|
|
137
|
+
if (typeof config[field] !== "string") {
|
|
138
|
+
// Cache negative result
|
|
139
|
+
validatedConfigs.set(obj, {
|
|
140
|
+
result: false,
|
|
141
|
+
timestamp: Date.now()
|
|
142
|
+
})
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
// SECURITY: Path traversal validation ALWAYS runs (not subject to shouldValidate)
|
|
146
|
+
// This ensures paths are safe regardless of environment or validation mode
|
|
147
|
+
if (field.includes("path") && !isPathTraversalSafe(config[field])) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[SHAKAPACKER SECURITY] Invalid path in ${field}: ${config[field]}`
|
|
150
|
+
)
|
|
151
|
+
validatedConfigs.set(obj, {
|
|
152
|
+
result: false,
|
|
153
|
+
timestamp: Date.now()
|
|
154
|
+
})
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check required boolean fields
|
|
160
|
+
const requiredBooleanFields = [
|
|
161
|
+
"nested_entries",
|
|
162
|
+
"css_extract_ignore_order_warnings",
|
|
163
|
+
"webpack_compile_output",
|
|
164
|
+
"shakapacker_precompile",
|
|
165
|
+
"cache_manifest",
|
|
166
|
+
"ensure_consistent_versioning",
|
|
167
|
+
"useContentHash",
|
|
168
|
+
"compile"
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for (const field of requiredBooleanFields) {
|
|
172
|
+
if (typeof config[field] !== "boolean") {
|
|
173
|
+
// Cache negative result
|
|
174
|
+
validatedConfigs.set(obj, {
|
|
175
|
+
result: false,
|
|
176
|
+
timestamp: Date.now()
|
|
177
|
+
})
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check arrays
|
|
183
|
+
if (!Array.isArray(config.additional_paths)) {
|
|
184
|
+
// Cache negative result
|
|
185
|
+
validatedConfigs.set(obj, {
|
|
186
|
+
result: false,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
})
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// SECURITY: Path traversal validation for additional_paths ALWAYS runs (not subject to shouldValidate)
|
|
193
|
+
// This critical security check ensures user-provided paths cannot escape the project directory
|
|
194
|
+
for (const additionalPath of config.additional_paths as string[]) {
|
|
195
|
+
if (!isPathTraversalSafe(additionalPath)) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`[SHAKAPACKER SECURITY] Invalid additional_path: ${additionalPath}`
|
|
198
|
+
)
|
|
199
|
+
validatedConfigs.set(obj, {
|
|
200
|
+
result: false,
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
})
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// In production, skip deep validation of optional fields unless explicitly enabled
|
|
208
|
+
// Security checks above still run regardless of this flag
|
|
209
|
+
if (!shouldValidate()) {
|
|
210
|
+
// Cache positive result - basic structure and security validated
|
|
211
|
+
validatedConfigs.set(obj, { result: true, timestamp: Date.now() })
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Deep validation of optional fields (only in development or with SHAKAPACKER_STRICT_VALIDATION=true)
|
|
216
|
+
if (
|
|
217
|
+
config.dev_server !== undefined &&
|
|
218
|
+
!isValidDevServerConfig(config.dev_server)
|
|
219
|
+
) {
|
|
220
|
+
// Cache negative result
|
|
221
|
+
validatedConfigs.set(obj, {
|
|
222
|
+
result: false,
|
|
223
|
+
timestamp: Date.now()
|
|
224
|
+
})
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (config.integrity !== undefined) {
|
|
229
|
+
const integrity = config.integrity as Record<string, unknown>
|
|
230
|
+
if (
|
|
231
|
+
typeof integrity.enabled !== "boolean" ||
|
|
232
|
+
typeof integrity.cross_origin !== "string"
|
|
233
|
+
) {
|
|
234
|
+
// Cache negative result
|
|
235
|
+
validatedConfigs.set(obj, {
|
|
236
|
+
result: false,
|
|
237
|
+
timestamp: Date.now()
|
|
238
|
+
})
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Cache positive result
|
|
244
|
+
validatedConfigs.set(obj, { result: true, timestamp: Date.now() })
|
|
245
|
+
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Type guard to validate Rspack plugin instance
|
|
251
|
+
* Checks if an object looks like a valid Rspack plugin
|
|
252
|
+
*/
|
|
253
|
+
export function isValidRspackPlugin(obj: unknown): boolean {
|
|
254
|
+
if (typeof obj !== "object" || obj === null) {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const plugin = obj as Record<string, unknown>
|
|
259
|
+
|
|
260
|
+
// Check for common plugin patterns
|
|
261
|
+
// Most rspack plugins should have an apply method
|
|
262
|
+
if (typeof plugin.apply === "function") {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for constructor name pattern (e.g., HtmlRspackPlugin)
|
|
267
|
+
const constructorName = plugin.constructor?.name || ""
|
|
268
|
+
if (
|
|
269
|
+
constructorName.includes("Plugin") ||
|
|
270
|
+
constructorName.includes("Rspack")
|
|
271
|
+
) {
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for common plugin properties
|
|
276
|
+
if ("name" in plugin && typeof plugin.name === "string") {
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Type guard to validate array of Rspack plugins
|
|
285
|
+
* Ensures all items in the array are valid plugin instances
|
|
286
|
+
*/
|
|
287
|
+
export function isValidRspackPluginArray(arr: unknown): boolean {
|
|
288
|
+
if (!Array.isArray(arr)) {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return arr.every((item) => isValidRspackPlugin(item))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type guard to validate YamlConfig structure
|
|
297
|
+
* In production, performs minimal validation for performance
|
|
298
|
+
*/
|
|
299
|
+
export function isValidYamlConfig(obj: unknown): obj is YamlConfig {
|
|
300
|
+
if (typeof obj !== "object" || obj === null) {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// In production, skip deep validation unless explicitly enabled
|
|
305
|
+
if (!shouldValidate()) {
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const config = obj as Record<string, unknown>
|
|
310
|
+
|
|
311
|
+
// Each key should map to an object
|
|
312
|
+
for (const env of Object.keys(config)) {
|
|
313
|
+
if (typeof config[env] !== "object" || config[env] === null) {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Validates partial config used for merging
|
|
323
|
+
* Ensures that if fields are present, they have the correct types
|
|
324
|
+
* In production, performs minimal validation for performance
|
|
325
|
+
*/
|
|
326
|
+
export function isPartialConfig(obj: unknown): obj is Partial<Config> {
|
|
327
|
+
if (typeof obj !== "object" || obj === null) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// In production, skip deep validation unless explicitly enabled
|
|
332
|
+
if (!shouldValidate()) {
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const config = obj as Record<string, unknown>
|
|
337
|
+
|
|
338
|
+
// Check string fields if present
|
|
339
|
+
const stringFields = [
|
|
340
|
+
"source_path",
|
|
341
|
+
"source_entry_path",
|
|
342
|
+
"public_root_path",
|
|
343
|
+
"public_output_path",
|
|
344
|
+
"cache_path",
|
|
345
|
+
"javascript_transpiler"
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
for (const field of stringFields) {
|
|
349
|
+
if (field in config && typeof config[field] !== "string") {
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check boolean fields if present
|
|
355
|
+
const booleanFields = [
|
|
356
|
+
"nested_entries",
|
|
357
|
+
"css_extract_ignore_order_warnings",
|
|
358
|
+
"webpack_compile_output",
|
|
359
|
+
"shakapacker_precompile",
|
|
360
|
+
"cache_manifest",
|
|
361
|
+
"ensure_consistent_versioning"
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
for (const field of booleanFields) {
|
|
365
|
+
if (field in config && typeof config[field] !== "boolean") {
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check arrays if present
|
|
371
|
+
if ("additional_paths" in config && !Array.isArray(config.additional_paths)) {
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Creates a validation error with helpful context
|
|
380
|
+
*/
|
|
381
|
+
export function createConfigValidationError(
|
|
382
|
+
configPath: string,
|
|
383
|
+
environment: string,
|
|
384
|
+
details?: string
|
|
385
|
+
): Error {
|
|
386
|
+
const message = `Invalid configuration in ${configPath} for environment '${environment}'`
|
|
387
|
+
return new Error(details ? `${message}: ${details}` : message)
|
|
388
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that required dependencies are installed for the selected bundler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { moduleExists } = require("./helpers")
|
|
6
|
+
const { error } = require("./debug")
|
|
7
|
+
|
|
8
|
+
const validateRspackDependencies = (): void => {
|
|
9
|
+
const requiredDependencies = ["@rspack/core", "rspack-manifest-plugin"]
|
|
10
|
+
|
|
11
|
+
const missingDependencies = requiredDependencies.filter(
|
|
12
|
+
(dep) => !moduleExists(dep)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if (missingDependencies.length > 0) {
|
|
16
|
+
error(
|
|
17
|
+
`Missing required dependencies for RSpack:\n${missingDependencies
|
|
18
|
+
.map((dep) => ` - ${dep}`)
|
|
19
|
+
.join(
|
|
20
|
+
"\n"
|
|
21
|
+
)}\n\nPlease install them with:\n npm install ${missingDependencies.join(
|
|
22
|
+
" "
|
|
23
|
+
)}`
|
|
24
|
+
)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Missing RSpack dependencies: ${missingDependencies.join(", ")}`
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const validateWebpackDependencies = (): void => {
|
|
32
|
+
const requiredDependencies = [
|
|
33
|
+
"webpack",
|
|
34
|
+
"webpack-cli",
|
|
35
|
+
"webpack-assets-manifest"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const missingDependencies = requiredDependencies.filter(
|
|
39
|
+
(dep) => !moduleExists(dep)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (missingDependencies.length > 0) {
|
|
43
|
+
error(
|
|
44
|
+
`Missing required dependencies for Webpack:\n${missingDependencies
|
|
45
|
+
.map((dep) => ` - ${dep}`)
|
|
46
|
+
.join(
|
|
47
|
+
"\n"
|
|
48
|
+
)}\n\nPlease install them with:\n npm install ${missingDependencies.join(
|
|
49
|
+
" "
|
|
50
|
+
)}`
|
|
51
|
+
)
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Missing Webpack dependencies: ${missingDependencies.join(", ")}`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export = {
|
|
59
|
+
validateRspackDependencies,
|
|
60
|
+
validateWebpackDependencies
|
|
61
|
+
}
|