shakapacker 8.4.0 → 9.0.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.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.eslintrc.fast.js +40 -0
  4. data/.eslintrc.js +48 -0
  5. data/.github/STATUS.md +1 -0
  6. data/.github/workflows/claude-code-review.yml +54 -0
  7. data/.github/workflows/claude.yml +50 -0
  8. data/.github/workflows/dummy.yml +8 -4
  9. data/.github/workflows/generator.yml +17 -14
  10. data/.github/workflows/node.yml +23 -1
  11. data/.github/workflows/ruby.yml +11 -0
  12. data/.github/workflows/test-bundlers.yml +170 -0
  13. data/.gitignore +17 -0
  14. data/.husky/pre-commit +2 -0
  15. data/.npmignore +56 -0
  16. data/.prettierignore +3 -0
  17. data/.rubocop.yml +1 -0
  18. data/.yalcignore +26 -0
  19. data/CHANGELOG.md +156 -18
  20. data/CLAUDE.md +29 -0
  21. data/CONTRIBUTING.md +138 -20
  22. data/Gemfile.lock +3 -3
  23. data/README.md +130 -5
  24. data/Rakefile +39 -4
  25. data/TODO.md +50 -0
  26. data/TODO_v9.md +87 -0
  27. data/conductor-setup.sh +70 -0
  28. data/conductor.json +7 -0
  29. data/docs/cdn_setup.md +379 -0
  30. data/docs/css-modules-export-mode.md +512 -0
  31. data/docs/deployment.md +10 -1
  32. data/docs/optional-peer-dependencies.md +198 -0
  33. data/docs/peer-dependencies.md +60 -0
  34. data/docs/rspack.md +190 -0
  35. data/docs/rspack_migration_guide.md +202 -0
  36. data/docs/transpiler-migration.md +188 -0
  37. data/docs/transpiler-performance.md +179 -0
  38. data/docs/troubleshooting.md +5 -0
  39. data/docs/typescript-migration.md +378 -0
  40. data/docs/typescript.md +99 -0
  41. data/docs/using_esbuild_loader.md +3 -3
  42. data/docs/using_swc_loader.md +5 -3
  43. data/docs/v6_upgrade.md +10 -0
  44. data/docs/v9_upgrade.md +413 -0
  45. data/lib/install/bin/shakapacker +3 -5
  46. data/lib/install/config/rspack/rspack.config.js +6 -0
  47. data/lib/install/config/rspack/rspack.config.ts +7 -0
  48. data/lib/install/config/shakapacker.yml +12 -2
  49. data/lib/install/config/webpack/webpack.config.ts +7 -0
  50. data/lib/install/package.json +38 -0
  51. data/lib/install/template.rb +194 -44
  52. data/lib/shakapacker/configuration.rb +141 -0
  53. data/lib/shakapacker/dev_server_runner.rb +25 -5
  54. data/lib/shakapacker/doctor.rb +844 -0
  55. data/lib/shakapacker/manifest.rb +4 -2
  56. data/lib/shakapacker/rspack_runner.rb +19 -0
  57. data/lib/shakapacker/runner.rb +144 -4
  58. data/lib/shakapacker/swc_migrator.rb +376 -0
  59. data/lib/shakapacker/utils/manager.rb +2 -0
  60. data/lib/shakapacker/version.rb +1 -1
  61. data/lib/shakapacker/version_checker.rb +1 -1
  62. data/lib/shakapacker/webpack_runner.rb +4 -42
  63. data/lib/shakapacker.rb +2 -1
  64. data/lib/tasks/shakapacker/doctor.rake +8 -0
  65. data/lib/tasks/shakapacker/install.rake +12 -2
  66. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  67. data/lib/tasks/shakapacker.rake +1 -0
  68. data/package/.npmignore +4 -0
  69. data/package/babel/preset.ts +56 -0
  70. data/package/config.ts +175 -0
  71. data/package/{dev_server.js → dev_server.ts} +8 -5
  72. data/package/env.ts +92 -0
  73. data/package/environments/base.ts +138 -0
  74. data/package/environments/development.ts +90 -0
  75. data/package/environments/production.ts +80 -0
  76. data/package/environments/test.ts +53 -0
  77. data/package/environments/types.ts +90 -0
  78. data/package/esbuild/index.ts +42 -0
  79. data/package/index.d.ts +3 -97
  80. data/package/index.ts +52 -0
  81. data/package/loaders.d.ts +28 -0
  82. data/package/optimization/rspack.ts +36 -0
  83. data/package/optimization/webpack.ts +57 -0
  84. data/package/plugins/rspack.ts +103 -0
  85. data/package/plugins/webpack.ts +62 -0
  86. data/package/rspack/index.ts +64 -0
  87. data/package/rules/{babel.js → babel.ts} +2 -2
  88. data/package/rules/{coffee.js → coffee.ts} +1 -1
  89. data/package/rules/css.ts +3 -0
  90. data/package/rules/{erb.js → erb.ts} +1 -1
  91. data/package/rules/esbuild.ts +10 -0
  92. data/package/rules/file.ts +40 -0
  93. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  94. data/package/rules/{less.js → less.ts} +4 -4
  95. data/package/rules/raw.ts +25 -0
  96. data/package/rules/rspack.ts +176 -0
  97. data/package/rules/{sass.js → sass.ts} +7 -3
  98. data/package/rules/{stylus.js → stylus.ts} +4 -8
  99. data/package/rules/swc.ts +10 -0
  100. data/package/rules/{index.js → webpack.ts} +1 -1
  101. data/package/swc/index.ts +54 -0
  102. data/package/types/README.md +87 -0
  103. data/package/types/index.ts +60 -0
  104. data/package/types.ts +108 -0
  105. data/package/utils/configPath.ts +6 -0
  106. data/package/utils/debug.ts +49 -0
  107. data/package/utils/defaultConfigPath.ts +4 -0
  108. data/package/utils/errorCodes.ts +219 -0
  109. data/package/utils/errorHelpers.ts +143 -0
  110. data/package/utils/getStyleRule.ts +64 -0
  111. data/package/utils/helpers.ts +85 -0
  112. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  113. data/package/utils/pathValidation.ts +139 -0
  114. data/package/utils/requireOrError.ts +15 -0
  115. data/package/utils/snakeToCamelCase.ts +5 -0
  116. data/package/utils/typeGuards.ts +342 -0
  117. data/package/utils/validateDependencies.ts +61 -0
  118. data/package/webpack-types.d.ts +33 -0
  119. data/package/webpackDevServerConfig.ts +117 -0
  120. data/package.json +134 -9
  121. data/scripts/remove-use-strict.js +45 -0
  122. data/scripts/type-check-no-emit.js +27 -0
  123. data/test/package/config.test.js +3 -0
  124. data/test/package/env.test.js +42 -7
  125. data/test/package/environments/base.test.js +5 -1
  126. data/test/package/rules/babel.test.js +16 -0
  127. data/test/package/rules/esbuild.test.js +1 -1
  128. data/test/package/rules/raw.test.js +40 -7
  129. data/test/package/rules/swc.test.js +1 -1
  130. data/test/package/rules/webpack.test.js +35 -0
  131. data/test/package/staging.test.js +4 -3
  132. data/test/package/transpiler-defaults.test.js +127 -0
  133. data/test/peer-dependencies.sh +85 -0
  134. data/test/scripts/remove-use-strict.test.js +125 -0
  135. data/test/typescript/build.test.js +118 -0
  136. data/test/typescript/environments.test.js +107 -0
  137. data/test/typescript/pathValidation.test.js +142 -0
  138. data/test/typescript/securityValidation.test.js +182 -0
  139. data/tools/README.md +124 -0
  140. data/tools/css-modules-v9-codemod.js +179 -0
  141. data/tsconfig.eslint.json +16 -0
  142. data/tsconfig.json +38 -0
  143. data/yarn.lock +2704 -767
  144. metadata +111 -41
  145. data/package/babel/preset.js +0 -48
  146. data/package/config.js +0 -56
  147. data/package/env.js +0 -48
  148. data/package/environments/base.js +0 -171
  149. data/package/environments/development.js +0 -13
  150. data/package/environments/production.js +0 -88
  151. data/package/environments/test.js +0 -3
  152. data/package/esbuild/index.js +0 -40
  153. data/package/index.js +0 -40
  154. data/package/rules/css.js +0 -3
  155. data/package/rules/esbuild.js +0 -10
  156. data/package/rules/file.js +0 -29
  157. data/package/rules/raw.js +0 -5
  158. data/package/rules/swc.js +0 -10
  159. data/package/swc/index.js +0 -50
  160. data/package/utils/configPath.js +0 -4
  161. data/package/utils/defaultConfigPath.js +0 -2
  162. data/package/utils/getStyleRule.js +0 -40
  163. data/package/utils/helpers.js +0 -62
  164. data/package/utils/snakeToCamelCase.js +0 -5
  165. data/package/webpackDevServerConfig.js +0 -71
  166. data/test/package/rules/index.test.js +0 -16
@@ -0,0 +1,64 @@
1
+ /* eslint global-require: 0 */
2
+ const { canProcess, moduleExists } = require("./helpers")
3
+ const { requireOrError } = require("./requireOrError")
4
+ const config = require("../config")
5
+ const inliningCss = require("./inliningCss")
6
+
7
+ interface StyleRule {
8
+ test: RegExp
9
+ use: any[]
10
+ type?: string
11
+ }
12
+
13
+ const getStyleRule = (test: RegExp, preprocessors: any[] = []): StyleRule | null => {
14
+ if (moduleExists("css-loader")) {
15
+ const tryPostcss = () =>
16
+ canProcess("postcss-loader", (loaderPath: string) => ({
17
+ loader: loaderPath,
18
+ options: { sourceMap: true }
19
+ }))
20
+
21
+ // style-loader is required when using css modules with HMR on the webpack-dev-server
22
+
23
+ const extractionPlugin =
24
+ config.assets_bundler === "rspack"
25
+ ? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
26
+ : requireOrError("mini-css-extract-plugin").loader
27
+
28
+ const use = [
29
+ inliningCss ? "style-loader" : extractionPlugin,
30
+ {
31
+ loader: require.resolve("css-loader"),
32
+ options: {
33
+ sourceMap: true,
34
+ importLoaders: 2,
35
+ modules: {
36
+ auto: true,
37
+ // v9 defaults: Use named exports with camelCase conversion
38
+ // Note: css-loader requires 'camelCaseOnly' or 'dashesOnly' when namedExport is true
39
+ // Using 'camelCase' with namedExport: true causes a build error
40
+ namedExport: true,
41
+ exportLocalsConvention: 'camelCaseOnly'
42
+ }
43
+ }
44
+ },
45
+ tryPostcss(),
46
+ ...preprocessors
47
+ ].filter(Boolean)
48
+
49
+ const result: StyleRule = {
50
+ test,
51
+ use
52
+ }
53
+
54
+ if (config.assets_bundler === "rspack") {
55
+ result.type = "javascript/auto"
56
+ }
57
+
58
+ return result
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ export = { getStyleRule }
@@ -0,0 +1,85 @@
1
+ const { isModuleNotFoundError, getErrorMessage } = require("./errorHelpers")
2
+
3
+ const isBoolean = (str: string): boolean => /^true/.test(str) || /^false/.test(str)
4
+
5
+ const ensureTrailingSlash = (path: string): string => (path.endsWith("/") ? path : `${path}/`)
6
+
7
+ const resolvedPath = (packageName: string): string | null => {
8
+ try {
9
+ return require.resolve(packageName)
10
+ } catch (error: unknown) {
11
+ if (!isModuleNotFoundError(error)) {
12
+ throw error
13
+ }
14
+ return null
15
+ }
16
+ }
17
+
18
+ const moduleExists = (packageName: string): boolean => !!resolvedPath(packageName)
19
+
20
+ const canProcess = <T = unknown>(rule: string, fn: (modulePath: string) => T): T | null => {
21
+ const modulePath = resolvedPath(rule)
22
+
23
+ if (modulePath) {
24
+ return fn(modulePath)
25
+ }
26
+
27
+ return null
28
+ }
29
+
30
+ const loaderMatches = <T = unknown>(configLoader: string, loaderToCheck: string, fn: () => T): T | null => {
31
+ if (configLoader !== loaderToCheck) {
32
+ return null
33
+ }
34
+
35
+ const loaderName = `${configLoader}-loader`
36
+
37
+ if (!moduleExists(loaderName)) {
38
+ throw new Error(
39
+ `Your Shakapacker config specified using ${configLoader}, but ${loaderName} package is not installed.\n` +
40
+ `\nTo fix this issue, run one of the following commands:\n` +
41
+ ` npm install --save-dev ${loaderName}\n` +
42
+ ` yarn add --dev ${loaderName}\n` +
43
+ `\nOr change your 'javascript_transpiler' setting in shakapacker.yml to use a different loader.`
44
+ )
45
+ }
46
+
47
+ return fn()
48
+ }
49
+
50
+ const packageFullVersion = (packageName: string): string => {
51
+ try {
52
+ // eslint-disable-next-line import/no-dynamic-require
53
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
54
+ // eslint-disable-next-line import/no-dynamic-require, global-require
55
+ const packageJson = require(packageJsonPath) as { version: string }
56
+ return packageJson.version
57
+ } catch (error: any) {
58
+ // Re-throw the error with proper code to maintain compatibility with babel preset
59
+ // The preset expects MODULE_NOT_FOUND errors to handle missing core-js gracefully
60
+ if (error.code === "MODULE_NOT_FOUND") {
61
+ throw error
62
+ }
63
+ // For other errors, warn and re-throw
64
+ console.warn(
65
+ `[SHAKAPACKER WARNING] Failed to get version for package ${packageName}: ${getErrorMessage(error)}`
66
+ )
67
+ throw error
68
+ }
69
+ }
70
+
71
+ const packageMajorVersion = (packageName: string): string => {
72
+ const match = packageFullVersion(packageName).match(/^\d+/)
73
+ return match ? match[0] : "0"
74
+ }
75
+
76
+ export {
77
+ isBoolean,
78
+ ensureTrailingSlash,
79
+ canProcess,
80
+ moduleExists,
81
+ loaderMatches,
82
+ packageFullVersion,
83
+ packageMajorVersion,
84
+ resolvedPath
85
+ }
@@ -2,7 +2,7 @@ const { runningWebpackDevServer } = require("../env")
2
2
  const devServer = require("../dev_server")
3
3
 
4
4
  // This logic is tied to lib/shakapacker/instance.rb
5
- const inliningCss =
6
- runningWebpackDevServer && devServer.hmr && devServer.inline_css !== false
5
+ const inliningCss: boolean =
6
+ runningWebpackDevServer && !!devServer.hmr && devServer.inline_css !== false
7
7
 
8
- module.exports = inliningCss
8
+ export = inliningCss
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ /* eslint global-require: 0 */
2
+ /* eslint import/no-dynamic-require: 0 */
3
+ const config = require("../config")
4
+
5
+ const requireOrError = (moduleName: string): any => {
6
+ try {
7
+ return require(moduleName)
8
+ } catch (error) {
9
+ throw new Error(
10
+ `[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
+ )
12
+ }
13
+ }
14
+
15
+ export = { requireOrError }
@@ -0,0 +1,5 @@
1
+ function snakeToCamelCase(s: string): string {
2
+ return s.replace(/(_\w)/g, (match) => match[1].toUpperCase())
3
+ }
4
+
5
+ export = snakeToCamelCase
@@ -0,0 +1,342 @@
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 = 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
+ }
44
+
45
+ // Only validate in development or when explicitly enabled
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
+ }
64
+
65
+ /**
66
+ * Type guard to validate Config object at runtime
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.
71
+ */
72
+ export function isValidConfig(obj: unknown): obj is Config {
73
+ if (typeof obj !== 'object' || obj === null) {
74
+ return false
75
+ }
76
+
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
84
+ }
85
+
86
+ const config = obj as Record<string, unknown>
87
+
88
+ // Check required string fields
89
+ const requiredStringFields = [
90
+ 'source_path',
91
+ 'source_entry_path',
92
+ 'public_root_path',
93
+ 'public_output_path',
94
+ 'cache_path',
95
+ 'javascript_transpiler'
96
+ ]
97
+
98
+ for (const field of requiredStringFields) {
99
+ if (typeof config[field] !== 'string') {
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() })
109
+ return false
110
+ }
111
+ }
112
+
113
+ // Check required boolean fields
114
+ const requiredBooleanFields = [
115
+ 'nested_entries',
116
+ 'css_extract_ignore_order_warnings',
117
+ 'webpack_compile_output',
118
+ 'shakapacker_precompile',
119
+ 'cache_manifest',
120
+ 'ensure_consistent_versioning',
121
+ 'useContentHash',
122
+ 'compile'
123
+ ]
124
+
125
+ for (const field of requiredBooleanFields) {
126
+ if (typeof config[field] !== 'boolean') {
127
+ // Cache negative result
128
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
129
+ return false
130
+ }
131
+ }
132
+
133
+ // Check arrays
134
+ if (!Array.isArray(config.additional_paths)) {
135
+ // Cache negative result
136
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
137
+ return false
138
+ }
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
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() })
162
+ return false
163
+ }
164
+
165
+ if (config.integrity !== undefined) {
166
+ const integrity = config.integrity as Record<string, unknown>
167
+ if (typeof integrity.enabled !== 'boolean' ||
168
+ typeof integrity.cross_origin !== 'string') {
169
+ // Cache negative result
170
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
171
+ return false
172
+ }
173
+ }
174
+
175
+ // Cache positive result
176
+ validatedConfigs.set(obj as object, { result: true, timestamp: Date.now() })
177
+
178
+ return true
179
+ }
180
+
181
+ /**
182
+ * Type guard to validate DevServerConfig object at runtime
183
+ * In production, performs minimal validation for performance
184
+ */
185
+ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
186
+ if (typeof obj !== 'object' || obj === null) {
187
+ return false
188
+ }
189
+
190
+ // In production, skip deep validation unless explicitly enabled
191
+ if (!shouldValidate()) {
192
+ return true
193
+ }
194
+
195
+ const config = obj as Record<string, unknown>
196
+
197
+ // All fields are optional, just check types if present
198
+ if (config.hmr !== undefined &&
199
+ typeof config.hmr !== 'boolean' &&
200
+ config.hmr !== 'only') {
201
+ return false
202
+ }
203
+
204
+ if (config.port !== undefined && !validatePort(config.port)) {
205
+ return false
206
+ }
207
+
208
+ return true
209
+ }
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
+
254
+ /**
255
+ * Type guard to validate YamlConfig structure
256
+ * In production, performs minimal validation for performance
257
+ */
258
+ export function isValidYamlConfig(obj: unknown): obj is YamlConfig {
259
+ if (typeof obj !== 'object' || obj === null) {
260
+ return false
261
+ }
262
+
263
+ // In production, skip deep validation unless explicitly enabled
264
+ if (!shouldValidate()) {
265
+ return true
266
+ }
267
+
268
+ const config = obj as Record<string, unknown>
269
+
270
+ // Each key should map to an object
271
+ for (const env of Object.keys(config)) {
272
+ if (typeof config[env] !== 'object' || config[env] === null) {
273
+ return false
274
+ }
275
+ }
276
+
277
+ return true
278
+ }
279
+
280
+ /**
281
+ * Validates partial config used for merging
282
+ * Ensures that if fields are present, they have the correct types
283
+ * In production, performs minimal validation for performance
284
+ */
285
+ export function isPartialConfig(obj: unknown): obj is Partial<Config> {
286
+ if (typeof obj !== 'object' || obj === null) {
287
+ return false
288
+ }
289
+
290
+ // In production, skip deep validation unless explicitly enabled
291
+ if (!shouldValidate()) {
292
+ return true
293
+ }
294
+
295
+ const config = obj as Record<string, unknown>
296
+
297
+ // Check string fields if present
298
+ const stringFields = [
299
+ 'source_path', 'source_entry_path', 'public_root_path',
300
+ 'public_output_path', 'cache_path', 'javascript_transpiler'
301
+ ]
302
+
303
+ for (const field of stringFields) {
304
+ if (field in config && typeof config[field] !== 'string') {
305
+ return false
306
+ }
307
+ }
308
+
309
+ // Check boolean fields if present
310
+ const booleanFields = [
311
+ 'nested_entries', 'css_extract_ignore_order_warnings',
312
+ 'webpack_compile_output', 'shakapacker_precompile',
313
+ 'cache_manifest', 'ensure_consistent_versioning'
314
+ ]
315
+
316
+ for (const field of booleanFields) {
317
+ if (field in config && typeof config[field] !== 'boolean') {
318
+ return false
319
+ }
320
+ }
321
+
322
+ // Check arrays if present
323
+ if ('additional_paths' in config && !Array.isArray(config.additional_paths)) {
324
+ return false
325
+ }
326
+
327
+ return true
328
+ }
329
+
330
+ /**
331
+ * Creates a validation error with helpful context
332
+ */
333
+ export function createConfigValidationError(
334
+ configPath: string,
335
+ environment: string,
336
+ details?: string
337
+ ): Error {
338
+ const message = `Invalid configuration in ${configPath} for environment '${environment}'`
339
+ return new Error(details ? `${message}: ${details}` : message)
340
+ }
341
+
342
+