shakapacker 8.0.2 → 9.2.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 (198) 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 +9 -4
  9. data/.github/workflows/generator.yml +32 -10
  10. data/.github/workflows/node.yml +23 -1
  11. data/.github/workflows/ruby.yml +33 -2
  12. data/.github/workflows/test-bundlers.yml +170 -0
  13. data/.gitignore +20 -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 +302 -16
  20. data/CLAUDE.md +29 -0
  21. data/CONTRIBUTING.md +138 -20
  22. data/Gemfile.lock +83 -89
  23. data/README.md +343 -105
  24. data/Rakefile +39 -4
  25. data/TODO.md +50 -0
  26. data/TODO_v9.md +87 -0
  27. data/bin/export-bundler-config +11 -0
  28. data/conductor-setup.sh +70 -0
  29. data/conductor.json +7 -0
  30. data/docs/cdn_setup.md +379 -0
  31. data/docs/common-upgrades.md +615 -0
  32. data/docs/css-modules-export-mode.md +512 -0
  33. data/docs/deployment.md +62 -9
  34. data/docs/optional-peer-dependencies.md +198 -0
  35. data/docs/peer-dependencies.md +60 -0
  36. data/docs/react.md +6 -14
  37. data/docs/releasing.md +197 -0
  38. data/docs/rspack.md +190 -0
  39. data/docs/rspack_migration_guide.md +305 -0
  40. data/docs/subresource_integrity.md +54 -0
  41. data/docs/transpiler-migration.md +209 -0
  42. data/docs/transpiler-performance.md +179 -0
  43. data/docs/troubleshooting.md +157 -22
  44. data/docs/typescript-migration.md +379 -0
  45. data/docs/typescript.md +99 -0
  46. data/docs/using_esbuild_loader.md +3 -3
  47. data/docs/using_swc_loader.md +112 -10
  48. data/docs/v6_upgrade.md +10 -0
  49. data/docs/v8_upgrade.md +3 -5
  50. data/docs/v9_upgrade.md +458 -0
  51. data/gemfiles/Gemfile-rails.6.0.x +2 -1
  52. data/gemfiles/Gemfile-rails.6.1.x +1 -1
  53. data/gemfiles/Gemfile-rails.7.0.x +2 -2
  54. data/gemfiles/Gemfile-rails.7.1.x +1 -2
  55. data/gemfiles/Gemfile-rails.7.2.x +11 -0
  56. data/gemfiles/Gemfile-rails.8.0.x +11 -0
  57. data/lib/install/bin/export-bundler-config +11 -0
  58. data/lib/install/bin/shakapacker +4 -6
  59. data/lib/install/bin/shakapacker-dev-server +1 -1
  60. data/lib/install/config/rspack/rspack.config.js +6 -0
  61. data/lib/install/config/rspack/rspack.config.ts +7 -0
  62. data/lib/install/config/shakapacker.yml +25 -5
  63. data/lib/install/config/webpack/webpack.config.ts +7 -0
  64. data/lib/install/package.json +38 -0
  65. data/lib/install/template.rb +194 -44
  66. data/lib/shakapacker/bundler_switcher.rb +329 -0
  67. data/lib/shakapacker/compiler.rb +2 -1
  68. data/lib/shakapacker/compiler_strategy.rb +2 -2
  69. data/lib/shakapacker/configuration.rb +173 -2
  70. data/lib/shakapacker/dev_server_runner.rb +29 -8
  71. data/lib/shakapacker/digest_strategy.rb +2 -1
  72. data/lib/shakapacker/doctor.rb +905 -0
  73. data/lib/shakapacker/helper.rb +64 -16
  74. data/lib/shakapacker/manifest.rb +10 -3
  75. data/lib/shakapacker/mtime_strategy.rb +1 -1
  76. data/lib/shakapacker/railtie.rb +4 -4
  77. data/lib/shakapacker/rspack_runner.rb +19 -0
  78. data/lib/shakapacker/runner.rb +159 -10
  79. data/lib/shakapacker/swc_migrator.rb +384 -0
  80. data/lib/shakapacker/utils/manager.rb +15 -2
  81. data/lib/shakapacker/version.rb +1 -1
  82. data/lib/shakapacker/version_checker.rb +2 -2
  83. data/lib/shakapacker/webpack_runner.rb +6 -43
  84. data/lib/shakapacker.rb +22 -11
  85. data/lib/tasks/shakapacker/doctor.rake +8 -0
  86. data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
  87. data/lib/tasks/shakapacker/install.rake +12 -2
  88. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  89. data/lib/tasks/shakapacker/switch_bundler.rake +82 -0
  90. data/lib/tasks/shakapacker.rake +2 -0
  91. data/package/.npmignore +4 -0
  92. data/package/babel/preset.ts +56 -0
  93. data/package/config.ts +175 -0
  94. data/package/configExporter/cli.ts +683 -0
  95. data/package/configExporter/configDocs.ts +102 -0
  96. data/package/configExporter/fileWriter.ts +92 -0
  97. data/package/configExporter/index.ts +5 -0
  98. data/package/configExporter/types.ts +36 -0
  99. data/package/configExporter/yamlSerializer.ts +266 -0
  100. data/package/{dev_server.js → dev_server.ts} +8 -5
  101. data/package/env.ts +92 -0
  102. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +30 -0
  103. data/package/environments/{base.js → base.ts} +56 -60
  104. data/package/environments/development.ts +90 -0
  105. data/package/environments/production.ts +80 -0
  106. data/package/environments/test.ts +53 -0
  107. data/package/environments/types.ts +98 -0
  108. data/package/esbuild/index.ts +42 -0
  109. data/package/index.d.ts +3 -60
  110. data/package/index.ts +55 -0
  111. data/package/loaders.d.ts +28 -0
  112. data/package/optimization/rspack.ts +36 -0
  113. data/package/optimization/webpack.ts +57 -0
  114. data/package/plugins/rspack.ts +103 -0
  115. data/package/plugins/webpack.ts +62 -0
  116. data/package/rspack/index.ts +64 -0
  117. data/package/rules/{babel.js → babel.ts} +2 -2
  118. data/package/rules/{coffee.js → coffee.ts} +1 -1
  119. data/package/rules/css.ts +3 -0
  120. data/package/rules/{erb.js → erb.ts} +1 -1
  121. data/package/rules/esbuild.ts +10 -0
  122. data/package/rules/file.ts +40 -0
  123. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  124. data/package/rules/{less.js → less.ts} +4 -4
  125. data/package/rules/raw.ts +25 -0
  126. data/package/rules/rspack.ts +176 -0
  127. data/package/rules/{sass.js → sass.ts} +7 -3
  128. data/package/rules/{stylus.js → stylus.ts} +4 -8
  129. data/package/rules/swc.ts +10 -0
  130. data/package/rules/webpack.ts +16 -0
  131. data/package/swc/index.ts +56 -0
  132. data/package/types/README.md +88 -0
  133. data/package/types/index.ts +61 -0
  134. data/package/types.ts +108 -0
  135. data/package/utils/configPath.ts +6 -0
  136. data/package/utils/debug.ts +49 -0
  137. data/package/utils/defaultConfigPath.ts +4 -0
  138. data/package/utils/errorCodes.ts +219 -0
  139. data/package/utils/errorHelpers.ts +143 -0
  140. data/package/utils/getStyleRule.ts +64 -0
  141. data/package/utils/helpers.ts +85 -0
  142. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  143. data/package/utils/pathValidation.ts +139 -0
  144. data/package/utils/requireOrError.ts +15 -0
  145. data/package/utils/snakeToCamelCase.ts +5 -0
  146. data/package/utils/typeGuards.ts +342 -0
  147. data/package/utils/validateDependencies.ts +61 -0
  148. data/package/webpack-types.d.ts +33 -0
  149. data/package/webpackDevServerConfig.ts +117 -0
  150. data/package-lock.json +13047 -0
  151. data/package.json +154 -18
  152. data/scripts/remove-use-strict.js +45 -0
  153. data/scripts/type-check-no-emit.js +27 -0
  154. data/test/helpers.js +1 -1
  155. data/test/package/config.test.js +43 -0
  156. data/test/package/env.test.js +42 -7
  157. data/test/package/environments/base.test.js +5 -1
  158. data/test/package/rules/babel.test.js +16 -0
  159. data/test/package/rules/esbuild.test.js +1 -1
  160. data/test/package/rules/raw.test.js +40 -7
  161. data/test/package/rules/swc.test.js +1 -1
  162. data/test/package/rules/webpack.test.js +35 -0
  163. data/test/package/staging.test.js +4 -3
  164. data/test/package/transpiler-defaults.test.js +127 -0
  165. data/test/peer-dependencies.sh +85 -0
  166. data/test/scripts/remove-use-strict.test.js +125 -0
  167. data/test/typescript/build.test.js +118 -0
  168. data/test/typescript/environments.test.js +107 -0
  169. data/test/typescript/pathValidation.test.js +142 -0
  170. data/test/typescript/securityValidation.test.js +182 -0
  171. data/tools/README.md +124 -0
  172. data/tools/css-modules-v9-codemod.js +179 -0
  173. data/tsconfig.eslint.json +16 -0
  174. data/tsconfig.json +38 -0
  175. data/yarn.lock +4165 -2706
  176. metadata +129 -41
  177. data/package/babel/preset.js +0 -37
  178. data/package/config.js +0 -54
  179. data/package/env.js +0 -48
  180. data/package/environments/development.js +0 -13
  181. data/package/environments/production.js +0 -88
  182. data/package/environments/test.js +0 -3
  183. data/package/esbuild/index.js +0 -40
  184. data/package/index.js +0 -40
  185. data/package/rules/css.js +0 -3
  186. data/package/rules/esbuild.js +0 -10
  187. data/package/rules/file.js +0 -29
  188. data/package/rules/index.js +0 -20
  189. data/package/rules/raw.js +0 -5
  190. data/package/rules/swc.js +0 -10
  191. data/package/swc/index.js +0 -50
  192. data/package/utils/configPath.js +0 -4
  193. data/package/utils/defaultConfigPath.js +0 -2
  194. data/package/utils/getStyleRule.js +0 -40
  195. data/package/utils/helpers.js +0 -58
  196. data/package/utils/snakeToCamelCase.js +0 -5
  197. data/package/webpackDevServerConfig.js +0 -71
  198. data/test/package/rules/index.test.js +0 -16
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Error codes for programmatic error handling in Shakapacker
3
+ * These codes allow consumers to handle specific errors programmatically
4
+ * @module shakapacker/utils/errorCodes
5
+ */
6
+
7
+ /**
8
+ * Error code enumeration for Shakapacker errors
9
+ */
10
+ export enum ErrorCode {
11
+ // Configuration errors (1xxx)
12
+ CONFIG_NOT_FOUND = 'SHAKAPACKER_1001',
13
+ CONFIG_INVALID_YAML = 'SHAKAPACKER_1002',
14
+ CONFIG_MISSING_REQUIRED = 'SHAKAPACKER_1003',
15
+ CONFIG_VALIDATION_FAILED = 'SHAKAPACKER_1004',
16
+ CONFIG_MERGE_FAILED = 'SHAKAPACKER_1005',
17
+ CONFIG_TYPE_MISMATCH = 'SHAKAPACKER_1006',
18
+
19
+ // File system errors (2xxx)
20
+ FILE_NOT_FOUND = 'SHAKAPACKER_2001',
21
+ FILE_READ_ERROR = 'SHAKAPACKER_2002',
22
+ FILE_WRITE_ERROR = 'SHAKAPACKER_2003',
23
+ FILE_PERMISSION_DENIED = 'SHAKAPACKER_2004',
24
+ PATH_TRAVERSAL_DETECTED = 'SHAKAPACKER_2005',
25
+ INVALID_PATH = 'SHAKAPACKER_2006',
26
+
27
+ // Module errors (3xxx)
28
+ MODULE_NOT_FOUND = 'SHAKAPACKER_3001',
29
+ MODULE_LOAD_FAILED = 'SHAKAPACKER_3002',
30
+ MODULE_INVALID_EXPORT = 'SHAKAPACKER_3003',
31
+ LOADER_NOT_FOUND = 'SHAKAPACKER_3004',
32
+ PLUGIN_NOT_FOUND = 'SHAKAPACKER_3005',
33
+ PLUGIN_INVALID = 'SHAKAPACKER_3006',
34
+
35
+ // Environment errors (4xxx)
36
+ ENV_INVALID_NODE_ENV = 'SHAKAPACKER_4001',
37
+ ENV_MISSING_REQUIRED = 'SHAKAPACKER_4002',
38
+ ENV_INVALID_VALUE = 'SHAKAPACKER_4003',
39
+ ENV_SANITIZATION_REQUIRED = 'SHAKAPACKER_4004',
40
+
41
+ // Bundler errors (5xxx)
42
+ BUNDLER_UNSUPPORTED = 'SHAKAPACKER_5001',
43
+ BUNDLER_CONFIG_INVALID = 'SHAKAPACKER_5002',
44
+ WEBPACK_CONFIG_INVALID = 'SHAKAPACKER_5003',
45
+ RSPACK_CONFIG_INVALID = 'SHAKAPACKER_5004',
46
+ TRANSPILER_NOT_FOUND = 'SHAKAPACKER_5005',
47
+ TRANSPILER_CONFIG_INVALID = 'SHAKAPACKER_5006',
48
+
49
+ // Dev server errors (6xxx)
50
+ DEVSERVER_CONFIG_INVALID = 'SHAKAPACKER_6001',
51
+ DEVSERVER_PORT_INVALID = 'SHAKAPACKER_6002',
52
+ DEVSERVER_PORT_IN_USE = 'SHAKAPACKER_6003',
53
+ DEVSERVER_START_FAILED = 'SHAKAPACKER_6004',
54
+
55
+ // Security errors (7xxx)
56
+ SECURITY_PATH_TRAVERSAL = 'SHAKAPACKER_7001',
57
+ SECURITY_INVALID_INPUT = 'SHAKAPACKER_7002',
58
+ SECURITY_CONTROL_CHARS = 'SHAKAPACKER_7003',
59
+ SECURITY_INJECTION_ATTEMPT = 'SHAKAPACKER_7004',
60
+
61
+ // Validation errors (8xxx)
62
+ VALIDATION_FAILED = 'SHAKAPACKER_8001',
63
+ VALIDATION_TYPE_ERROR = 'SHAKAPACKER_8002',
64
+ VALIDATION_RANGE_ERROR = 'SHAKAPACKER_8003',
65
+ VALIDATION_FORMAT_ERROR = 'SHAKAPACKER_8004',
66
+ VALIDATION_CONSTRAINT_ERROR = 'SHAKAPACKER_8005',
67
+
68
+ // Generic errors (9xxx)
69
+ UNKNOWN_ERROR = 'SHAKAPACKER_9000',
70
+ INTERNAL_ERROR = 'SHAKAPACKER_9001',
71
+ DEPRECATED_FEATURE = 'SHAKAPACKER_9002',
72
+ NOT_IMPLEMENTED = 'SHAKAPACKER_9003',
73
+ OPERATION_FAILED = 'SHAKAPACKER_9004'
74
+ }
75
+
76
+ /**
77
+ * Error message templates for each error code
78
+ */
79
+ export const ErrorMessages: Record<ErrorCode, string> = {
80
+ // Configuration errors
81
+ [ErrorCode.CONFIG_NOT_FOUND]: 'Configuration file not found: {path}',
82
+ [ErrorCode.CONFIG_INVALID_YAML]: 'Invalid YAML in configuration file: {path}',
83
+ [ErrorCode.CONFIG_MISSING_REQUIRED]: 'Missing required configuration field: {field}',
84
+ [ErrorCode.CONFIG_VALIDATION_FAILED]: 'Configuration validation failed: {reason}',
85
+ [ErrorCode.CONFIG_MERGE_FAILED]: 'Failed to merge configurations: {reason}',
86
+ [ErrorCode.CONFIG_TYPE_MISMATCH]: 'Configuration type mismatch for {field}: expected {expected}, got {actual}',
87
+
88
+ // File system errors
89
+ [ErrorCode.FILE_NOT_FOUND]: 'File not found: {path}',
90
+ [ErrorCode.FILE_READ_ERROR]: 'Error reading file: {path}',
91
+ [ErrorCode.FILE_WRITE_ERROR]: 'Error writing file: {path}',
92
+ [ErrorCode.FILE_PERMISSION_DENIED]: 'Permission denied accessing: {path}',
93
+ [ErrorCode.PATH_TRAVERSAL_DETECTED]: 'Path traversal attempt detected: {path}',
94
+ [ErrorCode.INVALID_PATH]: 'Invalid path: {path}',
95
+
96
+ // Module errors
97
+ [ErrorCode.MODULE_NOT_FOUND]: 'Module not found: {module}',
98
+ [ErrorCode.MODULE_LOAD_FAILED]: 'Failed to load module: {module}',
99
+ [ErrorCode.MODULE_INVALID_EXPORT]: 'Invalid export from module: {module}',
100
+ [ErrorCode.LOADER_NOT_FOUND]: 'Loader not found: {loader}',
101
+ [ErrorCode.PLUGIN_NOT_FOUND]: 'Plugin not found: {plugin}',
102
+ [ErrorCode.PLUGIN_INVALID]: 'Invalid plugin: {plugin}',
103
+
104
+ // Environment errors
105
+ [ErrorCode.ENV_INVALID_NODE_ENV]: 'Invalid NODE_ENV value: {value}. Valid values are: {valid}',
106
+ [ErrorCode.ENV_MISSING_REQUIRED]: 'Missing required environment variable: {variable}',
107
+ [ErrorCode.ENV_INVALID_VALUE]: 'Invalid value for environment variable {variable}: {value}',
108
+ [ErrorCode.ENV_SANITIZATION_REQUIRED]: 'Environment variable {variable} contained unsafe characters and was sanitized',
109
+
110
+ // Bundler errors
111
+ [ErrorCode.BUNDLER_UNSUPPORTED]: 'Unsupported bundler: {bundler}',
112
+ [ErrorCode.BUNDLER_CONFIG_INVALID]: 'Invalid bundler configuration: {reason}',
113
+ [ErrorCode.WEBPACK_CONFIG_INVALID]: 'Invalid webpack configuration: {reason}',
114
+ [ErrorCode.RSPACK_CONFIG_INVALID]: 'Invalid rspack configuration: {reason}',
115
+ [ErrorCode.TRANSPILER_NOT_FOUND]: 'Transpiler not found: {transpiler}',
116
+ [ErrorCode.TRANSPILER_CONFIG_INVALID]: 'Invalid transpiler configuration: {reason}',
117
+
118
+ // Dev server errors
119
+ [ErrorCode.DEVSERVER_CONFIG_INVALID]: 'Invalid dev server configuration: {reason}',
120
+ [ErrorCode.DEVSERVER_PORT_INVALID]: 'Invalid port: {port}',
121
+ [ErrorCode.DEVSERVER_PORT_IN_USE]: 'Port {port} is already in use',
122
+ [ErrorCode.DEVSERVER_START_FAILED]: 'Failed to start dev server: {reason}',
123
+
124
+ // Security errors
125
+ [ErrorCode.SECURITY_PATH_TRAVERSAL]: 'Security: Path traversal attempt blocked: {path}',
126
+ [ErrorCode.SECURITY_INVALID_INPUT]: 'Security: Invalid input detected: {input}',
127
+ [ErrorCode.SECURITY_CONTROL_CHARS]: 'Security: Control characters detected and removed from: {field}',
128
+ [ErrorCode.SECURITY_INJECTION_ATTEMPT]: 'Security: Potential injection attempt blocked: {details}',
129
+
130
+ // Validation errors
131
+ [ErrorCode.VALIDATION_FAILED]: 'Validation failed: {reason}',
132
+ [ErrorCode.VALIDATION_TYPE_ERROR]: 'Type validation error: {field} should be {type}',
133
+ [ErrorCode.VALIDATION_RANGE_ERROR]: 'Value out of range: {field} must be between {min} and {max}',
134
+ [ErrorCode.VALIDATION_FORMAT_ERROR]: 'Format error: {field} does not match expected format',
135
+ [ErrorCode.VALIDATION_CONSTRAINT_ERROR]: 'Constraint violation: {constraint}',
136
+
137
+ // Generic errors
138
+ [ErrorCode.UNKNOWN_ERROR]: 'An unknown error occurred',
139
+ [ErrorCode.INTERNAL_ERROR]: 'Internal error: {details}',
140
+ [ErrorCode.DEPRECATED_FEATURE]: 'Deprecated feature: {feature}. Use {alternative} instead',
141
+ [ErrorCode.NOT_IMPLEMENTED]: 'Feature not yet implemented: {feature}',
142
+ [ErrorCode.OPERATION_FAILED]: 'Operation failed: {operation}'
143
+ }
144
+
145
+ /**
146
+ * Shakapacker error class with error code support
147
+ */
148
+ export class ShakapackerError extends Error {
149
+ public readonly code: ErrorCode
150
+ public readonly details?: Record<string, any>
151
+
152
+ constructor(code: ErrorCode, details?: Record<string, any>, customMessage?: string) {
153
+ const template = ErrorMessages[code] || 'An error occurred'
154
+ const message = customMessage || ShakapackerError.formatMessage(template, details)
155
+
156
+ super(message)
157
+ this.name = 'ShakapackerError'
158
+ this.code = code
159
+ this.details = details
160
+
161
+ // Maintain proper stack trace for where error was thrown
162
+ if (Error.captureStackTrace) {
163
+ Error.captureStackTrace(this, ShakapackerError)
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Format error message with template values
169
+ */
170
+ private static formatMessage(template: string, details?: Record<string, any>): string {
171
+ if (!details) return template
172
+
173
+ return template.replace(/{(\w+)}/g, (match, key) => {
174
+ const value = details[key]
175
+ if (value === undefined) return match
176
+ if (typeof value === 'object') {
177
+ return JSON.stringify(value)
178
+ }
179
+ return String(value)
180
+ })
181
+ }
182
+
183
+ /**
184
+ * Convert error to JSON for logging or API responses
185
+ */
186
+ toJSON(): Record<string, any> {
187
+ return {
188
+ name: this.name,
189
+ code: this.code,
190
+ message: this.message,
191
+ details: this.details,
192
+ stack: this.stack
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Helper function to create a Shakapacker error
199
+ */
200
+ export function createError(code: ErrorCode, details?: Record<string, any>): ShakapackerError {
201
+ return new ShakapackerError(code, details)
202
+ }
203
+
204
+ /**
205
+ * Helper function to check if an error is a Shakapacker error
206
+ */
207
+ export function isShakapackerError(error: unknown): error is ShakapackerError {
208
+ return error instanceof ShakapackerError
209
+ }
210
+
211
+ /**
212
+ * Helper function to get error code from any error
213
+ */
214
+ export function getErrorCode(error: unknown): ErrorCode | null {
215
+ if (isShakapackerError(error)) {
216
+ return error.code
217
+ }
218
+ return null
219
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Error handling utilities for consistent error management
3
+ */
4
+
5
+ import { ErrorCode, ShakapackerError } from './errorCodes'
6
+
7
+ /**
8
+ * Checks if an error is a file not found error (ENOENT)
9
+ */
10
+ export function isFileNotFoundError(error: unknown): boolean {
11
+ return (
12
+ error !== null &&
13
+ typeof error === 'object' &&
14
+ 'code' in error &&
15
+ (error as NodeJS.ErrnoException).code === 'ENOENT'
16
+ )
17
+ }
18
+
19
+ /**
20
+ * Checks if an error is a module not found error
21
+ */
22
+ export function isModuleNotFoundError(error: unknown): boolean {
23
+ return (
24
+ error !== null &&
25
+ typeof error === 'object' &&
26
+ 'code' in error &&
27
+ (error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND'
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Creates a consistent error message for file operations
33
+ */
34
+ export function createFileOperationError(
35
+ operation: 'read' | 'write' | 'delete',
36
+ filePath: string,
37
+ details?: string
38
+ ): ShakapackerError {
39
+ const errorCode = operation === 'read'
40
+ ? ErrorCode.FILE_READ_ERROR
41
+ : operation === 'write'
42
+ ? ErrorCode.FILE_WRITE_ERROR
43
+ : ErrorCode.FILE_NOT_FOUND
44
+
45
+ return new ShakapackerError(errorCode, {
46
+ path: filePath,
47
+ operation,
48
+ details
49
+ })
50
+ }
51
+
52
+ /**
53
+ * Creates a consistent error message for file operations (backward compatibility)
54
+ */
55
+ export function createFileOperationErrorLegacy(
56
+ operation: 'read' | 'write' | 'delete',
57
+ filePath: string,
58
+ details?: string
59
+ ): Error {
60
+ const baseMessage = `Failed to ${operation} file at path '${filePath}'`
61
+ const errorDetails = details ? ` - ${details}` : ''
62
+ const suggestion = operation === 'read'
63
+ ? ' (check if file exists and permissions are correct)'
64
+ : operation === 'write'
65
+ ? ' (check write permissions and disk space)'
66
+ : ' (check permissions)'
67
+ return new Error(`${baseMessage}${errorDetails}${suggestion}`)
68
+ }
69
+
70
+ /**
71
+ * Safely gets error message from unknown error type
72
+ */
73
+ export function getErrorMessage(error: unknown): string {
74
+ if (error instanceof Error) {
75
+ // Include stack trace for better debugging in development
76
+ const isDev = process.env.NODE_ENV === 'development'
77
+ return isDev && error.stack ? `${error.message}\n${error.stack}` : error.message
78
+ }
79
+ if (typeof error === 'string') {
80
+ return error
81
+ }
82
+ if (error && typeof error === 'object' && 'message' in error) {
83
+ return String((error as { message: unknown }).message)
84
+ }
85
+ // Provide more context for truly unknown errors
86
+ return `Unknown error occurred (type: ${typeof error}, value: ${JSON.stringify(error)})`
87
+ }
88
+
89
+ /**
90
+ * Type guard for NodeJS errors with errno
91
+ */
92
+ export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
93
+ return (
94
+ error instanceof Error &&
95
+ 'code' in error &&
96
+ typeof (error as NodeJS.ErrnoException).code === 'string'
97
+ )
98
+ }
99
+
100
+ /**
101
+ * Creates a configuration validation error
102
+ */
103
+ export function createConfigValidationErrorWithCode(
104
+ configPath: string,
105
+ environment: string,
106
+ reason: string
107
+ ): ShakapackerError {
108
+ return new ShakapackerError(ErrorCode.CONFIG_VALIDATION_FAILED, {
109
+ path: configPath,
110
+ environment,
111
+ reason
112
+ })
113
+ }
114
+
115
+ /**
116
+ * Creates a module not found error
117
+ */
118
+ export function createModuleNotFoundError(moduleName: string, details?: string): ShakapackerError {
119
+ return new ShakapackerError(ErrorCode.MODULE_NOT_FOUND, {
120
+ module: moduleName,
121
+ details
122
+ })
123
+ }
124
+
125
+ /**
126
+ * Creates a path traversal security error
127
+ */
128
+ export function createPathTraversalError(path: string): ShakapackerError {
129
+ return new ShakapackerError(ErrorCode.SECURITY_PATH_TRAVERSAL, {
130
+ path
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Creates a port validation error
136
+ */
137
+ export function createPortValidationError(port: unknown): ShakapackerError {
138
+ return new ShakapackerError(ErrorCode.DEVSERVER_PORT_INVALID, {
139
+ port: String(port)
140
+ })
141
+ }
142
+
143
+
@@ -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