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.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/address-review.md +206 -0
  3. data/.claude/commands/update-changelog.md +354 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
  6. data/.github/STATUS.md +1 -0
  7. data/.github/actionlint-matcher.json +17 -0
  8. data/.github/workflows/claude-code-review.yml +45 -0
  9. data/.github/workflows/claude.yml +55 -0
  10. data/.github/workflows/dummy.yml +18 -5
  11. data/.github/workflows/eslint-validation.yml +46 -0
  12. data/.github/workflows/generator.yml +38 -22
  13. data/.github/workflows/node.yml +116 -2
  14. data/.github/workflows/ruby.yml +57 -15
  15. data/.github/workflows/test-bundlers.yml +180 -0
  16. data/.gitignore +27 -0
  17. data/.husky/pre-commit +2 -0
  18. data/.npmignore +56 -0
  19. data/.prettierignore +7 -0
  20. data/.rubocop.yml +2 -0
  21. data/.yalcignore +26 -0
  22. data/CHANGELOG.md +487 -19
  23. data/CLAUDE.md +63 -0
  24. data/CONTRIBUTING.md +268 -21
  25. data/ESLINT_TECHNICAL_DEBT.md +165 -0
  26. data/README.md +497 -137
  27. data/Rakefile +44 -4
  28. data/TODO.md +58 -0
  29. data/TODO_v9.md +97 -0
  30. data/bin/conductor-exec +24 -0
  31. data/bin/shakapacker-config +11 -0
  32. data/conductor-setup.sh +147 -0
  33. data/conductor.json +9 -0
  34. data/docs/api-reference.md +519 -0
  35. data/docs/cdn_setup.md +384 -0
  36. data/docs/common-upgrades.md +695 -0
  37. data/docs/configuration.md +845 -0
  38. data/docs/css-modules-export-mode.md +566 -0
  39. data/docs/customizing_babel_config.md +16 -16
  40. data/docs/deployment.md +78 -7
  41. data/docs/developing_shakapacker.md +6 -0
  42. data/docs/early_hints.md +433 -0
  43. data/docs/early_hints_manual_api.md +454 -0
  44. data/docs/feature_testing.md +492 -0
  45. data/docs/node_package_api.md +70 -0
  46. data/docs/optional-peer-dependencies.md +203 -0
  47. data/docs/peer-dependencies.md +71 -0
  48. data/docs/precompile_hook.md +486 -0
  49. data/docs/preventing_fouc.md +132 -0
  50. data/docs/react.md +58 -48
  51. data/docs/releasing.md +288 -0
  52. data/docs/rspack.md +218 -0
  53. data/docs/rspack_migration_guide.md +862 -0
  54. data/docs/sprockets.md +1 -0
  55. data/docs/style_loader_vs_mini_css.md +12 -12
  56. data/docs/subresource_integrity.md +13 -7
  57. data/docs/transpiler-migration.md +212 -0
  58. data/docs/transpiler-performance.md +200 -0
  59. data/docs/troubleshooting.md +272 -24
  60. data/docs/typescript-migration.md +388 -0
  61. data/docs/typescript.md +103 -0
  62. data/docs/using_esbuild_loader.md +12 -12
  63. data/docs/using_swc_loader.md +121 -16
  64. data/docs/v6_upgrade.md +42 -19
  65. data/docs/v7_upgrade.md +8 -6
  66. data/docs/v8_upgrade.md +13 -12
  67. data/docs/v9_upgrade.md +616 -0
  68. data/eslint.config.fast.js +254 -0
  69. data/eslint.config.js +309 -0
  70. data/jest.config.js +8 -1
  71. data/knip.ts +61 -0
  72. data/lib/install/bin/shakapacker +4 -6
  73. data/lib/install/bin/shakapacker-config +11 -0
  74. data/lib/install/bin/shakapacker-dev-server +1 -1
  75. data/lib/install/binstubs.rb +6 -2
  76. data/lib/install/config/rspack/rspack.config.js +6 -0
  77. data/lib/install/config/rspack/rspack.config.ts +7 -0
  78. data/lib/install/config/shakapacker.yml +75 -12
  79. data/lib/install/config/webpack/webpack.config.ts +7 -0
  80. data/lib/install/package.json +38 -0
  81. data/lib/install/template.rb +207 -45
  82. data/lib/shakapacker/build_config_loader.rb +147 -0
  83. data/lib/shakapacker/bundler_switcher.rb +415 -0
  84. data/lib/shakapacker/compiler.rb +87 -0
  85. data/lib/shakapacker/configuration.rb +475 -6
  86. data/lib/shakapacker/dev_server.rb +88 -1
  87. data/lib/shakapacker/dev_server_runner.rb +240 -6
  88. data/lib/shakapacker/doctor.rb +1191 -0
  89. data/lib/shakapacker/env.rb +19 -3
  90. data/lib/shakapacker/helper.rb +411 -14
  91. data/lib/shakapacker/install/env.rb +33 -0
  92. data/lib/shakapacker/instance.rb +93 -4
  93. data/lib/shakapacker/manifest.rb +167 -30
  94. data/lib/shakapacker/railtie.rb +4 -0
  95. data/lib/shakapacker/rspack_runner.rb +19 -0
  96. data/lib/shakapacker/runner.rb +668 -9
  97. data/lib/shakapacker/swc_migrator.rb +384 -0
  98. data/lib/shakapacker/utils/manager.rb +2 -0
  99. data/lib/shakapacker/utils/version_syntax_converter.rb +1 -1
  100. data/lib/shakapacker/version.rb +1 -1
  101. data/lib/shakapacker/version_checker.rb +1 -1
  102. data/lib/shakapacker/webpack_runner.rb +4 -42
  103. data/lib/shakapacker.rb +159 -1
  104. data/lib/tasks/shakapacker/binstubs.rake +4 -2
  105. data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
  106. data/lib/tasks/shakapacker/doctor.rake +48 -0
  107. data/lib/tasks/shakapacker/export_bundler_config.rake +68 -0
  108. data/lib/tasks/shakapacker/install.rake +16 -4
  109. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  110. data/lib/tasks/shakapacker/switch_bundler.rake +72 -0
  111. data/lib/tasks/shakapacker.rake +2 -0
  112. data/package/.npmignore +4 -0
  113. data/package/babel/preset.ts +59 -0
  114. data/package/config.ts +189 -0
  115. data/package/configExporter/buildValidator.ts +906 -0
  116. data/package/configExporter/cli.ts +1748 -0
  117. data/package/configExporter/configDocs.ts +102 -0
  118. data/package/configExporter/configFile.ts +663 -0
  119. data/package/configExporter/fileWriter.ts +112 -0
  120. data/package/configExporter/index.ts +15 -0
  121. data/package/configExporter/types.ts +159 -0
  122. data/package/configExporter/yamlSerializer.ts +391 -0
  123. data/package/dev_server.ts +27 -0
  124. data/package/env.ts +92 -0
  125. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +36 -0
  126. data/package/environments/base.ts +147 -0
  127. data/package/environments/development.ts +88 -0
  128. data/package/environments/production.ts +82 -0
  129. data/package/environments/test.ts +55 -0
  130. data/package/environments/types.ts +98 -0
  131. data/package/esbuild/index.ts +40 -0
  132. data/package/index.d.ts +68 -93
  133. data/package/index.d.ts.template +72 -0
  134. data/package/index.ts +104 -0
  135. data/package/loaders.d.ts +28 -0
  136. data/package/optimization/rspack.ts +36 -0
  137. data/package/optimization/webpack.ts +55 -0
  138. data/package/plugins/envFilter.ts +82 -0
  139. data/package/plugins/rspack.ts +119 -0
  140. data/package/plugins/webpack.ts +82 -0
  141. data/package/rspack/index.ts +91 -0
  142. data/package/rules/{babel.js → babel.ts} +2 -2
  143. data/package/rules/{coffee.js → coffee.ts} +1 -1
  144. data/package/rules/css.ts +3 -0
  145. data/package/rules/{erb.js → erb.ts} +1 -1
  146. data/package/rules/esbuild.ts +10 -0
  147. data/package/rules/file.ts +41 -0
  148. data/package/rules/{jscommon.js → jscommon.ts} +5 -4
  149. data/package/rules/{less.js → less.ts} +4 -4
  150. data/package/rules/raw.ts +28 -0
  151. data/package/rules/rspack.ts +174 -0
  152. data/package/rules/sass.ts +21 -0
  153. data/package/rules/{stylus.js → stylus.ts} +4 -8
  154. data/package/rules/swc.ts +10 -0
  155. data/package/rules/{index.js → webpack.ts} +1 -2
  156. data/package/swc/index.ts +54 -0
  157. data/package/types/README.md +90 -0
  158. data/package/types/index.ts +69 -0
  159. data/package/types.ts +105 -0
  160. data/package/utils/bundlerUtils.ts +232 -0
  161. data/package/utils/configPath.ts +6 -0
  162. data/package/utils/debug.ts +45 -0
  163. data/package/utils/defaultConfigPath.ts +7 -0
  164. data/package/utils/ensureManifestExists.ts +17 -0
  165. data/package/utils/errorCodes.ts +249 -0
  166. data/package/utils/errorHelpers.ts +152 -0
  167. data/package/utils/getStyleRule.ts +75 -0
  168. data/package/utils/helpers.ts +99 -0
  169. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  170. data/package/utils/pathValidation.ts +207 -0
  171. data/package/utils/requireOrError.ts +24 -0
  172. data/package/utils/snakeToCamelCase.ts +5 -0
  173. data/package/utils/typeGuards.ts +388 -0
  174. data/package/utils/validateDependencies.ts +61 -0
  175. data/package/webpack-types.d.ts +33 -0
  176. data/package/webpackDevServerConfig.ts +130 -0
  177. data/package.json +157 -18
  178. data/scripts/remove-use-strict.js +44 -0
  179. data/scripts/type-check-no-emit.js +27 -0
  180. data/shakapacker.gemspec +4 -2
  181. data/sig/shakapacker/commands.rbs +35 -0
  182. data/sig/shakapacker/compiler.rbs +65 -0
  183. data/sig/shakapacker/compiler_strategy.rbs +41 -0
  184. data/sig/shakapacker/configuration.rbs +140 -0
  185. data/sig/shakapacker/dev_server.rbs +56 -0
  186. data/sig/shakapacker/env.rbs +25 -0
  187. data/sig/shakapacker/helper.rbs +98 -0
  188. data/sig/shakapacker/instance.rbs +46 -0
  189. data/sig/shakapacker/manifest.rbs +69 -0
  190. data/sig/shakapacker/version.rbs +4 -0
  191. data/sig/shakapacker.rbs +66 -0
  192. data/test/configExporter/buildValidator.test.js +1295 -0
  193. data/test/configExporter/configFile.test.js +393 -0
  194. data/test/configExporter/integration.test.js +262 -0
  195. data/test/helpers.js +1 -1
  196. data/test/package/bundlerUtils.rspack.test.js +145 -0
  197. data/test/package/bundlerUtils.test.js +97 -0
  198. data/test/package/config.test.js +14 -0
  199. data/test/package/configExporter/cli.test.js +440 -0
  200. data/test/package/configExporter/types.test.js +163 -0
  201. data/test/package/configExporter.test.js +491 -0
  202. data/test/package/env.test.js +42 -7
  203. data/test/package/environments/base.test.js +14 -4
  204. data/test/package/helpers.test.js +2 -2
  205. data/test/package/plugins/envFiltering.test.js +453 -0
  206. data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
  207. data/test/package/rspack/index.test.js +293 -0
  208. data/test/package/rspack/optimization.test.js +86 -0
  209. data/test/package/rspack/plugins.test.js +185 -0
  210. data/test/package/rspack/rules.test.js +229 -0
  211. data/test/package/rules/babel.test.js +65 -38
  212. data/test/package/rules/esbuild.test.js +13 -4
  213. data/test/package/rules/file.test.js +7 -1
  214. data/test/package/rules/raw.test.js +40 -7
  215. data/test/package/rules/sass-version-parsing.test.js +71 -0
  216. data/test/package/rules/sass.test.js +11 -6
  217. data/test/package/rules/sass1.test.js +8 -5
  218. data/test/package/rules/sass16.test.js +24 -0
  219. data/test/package/rules/swc.test.js +50 -39
  220. data/test/package/rules/webpack.test.js +35 -0
  221. data/test/package/staging.test.js +4 -3
  222. data/test/package/transpiler-defaults.test.js +169 -0
  223. data/test/package/utils/ensureManifestExists.test.js +51 -0
  224. data/test/package/yamlSerializer.test.js +204 -0
  225. data/test/peer-dependencies.sh +85 -0
  226. data/test/resolver.js +34 -3
  227. data/test/scripts/remove-use-strict.test.js +125 -0
  228. data/test/typescript/build.test.js +118 -0
  229. data/test/typescript/environments.test.js +107 -0
  230. data/test/typescript/pathValidation.test.js +186 -0
  231. data/test/typescript/requireOrError.test.js +49 -0
  232. data/test/typescript/securityValidation.test.js +182 -0
  233. data/tools/README.md +134 -0
  234. data/tools/css-modules-v9-codemod.js +179 -0
  235. data/tsconfig.eslint.json +9 -0
  236. data/tsconfig.json +38 -0
  237. data/yarn.lock +3202 -1097
  238. metadata +212 -44
  239. data/.eslintignore +0 -4
  240. data/.eslintrc.js +0 -36
  241. data/Gemfile.lock +0 -251
  242. data/package/babel/preset.js +0 -48
  243. data/package/config.js +0 -56
  244. data/package/dev_server.js +0 -23
  245. data/package/env.js +0 -48
  246. data/package/environments/base.js +0 -171
  247. data/package/environments/development.js +0 -13
  248. data/package/environments/production.js +0 -88
  249. data/package/environments/test.js +0 -3
  250. data/package/esbuild/index.js +0 -40
  251. data/package/index.js +0 -40
  252. data/package/rules/css.js +0 -3
  253. data/package/rules/esbuild.js +0 -10
  254. data/package/rules/file.js +0 -29
  255. data/package/rules/raw.js +0 -5
  256. data/package/rules/sass.js +0 -18
  257. data/package/rules/swc.js +0 -10
  258. data/package/swc/index.js +0 -50
  259. data/package/utils/configPath.js +0 -4
  260. data/package/utils/defaultConfigPath.js +0 -2
  261. data/package/utils/getStyleRule.js +0 -40
  262. data/package/utils/helpers.js +0 -62
  263. data/package/utils/snakeToCamelCase.js +0 -5
  264. data/package/webpackDevServerConfig.js +0 -71
  265. 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,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,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
+ }