shakapacker 9.3.0.beta.7 → 9.3.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/update-changelog.md +224 -0
  3. data/.github/actionlint-matcher.json +17 -0
  4. data/.github/workflows/dummy.yml +9 -0
  5. data/.github/workflows/generator.yml +13 -0
  6. data/.github/workflows/node.yml +83 -0
  7. data/.github/workflows/ruby.yml +11 -0
  8. data/.github/workflows/test-bundlers.yml +10 -0
  9. data/CHANGELOG.md +55 -111
  10. data/CLAUDE.md +6 -10
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile.lock +1 -1
  13. data/README.md +84 -8
  14. data/docs/api-reference.md +519 -0
  15. data/docs/configuration.md +38 -4
  16. data/docs/css-modules-export-mode.md +40 -6
  17. data/docs/rspack_migration_guide.md +238 -2
  18. data/docs/transpiler-migration.md +12 -9
  19. data/docs/troubleshooting.md +21 -21
  20. data/docs/using_swc_loader.md +13 -10
  21. data/docs/v9_upgrade.md +11 -2
  22. data/eslint.config.fast.js +128 -8
  23. data/eslint.config.js +89 -33
  24. data/knip.ts +8 -1
  25. data/lib/install/config/shakapacker.yml +20 -7
  26. data/lib/shakapacker/configuration.rb +274 -8
  27. data/lib/shakapacker/dev_server.rb +88 -1
  28. data/lib/shakapacker/dev_server_runner.rb +4 -0
  29. data/lib/shakapacker/doctor.rb +5 -5
  30. data/lib/shakapacker/instance.rb +85 -1
  31. data/lib/shakapacker/manifest.rb +85 -11
  32. data/lib/shakapacker/version.rb +1 -1
  33. data/lib/shakapacker.rb +143 -3
  34. data/lib/tasks/shakapacker/doctor.rake +1 -1
  35. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  36. data/package/config.ts +2 -4
  37. data/package/configExporter/buildValidator.ts +53 -29
  38. data/package/configExporter/cli.ts +106 -76
  39. data/package/configExporter/configFile.ts +33 -26
  40. data/package/configExporter/types.ts +64 -0
  41. data/package/configExporter/yamlSerializer.ts +118 -43
  42. data/package/dev_server.ts +3 -2
  43. data/package/env.ts +2 -2
  44. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +6 -6
  45. data/package/environments/base.ts +6 -6
  46. data/package/environments/development.ts +7 -9
  47. data/package/environments/production.ts +7 -8
  48. data/package/environments/test.ts +4 -2
  49. data/package/esbuild/index.ts +0 -2
  50. data/package/index.d.ts +1 -0
  51. data/package/index.d.ts.template +1 -0
  52. data/package/index.ts +28 -5
  53. data/package/loaders.d.ts +2 -2
  54. data/package/optimization/webpack.ts +29 -31
  55. data/package/plugins/rspack.ts +3 -1
  56. data/package/plugins/webpack.ts +5 -3
  57. data/package/rspack/index.ts +5 -4
  58. data/package/rules/file.ts +2 -1
  59. data/package/rules/jscommon.ts +1 -0
  60. data/package/rules/raw.ts +3 -1
  61. data/package/rules/rspack.ts +0 -2
  62. data/package/rules/sass.ts +0 -2
  63. data/package/rules/webpack.ts +0 -1
  64. data/package/swc/index.ts +0 -2
  65. data/package/types.ts +8 -11
  66. data/package/utils/debug.ts +0 -4
  67. data/package/utils/getStyleRule.ts +17 -9
  68. data/package/utils/helpers.ts +8 -4
  69. data/package/utils/pathValidation.ts +78 -18
  70. data/package/utils/requireOrError.ts +14 -5
  71. data/package/utils/typeGuards.ts +43 -46
  72. data/package/webpack-types.d.ts +2 -2
  73. data/package/webpackDevServerConfig.ts +5 -4
  74. data/package.json +2 -3
  75. data/test/package/configExporter/cli.test.js +440 -0
  76. data/test/package/configExporter/types.test.js +163 -0
  77. data/test/package/configExporter.test.js +264 -0
  78. data/test/package/transpiler-defaults.test.js +42 -0
  79. data/test/package/yamlSerializer.test.js +204 -0
  80. data/test/typescript/pathValidation.test.js +44 -0
  81. data/test/typescript/requireOrError.test.js +49 -0
  82. data/yarn.lock +0 -32
  83. metadata +14 -5
  84. data/.eslintrc.fast.js +0 -40
  85. data/.eslintrc.js +0 -84
@@ -3,9 +3,9 @@ import { resolve, relative, isAbsolute } from "path"
3
3
  import { load as loadYaml, FAILSAFE_SCHEMA } from "js-yaml"
4
4
  import {
5
5
  BundlerConfigFile,
6
- BuildConfig,
7
6
  ResolvedBuildConfig,
8
- ExportOptions
7
+ ExportOptions,
8
+ DEFAULT_CONFIG_FILE
9
9
  } from "./types"
10
10
 
11
11
  /**
@@ -18,12 +18,12 @@ export class ConfigFileLoader {
18
18
  private configFilePath: string
19
19
 
20
20
  /**
21
- * @param configFilePath - Path to config file (defaults to config/shakapacker-builds.yml in cwd)
21
+ * @param configFilePath - Path to config file (defaults to DEFAULT_CONFIG_FILE in cwd)
22
22
  * @throws Error if path is outside project directory
23
23
  */
24
24
  constructor(configFilePath?: string) {
25
25
  this.configFilePath =
26
- configFilePath || resolve(process.cwd(), "config/shakapacker-builds.yml")
26
+ configFilePath || resolve(process.cwd(), DEFAULT_CONFIG_FILE)
27
27
  this.validateConfigPath()
28
28
  }
29
29
 
@@ -46,7 +46,7 @@ export class ConfigFileLoader {
46
46
  // If file doesn't exist yet, just use the resolved path
47
47
  realPath = absPath
48
48
  }
49
- } catch (error) {
49
+ } catch {
50
50
  // If we can't resolve the path, use the original
51
51
  realPath = absPath
52
52
  }
@@ -92,16 +92,18 @@ export class ConfigFileLoader {
92
92
  json: true
93
93
  }) as BundlerConfigFile
94
94
 
95
- this.validate(parsed)
95
+ ConfigFileLoader.validate(parsed)
96
96
  return parsed
97
- } catch (error: any) {
97
+ } catch (error: unknown) {
98
+ const errorMessage =
99
+ error instanceof Error ? error.message : String(error)
98
100
  throw new Error(
99
- `Failed to load config file ${this.configFilePath}: ${error.message}`
101
+ `Failed to load config file ${this.configFilePath}: ${errorMessage}`
100
102
  )
101
103
  }
102
104
  }
103
105
 
104
- private validate(config: BundlerConfigFile): void {
106
+ private static validate(config: BundlerConfigFile): void {
105
107
  if (!config.builds || typeof config.builds !== "object") {
106
108
  throw new Error("Config file must contain a 'builds' object")
107
109
  }
@@ -193,7 +195,7 @@ export class ConfigFileLoader {
193
195
  }
194
196
 
195
197
  // Resolve bundler with precedence
196
- const bundler = this.resolveBundler(
198
+ const bundler = ConfigFileLoader.resolveBundler(
197
199
  options.bundler,
198
200
  build.bundler,
199
201
  config.default_bundler,
@@ -207,7 +209,9 @@ export class ConfigFileLoader {
207
209
  )
208
210
 
209
211
  // Convert bundler_env to CLI args
210
- const bundlerEnvArgs = this.convertBundlerEnvToArgs(build.bundler_env || {})
212
+ const bundlerEnvArgs = ConfigFileLoader.convertBundlerEnvToArgs(
213
+ build.bundler_env || {}
214
+ )
211
215
 
212
216
  // Resolve and validate outputs
213
217
  const outputs = build.outputs || []
@@ -264,7 +268,7 @@ export class ConfigFileLoader {
264
268
  }
265
269
  }
266
270
 
267
- private resolveBundler(
271
+ private static resolveBundler(
268
272
  cliFlag?: "webpack" | "rspack",
269
273
  buildBundler?: "webpack" | "rspack",
270
274
  defaultBundler?: "webpack" | "rspack",
@@ -293,9 +297,9 @@ export class ConfigFileLoader {
293
297
  // Replace ${VAR:-default} with VAR value or default
294
298
  expanded = expanded.replace(
295
299
  /\$\{([^}:]+):-([^}]*)\}/g,
296
- (_, varName, defaultValue) => {
300
+ (_: string, varName: string, defaultValue: string) => {
297
301
  // Validate env var name to prevent regex injection
298
- if (!this.isValidEnvVarName(varName)) {
302
+ if (!ConfigFileLoader.isValidEnvVarName(varName)) {
299
303
  console.warn(
300
304
  `[Config Exporter] Warning: Invalid environment variable name: ${varName}`
301
305
  )
@@ -306,16 +310,19 @@ export class ConfigFileLoader {
306
310
  )
307
311
 
308
312
  // Replace ${VAR} with VAR value
309
- expanded = expanded.replace(/\$\{([^}:]+)\}/g, (_, varName) => {
310
- // Validate env var name to prevent regex injection
311
- if (!this.isValidEnvVarName(varName)) {
312
- console.warn(
313
- `[Config Exporter] Warning: Invalid environment variable name: ${varName}`
314
- )
315
- return `\${${varName}}`
313
+ expanded = expanded.replace(
314
+ /\$\{([^}:]+)\}/g,
315
+ (_: string, varName: string) => {
316
+ // Validate env var name to prevent regex injection
317
+ if (!ConfigFileLoader.isValidEnvVarName(varName)) {
318
+ console.warn(
319
+ `[Config Exporter] Warning: Invalid environment variable name: ${varName}`
320
+ )
321
+ return `\${${varName}}`
322
+ }
323
+ return process.env[varName] || ""
316
324
  }
317
- return process.env[varName] || ""
318
- })
325
+ )
319
326
 
320
327
  return expanded
321
328
  }
@@ -326,11 +333,11 @@ export class ConfigFileLoader {
326
333
  * @param name - The variable name to validate
327
334
  * @returns true if valid, false otherwise
328
335
  */
329
- private isValidEnvVarName(name: string): boolean {
336
+ private static isValidEnvVarName(name: string): boolean {
330
337
  return /^[A-Z_][A-Z0-9_]*$/i.test(name)
331
338
  }
332
339
 
333
- private convertBundlerEnvToArgs(
340
+ private static convertBundlerEnvToArgs(
334
341
  bundlerEnv: Record<string, string | boolean>
335
342
  ): string[] {
336
343
  const args: string[] = []
@@ -357,7 +364,7 @@ export class ConfigFileLoader {
357
364
  */
358
365
  listBuilds(): void {
359
366
  const config = this.load()
360
- const builds = config.builds
367
+ const { builds } = config
361
368
 
362
369
  console.log(`\nAvailable builds in ${this.configFilePath}:\n`)
363
370
 
@@ -1,3 +1,67 @@
1
+ /**
2
+ * Environment variable names that can be set by build configurations.
3
+ * These are the only environment variables that build configs are allowed to set.
4
+ * This whitelist prevents malicious configs from modifying critical system variables.
5
+ */
6
+ export const BUILD_ENV_VARS = [
7
+ "NODE_ENV",
8
+ "RAILS_ENV",
9
+ "NODE_OPTIONS",
10
+ "BABEL_ENV",
11
+ "WEBPACK_SERVE",
12
+ "CLIENT_BUNDLE_ONLY",
13
+ "SERVER_BUNDLE_ONLY"
14
+ ] as const
15
+
16
+ /**
17
+ * Environment variables that must never be set by build configurations.
18
+ * Setting these could compromise system security or cause unexpected behavior.
19
+ */
20
+ export const DANGEROUS_ENV_VARS = [
21
+ "PATH",
22
+ "HOME",
23
+ "LD_PRELOAD",
24
+ "LD_LIBRARY_PATH",
25
+ "DYLD_LIBRARY_PATH",
26
+ "DYLD_INSERT_LIBRARIES"
27
+ ] as const
28
+
29
+ /**
30
+ * Type predicate to check if a string is in the BUILD_ENV_VARS whitelist
31
+ *
32
+ * Note: The type assertion is necessary because TypeScript's type system cannot
33
+ * infer that .includes() on a readonly const array will properly narrow the type.
34
+ * The assertion is safe because we're only widening the type for the includes() check.
35
+ */
36
+ export function isBuildEnvVar(
37
+ key: string
38
+ ): key is (typeof BUILD_ENV_VARS)[number] {
39
+ return (BUILD_ENV_VARS as readonly string[]).includes(key)
40
+ }
41
+
42
+ /**
43
+ * Type predicate to check if a string is in the DANGEROUS_ENV_VARS blacklist
44
+ *
45
+ * Note: The type assertion is necessary because TypeScript's type system cannot
46
+ * infer that .includes() on a readonly const array will properly narrow the type.
47
+ * The assertion is safe because we're only widening the type for the includes() check.
48
+ */
49
+ export function isDangerousEnvVar(
50
+ key: string
51
+ ): key is (typeof DANGEROUS_ENV_VARS)[number] {
52
+ return (DANGEROUS_ENV_VARS as readonly string[]).includes(key)
53
+ }
54
+
55
+ /**
56
+ * Default directory for config exports when using --doctor or file output modes.
57
+ */
58
+ export const DEFAULT_EXPORT_DIR = "shakapacker-config-exports"
59
+
60
+ /**
61
+ * Default config file path for bundler build configurations.
62
+ */
63
+ export const DEFAULT_CONFIG_FILE = "config/shakapacker-builds.yml"
64
+
1
65
  export interface ExportOptions {
2
66
  doctor?: boolean
3
67
  saveDir?: string
@@ -1,6 +1,6 @@
1
+ import { relative, isAbsolute } from "path"
1
2
  import { ConfigMetadata } from "./types"
2
3
  import { getDocForKey } from "./configDocs"
3
- import { relative, isAbsolute } from "path"
4
4
 
5
5
  /**
6
6
  * Serializes webpack/rspack config to YAML format with optional inline documentation.
@@ -8,6 +8,7 @@ import { relative, isAbsolute } from "path"
8
8
  */
9
9
  export class YamlSerializer {
10
10
  private annotate: boolean
11
+
11
12
  private appRoot: string
12
13
 
13
14
  constructor(options: { annotate: boolean; appRoot: string }) {
@@ -18,11 +19,11 @@ export class YamlSerializer {
18
19
  /**
19
20
  * Serialize a config object to YAML string with metadata header
20
21
  */
21
- serialize(config: any, metadata: ConfigMetadata): string {
22
+ serialize(config: unknown, metadata: ConfigMetadata): string {
22
23
  const output: string[] = []
23
24
 
24
25
  // Add metadata header
25
- output.push(this.createHeader(metadata))
26
+ output.push(YamlSerializer.createHeader(metadata))
26
27
  output.push("")
27
28
 
28
29
  // Serialize the config
@@ -31,9 +32,9 @@ export class YamlSerializer {
31
32
  return output.join("\n")
32
33
  }
33
34
 
34
- private createHeader(metadata: ConfigMetadata): string {
35
+ private static createHeader(metadata: ConfigMetadata): string {
35
36
  const lines: string[] = []
36
- lines.push("# " + "=".repeat(77))
37
+ lines.push(`# ${"=".repeat(77)}`)
37
38
  lines.push("# Webpack/Rspack Configuration Export")
38
39
  lines.push(`# Generated: ${metadata.exportedAt}`)
39
40
  lines.push(`# Environment: ${metadata.environment}`)
@@ -42,11 +43,15 @@ export class YamlSerializer {
42
43
  if (metadata.configCount > 1) {
43
44
  lines.push(`# Total Configs: ${metadata.configCount}`)
44
45
  }
45
- lines.push("# " + "=".repeat(77))
46
+ lines.push(`# ${"=".repeat(77)}`)
46
47
  return lines.join("\n")
47
48
  }
48
49
 
49
- private serializeValue(value: any, indent: number, keyPath: string): string {
50
+ private serializeValue(
51
+ value: unknown,
52
+ indent: number,
53
+ keyPath: string
54
+ ): string {
50
55
  if (value === null || value === undefined) {
51
56
  return "null"
52
57
  }
@@ -64,11 +69,28 @@ export class YamlSerializer {
64
69
  }
65
70
 
66
71
  if (typeof value === "function") {
67
- return this.serializeFunction(value)
72
+ return this.serializeFunction(
73
+ value as (...args: unknown[]) => unknown,
74
+ indent
75
+ )
68
76
  }
69
77
 
70
78
  if (value instanceof RegExp) {
71
- return this.serializeString(value.toString())
79
+ // Extract pattern without surrounding slashes: "/pattern/flags" -> "pattern"
80
+ // Include flags as inline comment when present for semantic clarity
81
+ const regexStr = value.toString()
82
+ const lastSlash = regexStr.lastIndexOf("/")
83
+ const pattern = regexStr.slice(1, lastSlash)
84
+ const flags = regexStr.slice(lastSlash + 1)
85
+
86
+ const serializedPattern = this.serializeString(pattern, indent)
87
+
88
+ // Add flags as inline comment if present (e.g., "pattern" # flags: gi)
89
+ if (flags) {
90
+ return `${serializedPattern} # flags: ${flags}`
91
+ }
92
+
93
+ return serializedPattern
72
94
  }
73
95
 
74
96
  if (Array.isArray(value)) {
@@ -76,10 +98,20 @@ export class YamlSerializer {
76
98
  }
77
99
 
78
100
  if (typeof value === "object") {
79
- return this.serializeObject(value, indent, keyPath)
101
+ return this.serializeObject(
102
+ value as Record<string, unknown>,
103
+ indent,
104
+ keyPath
105
+ )
80
106
  }
81
107
 
82
- return String(value)
108
+ // Handle remaining types explicitly
109
+ if (typeof value === "symbol") return value.toString()
110
+ if (typeof value === "bigint") return value.toString()
111
+
112
+ // All remaining types are primitives (string, number, boolean, null, undefined)
113
+ // that String() handles safely - cast to exclude objects since we've already handled them
114
+ return String(value as string | number | boolean | null | undefined)
83
115
  }
84
116
 
85
117
  private serializeString(str: string, indent: number = 0): string {
@@ -90,15 +122,28 @@ export class YamlSerializer {
90
122
  if (cleaned.includes("\n")) {
91
123
  const lines = cleaned.split("\n")
92
124
  const lineIndent = " ".repeat(indent + 2)
93
- return "|\n" + lines.map((line) => lineIndent + line).join("\n")
125
+ return `|\n${lines.map((line) => lineIndent + line).join("\n")}`
94
126
  }
95
127
 
96
- // Escape strings that need quoting
128
+ // Escape strings that need quoting in YAML
129
+ // YAML has many special characters that can cause parsing errors:
130
+ // : # ' " (basic delimiters)
131
+ // [ ] { } (flow collections)
132
+ // * & ! @ ` (special constructs: aliases, anchors, tags)
97
133
  if (
98
134
  cleaned.includes(":") ||
99
135
  cleaned.includes("#") ||
100
136
  cleaned.includes("'") ||
101
137
  cleaned.includes('"') ||
138
+ cleaned.includes("[") ||
139
+ cleaned.includes("]") ||
140
+ cleaned.includes("{") ||
141
+ cleaned.includes("}") ||
142
+ cleaned.includes("*") ||
143
+ cleaned.includes("&") ||
144
+ cleaned.includes("!") ||
145
+ cleaned.includes("@") ||
146
+ cleaned.includes("`") ||
102
147
  cleaned.startsWith(" ") ||
103
148
  cleaned.endsWith(" ")
104
149
  ) {
@@ -109,7 +154,10 @@ export class YamlSerializer {
109
154
  return cleaned
110
155
  }
111
156
 
112
- private serializeFunction(fn: Function): string {
157
+ private serializeFunction(
158
+ fn: (...args: unknown[]) => unknown,
159
+ indent: number = 0
160
+ ): string {
113
161
  // Get function source code
114
162
  const source = fn.toString()
115
163
 
@@ -122,21 +170,24 @@ export class YamlSerializer {
122
170
  const displayLines = truncated ? lines.slice(0, maxLines) : lines
123
171
 
124
172
  // Clean up indentation while preserving structure
125
- const minIndent = Math.min(
126
- ...displayLines
127
- .filter((l) => l.trim().length > 0)
128
- .map((l) => l.match(/^\s*/)?.[0].length || 0)
129
- )
173
+ const indentLevels = displayLines
174
+ .filter((l) => l.trim().length > 0)
175
+ .map((l) => l.match(/^\s*/)?.[0].length || 0)
176
+ const minIndent = indentLevels.length > 0 ? Math.min(...indentLevels) : 0
130
177
 
131
178
  const formatted =
132
179
  displayLines.map((line) => line.substring(minIndent)).join("\n") +
133
180
  (truncated ? "\n..." : "")
134
181
 
135
- // Use serializeString to properly handle multiline
136
- return this.serializeString(formatted)
182
+ // Use serializeString to properly handle multiline with correct indentation
183
+ return this.serializeString(formatted, indent)
137
184
  }
138
185
 
139
- private serializeArray(arr: any[], indent: number, keyPath: string): string {
186
+ private serializeArray(
187
+ arr: unknown[],
188
+ indent: number,
189
+ keyPath: string
190
+ ): string {
140
191
  if (arr.length === 0) {
141
192
  return "[]"
142
193
  }
@@ -149,7 +200,7 @@ export class YamlSerializer {
149
200
  const itemPath = `${keyPath}[${index}]`
150
201
 
151
202
  // Check if this is a plugin object and add its name as a comment
152
- const pluginName = this.getConstructorName(item)
203
+ const pluginName = YamlSerializer.getConstructorName(item)
153
204
  const isPlugin = pluginName && /(^|\.)plugins\[\d+\]/.test(itemPath)
154
205
  const isEmpty =
155
206
  typeof item === "object" &&
@@ -180,11 +231,11 @@ export class YamlSerializer {
180
231
  .split("\n")
181
232
  .filter((line: string) => line.trim().length > 0)
182
233
  // Compute minimum leading whitespace to preserve relative indentation
183
- const minIndent = Math.min(
184
- ...nonEmptyLines.map(
185
- (line: string) => line.match(/^\s*/)?.[0].length || 0
186
- )
234
+ const indentLevels = nonEmptyLines.map(
235
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
187
236
  )
237
+ const minIndent =
238
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
188
239
  nonEmptyLines.forEach((line: string) => {
189
240
  // Remove only the common indent, preserving relative indentation
190
241
  lines.push(contentIndent + line.substring(minIndent))
@@ -196,11 +247,11 @@ export class YamlSerializer {
196
247
  .split("\n")
197
248
  .filter((line: string) => line.trim().length > 0)
198
249
  // Compute minimum leading whitespace to preserve relative indentation
199
- const minIndent = Math.min(
200
- ...nonEmptyLines.map(
201
- (line: string) => line.match(/^\s*/)?.[0].length || 0
202
- )
250
+ const indentLevels = nonEmptyLines.map(
251
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
203
252
  )
253
+ const minIndent =
254
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
204
255
  nonEmptyLines.forEach((line: string) => {
205
256
  // Remove only the common indent, preserving relative indentation
206
257
  lines.push(contentIndent + line.substring(minIndent))
@@ -211,12 +262,16 @@ export class YamlSerializer {
211
262
  }
212
263
  })
213
264
 
214
- return "\n" + lines.join("\n")
265
+ return `\n${lines.join("\n")}`
215
266
  }
216
267
 
217
- private serializeObject(obj: any, indent: number, keyPath: string): string {
218
- const keys = Object.keys(obj)
219
- const constructorName = this.getConstructorName(obj)
268
+ private serializeObject(
269
+ obj: Record<string, unknown>,
270
+ indent: number,
271
+ keyPath: string
272
+ ): string {
273
+ const keys = Object.keys(obj).sort()
274
+ const constructorName = YamlSerializer.getConstructorName(obj)
220
275
 
221
276
  // For empty objects, show constructor name if available
222
277
  if (keys.length === 0) {
@@ -248,6 +303,12 @@ export class YamlSerializer {
248
303
  for (const line of value.split("\n")) {
249
304
  lines.push(`${valueIndent}${line}`)
250
305
  }
306
+ } else if (value instanceof RegExp || typeof value === "function") {
307
+ // Handle RegExp and functions explicitly before the generic object check
308
+ // to prevent them from being treated as empty objects (RegExp/functions
309
+ // have no enumerable keys but should serialize as their string representation)
310
+ const serialized = this.serializeValue(value, indent + 2, fullKeyPath)
311
+ lines.push(`${keyIndent}${key}: ${serialized}`)
251
312
  } else if (
252
313
  typeof value === "object" &&
253
314
  value !== null &&
@@ -258,7 +319,7 @@ export class YamlSerializer {
258
319
  } else {
259
320
  lines.push(`${keyIndent}${key}:`)
260
321
  const nestedLines = this.serializeObject(
261
- value,
322
+ value as Record<string, unknown>,
262
323
  indent + 2,
263
324
  fullKeyPath
264
325
  )
@@ -282,7 +343,6 @@ export class YamlSerializer {
282
343
  }
283
344
 
284
345
  private makePathRelative(str: string): string {
285
- if (typeof str !== "string") return str
286
346
  if (!isAbsolute(str)) return str
287
347
 
288
348
  // Convert absolute paths to relative paths using path.relative
@@ -297,20 +357,35 @@ export class YamlSerializer {
297
357
  return str
298
358
  }
299
359
 
300
- return "./" + rel
360
+ return `./${rel}`
301
361
  }
302
362
 
303
363
  /**
304
364
  * Extracts the constructor name from an object
305
- * Returns null for plain objects (Object constructor)
365
+ * Returns null for plain objects (Object constructor) or objects without prototypes
306
366
  */
307
- private getConstructorName(obj: any): string | null {
367
+ private static getConstructorName(obj: unknown): string | null {
308
368
  if (!obj || typeof obj !== "object") return null
309
369
  if (Array.isArray(obj)) return null
310
370
 
311
- const constructorName = obj.constructor?.name
312
- if (!constructorName || constructorName === "Object") return null
371
+ // Use Object.getPrototypeOf for safer access to constructor
372
+ // This handles Object.create(null) and unusual prototypes correctly
373
+ try {
374
+ const proto = Object.getPrototypeOf(obj) as {
375
+ constructor?: { name?: string }
376
+ } | null
377
+ if (!proto || proto === Object.prototype) return null
313
378
 
314
- return constructorName
379
+ const { constructor } = proto
380
+ if (!constructor || typeof constructor !== "function") return null
381
+
382
+ const constructorName = constructor.name
383
+ if (!constructorName || constructorName === "Object") return null
384
+
385
+ return constructorName
386
+ } catch {
387
+ // Handle frozen objects or other edge cases
388
+ return null
389
+ }
315
390
  }
316
391
  }
@@ -1,7 +1,8 @@
1
1
  // These are the raw shakapacker dev server config settings from the YML file with ENV overrides applied.
2
+ import type { DevServerConfig, Config } from "./types"
3
+
2
4
  const { isBoolean } = require("./utils/helpers")
3
- const config = require("./config")
4
- import { DevServerConfig } from "./types"
5
+ const config = require("./config") as Config
5
6
 
6
7
  const envFetch = (key: string): string | boolean | undefined => {
7
8
  const value = process.env[key]
data/package/env.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { load } from "js-yaml"
2
2
  import { readFileSync } from "fs"
3
+
3
4
  const defaultConfigPath = require("./utils/defaultConfigPath")
4
5
  const configPath = require("./utils/configPath")
5
6
  const { isFileNotFoundError } = require("./utils/errorHelpers")
@@ -50,7 +51,7 @@ try {
50
51
  // File not found, use default configuration
51
52
  try {
52
53
  config = load(readFileSync(defaultConfigPath, "utf8")) as ConfigFile
53
- } catch (defaultError) {
54
+ } catch (_defaultError) {
54
55
  throw new Error(
55
56
  `Failed to load Shakapacker configuration.\n` +
56
57
  `Neither user config (${configPath}) nor default config (${defaultConfigPath}) could be loaded.\n\n` +
@@ -76,7 +77,6 @@ const validatedRailsEnv =
76
77
  initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
77
78
 
78
79
  if (initialRailsEnv && validatedRailsEnv !== initialRailsEnv) {
79
- /* eslint no-console:0 */
80
80
  console.warn(
81
81
  `[SHAKAPACKER WARNING] Environment '${initialRailsEnv}' not found in the configuration.\n` +
82
82
  `Using '${DEFAULT}' configuration as a fallback.`
@@ -9,26 +9,26 @@
9
9
  import type { RspackPlugin, RspackPluginInstance } from "../types"
10
10
 
11
11
  // Test 1: RspackPlugin should be assignable to RspackPluginInstance
12
- const testPluginToInstance = (plugin: RspackPlugin): RspackPluginInstance =>
12
+ const _testPluginToInstance = (plugin: RspackPlugin): RspackPluginInstance =>
13
13
  plugin
14
14
 
15
15
  // Test 2: RspackPluginInstance should be assignable to RspackPlugin
16
- const testInstanceToPlugin = (instance: RspackPluginInstance): RspackPlugin =>
16
+ const _testInstanceToPlugin = (instance: RspackPluginInstance): RspackPlugin =>
17
17
  instance
18
18
 
19
19
  // Test 3: Array compatibility
20
- const testArrayCompatibility = (
20
+ const _testArrayCompatibility = (
21
21
  plugins: RspackPlugin[]
22
22
  ): RspackPluginInstance[] => plugins
23
- const testArrayCompatibilityReverse = (
23
+ const _testArrayCompatibilityReverse = (
24
24
  instances: RspackPluginInstance[]
25
25
  ): RspackPlugin[] => instances
26
26
 
27
27
  // Test 4: Optional parameter compatibility
28
- const testOptionalParam = (
28
+ const _testOptionalParam = (
29
29
  plugin?: RspackPlugin
30
30
  ): RspackPluginInstance | undefined => plugin
31
- const testOptionalParamReverse = (
31
+ const _testOptionalParamReverse = (
32
32
  instance?: RspackPluginInstance
33
33
  ): RspackPlugin | undefined => instance
34
34
 
@@ -1,13 +1,13 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
3
+ import { Dirent } from "fs"
4
+ import type { Configuration, Entry } from "webpack"
5
+ import type { Config } from "../types"
6
+
4
7
  const { basename, dirname, join, relative, resolve } = require("path")
5
8
  const { existsSync, readdirSync } = require("fs")
6
- import { Dirent } from "fs"
7
9
  const extname = require("path-complete-extname")
8
- // @ts-ignore: webpack is an optional peer dependency (using type-only import)
9
- import type { Configuration, Entry } from "webpack"
10
- const config = require("../config")
10
+ const config = require("../config") as Config
11
11
  const { isProduction } = require("../env")
12
12
 
13
13
  const pluginsPath = resolve(
@@ -73,7 +73,7 @@ const getEntryObject = (): Entry => {
73
73
  const previousPaths = entries[name]
74
74
  if (previousPaths) {
75
75
  const pathArray = Array.isArray(previousPaths)
76
- ? (previousPaths as string[])
76
+ ? previousPaths
77
77
  : [previousPaths as string]
78
78
  pathArray.push(assetPath)
79
79
  entries[name] = pathArray
@@ -3,18 +3,18 @@
3
3
  * @module environments/development
4
4
  */
5
5
 
6
+ import type {
7
+ WebpackConfigWithDevServer,
8
+ RspackConfigWithDevServer
9
+ } from "./types"
10
+ import type { Config } from "../types"
11
+
6
12
  const { merge } = require("webpack-merge")
7
- const config = require("../config")
13
+ const config = require("../config") as Config
8
14
  const baseConfig = require("./base")
9
15
  const webpackDevServerConfig = require("../webpackDevServerConfig")
10
16
  const { runningWebpackDevServer } = require("../env")
11
17
  const { moduleExists } = require("../utils/helpers")
12
- import type {
13
- WebpackConfigWithDevServer,
14
- RspackConfigWithDevServer,
15
- ReactRefreshWebpackPlugin,
16
- ReactRefreshRspackPlugin
17
- } from "./types"
18
18
 
19
19
  /**
20
20
  * Base development configuration shared between webpack and rspack
@@ -40,7 +40,6 @@ const webpackDevConfig = (): WebpackConfigWithDevServer => {
40
40
  devServerConfig.hot &&
41
41
  moduleExists("@pmmmwh/react-refresh-webpack-plugin")
42
42
  ) {
43
- // eslint-disable-next-line global-require
44
43
  const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")
45
44
  webpackConfig.plugins = [
46
45
  ...(webpackConfig.plugins || []),
@@ -73,7 +72,6 @@ const rspackDevConfig = (): RspackConfigWithDevServer => {
73
72
  devServerConfig.hot &&
74
73
  moduleExists("@rspack/plugin-react-refresh")
75
74
  ) {
76
- // eslint-disable-next-line global-require
77
75
  const ReactRefreshPlugin = require("@rspack/plugin-react-refresh")
78
76
  rspackConfig.plugins = [
79
77
  ...(rspackConfig.plugins || []),