shakapacker 9.0.0.beta.6 → 9.0.0.beta.8

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.fast.js +40 -0
  3. data/.eslintrc.js +48 -0
  4. data/.github/workflows/generator.yml +6 -0
  5. data/.gitignore +1 -4
  6. data/.npmignore +56 -0
  7. data/CHANGELOG.md +64 -1
  8. data/CONTRIBUTING.md +75 -21
  9. data/Gemfile.lock +1 -1
  10. data/README.md +4 -0
  11. data/TODO.md +15 -16
  12. data/docs/transpiler-migration.md +191 -0
  13. data/docs/typescript-migration.md +378 -0
  14. data/lib/install/template.rb +54 -7
  15. data/lib/shakapacker/version.rb +1 -1
  16. data/package/.npmignore +4 -0
  17. data/package/babel/preset.ts +56 -0
  18. data/package/config.ts +23 -10
  19. data/package/env.ts +15 -2
  20. data/package/environments/{development.js → development.ts} +30 -8
  21. data/package/environments/{production.js → production.ts} +18 -4
  22. data/package/environments/test.ts +53 -0
  23. data/package/environments/types.ts +90 -0
  24. data/package/esbuild/index.ts +42 -0
  25. data/package/optimization/rspack.ts +36 -0
  26. data/package/optimization/{webpack.js → webpack.ts} +12 -4
  27. data/package/plugins/{rspack.js → rspack.ts} +20 -5
  28. data/package/plugins/{webpack.js → webpack.ts} +2 -2
  29. data/package/rspack/{index.js → index.ts} +17 -10
  30. data/package/rules/{babel.js → babel.ts} +1 -1
  31. data/package/rules/{coffee.js → coffee.ts} +1 -1
  32. data/package/rules/{css.js → css.ts} +1 -1
  33. data/package/rules/{erb.js → erb.ts} +1 -1
  34. data/package/rules/{esbuild.js → esbuild.ts} +2 -2
  35. data/package/rules/{file.js → file.ts} +11 -6
  36. data/package/rules/{jscommon.js → jscommon.ts} +4 -4
  37. data/package/rules/{less.js → less.ts} +3 -3
  38. data/package/rules/raw.ts +25 -0
  39. data/package/rules/{rspack.js → rspack.ts} +21 -11
  40. data/package/rules/{sass.js → sass.ts} +1 -1
  41. data/package/rules/{stylus.js → stylus.ts} +3 -7
  42. data/package/rules/{swc.js → swc.ts} +2 -2
  43. data/package/rules/{webpack.js → webpack.ts} +1 -1
  44. data/package/swc/index.ts +54 -0
  45. data/package/types/README.md +87 -0
  46. data/package/types/index.ts +60 -0
  47. data/package/utils/errorCodes.ts +219 -0
  48. data/package/utils/errorHelpers.ts +68 -2
  49. data/package/utils/pathValidation.ts +139 -0
  50. data/package/utils/typeGuards.ts +161 -47
  51. data/package.json +26 -4
  52. data/scripts/remove-use-strict.js +45 -0
  53. data/scripts/type-check-no-emit.js +27 -0
  54. data/test/package/rules/raw.test.js +40 -7
  55. data/test/package/rules/webpack.test.js +21 -2
  56. data/test/package/transpiler-defaults.test.js +127 -0
  57. data/test/scripts/remove-use-strict.test.js +125 -0
  58. data/test/typescript/build.test.js +3 -2
  59. data/test/typescript/environments.test.js +107 -0
  60. data/test/typescript/pathValidation.test.js +142 -0
  61. data/test/typescript/securityValidation.test.js +182 -0
  62. data/tsconfig.eslint.json +16 -0
  63. data/tsconfig.json +9 -10
  64. data/yarn.lock +415 -6
  65. metadata +50 -28
  66. data/package/babel/preset.js +0 -48
  67. data/package/environments/base.js +0 -103
  68. data/package/environments/test.js +0 -19
  69. data/package/esbuild/index.js +0 -40
  70. data/package/optimization/rspack.js +0 -29
  71. data/package/rules/raw.js +0 -15
  72. data/package/swc/index.js +0 -50
@@ -2,6 +2,8 @@
2
2
  * Error handling utilities for consistent error management
3
3
  */
4
4
 
5
+ import { ErrorCode, ShakapackerError } from './errorCodes'
6
+
5
7
  /**
6
8
  * Checks if an error is a file not found error (ENOENT)
7
9
  */
@@ -33,10 +35,31 @@ export function createFileOperationError(
33
35
  operation: 'read' | 'write' | 'delete',
34
36
  filePath: string,
35
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
36
59
  ): Error {
37
60
  const baseMessage = `Failed to ${operation} file at path '${filePath}'`
38
61
  const errorDetails = details ? ` - ${details}` : ''
39
- const suggestion = operation === 'read'
62
+ const suggestion = operation === 'read'
40
63
  ? ' (check if file exists and permissions are correct)'
41
64
  : operation === 'write'
42
65
  ? ' (check write permissions and disk space)'
@@ -70,8 +93,51 @@ export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
70
93
  return (
71
94
  error instanceof Error &&
72
95
  'code' in error &&
73
- typeof (error as any).code === 'string'
96
+ typeof (error as NodeJS.ErrnoException).code === 'string'
74
97
  )
75
98
  }
76
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
+
77
143
 
@@ -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
+ }
@@ -1,43 +1,111 @@
1
1
  import { Config, DevServerConfig, YamlConfig } from "../types"
2
+ import { isPathTraversalSafe, validatePort } from "./pathValidation"
2
3
 
3
- // Cache for validated configs in production
4
- const validatedConfigs = new WeakMap<object, boolean>()
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
+ }
5
44
 
6
45
  // Only validate in development or when explicitly enabled
7
- const shouldValidate = process.env.NODE_ENV !== 'production' || process.env.SHAKAPACKER_STRICT_VALIDATION === 'true'
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
+ }
8
64
 
9
65
  /**
10
66
  * Type guard to validate Config object at runtime
11
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.
12
71
  */
13
72
  export function isValidConfig(obj: unknown): obj is Config {
14
73
  if (typeof obj !== 'object' || obj === null) {
15
74
  return false
16
75
  }
17
76
 
18
- // Quick return for production with cached results
19
- if (!shouldValidate && validatedConfigs.has(obj as object)) {
20
- return validatedConfigs.get(obj as object) as boolean
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
21
84
  }
22
85
 
23
86
  const config = obj as Record<string, unknown>
24
-
87
+
25
88
  // Check required string fields
26
89
  const requiredStringFields = [
27
90
  'source_path',
28
- 'source_entry_path',
91
+ 'source_entry_path',
29
92
  'public_root_path',
30
93
  'public_output_path',
31
94
  'cache_path',
32
95
  'javascript_transpiler'
33
96
  ]
34
-
97
+
35
98
  for (const field of requiredStringFields) {
36
99
  if (typeof config[field] !== 'string') {
37
- // Cache negative result in production
38
- if (!shouldValidate) {
39
- validatedConfigs.set(obj as object, false)
40
- }
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() })
41
109
  return false
42
110
  }
43
111
  }
@@ -56,52 +124,58 @@ export function isValidConfig(obj: unknown): obj is Config {
56
124
 
57
125
  for (const field of requiredBooleanFields) {
58
126
  if (typeof config[field] !== 'boolean') {
59
- // Cache negative result in production
60
- if (!shouldValidate) {
61
- validatedConfigs.set(obj as object, false)
62
- }
127
+ // Cache negative result
128
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
63
129
  return false
64
130
  }
65
131
  }
66
132
 
67
133
  // Check arrays
68
134
  if (!Array.isArray(config.additional_paths)) {
69
- // Cache negative result in production
70
- if (!shouldValidate) {
71
- validatedConfigs.set(obj as object, false)
72
- }
135
+ // Cache negative result
136
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
73
137
  return false
74
138
  }
75
-
76
- // Check optional fields
77
- if (config.dev_server !== undefined && !isValidDevServerConfig(config.dev_server)) {
78
- // Cache negative result in production
79
- if (!shouldValidate) {
80
- validatedConfigs.set(obj as object, false)
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
81
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() })
82
162
  return false
83
163
  }
84
-
164
+
85
165
  if (config.integrity !== undefined) {
86
166
  const integrity = config.integrity as Record<string, unknown>
87
- if (typeof integrity.enabled !== 'boolean' ||
167
+ if (typeof integrity.enabled !== 'boolean' ||
88
168
  typeof integrity.cross_origin !== 'string') {
89
- // Cache negative result in production
90
- if (!shouldValidate) {
91
- validatedConfigs.set(obj as object, false)
92
- }
169
+ // Cache negative result
170
+ validatedConfigs.set(obj as object, { result: false, timestamp: Date.now() })
93
171
  return false
94
172
  }
95
173
  }
96
174
 
97
- const result = true
98
-
99
- // Cache result in production
100
- if (!shouldValidate) {
101
- validatedConfigs.set(obj as object, result)
102
- }
175
+ // Cache positive result
176
+ validatedConfigs.set(obj as object, { result: true, timestamp: Date.now() })
103
177
 
104
- return result
178
+ return true
105
179
  }
106
180
 
107
181
  /**
@@ -114,7 +188,7 @@ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
114
188
  }
115
189
 
116
190
  // In production, skip deep validation unless explicitly enabled
117
- if (!shouldValidate) {
191
+ if (!shouldValidate()) {
118
192
  return true
119
193
  }
120
194
 
@@ -127,16 +201,56 @@ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
127
201
  return false
128
202
  }
129
203
 
130
- if (config.port !== undefined &&
131
- typeof config.port !== 'number' &&
132
- typeof config.port !== 'string' &&
133
- config.port !== 'auto') {
204
+ if (config.port !== undefined && !validatePort(config.port)) {
134
205
  return false
135
206
  }
136
207
 
137
208
  return true
138
209
  }
139
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
+
140
254
  /**
141
255
  * Type guard to validate YamlConfig structure
142
256
  * In production, performs minimal validation for performance
@@ -147,7 +261,7 @@ export function isValidYamlConfig(obj: unknown): obj is YamlConfig {
147
261
  }
148
262
 
149
263
  // In production, skip deep validation unless explicitly enabled
150
- if (!shouldValidate) {
264
+ if (!shouldValidate()) {
151
265
  return true
152
266
  }
153
267
 
@@ -174,7 +288,7 @@ export function isPartialConfig(obj: unknown): obj is Partial<Config> {
174
288
  }
175
289
 
176
290
  // In production, skip deep validation unless explicitly enabled
177
- if (!shouldValidate) {
291
+ if (!shouldValidate()) {
178
292
  return true
179
293
  }
180
294
 
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "9.0.0-beta.6",
3
+ "version": "9.0.0-beta.8",
4
4
  "description": "Use webpack to manage app-like JavaScript modules in Rails",
5
5
  "homepage": "https://github.com/shakacode/shakapacker",
6
6
  "bugs": {
@@ -16,6 +16,7 @@
16
16
  "types": "package/index.d.ts",
17
17
  "exports": {
18
18
  ".": "./package/index.js",
19
+ "./types": "./package/types/index.js",
19
20
  "./webpack": "./package/webpack/index.js",
20
21
  "./rspack": "./package/rspack/index.js",
21
22
  "./swc": "./package/swc/index.js",
@@ -30,11 +31,13 @@
30
31
  ],
31
32
  "scripts": {
32
33
  "clean:ts": "find package -name '*.ts' -not -name '*.d.ts' | sed 's/\\.ts$//' | xargs -I {} rm -f {}.js {}.d.ts {}.d.ts.map {}.js.map",
33
- "build": "tsc",
34
+ "build": "tsc && node scripts/remove-use-strict.js && yarn prettier --write 'package/**/*.js'",
34
35
  "build:types": "tsc",
35
36
  "lint": "eslint .",
37
+ "lint:fast": "eslint . --ext .js,.jsx,.ts,.tsx --config .eslintrc.fast.js",
36
38
  "test": "jest",
37
- "type-check": "tsc --noEmit"
39
+ "type-check": "tsc --noEmit",
40
+ "prepublishOnly": "yarn build && yarn type-check"
38
41
  },
39
42
  "dependencies": {
40
43
  "js-yaml": "^4.1.0",
@@ -44,12 +47,15 @@
44
47
  "devDependencies": {
45
48
  "@rspack/cli": "^1.4.11",
46
49
  "@rspack/core": "^1.4.11",
50
+ "@types/babel__core": "^7.20.5",
47
51
  "@types/js-yaml": "^4.0.9",
48
52
  "@types/node": "^24.5.2",
49
53
  "@types/path-complete-extname": "^1.0.3",
50
54
  "@types/webpack": "^5.28.5",
51
55
  "@types/webpack-dev-server": "^4.7.2",
52
56
  "@types/webpack-merge": "^5.0.0",
57
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
58
+ "@typescript-eslint/parser": "^8.45.0",
53
59
  "babel-loader": "^8.2.4",
54
60
  "compression-webpack-plugin": "^9.0.0",
55
61
  "css-loader": "^7.1.2",
@@ -63,7 +69,9 @@
63
69
  "eslint-plugin-prettier": "^5.2.6",
64
70
  "eslint-plugin-react": "^7.37.5",
65
71
  "eslint-plugin-react-hooks": "^4.6.0",
72
+ "husky": "^9.1.7",
66
73
  "jest": "^29.7.0",
74
+ "lint-staged": "^15.2.10",
67
75
  "memory-fs": "^0.5.0",
68
76
  "mini-css-extract-plugin": "^2.9.4",
69
77
  "prettier": "^3.2.5",
@@ -81,8 +89,8 @@
81
89
  "@babel/plugin-transform-runtime": "^7.17.0",
82
90
  "@babel/preset-env": "^7.16.11",
83
91
  "@babel/runtime": "^7.17.9",
84
- "@rspack/core": "^1.0.0",
85
92
  "@rspack/cli": "^1.0.0",
93
+ "@rspack/core": "^1.0.0",
86
94
  "@rspack/plugin-react-refresh": "^1.0.0",
87
95
  "@types/babel__core": "^7.0.0",
88
96
  "@types/webpack": "^5.0.0",
@@ -181,6 +189,20 @@
181
189
  }
182
190
  },
183
191
  "packageManager": "yarn@1.22.22",
192
+ "lint-staged": {
193
+ "*.{js,jsx}": [
194
+ "eslint --fix",
195
+ "prettier --write"
196
+ ],
197
+ "*.{ts,tsx}": [
198
+ "eslint --fix",
199
+ "prettier --write",
200
+ "node scripts/type-check-no-emit.js"
201
+ ],
202
+ "*.{json,yml,yaml,md}": [
203
+ "prettier --write"
204
+ ]
205
+ },
184
206
  "engines": {
185
207
  "node": ">= 14",
186
208
  "yarn": ">=1 <5"
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs")
3
+ const path = require("path")
4
+
5
+ // Recursively find all .js files in a directory
6
+ function findJsFiles(dir) {
7
+ const files = []
8
+ const items = fs.readdirSync(dir, { withFileTypes: true })
9
+
10
+ items.forEach((item) => {
11
+ const fullPath = path.join(dir, item.name)
12
+ if (item.isDirectory()) {
13
+ files.push(...findJsFiles(fullPath))
14
+ } else if (item.isFile() && item.name.endsWith(".js")) {
15
+ files.push(fullPath)
16
+ }
17
+ })
18
+
19
+ return files
20
+ }
21
+
22
+ // Find all .js files in package directory
23
+ const files = findJsFiles("package")
24
+
25
+ files.forEach((file) => {
26
+ let content = fs.readFileSync(file, "utf8")
27
+
28
+ // Remove "use strict" directive with various quote styles and formatting
29
+ // Handles: optional whitespace, single/double/unicode quotes, optional semicolon,
30
+ // and any trailing whitespace/newline sequences
31
+ content = content.replace(
32
+ /^\s*["'\u2018\u2019\u201C\u201D]use\s+strict["'\u2018\u2019\u201C\u201D]\s*;?\s*[\r\n]*/,
33
+ ""
34
+ )
35
+
36
+ // Ensure file ends with exactly one newline
37
+ if (!content.endsWith("\n")) {
38
+ content += "\n"
39
+ }
40
+
41
+ fs.writeFileSync(file, content, "utf8")
42
+ })
43
+
44
+ // eslint-disable-next-line no-console
45
+ console.log(`Removed "use strict" from ${files.length} files`)
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Type-check script for lint-staged
5
+ *
6
+ * This script runs TypeScript type checking without emitting files.
7
+ * It ignores any arguments passed by lint-staged to ensure tsc uses
8
+ * the project's tsconfig.json rather than trying to compile individual files.
9
+ *
10
+ * Without this wrapper, lint-staged would pass staged file paths as arguments
11
+ * to tsc, causing it to ignore tsconfig.json and fail type checking.
12
+ */
13
+
14
+ const { execSync } = require("child_process")
15
+
16
+ try {
17
+ // Run tsc with no arguments (ignoring any passed by lint-staged)
18
+ // This ensures it uses tsconfig.json properly
19
+ execSync("npx tsc --noEmit", {
20
+ stdio: "inherit",
21
+ cwd: process.cwd()
22
+ })
23
+ process.exit(0)
24
+ } catch (error) {
25
+ // Type checking failed
26
+ process.exit(1)
27
+ }