shakapacker 9.3.0.beta.5 → 9.3.0.beta.7

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.
@@ -351,7 +351,7 @@ QUICK START (for troubleshooting):
351
351
 
352
352
  # Advanced output options
353
353
  bin/shakapacker-config --build=dev --stdout # View in terminal
354
- bin/shakapacker-config --build=dev --output=config.yaml # Save to specific file`
354
+ bin/shakapacker-config --build=dev --output=config.yml # Save to specific file`
355
355
  )
356
356
  .strict()
357
357
  .parseSync()
@@ -566,7 +566,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
566
566
  }
567
567
  }
568
568
 
569
- console.log("\n" + "=".repeat(80))
569
+ console.log(`\n${"=".repeat(80)}`)
570
570
  console.log("🔍 Validating Builds")
571
571
  console.log("=".repeat(80))
572
572
  console.log(`\nValidating ${buildsToValidate.length} build(s)...\n`)
@@ -577,7 +577,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
577
577
  " This includes all webpack/rspack compilation logs, warnings, and progress messages"
578
578
  )
579
579
  console.log(" Use without --verbose to see only errors and summaries\n")
580
- console.log("=".repeat(80) + "\n")
580
+ console.log(`${"=".repeat(80)}\n`)
581
581
  }
582
582
 
583
583
  const results = []
@@ -585,7 +585,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
585
585
  // Validate each build
586
586
  for (const buildName of buildsToValidate) {
587
587
  if (options.verbose) {
588
- console.log("\n" + "=".repeat(80))
588
+ console.log(`\n${"=".repeat(80)}`)
589
589
  console.log(`📦 VALIDATING BUILD: ${buildName}`)
590
590
  console.log("=".repeat(80))
591
591
  } else {
@@ -596,6 +596,9 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
596
596
  clearBuildEnvironmentVariables()
597
597
  restoreBuildEnvironmentVariables(savedEnv)
598
598
 
599
+ // Clear shakapacker config cache between builds
600
+ shakapackerConfigCache = null
601
+
599
602
  // Get the build's environment to use for auto-detection
600
603
  const buildConfig = config.builds[buildName]
601
604
  const buildEnv =
@@ -608,7 +611,11 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
608
611
  "development"
609
612
 
610
613
  // Auto-detect bundler using the build's environment
611
- const defaultBundler = await autoDetectBundler(buildEnv, appRoot)
614
+ const defaultBundler = await autoDetectBundler(
615
+ buildEnv,
616
+ appRoot,
617
+ options.verbose
618
+ )
612
619
 
613
620
  // Resolve build config with the correct default bundler
614
621
  const resolvedBuild = loader.resolveBuild(
@@ -692,6 +699,9 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
692
699
  clearBuildEnvironmentVariables()
693
700
  restoreBuildEnvironmentVariables(savedEnv)
694
701
 
702
+ // Clear shakapacker config cache between builds
703
+ shakapackerConfigCache = null
704
+
695
705
  // Create a modified options object for this build
696
706
  const buildOptions = { ...resolvedOptions, build: buildName }
697
707
  const configs = await loadConfigsForEnv(undefined, buildOptions, appRoot)
@@ -713,7 +723,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
713
723
  }
714
724
 
715
725
  // Print summary
716
- console.log("\n" + "=".repeat(80))
726
+ console.log(`\n${"=".repeat(80)}`)
717
727
  console.log("✅ All Builds Exported!")
718
728
  console.log("=".repeat(80))
719
729
  console.log(`\nCreated ${createdFiles.length} configuration file(s) in:`)
@@ -722,7 +732,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
722
732
  createdFiles.forEach((file) => {
723
733
  console.log(` ✓ ${basename(file)}`)
724
734
  })
725
- console.log("\n" + "=".repeat(80) + "\n")
735
+ console.log(`\n${"=".repeat(80)}\n`)
726
736
 
727
737
  return 0
728
738
  } catch (error: unknown) {
@@ -743,7 +753,7 @@ async function runDoctorMode(
743
753
  const savedEnv = saveBuildEnvironmentVariables()
744
754
 
745
755
  try {
746
- console.log("\n" + "=".repeat(80))
756
+ console.log(`\n${"=".repeat(80)}`)
747
757
  console.log("🔍 Config Exporter - Doctor Mode")
748
758
  console.log("=".repeat(80))
749
759
 
@@ -770,6 +780,9 @@ async function runDoctorMode(
770
780
  clearBuildEnvironmentVariables()
771
781
  restoreBuildEnvironmentVariables(savedEnv)
772
782
 
783
+ // Clear shakapacker config cache between builds
784
+ shakapackerConfigCache = null
785
+
773
786
  const configs = await loadConfigsForEnv(
774
787
  undefined,
775
788
  { ...options, build: buildName },
@@ -798,9 +811,13 @@ async function runDoctorMode(
798
811
  // If config file exists but is invalid, show error and exit
799
812
  const errorMessage =
800
813
  error instanceof Error ? error.message : String(error)
801
- console.error(`\n❌ Config file found but invalid: ${errorMessage}`)
814
+ console.error(`\n❌ Error loading build configuration:`)
815
+ console.error(`\n${errorMessage}`)
816
+ console.error(
817
+ `\n💡 To fix this issue, check your build config in ${configFilePath}`
818
+ )
802
819
  console.error(
803
- `Fix the config file or run: bin/shakapacker-config --init\n`
820
+ ` or run: bin/shakapacker-config --init to regenerate it.\n`
804
821
  )
805
822
  throw error
806
823
  }
@@ -825,6 +842,9 @@ async function runDoctorMode(
825
842
  clearBuildEnvironmentVariables()
826
843
  restoreBuildEnvironmentVariables(savedEnv)
827
844
 
845
+ // Clear shakapacker config cache between builds
846
+ shakapackerConfigCache = null
847
+
828
848
  // Set WEBPACK_SERVE for HMR config
829
849
  if (hmr) {
830
850
  process.env.WEBPACK_SERVE = "true"
@@ -883,7 +903,7 @@ async function runDoctorMode(
883
903
 
884
904
  function printDoctorSummary(createdFiles: string[], targetDir: string): void {
885
905
  // Print summary
886
- console.log("\n" + "=".repeat(80))
906
+ console.log(`\n${"=".repeat(80)}`)
887
907
  console.log("✅ Export Complete!")
888
908
  console.log("=".repeat(80))
889
909
  console.log(`\nCreated ${createdFiles.length} configuration file(s) in:`)
@@ -906,14 +926,14 @@ function printDoctorSummary(createdFiles: string[], targetDir: string): void {
906
926
  }
907
927
 
908
928
  if (shouldSuggestGitignore) {
909
- console.log("\n" + "─".repeat(80))
929
+ console.log(`\n${"─".repeat(80)}`)
910
930
  console.log(
911
931
  "💡 Tip: Add the export directory to .gitignore to avoid committing config files:"
912
932
  )
913
933
  console.log(`\n echo "${dirName}/" >> .gitignore\n`)
914
934
  }
915
935
 
916
- console.log("\n" + "=".repeat(80) + "\n")
936
+ console.log(`\n${"=".repeat(80)}\n`)
917
937
  }
918
938
 
919
939
  async function runSaveMode(
@@ -930,7 +950,7 @@ async function runSaveMode(
930
950
  if (options.output) {
931
951
  // Single file output
932
952
  const combined = configs.map((c) => c.config)
933
- const metadata = configs[0].metadata
953
+ const { metadata } = configs[0]
934
954
  metadata.configCount = combined.length
935
955
 
936
956
  const output = formatConfig(
@@ -970,15 +990,15 @@ async function runStdoutMode(
970
990
  options: ExportOptions,
971
991
  appRoot: string
972
992
  ): Promise<void> {
973
- const configs = await loadConfigsForEnv(options.env!, options, appRoot)
993
+ const configs = await loadConfigsForEnv(options.env, options, appRoot)
974
994
  const combined = configs.map((c) => c.config)
975
- const metadata = configs[0].metadata
995
+ const { metadata } = configs[0]
976
996
  metadata.configCount = combined.length
977
997
 
978
998
  const config = combined.length === 1 ? combined[0] : combined
979
999
  const output = formatConfig(config, metadata, options, appRoot)
980
1000
 
981
- console.log("\n" + "=".repeat(80) + "\n")
1001
+ console.log(`\n${"=".repeat(80)}\n`)
982
1002
  console.log(output)
983
1003
  }
984
1004
 
@@ -986,9 +1006,9 @@ async function runSingleFileMode(
986
1006
  options: ExportOptions,
987
1007
  appRoot: string
988
1008
  ): Promise<void> {
989
- const configs = await loadConfigsForEnv(options.env!, options, appRoot)
1009
+ const configs = await loadConfigsForEnv(options.env, options, appRoot)
990
1010
  const combined = configs.map((c) => c.config)
991
- const metadata = configs[0].metadata
1011
+ const { metadata } = configs[0]
992
1012
  metadata.configCount = combined.length
993
1013
 
994
1014
  const config = combined.length === 1 ? combined[0] : combined
@@ -1015,7 +1035,11 @@ async function loadConfigsForEnv(
1015
1035
  // Use a temporary env for auto-detection, will be overridden by build config
1016
1036
  const tempEnv = env || "development"
1017
1037
  const loader = new ConfigFileLoader(options.configFile)
1018
- const defaultBundler = await autoDetectBundler(tempEnv, appRoot)
1038
+ const defaultBundler = await autoDetectBundler(
1039
+ tempEnv,
1040
+ appRoot,
1041
+ options.verbose
1042
+ )
1019
1043
  const resolvedBuild = loader.resolveBuild(
1020
1044
  options.build,
1021
1045
  options,
@@ -1039,6 +1063,12 @@ async function loadConfigsForEnv(
1039
1063
  "DYLD_INSERT_LIBRARIES"
1040
1064
  ]
1041
1065
 
1066
+ if (options.verbose) {
1067
+ console.log(
1068
+ `[Config Exporter] Setting environment variables from build config...`
1069
+ )
1070
+ }
1071
+
1042
1072
  for (const [key, value] of Object.entries(resolvedBuild.environment)) {
1043
1073
  if (DANGEROUS_ENV_VARS.includes(key)) {
1044
1074
  console.warn(
@@ -1053,6 +1083,9 @@ async function loadConfigsForEnv(
1053
1083
  )
1054
1084
  continue
1055
1085
  }
1086
+ if (options.verbose) {
1087
+ console.log(`[Config Exporter] ${key}=${value}`)
1088
+ }
1056
1089
  process.env[key] = value
1057
1090
  }
1058
1091
 
@@ -1080,18 +1113,36 @@ async function loadConfigsForEnv(
1080
1113
  const railsEnv =
1081
1114
  options.env || resolvedBuild.environment.RAILS_ENV || finalEnv
1082
1115
  process.env.RAILS_ENV = railsEnv
1116
+
1117
+ // Auto-set CLIENT_BUNDLE_ONLY/SERVER_BUNDLE_ONLY from outputs if not already in environment
1118
+ // This allows webpack configs to return the correct number of bundles
1119
+ if (
1120
+ !resolvedBuild.environment.CLIENT_BUNDLE_ONLY &&
1121
+ !resolvedBuild.environment.SERVER_BUNDLE_ONLY
1122
+ ) {
1123
+ if (buildOutputs.length === 1) {
1124
+ if (buildOutputs[0] === "client") {
1125
+ process.env.CLIENT_BUNDLE_ONLY = "yes"
1126
+ } else if (buildOutputs[0] === "server") {
1127
+ process.env.SERVER_BUNDLE_ONLY = "yes"
1128
+ }
1129
+ }
1130
+ }
1083
1131
  } else {
1084
1132
  // No build config - use CLI env or default
1085
1133
  finalEnv = env || "development"
1086
1134
 
1087
1135
  // Auto-detect bundler if not specified
1088
- bundler = options.bundler || (await autoDetectBundler(finalEnv, appRoot))
1136
+ bundler =
1137
+ options.bundler ||
1138
+ (await autoDetectBundler(finalEnv, appRoot, options.verbose))
1089
1139
 
1090
1140
  // Set environment variables
1091
1141
  process.env.NODE_ENV = finalEnv
1092
1142
  process.env.RAILS_ENV = finalEnv
1093
1143
  }
1094
1144
 
1145
+ // Handle CLI flags for client/server only
1095
1146
  if (options.clientOnly) {
1096
1147
  process.env.CLIENT_BUNDLE_ONLY = "yes"
1097
1148
  } else if (options.serverOnly) {
@@ -1100,9 +1151,10 @@ async function loadConfigsForEnv(
1100
1151
 
1101
1152
  // Find and load config file
1102
1153
  const configFile =
1103
- customConfigFile || findConfigFile(bundler, appRoot, finalEnv)
1154
+ customConfigFile ||
1155
+ findConfigFile(bundler, appRoot, finalEnv, options.verbose)
1104
1156
  // Quiet mode for cleaner output - only show if verbose or errors
1105
- if (process.env.VERBOSE) {
1157
+ if (options.verbose) {
1106
1158
  console.log(`[Config Exporter] Loading config: ${configFile}`)
1107
1159
  console.log(`[Config Exporter] Environment: ${finalEnv}`)
1108
1160
  console.log(`[Config Exporter] Bundler: ${bundler}`)
@@ -1115,7 +1167,6 @@ async function loadConfigsForEnv(
1115
1167
  // Register ts-node for TypeScript config files
1116
1168
  if (configFile.endsWith(".ts")) {
1117
1169
  try {
1118
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1119
1170
  require("ts-node/register/transpile-only")
1120
1171
  } catch (error) {
1121
1172
  throw new Error(
@@ -1164,8 +1215,19 @@ async function loadConfigsForEnv(
1164
1215
  }
1165
1216
  })
1166
1217
 
1167
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1168
- let loadedConfig = require(configFile)
1218
+ let loadedConfig: any
1219
+ try {
1220
+ loadedConfig = require(configFile)
1221
+ } catch (error: unknown) {
1222
+ const errorMessage = error instanceof Error ? error.message : String(error)
1223
+ throw new Error(
1224
+ `Failed to load webpack/rspack config file.\n\n` +
1225
+ `Config file: ${configFile}\n` +
1226
+ `Build: ${buildName || "default"}\n` +
1227
+ `Error: ${errorMessage}\n\n` +
1228
+ `Tip: Check that the config file is valid and doesn't have syntax errors.`
1229
+ )
1230
+ }
1169
1231
 
1170
1232
  // Handle ES module default export
1171
1233
  if (typeof loadedConfig === "object" && "default" in loadedConfig) {
@@ -1198,10 +1260,41 @@ async function loadConfigsForEnv(
1198
1260
  } catch (error: unknown) {
1199
1261
  const errorMessage =
1200
1262
  error instanceof Error ? error.message : String(error)
1263
+
1264
+ // Build detailed environment information for debugging
1265
+ const envDetails = [
1266
+ `Config file: ${configFile}`,
1267
+ `Build: ${buildName || "default"}`,
1268
+ ``,
1269
+ `Current Environment Variables:`,
1270
+ ` NODE_ENV: ${process.env.NODE_ENV || "(not set)"}`,
1271
+ ` RAILS_ENV: ${process.env.RAILS_ENV || "(not set)"}`,
1272
+ ` CLIENT_BUNDLE_ONLY: ${process.env.CLIENT_BUNDLE_ONLY || "(not set)"}`,
1273
+ ` SERVER_BUNDLE_ONLY: ${process.env.SERVER_BUNDLE_ONLY || "(not set)"}`,
1274
+ ` WEBPACK_SERVE: ${process.env.WEBPACK_SERVE || "(not set)"}`,
1275
+ ``,
1276
+ `Bundler env args: ${JSON.stringify(envObject)}`,
1277
+ `Mode: ${finalEnv}`,
1278
+ ``,
1279
+ `Error: ${errorMessage}`,
1280
+ ``
1281
+ ]
1282
+
1283
+ // Add suggestion based on common error patterns
1284
+ let suggestion = `Check your webpack/rspack config for errors. The config function threw an exception when called.`
1285
+ if (errorMessage.includes("NODE_ENV") && !process.env.NODE_ENV) {
1286
+ suggestion =
1287
+ `NODE_ENV is not set. ` +
1288
+ `Your build config should set NODE_ENV in the 'environment' section.\n` +
1289
+ `Example:\n` +
1290
+ ` environment:\n` +
1291
+ ` NODE_ENV: "development"`
1292
+ }
1293
+
1201
1294
  throw new Error(
1202
- `Failed to execute config function: ${errorMessage}\n` +
1203
- `Config file: ${configFile}\n` +
1204
- `Environment: ${JSON.stringify(envObject)}`
1295
+ `Failed to execute config function: ${errorMessage}\n${envDetails.join(
1296
+ "\n"
1297
+ )}\nTip: ${suggestion}`
1205
1298
  )
1206
1299
  }
1207
1300
  }
@@ -1212,29 +1305,81 @@ async function loadConfigsForEnv(
1212
1305
  : [loadedConfig]
1213
1306
  const results: Array<{ config: any; metadata: ConfigMetadata }> = []
1214
1307
 
1308
+ // Validate config count matches expected outputs
1309
+ if (buildOutputs.length > 0 && configs.length !== buildOutputs.length) {
1310
+ const errorLines = [
1311
+ `Webpack config returned ${configs.length} config(s) but outputs array specifies ${buildOutputs.length}.`,
1312
+ ``,
1313
+ `Build: ${buildName || "default"}`,
1314
+ `Config file: ${configFile}`,
1315
+ `Expected outputs: [${buildOutputs.join(", ")}]`,
1316
+ `Actual configs returned: ${configs.length}`,
1317
+ ``,
1318
+ `This mismatch means:`
1319
+ ]
1320
+
1321
+ if (configs.length < buildOutputs.length) {
1322
+ errorLines.push(
1323
+ ` - Your webpack config is returning FEWER configs than expected.`,
1324
+ ` - Either update your webpack config to return ${buildOutputs.length} config(s),`,
1325
+ ` - Or update the 'outputs' array in your build config to match what webpack returns.`
1326
+ )
1327
+ } else {
1328
+ errorLines.push(
1329
+ ` - Your webpack config is returning MORE configs than expected.`,
1330
+ ` - Either update the 'outputs' array to include all ${configs.length} outputs,`,
1331
+ ` - Or update your webpack config to return only ${buildOutputs.length} config(s).`
1332
+ )
1333
+ }
1334
+
1335
+ errorLines.push(
1336
+ ``,
1337
+ `Example fix in build config:`,
1338
+ ` outputs:`,
1339
+ ...Array.from({ length: configs.length }, (_, i) =>
1340
+ i < buildOutputs.length
1341
+ ? ` - ${buildOutputs[i]}`
1342
+ : ` - config-${i + 1} # Add a name for this config`
1343
+ )
1344
+ )
1345
+
1346
+ throw new Error(errorLines.join("\n"))
1347
+ }
1348
+
1349
+ // Debug logging
1350
+ if (options.verbose || buildOutputs.length > 0) {
1351
+ console.log(
1352
+ `[Config Exporter] Webpack returned ${configs.length} config(s), buildOutputs: [${buildOutputs.join(", ")}]`
1353
+ )
1354
+ if (buildOutputs.length > 0 && configs.length === buildOutputs.length) {
1355
+ console.log(
1356
+ `[Config Exporter] ✓ Config count matches outputs array (${configs.length})`
1357
+ )
1358
+ }
1359
+ }
1360
+
1215
1361
  configs.forEach((cfg, index) => {
1216
- let configType: "client" | "server" | "all" = "all"
1362
+ let configType: string = "all"
1217
1363
 
1218
1364
  // Use outputs from build config if available
1219
- if (
1220
- buildOutputs.length > 0 &&
1221
- index < buildOutputs.length &&
1222
- buildOutputs[index]
1223
- ) {
1224
- const outputValue = buildOutputs[index]
1225
- // Validate the output value is a valid config type
1226
- if (
1227
- outputValue === "client" ||
1228
- outputValue === "server" ||
1229
- outputValue === "all"
1230
- ) {
1231
- configType = outputValue
1232
- } else {
1233
- throw new Error(
1234
- `Invalid output type '${outputValue}' at index ${index} in build '${buildName}'. ` +
1235
- `Allowed values are: client, server, all`
1365
+ if (buildOutputs.length > 0) {
1366
+ // If outputs are specified, skip configs beyond the outputs array
1367
+ if (index >= buildOutputs.length) {
1368
+ console.log(
1369
+ `[Config Exporter] Skipping config[${index}] - beyond outputs array`
1236
1370
  )
1371
+ return // Skip this config
1372
+ }
1373
+
1374
+ const outputValue = buildOutputs[index]
1375
+ if (!outputValue || outputValue.trim() === "") {
1376
+ return // Skip null/undefined/empty string entries
1237
1377
  }
1378
+
1379
+ // Accept any string as a valid output name
1380
+ // Built-in types: client, server, all, client-hmr
1381
+ // Custom types: client-modern, client-legacy, server-bundle, etc.
1382
+ configType = outputValue
1238
1383
  } else if (configs.length === 2) {
1239
1384
  // Likely client and server configs
1240
1385
  configType = index === 0 ? "client" : "server"
@@ -1306,39 +1451,37 @@ function formatConfig(
1306
1451
  return value
1307
1452
  }
1308
1453
  return JSON.stringify({ metadata, config }, jsonReplacer, 2)
1309
- } else {
1310
- // inspect format
1311
- const inspectOptions = {
1312
- depth: options.depth,
1313
- colors: false,
1314
- maxArrayLength: null,
1315
- maxStringLength: null,
1316
- breakLength: 120,
1317
- compact: false
1318
- }
1319
-
1320
- let output =
1321
- "=== METADATA ===\n\n" + inspect(metadata, inspectOptions) + "\n\n"
1322
- output += "=== CONFIG ===\n\n"
1454
+ }
1455
+ // inspect format
1456
+ const inspectOptions = {
1457
+ depth: options.depth,
1458
+ colors: false,
1459
+ maxArrayLength: null,
1460
+ maxStringLength: null,
1461
+ breakLength: 120,
1462
+ compact: false
1463
+ }
1323
1464
 
1324
- if (Array.isArray(config)) {
1325
- output += `Total configs: ${config.length}\n\n`
1326
- config.forEach((cfg, index) => {
1327
- output += `--- Config [${index}] ---\n\n`
1328
- output += inspect(cfg, inspectOptions) + "\n\n"
1329
- })
1330
- } else {
1331
- output += inspect(config, inspectOptions) + "\n"
1332
- }
1465
+ let output = `=== METADATA ===\n\n${inspect(metadata, inspectOptions)}\n\n`
1466
+ output += "=== CONFIG ===\n\n"
1333
1467
 
1334
- return output
1468
+ if (Array.isArray(config)) {
1469
+ output += `Total configs: ${config.length}\n\n`
1470
+ config.forEach((cfg, index) => {
1471
+ output += `--- Config [${index}] ---\n\n`
1472
+ output += `${inspect(cfg, inspectOptions)}\n\n`
1473
+ })
1474
+ } else {
1475
+ output += `${inspect(config, inspectOptions)}\n`
1335
1476
  }
1477
+
1478
+ return output
1336
1479
  }
1337
1480
 
1338
1481
  function cleanConfig(obj: any, rootPath: string): any {
1339
1482
  const makePathRelative = (str: string): string => {
1340
1483
  if (typeof str === "string" && str.startsWith(rootPath)) {
1341
- return "./" + str.substring(rootPath.length + 1)
1484
+ return `./${str.substring(rootPath.length + 1)}`
1342
1485
  }
1343
1486
  return str
1344
1487
  }
@@ -1398,10 +1541,31 @@ function cleanConfig(obj: any, rootPath: string): any {
1398
1541
  /**
1399
1542
  * Loads and returns shakapacker.yml configuration
1400
1543
  */
1544
+ // Cache to avoid duplicate loading and logging
1545
+ let shakapackerConfigCache: {
1546
+ env: string
1547
+ result: { bundler: "webpack" | "rspack"; configPath: string }
1548
+ } | null = null
1549
+
1401
1550
  function loadShakapackerConfig(
1402
1551
  env: string,
1403
- appRoot: string
1552
+ appRoot: string,
1553
+ verbose = false
1404
1554
  ): { bundler: "webpack" | "rspack"; configPath: string } {
1555
+ // Return cached result if same environment
1556
+ if (shakapackerConfigCache && shakapackerConfigCache.env === env) {
1557
+ if (verbose) {
1558
+ console.log(
1559
+ `[Config Exporter] Using cached bundler config for env: ${env}`
1560
+ )
1561
+ }
1562
+ return shakapackerConfigCache.result
1563
+ }
1564
+
1565
+ if (verbose) {
1566
+ console.log(`[Config Exporter] Loading shakapacker config for env: ${env}`)
1567
+ }
1568
+
1405
1569
  try {
1406
1570
  const configFilePath =
1407
1571
  process.env.SHAKAPACKER_CONFIG ||
@@ -1417,10 +1581,12 @@ function loadShakapackerConfig(
1417
1581
  console.warn(
1418
1582
  `[Config Exporter] Invalid bundler '${bundler}' in shakapacker.yml, defaulting to webpack`
1419
1583
  )
1420
- return {
1421
- bundler: "webpack",
1584
+ const result = {
1585
+ bundler: "webpack" as const,
1422
1586
  configPath: bundler === "rspack" ? "config/rspack" : "config/webpack"
1423
1587
  }
1588
+ shakapackerConfigCache = { env, result }
1589
+ return result
1424
1590
  }
1425
1591
 
1426
1592
  // Get config path
@@ -1429,10 +1595,15 @@ function loadShakapackerConfig(
1429
1595
  customConfigPath ||
1430
1596
  (bundler === "rspack" ? "config/rspack" : "config/webpack")
1431
1597
 
1598
+ const result = { bundler, configPath }
1599
+ shakapackerConfigCache = { env, result }
1600
+
1601
+ // Only log on first call (when cache was empty)
1432
1602
  console.log(
1433
1603
  `[Config Exporter] Auto-detected bundler: ${bundler}, config path: ${configPath}`
1434
1604
  )
1435
- return { bundler, configPath }
1605
+
1606
+ return result
1436
1607
  }
1437
1608
  } catch (error: unknown) {
1438
1609
  console.warn(
@@ -1440,7 +1611,9 @@ function loadShakapackerConfig(
1440
1611
  )
1441
1612
  }
1442
1613
 
1443
- return { bundler: "webpack", configPath: "config/webpack" }
1614
+ const result = { bundler: "webpack" as const, configPath: "config/webpack" }
1615
+ shakapackerConfigCache = { env, result }
1616
+ return result
1444
1617
  }
1445
1618
 
1446
1619
  /**
@@ -1458,18 +1631,20 @@ function loadShakapackerConfig(
1458
1631
  */
1459
1632
  async function autoDetectBundler(
1460
1633
  env: string,
1461
- appRoot: string
1634
+ appRoot: string,
1635
+ verbose = false
1462
1636
  ): Promise<"webpack" | "rspack"> {
1463
- const { bundler } = loadShakapackerConfig(env, appRoot)
1637
+ const { bundler } = loadShakapackerConfig(env, appRoot, verbose)
1464
1638
  return bundler
1465
1639
  }
1466
1640
 
1467
1641
  function findConfigFile(
1468
1642
  bundler: "webpack" | "rspack",
1469
1643
  appRoot: string,
1470
- env: string
1644
+ env: string,
1645
+ verbose = false
1471
1646
  ): string {
1472
- const { configPath } = loadShakapackerConfig(env, appRoot)
1647
+ const { configPath } = loadShakapackerConfig(env, appRoot, verbose)
1473
1648
  const extensions = ["ts", "js"]
1474
1649
 
1475
1650
  if (bundler === "rspack") {
@@ -1527,7 +1702,6 @@ function setupNodePath(appRoot: string): void {
1527
1702
  ? `${nodePaths.join(delimiter)}${delimiter}${existingNodePath}`
1528
1703
  : nodePaths.join(delimiter)
1529
1704
 
1530
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1531
1705
  require("module").Module._initPaths()
1532
1706
  }
1533
1707
  }
@@ -46,24 +46,33 @@ export class FileWriter {
46
46
  * Generate filename for a config export
47
47
  * Format without build: {bundler}-{env}-{type}.{ext}
48
48
  * Format with build: {bundler}-{build}-{type}.{ext}
49
- * Examples:
50
- * webpack-development-client.yaml
51
- * rspack-production-server.yaml
52
- * webpack-test-all.json
53
- * webpack-development-client-hmr.yaml
54
- * webpack-dev-client.yaml (with build name)
55
- * rspack-cypress-dev-server.yaml (with build name)
49
+ *
50
+ * @param bundler - The bundler type (webpack, rspack)
51
+ * @param env - The environment (development, production, test)
52
+ * @param configType - Type of config. Built-in: "client", "server", "all", "client-hmr". Custom: any string from outputs array
53
+ * @param format - Output format (yaml, json, inspect)
54
+ * @param buildName - Optional build name that overrides env in filename
55
+ *
56
+ * @example
57
+ * // Built-in types
58
+ * generateFilename("webpack", "development", "client", "yaml")
59
+ * // => "webpack-development-client.yml"
60
+ *
61
+ * @example
62
+ * // Custom output names
63
+ * generateFilename("webpack", "development", "client-modern", "yaml", "dev-hmr")
64
+ * // => "webpack-dev-hmr-client-modern.yml"
56
65
  */
57
66
  static generateFilename(
58
67
  bundler: string,
59
68
  env: string,
60
- configType: "client" | "server" | "all" | "client-hmr",
69
+ configType: string,
61
70
  format: "yaml" | "json" | "inspect",
62
71
  buildName?: string
63
72
  ): string {
64
73
  let ext: string
65
74
  if (format === "yaml") {
66
- ext = "yaml"
75
+ ext = "yml"
67
76
  } else if (format === "json") {
68
77
  ext = "json"
69
78
  } else {
@@ -29,7 +29,12 @@ export interface ConfigMetadata {
29
29
  bundler: string
30
30
  environment: string
31
31
  configFile: string
32
- configType: "client" | "server" | "all" | "client-hmr"
32
+ /**
33
+ * Type of webpack/rspack config output.
34
+ * Built-in types: "client", "server", "all", "client-hmr"
35
+ * Custom types: Any string matching your outputs array (e.g., "client-modern", "client-legacy", "server-bundle")
36
+ */
37
+ configType: string
33
38
  configCount: number
34
39
  buildName?: string // New: name of the build from config file
35
40
  environmentVariables: {