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
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process"
2
2
  import { existsSync } from "fs"
3
- import { resolve, relative, sep } from "path"
3
+ import { resolve, relative } from "path"
4
4
  import { ResolvedBuildConfig, BuildValidationResult } from "./types"
5
5
 
6
6
  export interface ValidatorOptions {
@@ -173,7 +173,7 @@ export class BuildValidator {
173
173
  * @returns The resolved absolute path to the config file
174
174
  * @throws Error if the config file does not exist or is outside appRoot
175
175
  */
176
- private validateConfigPath(
176
+ private static validateConfigPath(
177
177
  configFile: string,
178
178
  appRoot: string,
179
179
  buildName: string
@@ -222,7 +222,7 @@ export class BuildValidator {
222
222
  const isHMR =
223
223
  build.environment.WEBPACK_SERVE === "true" ||
224
224
  build.environment.HMR === "true"
225
- const bundler = build.bundler
225
+ const { bundler } = build
226
226
 
227
227
  if (isHMR) {
228
228
  return this.validateHMRBuild(build, appRoot, bundler)
@@ -273,7 +273,7 @@ export class BuildValidator {
273
273
  // Add config file if specified
274
274
  if (build.configFile) {
275
275
  try {
276
- const configPath = this.validateConfigPath(
276
+ const configPath = BuildValidator.validateConfigPath(
277
277
  build.configFile,
278
278
  appRoot,
279
279
  build.name
@@ -301,7 +301,7 @@ export class BuildValidator {
301
301
  args.push(...build.bundlerEnvArgs)
302
302
  }
303
303
 
304
- return new Promise((resolve) => {
304
+ return new Promise((resolvePromise) => {
305
305
  const child = spawn(devServerBin, args, {
306
306
  cwd: appRoot,
307
307
  env: this.filterEnvironment(build.environment),
@@ -316,7 +316,7 @@ export class BuildValidator {
316
316
  const resolveOnce = (res: BuildValidationResult) => {
317
317
  if (!resolved) {
318
318
  resolved = true
319
- resolve(res)
319
+ resolvePromise(res)
320
320
  }
321
321
  }
322
322
 
@@ -397,8 +397,8 @@ export class BuildValidator {
397
397
  })
398
398
  }
399
399
 
400
- child.stdout?.on("data", (data) => processOutput(data))
401
- child.stderr?.on("data", (data) => processOutput(data))
400
+ child.stdout?.on("data", (data: Buffer) => processOutput(data))
401
+ child.stderr?.on("data", (data: Buffer) => processOutput(data))
402
402
 
403
403
  child.on("exit", (code) => {
404
404
  clearTimeout(timeoutId)
@@ -428,7 +428,7 @@ export class BuildValidator {
428
428
 
429
429
  // Check for specific error codes and provide actionable guidance
430
430
  if ("code" in err) {
431
- const code = (err as NodeJS.ErrnoException).code
431
+ const { code } = err as NodeJS.ErrnoException
432
432
  if (code === "ENOENT") {
433
433
  errorMessage += `. Binary not found. Install with: npm install -D ${devServerCmd}`
434
434
  } else if (code === "EMFILE" || code === "ENFILE") {
@@ -484,7 +484,7 @@ export class BuildValidator {
484
484
  // Add config file if specified
485
485
  if (build.configFile) {
486
486
  try {
487
- const configPath = this.validateConfigPath(
487
+ const configPath = BuildValidator.validateConfigPath(
488
488
  build.configFile,
489
489
  appRoot,
490
490
  build.name
@@ -515,7 +515,7 @@ export class BuildValidator {
515
515
  // Add --json for structured output (helps parse errors)
516
516
  args.push("--json")
517
517
 
518
- return new Promise((resolve) => {
518
+ return new Promise((resolvePromise) => {
519
519
  const child = spawn(bundlerBin, args, {
520
520
  cwd: appRoot,
521
521
  env: this.filterEnvironment(build.environment),
@@ -534,7 +534,7 @@ export class BuildValidator {
534
534
  `Timeout: ${bundler} did not complete within ${this.options.timeout}ms.`
535
535
  )
536
536
  child.kill("SIGTERM")
537
- resolve(result)
537
+ resolvePromise(result)
538
538
  }, this.options.timeout)
539
539
 
540
540
  child.stdout?.on("data", (data: Buffer) => {
@@ -603,7 +603,7 @@ export class BuildValidator {
603
603
 
604
604
  // Parse JSON output
605
605
  try {
606
- const jsonOutput: WebpackJsonOutput = JSON.parse(stdoutData)
606
+ const jsonOutput = JSON.parse(stdoutData) as WebpackJsonOutput
607
607
 
608
608
  // Extract output path if available
609
609
  if (jsonOutput.outputPath) {
@@ -613,10 +613,22 @@ export class BuildValidator {
613
613
  // Check for errors in webpack/rspack JSON output
614
614
  if (jsonOutput.errors && jsonOutput.errors.length > 0) {
615
615
  jsonOutput.errors.forEach((error) => {
616
- const errorMsg =
617
- typeof error === "string"
618
- ? error
619
- : error.message || String(error)
616
+ let errorMsg: string
617
+ if (typeof error === "string") {
618
+ errorMsg = error
619
+ } else if (error.message) {
620
+ errorMsg = error.message
621
+ } else {
622
+ // Attempt to extract useful info from malformed error using all enumerable props
623
+ try {
624
+ errorMsg = JSON.stringify(
625
+ error,
626
+ Object.getOwnPropertyNames(error)
627
+ )
628
+ } catch {
629
+ errorMsg = "[Error object with no message]"
630
+ }
631
+ }
620
632
  result.errors.push(errorMsg)
621
633
  // Also add to output for visibility
622
634
  if (!this.options.verbose) {
@@ -628,10 +640,22 @@ export class BuildValidator {
628
640
  // Check for warnings
629
641
  if (jsonOutput.warnings && jsonOutput.warnings.length > 0) {
630
642
  jsonOutput.warnings.forEach((warning) => {
631
- const warningMsg =
632
- typeof warning === "string"
633
- ? warning
634
- : warning.message || String(warning)
643
+ let warningMsg: string
644
+ if (typeof warning === "string") {
645
+ warningMsg = warning
646
+ } else if (warning.message) {
647
+ warningMsg = warning.message
648
+ } else {
649
+ // Attempt to extract useful info from malformed warning using all enumerable props
650
+ try {
651
+ warningMsg = JSON.stringify(
652
+ warning,
653
+ Object.getOwnPropertyNames(warning)
654
+ )
655
+ } catch {
656
+ warningMsg = "[Warning object with no message]"
657
+ }
658
+ }
635
659
  result.warnings.push(warningMsg)
636
660
  })
637
661
  }
@@ -683,7 +707,7 @@ export class BuildValidator {
683
707
  result.output.push(stderrData)
684
708
  }
685
709
 
686
- resolve(result)
710
+ resolvePromise(result)
687
711
  })
688
712
 
689
713
  child.on("error", (err) => {
@@ -693,7 +717,7 @@ export class BuildValidator {
693
717
 
694
718
  // Check for specific error codes and provide actionable guidance
695
719
  if ("code" in err) {
696
- const code = (err as NodeJS.ErrnoException).code
720
+ const { code } = err as NodeJS.ErrnoException
697
721
  if (code === "ENOENT") {
698
722
  errorMessage += `. Binary not found. Install with: npm install -D ${bundler}`
699
723
  } else if (code === "EMFILE" || code === "ENFILE") {
@@ -704,7 +728,7 @@ export class BuildValidator {
704
728
  }
705
729
 
706
730
  result.errors.push(errorMessage)
707
- resolve(result)
731
+ resolvePromise(result)
708
732
  })
709
733
  })
710
734
  }
@@ -776,19 +800,19 @@ export class BuildValidator {
776
800
  formatResults(results: BuildValidationResult[]): string {
777
801
  const lines: string[] = []
778
802
 
779
- lines.push("\n" + "=".repeat(80))
803
+ lines.push(`\n${"=".repeat(80)}`)
780
804
  lines.push("🔍 Build Validation Results")
781
- lines.push("=".repeat(80) + "\n")
805
+ lines.push(`${"=".repeat(80)}\n`)
782
806
 
783
- let totalBuilds = results.length
807
+ const totalBuilds = results.length
784
808
  let successCount = 0
785
809
  let failureCount = 0
786
810
 
787
811
  results.forEach((result) => {
788
812
  if (result.success) {
789
- successCount++
813
+ successCount += 1
790
814
  } else {
791
- failureCount++
815
+ failureCount += 1
792
816
  }
793
817
 
794
818
  const icon = result.success ? "✅" : "❌"
@@ -1,35 +1,40 @@
1
1
  // This will be a substantial file - the main CLI entry point
2
- // Migrating from bin/export-bundler-config but streamlined for TypeScript
2
+ // Originally migrated from bin/export-bundler-config, now bin/shakapacker-config
3
3
 
4
4
  import { existsSync, readFileSync, writeFileSync } from "fs"
5
5
  import { resolve, dirname, sep, delimiter, basename } from "path"
6
6
  import { inspect } from "util"
7
7
  import { load as loadYaml } from "js-yaml"
8
8
  import yargs from "yargs"
9
- import { ExportOptions, ConfigMetadata, FileOutput } from "./types"
9
+ import {
10
+ ExportOptions,
11
+ ConfigMetadata,
12
+ FileOutput,
13
+ BUILD_ENV_VARS,
14
+ isBuildEnvVar,
15
+ isDangerousEnvVar,
16
+ DEFAULT_EXPORT_DIR,
17
+ DEFAULT_CONFIG_FILE
18
+ } from "./types"
10
19
  import { YamlSerializer } from "./yamlSerializer"
11
20
  import { FileWriter } from "./fileWriter"
12
21
  import { ConfigFileLoader, generateSampleConfigFile } from "./configFile"
13
22
  import { BuildValidator } from "./buildValidator"
23
+ import { safeResolvePath } from "../utils/pathValidation"
14
24
 
15
25
  // Read version from package.json
16
- const packageJson = JSON.parse(
17
- readFileSync(resolve(__dirname, "../../package.json"), "utf8")
18
- )
19
- const VERSION = packageJson.version
20
-
21
- /**
22
- * Environment variable names that can be set by build configurations
23
- */
24
- const BUILD_ENV_VARS = [
25
- "NODE_ENV",
26
- "RAILS_ENV",
27
- "NODE_OPTIONS",
28
- "BABEL_ENV",
29
- "WEBPACK_SERVE",
30
- "CLIENT_BUNDLE_ONLY",
31
- "SERVER_BUNDLE_ONLY"
32
- ] as const
26
+ let VERSION = "unknown"
27
+ try {
28
+ const packageJson = JSON.parse(
29
+ readFileSync(resolve(__dirname, "../../package.json"), "utf8")
30
+ ) as { version?: string }
31
+ VERSION = packageJson.version || "unknown"
32
+ } catch (error) {
33
+ console.warn(
34
+ "Could not read version from package.json:",
35
+ error instanceof Error ? error.message : String(error)
36
+ )
37
+ }
33
38
 
34
39
  /**
35
40
  * Saves current values of build environment variables for later restoration
@@ -103,6 +108,15 @@ export async function run(args: string[]): Promise<number> {
103
108
  // Apply defaults
104
109
  const resolvedOptions = applyDefaults(options)
105
110
 
111
+ // Validate paths for security AFTER defaults are applied
112
+ // Use safeResolvePath which validates and resolves symlinks
113
+ if (resolvedOptions.output) {
114
+ safeResolvePath(appRoot, resolvedOptions.output)
115
+ }
116
+ if (resolvedOptions.saveDir) {
117
+ safeResolvePath(appRoot, resolvedOptions.saveDir)
118
+ }
119
+
106
120
  // Validate after defaults are applied
107
121
  if (resolvedOptions.annotate && resolvedOptions.format !== "yaml") {
108
122
  throw new Error(
@@ -114,8 +128,7 @@ export async function run(args: string[]): Promise<number> {
114
128
  if (resolvedOptions.build) {
115
129
  const loader = new ConfigFileLoader(resolvedOptions.configFile)
116
130
  if (!loader.exists()) {
117
- const configPath =
118
- resolvedOptions.configFile || "config/shakapacker-builds.yml"
131
+ const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
119
132
  throw new Error(
120
133
  `--build requires a config file but ${configPath} not found. Run --init to create it.`
121
134
  )
@@ -144,7 +157,7 @@ export async function run(args: string[]): Promise<number> {
144
157
  }
145
158
  }
146
159
 
147
- function parseArguments(args: string[]): ExportOptions {
160
+ export function parseArguments(args: string[]): ExportOptions {
148
161
  const argv = yargs(args)
149
162
  .version(VERSION)
150
163
  .usage(
@@ -164,8 +177,7 @@ QUICK START (for troubleshooting):
164
177
  .option("init", {
165
178
  type: "boolean",
166
179
  default: false,
167
- description:
168
- "Generate config/shakapacker-builds.yml (use with --ssr for SSR builds)"
180
+ description: `Generate ${DEFAULT_CONFIG_FILE} (use with --ssr for SSR builds)`
169
181
  })
170
182
  .option("ssr", {
171
183
  type: "boolean",
@@ -188,8 +200,7 @@ QUICK START (for troubleshooting):
188
200
  })
189
201
  .option("config-file", {
190
202
  type: "string",
191
- description:
192
- "Path to config file (default: config/shakapacker-builds.yml)"
203
+ description: `Path to config file (default: ${DEFAULT_CONFIG_FILE})`
193
204
  })
194
205
  // Validation Options
195
206
  .option("validate", {
@@ -235,11 +246,26 @@ QUICK START (for troubleshooting):
235
246
  "Enable inline documentation (YAML only, default with --doctor or file output)"
236
247
  })
237
248
  .option("depth", {
238
- type: "number",
249
+ // Note: type omitted to allow string "null" (yargs would reject it).
250
+ // Coerce function handles validation for both numbers and "null".
239
251
  default: 20,
240
252
  coerce: (value: number | string) => {
253
+ // Handle "null" string for unlimited depth
241
254
  if (value === "null" || value === null) return null
242
- return typeof value === "number" ? value : parseInt(String(value), 10)
255
+
256
+ // Reject non-numeric types (arrays, objects, etc.)
257
+ if (typeof value !== "number" && typeof value !== "string") {
258
+ throw new Error(
259
+ `--depth must be a number or 'null', got: ${typeof value}`
260
+ )
261
+ }
262
+
263
+ const parsed =
264
+ typeof value === "number" ? value : parseInt(String(value), 10)
265
+ if (Number.isNaN(parsed)) {
266
+ throw new Error(`--depth must be a number or 'null', got: ${value}`)
267
+ }
268
+ return parsed
243
269
  },
244
270
  description: "Inspection depth (use 'null' for unlimited)"
245
271
  })
@@ -283,43 +309,61 @@ QUICK START (for troubleshooting):
283
309
  description:
284
310
  "Generate only server config (fallback when no config file exists)"
285
311
  })
286
- .check((argv) => {
287
- if (argv.webpack && argv.rspack) {
312
+ .check((parsedArgs) => {
313
+ if (parsedArgs.webpack && parsedArgs.rspack) {
288
314
  throw new Error(
289
315
  "--webpack and --rspack are mutually exclusive. Please specify only one."
290
316
  )
291
317
  }
292
- if (argv["client-only"] && argv["server-only"]) {
318
+ if (parsedArgs["client-only"] && parsedArgs["server-only"]) {
293
319
  throw new Error(
294
320
  "--client-only and --server-only are mutually exclusive. Please specify only one."
295
321
  )
296
322
  }
297
- if (argv.output && argv["save-dir"]) {
323
+ if (parsedArgs.output && parsedArgs["save-dir"]) {
298
324
  throw new Error(
299
325
  "--output and --save-dir are mutually exclusive. Use one or the other."
300
326
  )
301
327
  }
302
- if (argv.stdout && argv["save-dir"]) {
328
+ if (parsedArgs.stdout && parsedArgs["save-dir"]) {
303
329
  throw new Error(
304
330
  "--stdout and --save-dir are mutually exclusive. Use one or the other."
305
331
  )
306
332
  }
307
- if (argv.build && argv["all-builds"]) {
333
+ if (parsedArgs.build && parsedArgs["all-builds"]) {
308
334
  throw new Error(
309
335
  "--build and --all-builds are mutually exclusive. Use one or the other."
310
336
  )
311
337
  }
312
- if (argv.validate && argv["validate-build"]) {
338
+ if (parsedArgs.validate && parsedArgs["validate-build"]) {
313
339
  throw new Error(
314
340
  "--validate and --validate-build are mutually exclusive. Use one or the other."
315
341
  )
316
342
  }
317
- if (argv.validate && (argv.build || argv["all-builds"])) {
343
+ if (
344
+ parsedArgs.validate &&
345
+ (parsedArgs.build || parsedArgs["all-builds"])
346
+ ) {
318
347
  throw new Error(
319
348
  "--validate cannot be used with --build or --all-builds."
320
349
  )
321
350
  }
322
- if (argv.ssr && !argv.init) {
351
+ if (parsedArgs["all-builds"] && parsedArgs.output) {
352
+ throw new Error(
353
+ "--all-builds and --output are mutually exclusive. Use --save-dir instead."
354
+ )
355
+ }
356
+ if (parsedArgs["all-builds"] && parsedArgs.stdout) {
357
+ throw new Error(
358
+ "--all-builds and --stdout are mutually exclusive. Use --save-dir instead."
359
+ )
360
+ }
361
+ if (parsedArgs.stdout && parsedArgs.output) {
362
+ throw new Error(
363
+ "--stdout and --output are mutually exclusive. Use one or the other."
364
+ )
365
+ }
366
+ if (parsedArgs.ssr && !parsedArgs.init) {
323
367
  throw new Error(
324
368
  "--ssr can only be used with --init. Use: bin/shakapacker-config --init --ssr"
325
369
  )
@@ -358,21 +402,18 @@ QUICK START (for troubleshooting):
358
402
 
359
403
  // Type assertions are safe here because yargs validates choices at runtime
360
404
  // Handle --webpack and --rspack flags
361
- let bundler: "webpack" | "rspack" | undefined = argv.bundler as
362
- | "webpack"
363
- | "rspack"
364
- | undefined
405
+ let { bundler } = argv
365
406
  if (argv.webpack) bundler = "webpack"
366
407
  if (argv.rspack) bundler = "rspack"
367
408
 
368
409
  return {
369
410
  bundler,
370
- env: argv.env as "development" | "production" | "test" | undefined,
411
+ env: argv.env,
371
412
  clientOnly: argv["client-only"],
372
413
  serverOnly: argv["server-only"],
373
414
  output: argv.output,
374
- depth: argv.depth as number | null,
375
- format: argv.format as "yaml" | "json" | "inspect" | undefined,
415
+ depth: argv.depth,
416
+ format: argv.format,
376
417
  help: false, // yargs handles help internally
377
418
  verbose: argv.verbose,
378
419
  doctor: argv.doctor,
@@ -411,17 +452,14 @@ function applyDefaults(options: ExportOptions): ExportOptions {
411
452
  !updatedOptions.output &&
412
453
  !updatedOptions.saveDir
413
454
  ) {
414
- updatedOptions.saveDir = resolve(
415
- process.cwd(),
416
- "shakapacker-config-exports"
417
- )
455
+ updatedOptions.saveDir = resolve(process.cwd(), DEFAULT_EXPORT_DIR)
418
456
  }
419
457
 
420
458
  return updatedOptions
421
459
  }
422
460
 
423
461
  function runInitCommand(options: ExportOptions): number {
424
- const configPath = options.configFile || "config/shakapacker-builds.yml"
462
+ const configPath = options.configFile || DEFAULT_CONFIG_FILE
425
463
  const fullPath = resolve(process.cwd(), configPath)
426
464
 
427
465
  // Check if SSR variant is requested via --ssr flag
@@ -504,7 +542,7 @@ end
504
542
  // Make executable
505
543
  try {
506
544
  chmodSync(binStubPath, 0o755)
507
- } catch (e) {
545
+ } catch (_e) {
508
546
  // chmod might fail on some systems, but mode in writeFileSync should handle it
509
547
  }
510
548
  }
@@ -528,7 +566,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
528
566
  // Validate that config file exists
529
567
  const loader = new ConfigFileLoader(options.configFile)
530
568
  if (!loader.exists()) {
531
- const configPath = options.configFile || "config/shakapacker-builds.yml"
569
+ const configPath = options.configFile || DEFAULT_CONFIG_FILE
532
570
  throw new Error(
533
571
  `Config file ${configPath} not found. Run --init to create it.`
534
572
  )
@@ -561,7 +599,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
561
599
  // Handle empty builds edge case
562
600
  if (buildsToValidate.length === 0) {
563
601
  throw new Error(
564
- `No builds found in config file. Add at least one build to config/shakapacker-builds.yml or run --init to see examples.`
602
+ `No builds found in config file. Add at least one build to ${DEFAULT_CONFIG_FILE} or run --init to see examples.`
565
603
  )
566
604
  }
567
605
  }
@@ -611,6 +649,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
611
649
  "development"
612
650
 
613
651
  // Auto-detect bundler using the build's environment
652
+ // eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
614
653
  const defaultBundler = await autoDetectBundler(
615
654
  buildEnv,
616
655
  appRoot,
@@ -625,6 +664,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
625
664
  )
626
665
 
627
666
  // Validate the build
667
+ // eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
628
668
  const result = await validator.validateBuild(resolvedBuild, appRoot)
629
669
  results.push(result)
630
670
 
@@ -674,8 +714,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
674
714
 
675
715
  const loader = new ConfigFileLoader(resolvedOptions.configFile)
676
716
  if (!loader.exists()) {
677
- const configPath =
678
- resolvedOptions.configFile || "config/shakapacker-builds.yml"
717
+ const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
679
718
  throw new Error(
680
719
  `Config file ${configPath} not found. Run --init to create it.`
681
720
  )
@@ -704,6 +743,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
704
743
 
705
744
  // Create a modified options object for this build
706
745
  const buildOptions = { ...resolvedOptions, build: buildName }
746
+ // eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
707
747
  const configs = await loadConfigsForEnv(undefined, buildOptions, appRoot)
708
748
 
709
749
  for (const { config: cfg, metadata } of configs) {
@@ -762,7 +802,7 @@ async function runDoctorMode(
762
802
  const createdFiles: string[] = []
763
803
 
764
804
  // Check if config file exists - always use it for doctor mode
765
- const configFilePath = options.configFile || "config/shakapacker-builds.yml"
805
+ const configFilePath = options.configFile || DEFAULT_CONFIG_FILE
766
806
  const loader = new ConfigFileLoader(configFilePath)
767
807
 
768
808
  if (loader.exists()) {
@@ -783,6 +823,7 @@ async function runDoctorMode(
783
823
  // Clear shakapacker config cache between builds
784
824
  shakapackerConfigCache = null
785
825
 
826
+ // eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
786
827
  const configs = await loadConfigsForEnv(
787
828
  undefined,
788
829
  { ...options, build: buildName },
@@ -850,6 +891,7 @@ async function runDoctorMode(
850
891
  process.env.WEBPACK_SERVE = "true"
851
892
  }
852
893
 
894
+ // eslint-disable-next-line no-await-in-loop -- Sequential execution required: each config modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
853
895
  const configs = await loadConfigsForEnv(env, options, appRoot)
854
896
 
855
897
  for (const { config, metadata } of configs) {
@@ -888,7 +930,6 @@ async function runDoctorMode(
888
930
  }
889
931
 
890
932
  const fullPath = resolve(targetDir, filename)
891
- const fileOutput: FileOutput = { filename, content: output, metadata }
892
933
  FileWriter.writeSingleFile(fullPath, output)
893
934
  createdFiles.push(fullPath)
894
935
  }
@@ -1054,15 +1095,6 @@ async function loadConfigsForEnv(
1054
1095
 
1055
1096
  // Set environment variables from config
1056
1097
  // Security: Only allow specific environment variables to prevent malicious configs
1057
- const DANGEROUS_ENV_VARS = [
1058
- "PATH",
1059
- "HOME",
1060
- "LD_PRELOAD",
1061
- "LD_LIBRARY_PATH",
1062
- "DYLD_LIBRARY_PATH",
1063
- "DYLD_INSERT_LIBRARIES"
1064
- ]
1065
-
1066
1098
  if (options.verbose) {
1067
1099
  console.log(
1068
1100
  `[Config Exporter] Setting environment variables from build config...`
@@ -1070,23 +1102,21 @@ async function loadConfigsForEnv(
1070
1102
  }
1071
1103
 
1072
1104
  for (const [key, value] of Object.entries(resolvedBuild.environment)) {
1073
- if (DANGEROUS_ENV_VARS.includes(key)) {
1105
+ if (isDangerousEnvVar(key)) {
1074
1106
  console.warn(
1075
1107
  `[Config Exporter] Warning: Skipping dangerous environment variable: ${key}`
1076
1108
  )
1077
- continue
1078
- }
1079
- if (!(BUILD_ENV_VARS as readonly string[]).includes(key)) {
1109
+ } else if (!isBuildEnvVar(key)) {
1080
1110
  console.warn(
1081
1111
  `[Config Exporter] Warning: Skipping non-whitelisted environment variable: ${key}. ` +
1082
1112
  `Allowed variables are: ${BUILD_ENV_VARS.join(", ")}`
1083
1113
  )
1084
- continue
1085
- }
1086
- if (options.verbose) {
1087
- console.log(`[Config Exporter] ${key}=${value}`)
1114
+ } else {
1115
+ if (options.verbose) {
1116
+ console.log(`[Config Exporter] ${key}=${value}`)
1117
+ }
1118
+ process.env[key] = value
1088
1119
  }
1089
- process.env[key] = value
1090
1120
  }
1091
1121
 
1092
1122
  // Determine final env: CLI flag > build config NODE_ENV > default
@@ -1168,7 +1198,7 @@ async function loadConfigsForEnv(
1168
1198
  if (configFile.endsWith(".ts")) {
1169
1199
  try {
1170
1200
  require("ts-node/register/transpile-only")
1171
- } catch (error) {
1201
+ } catch (_error) {
1172
1202
  throw new Error(
1173
1203
  "TypeScript config detected but ts-node is not available. " +
1174
1204
  "Install ts-node as a dev dependency: npm install --save-dev ts-node"
@@ -1605,7 +1635,7 @@ function loadShakapackerConfig(
1605
1635
 
1606
1636
  return result
1607
1637
  }
1608
- } catch (error: unknown) {
1638
+ } catch (_error: unknown) {
1609
1639
  console.warn(
1610
1640
  `[Config Exporter] Error loading shakapacker config, defaulting to webpack`
1611
1641
  )