shakapacker 9.3.0.beta.7 → 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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -109
  3. data/Gemfile.lock +1 -1
  4. data/README.md +53 -2
  5. data/docs/configuration.md +28 -0
  6. data/docs/rspack_migration_guide.md +238 -2
  7. data/docs/troubleshooting.md +21 -21
  8. data/eslint.config.fast.js +8 -0
  9. data/eslint.config.js +47 -10
  10. data/knip.ts +8 -1
  11. data/lib/install/config/shakapacker.yml +6 -6
  12. data/lib/shakapacker/configuration.rb +227 -4
  13. data/lib/shakapacker/dev_server.rb +88 -1
  14. data/lib/shakapacker/doctor.rb +4 -4
  15. data/lib/shakapacker/instance.rb +85 -1
  16. data/lib/shakapacker/manifest.rb +85 -11
  17. data/lib/shakapacker/version.rb +1 -1
  18. data/lib/shakapacker.rb +143 -3
  19. data/lib/tasks/shakapacker/doctor.rake +1 -1
  20. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  21. data/package/config.ts +0 -1
  22. data/package/configExporter/buildValidator.ts +53 -29
  23. data/package/configExporter/cli.ts +81 -56
  24. data/package/configExporter/configFile.ts +33 -26
  25. data/package/configExporter/types.ts +64 -0
  26. data/package/configExporter/yamlSerializer.ts +118 -43
  27. data/package/dev_server.ts +2 -1
  28. data/package/env.ts +1 -1
  29. data/package/environments/base.ts +4 -4
  30. data/package/environments/development.ts +7 -6
  31. data/package/environments/production.ts +6 -7
  32. data/package/environments/test.ts +2 -1
  33. data/package/index.ts +28 -4
  34. data/package/loaders.d.ts +2 -2
  35. data/package/optimization/webpack.ts +29 -31
  36. data/package/rspack/index.ts +2 -1
  37. data/package/rules/file.ts +1 -0
  38. data/package/rules/jscommon.ts +1 -0
  39. data/package/utils/helpers.ts +0 -1
  40. data/package/utils/pathValidation.ts +68 -7
  41. data/package/utils/requireOrError.ts +10 -2
  42. data/package/utils/typeGuards.ts +43 -46
  43. data/package/webpack-types.d.ts +2 -2
  44. data/package/webpackDevServerConfig.ts +1 -0
  45. data/package.json +2 -3
  46. data/test/package/configExporter/cli.test.js +440 -0
  47. data/test/package/configExporter/types.test.js +163 -0
  48. data/test/package/configExporter.test.js +264 -0
  49. data/test/package/yamlSerializer.test.js +204 -0
  50. data/test/typescript/pathValidation.test.js +44 -0
  51. data/test/typescript/requireOrError.test.js +49 -0
  52. data/yarn.lock +0 -32
  53. metadata +11 -5
  54. data/.eslintrc.fast.js +0 -40
  55. 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
  })
@@ -319,6 +345,21 @@ QUICK START (for troubleshooting):
319
345
  "--validate cannot be used with --build or --all-builds."
320
346
  )
321
347
  }
348
+ if (argv["all-builds"] && argv.output) {
349
+ throw new Error(
350
+ "--all-builds and --output are mutually exclusive. Use --save-dir instead."
351
+ )
352
+ }
353
+ if (argv["all-builds"] && argv.stdout) {
354
+ throw new Error(
355
+ "--all-builds and --stdout are mutually exclusive. Use --save-dir instead."
356
+ )
357
+ }
358
+ if (argv.stdout && argv.output) {
359
+ throw new Error(
360
+ "--stdout and --output are mutually exclusive. Use one or the other."
361
+ )
362
+ }
322
363
  if (argv.ssr && !argv.init) {
323
364
  throw new Error(
324
365
  "--ssr can only be used with --init. Use: bin/shakapacker-config --init --ssr"
@@ -358,21 +399,18 @@ QUICK START (for troubleshooting):
358
399
 
359
400
  // Type assertions are safe here because yargs validates choices at runtime
360
401
  // Handle --webpack and --rspack flags
361
- let bundler: "webpack" | "rspack" | undefined = argv.bundler as
362
- | "webpack"
363
- | "rspack"
364
- | undefined
402
+ let { bundler } = argv
365
403
  if (argv.webpack) bundler = "webpack"
366
404
  if (argv.rspack) bundler = "rspack"
367
405
 
368
406
  return {
369
407
  bundler,
370
- env: argv.env as "development" | "production" | "test" | undefined,
408
+ env: argv.env,
371
409
  clientOnly: argv["client-only"],
372
410
  serverOnly: argv["server-only"],
373
411
  output: argv.output,
374
- depth: argv.depth as number | null,
375
- format: argv.format as "yaml" | "json" | "inspect" | undefined,
412
+ depth: argv.depth,
413
+ format: argv.format,
376
414
  help: false, // yargs handles help internally
377
415
  verbose: argv.verbose,
378
416
  doctor: argv.doctor,
@@ -411,17 +449,14 @@ function applyDefaults(options: ExportOptions): ExportOptions {
411
449
  !updatedOptions.output &&
412
450
  !updatedOptions.saveDir
413
451
  ) {
414
- updatedOptions.saveDir = resolve(
415
- process.cwd(),
416
- "shakapacker-config-exports"
417
- )
452
+ updatedOptions.saveDir = resolve(process.cwd(), DEFAULT_EXPORT_DIR)
418
453
  }
419
454
 
420
455
  return updatedOptions
421
456
  }
422
457
 
423
458
  function runInitCommand(options: ExportOptions): number {
424
- const configPath = options.configFile || "config/shakapacker-builds.yml"
459
+ const configPath = options.configFile || DEFAULT_CONFIG_FILE
425
460
  const fullPath = resolve(process.cwd(), configPath)
426
461
 
427
462
  // Check if SSR variant is requested via --ssr flag
@@ -528,7 +563,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
528
563
  // Validate that config file exists
529
564
  const loader = new ConfigFileLoader(options.configFile)
530
565
  if (!loader.exists()) {
531
- const configPath = options.configFile || "config/shakapacker-builds.yml"
566
+ const configPath = options.configFile || DEFAULT_CONFIG_FILE
532
567
  throw new Error(
533
568
  `Config file ${configPath} not found. Run --init to create it.`
534
569
  )
@@ -561,7 +596,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
561
596
  // Handle empty builds edge case
562
597
  if (buildsToValidate.length === 0) {
563
598
  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.`
599
+ `No builds found in config file. Add at least one build to ${DEFAULT_CONFIG_FILE} or run --init to see examples.`
565
600
  )
566
601
  }
567
602
  }
@@ -674,8 +709,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
674
709
 
675
710
  const loader = new ConfigFileLoader(resolvedOptions.configFile)
676
711
  if (!loader.exists()) {
677
- const configPath =
678
- resolvedOptions.configFile || "config/shakapacker-builds.yml"
712
+ const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
679
713
  throw new Error(
680
714
  `Config file ${configPath} not found. Run --init to create it.`
681
715
  )
@@ -762,7 +796,7 @@ async function runDoctorMode(
762
796
  const createdFiles: string[] = []
763
797
 
764
798
  // Check if config file exists - always use it for doctor mode
765
- const configFilePath = options.configFile || "config/shakapacker-builds.yml"
799
+ const configFilePath = options.configFile || DEFAULT_CONFIG_FILE
766
800
  const loader = new ConfigFileLoader(configFilePath)
767
801
 
768
802
  if (loader.exists()) {
@@ -1054,15 +1088,6 @@ async function loadConfigsForEnv(
1054
1088
 
1055
1089
  // Set environment variables from config
1056
1090
  // 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
1091
  if (options.verbose) {
1067
1092
  console.log(
1068
1093
  `[Config Exporter] Setting environment variables from build config...`
@@ -1070,13 +1095,13 @@ async function loadConfigsForEnv(
1070
1095
  }
1071
1096
 
1072
1097
  for (const [key, value] of Object.entries(resolvedBuild.environment)) {
1073
- if (DANGEROUS_ENV_VARS.includes(key)) {
1098
+ if (isDangerousEnvVar(key)) {
1074
1099
  console.warn(
1075
1100
  `[Config Exporter] Warning: Skipping dangerous environment variable: ${key}`
1076
1101
  )
1077
1102
  continue
1078
1103
  }
1079
- if (!(BUILD_ENV_VARS as readonly string[]).includes(key)) {
1104
+ if (!isBuildEnvVar(key)) {
1080
1105
  console.warn(
1081
1106
  `[Config Exporter] Warning: Skipping non-whitelisted environment variable: ${key}. ` +
1082
1107
  `Allowed variables are: ${BUILD_ENV_VARS.join(", ")}`
@@ -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