shakapacker 9.3.0.beta.6 → 9.3.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -105
  3. data/ESLINT_TECHNICAL_DEBT.md +8 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +53 -2
  6. data/docs/configuration.md +28 -0
  7. data/docs/rspack_migration_guide.md +238 -2
  8. data/docs/troubleshooting.md +21 -21
  9. data/eslint.config.fast.js +8 -0
  10. data/eslint.config.js +47 -10
  11. data/knip.ts +8 -1
  12. data/lib/install/config/shakapacker.yml +6 -6
  13. data/lib/shakapacker/configuration.rb +227 -4
  14. data/lib/shakapacker/dev_server.rb +88 -1
  15. data/lib/shakapacker/doctor.rb +129 -72
  16. data/lib/shakapacker/instance.rb +85 -1
  17. data/lib/shakapacker/manifest.rb +85 -11
  18. data/lib/shakapacker/runner.rb +12 -8
  19. data/lib/shakapacker/swc_migrator.rb +7 -7
  20. data/lib/shakapacker/version.rb +1 -1
  21. data/lib/shakapacker.rb +143 -3
  22. data/lib/tasks/shakapacker/doctor.rake +1 -1
  23. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  24. data/package/config.ts +0 -1
  25. data/package/configExporter/buildValidator.ts +53 -29
  26. data/package/configExporter/cli.ts +152 -118
  27. data/package/configExporter/configFile.ts +33 -26
  28. data/package/configExporter/fileWriter.ts +3 -3
  29. data/package/configExporter/types.ts +64 -0
  30. data/package/configExporter/yamlSerializer.ts +147 -36
  31. data/package/dev_server.ts +2 -1
  32. data/package/env.ts +1 -1
  33. data/package/environments/base.ts +4 -4
  34. data/package/environments/development.ts +7 -6
  35. data/package/environments/production.ts +6 -7
  36. data/package/environments/test.ts +2 -1
  37. data/package/index.ts +28 -4
  38. data/package/loaders.d.ts +2 -2
  39. data/package/optimization/webpack.ts +29 -31
  40. data/package/plugins/webpack.ts +2 -1
  41. data/package/rspack/index.ts +2 -1
  42. data/package/rules/file.ts +1 -0
  43. data/package/rules/jscommon.ts +1 -0
  44. data/package/utils/helpers.ts +0 -1
  45. data/package/utils/pathValidation.ts +68 -7
  46. data/package/utils/requireOrError.ts +10 -2
  47. data/package/utils/typeGuards.ts +43 -46
  48. data/package/webpack-types.d.ts +2 -2
  49. data/package/webpackDevServerConfig.ts +1 -0
  50. data/package.json +2 -3
  51. data/test/configExporter/integration.test.js +8 -8
  52. data/test/package/configExporter/cli.test.js +440 -0
  53. data/test/package/configExporter/types.test.js +163 -0
  54. data/test/package/configExporter.test.js +271 -7
  55. data/test/package/yamlSerializer.test.js +204 -0
  56. data/test/typescript/pathValidation.test.js +44 -0
  57. data/test/typescript/requireOrError.test.js +49 -0
  58. data/yarn.lock +0 -32
  59. metadata +11 -6
  60. data/.eslintrc.fast.js +0 -40
  61. data/.eslintrc.js +0 -84
  62. data/package-lock.json +0 -13047
@@ -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
 
@@ -56,12 +56,12 @@ export class FileWriter {
56
56
  * @example
57
57
  * // Built-in types
58
58
  * generateFilename("webpack", "development", "client", "yaml")
59
- * // => "webpack-development-client.yaml"
59
+ * // => "webpack-development-client.yml"
60
60
  *
61
61
  * @example
62
62
  * // Custom output names
63
63
  * generateFilename("webpack", "development", "client-modern", "yaml", "dev-hmr")
64
- * // => "webpack-dev-hmr-client-modern.yaml"
64
+ * // => "webpack-dev-hmr-client-modern.yml"
65
65
  */
66
66
  static generateFilename(
67
67
  bundler: string,
@@ -72,7 +72,7 @@ export class FileWriter {
72
72
  ): string {
73
73
  let ext: string
74
74
  if (format === "yaml") {
75
- ext = "yaml"
75
+ ext = "yml"
76
76
  } else if (format === "json") {
77
77
  ext = "json"
78
78
  } else {
@@ -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
  }
@@ -147,6 +198,22 @@ export class YamlSerializer {
147
198
 
148
199
  arr.forEach((item, index) => {
149
200
  const itemPath = `${keyPath}[${index}]`
201
+
202
+ // Check if this is a plugin object and add its name as a comment
203
+ const pluginName = YamlSerializer.getConstructorName(item)
204
+ const isPlugin = pluginName && /(^|\.)plugins\[\d+\]/.test(itemPath)
205
+ const isEmpty =
206
+ typeof item === "object" &&
207
+ item !== null &&
208
+ !Array.isArray(item) &&
209
+ Object.keys(item).length === 0
210
+
211
+ // For non-empty plugins, add comment before the plugin
212
+ // For empty plugins, the name will be shown inline
213
+ if (isPlugin && !isEmpty) {
214
+ lines.push(`${itemIndent}# ${pluginName}`)
215
+ }
216
+
150
217
  const serialized = this.serializeValue(item, indent + 4, itemPath)
151
218
 
152
219
  // Add documentation for array items if available
@@ -164,11 +231,11 @@ export class YamlSerializer {
164
231
  .split("\n")
165
232
  .filter((line: string) => line.trim().length > 0)
166
233
  // Compute minimum leading whitespace to preserve relative indentation
167
- const minIndent = Math.min(
168
- ...nonEmptyLines.map(
169
- (line: string) => line.match(/^\s*/)?.[0].length || 0
170
- )
234
+ const indentLevels = nonEmptyLines.map(
235
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
171
236
  )
237
+ const minIndent =
238
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
172
239
  nonEmptyLines.forEach((line: string) => {
173
240
  // Remove only the common indent, preserving relative indentation
174
241
  lines.push(contentIndent + line.substring(minIndent))
@@ -180,11 +247,11 @@ export class YamlSerializer {
180
247
  .split("\n")
181
248
  .filter((line: string) => line.trim().length > 0)
182
249
  // 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
- )
250
+ const indentLevels = nonEmptyLines.map(
251
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
187
252
  )
253
+ const minIndent =
254
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
188
255
  nonEmptyLines.forEach((line: string) => {
189
256
  // Remove only the common indent, preserving relative indentation
190
257
  lines.push(contentIndent + line.substring(minIndent))
@@ -195,12 +262,22 @@ export class YamlSerializer {
195
262
  }
196
263
  })
197
264
 
198
- return "\n" + lines.join("\n")
265
+ return `\n${lines.join("\n")}`
199
266
  }
200
267
 
201
- private serializeObject(obj: any, indent: number, keyPath: string): string {
202
- const keys = Object.keys(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)
275
+
276
+ // For empty objects, show constructor name if available
203
277
  if (keys.length === 0) {
278
+ if (constructorName) {
279
+ return `{} # ${constructorName}`
280
+ }
204
281
  return "{}"
205
282
  }
206
283
 
@@ -226,6 +303,12 @@ export class YamlSerializer {
226
303
  for (const line of value.split("\n")) {
227
304
  lines.push(`${valueIndent}${line}`)
228
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}`)
229
312
  } else if (
230
313
  typeof value === "object" &&
231
314
  value !== null &&
@@ -236,7 +319,7 @@ export class YamlSerializer {
236
319
  } else {
237
320
  lines.push(`${keyIndent}${key}:`)
238
321
  const nestedLines = this.serializeObject(
239
- value,
322
+ value as Record<string, unknown>,
240
323
  indent + 2,
241
324
  fullKeyPath
242
325
  )
@@ -260,7 +343,6 @@ export class YamlSerializer {
260
343
  }
261
344
 
262
345
  private makePathRelative(str: string): string {
263
- if (typeof str !== "string") return str
264
346
  if (!isAbsolute(str)) return str
265
347
 
266
348
  // Convert absolute paths to relative paths using path.relative
@@ -275,6 +357,35 @@ export class YamlSerializer {
275
357
  return str
276
358
  }
277
359
 
278
- return "./" + rel
360
+ return `./${rel}`
361
+ }
362
+
363
+ /**
364
+ * Extracts the constructor name from an object
365
+ * Returns null for plain objects (Object constructor) or objects without prototypes
366
+ */
367
+ private static getConstructorName(obj: unknown): string | null {
368
+ if (!obj || typeof obj !== "object") return null
369
+ if (Array.isArray(obj)) return null
370
+
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
378
+
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
+ }
279
390
  }
280
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 { DevServerConfig } from "./types"
3
+
2
4
  const { isBoolean } = require("./utils/helpers")
3
5
  const config = require("./config")
4
- import { DevServerConfig } from "./types"
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")
@@ -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.`
@@ -1,12 +1,12 @@
1
1
  /* eslint global-require: 0 */
2
2
  /* eslint import/no-dynamic-require: 0 */
3
3
 
4
+ import { Dirent } from "fs"
5
+ import type { Configuration, Entry } from "webpack"
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
10
  const config = require("../config")
11
11
  const { isProduction } = require("../env")
12
12
 
@@ -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,12 +3,6 @@
3
3
  * @module environments/development
4
4
  */
5
5
 
6
- const { merge } = require("webpack-merge")
7
- const config = require("../config")
8
- const baseConfig = require("./base")
9
- const webpackDevServerConfig = require("../webpackDevServerConfig")
10
- const { runningWebpackDevServer } = require("../env")
11
- const { moduleExists } = require("../utils/helpers")
12
6
  import type {
13
7
  WebpackConfigWithDevServer,
14
8
  RspackConfigWithDevServer,
@@ -16,6 +10,13 @@ import type {
16
10
  ReactRefreshRspackPlugin
17
11
  } from "./types"
18
12
 
13
+ const { merge } = require("webpack-merge")
14
+ const config = require("../config")
15
+ const baseConfig = require("./base")
16
+ const webpackDevServerConfig = require("../webpackDevServerConfig")
17
+ const { runningWebpackDevServer } = require("../env")
18
+ const { moduleExists } = require("../utils/helpers")
19
+
19
20
  /**
20
21
  * Base development configuration shared between webpack and rspack
21
22
  */