shakapacker 9.1.0 → 9.2.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.
@@ -0,0 +1,683 @@
1
+ // This will be a substantial file - the main CLI entry point
2
+ // Migrating from bin/export-bundler-config but streamlined for TypeScript
3
+
4
+ import { existsSync, readFileSync } from "fs"
5
+ import { resolve, dirname, sep, delimiter, basename } from "path"
6
+ import { inspect } from "util"
7
+ import { load as loadYaml } from "js-yaml"
8
+ import { ExportOptions, ConfigMetadata, FileOutput } from "./types"
9
+ import { YamlSerializer } from "./yamlSerializer"
10
+ import { FileWriter } from "./fileWriter"
11
+
12
+ // Main CLI entry point
13
+ export async function run(args: string[]): Promise<number> {
14
+ try {
15
+ const options = parseArguments(args)
16
+
17
+ if (options.help) {
18
+ showHelp()
19
+ return 0
20
+ }
21
+
22
+ // Set up environment
23
+ const appRoot = findAppRoot()
24
+ process.chdir(appRoot)
25
+ setupNodePath(appRoot)
26
+
27
+ // Apply defaults
28
+ applyDefaults(options)
29
+
30
+ // Validate options
31
+ validateOptions(options)
32
+
33
+ // Execute based on mode
34
+ if (options.doctor) {
35
+ await runDoctorMode(options, appRoot)
36
+ } else if (options.save) {
37
+ await runSaveMode(options, appRoot)
38
+ } else {
39
+ await runStdoutMode(options, appRoot)
40
+ }
41
+
42
+ return 0
43
+ } catch (error: any) {
44
+ console.error(`[Config Exporter] Error: ${error.message}`)
45
+ return 1
46
+ }
47
+ }
48
+
49
+ function parseArguments(args: string[]): ExportOptions {
50
+ const options: ExportOptions = {
51
+ bundler: undefined,
52
+ env: "development",
53
+ clientOnly: false,
54
+ serverOnly: false,
55
+ output: undefined,
56
+ depth: 20,
57
+ format: undefined,
58
+ help: false,
59
+ verbose: false,
60
+ doctor: false,
61
+ save: false,
62
+ saveDir: undefined,
63
+ annotate: undefined
64
+ }
65
+
66
+ const parseValue = (arg: string, prefix: string): string => {
67
+ const value = arg.substring(prefix.length)
68
+ if (value.length === 0) {
69
+ throw new Error(`${prefix} requires a value`)
70
+ }
71
+ return value
72
+ }
73
+
74
+ for (const arg of args) {
75
+ if (arg === "--help" || arg === "-h") {
76
+ options.help = true
77
+ } else if (arg === "--doctor") {
78
+ options.doctor = true
79
+ } else if (arg === "--save") {
80
+ options.save = true
81
+ } else if (arg.startsWith("--save-dir=")) {
82
+ options.saveDir = parseValue(arg, "--save-dir=")
83
+ } else if (arg.startsWith("--bundler=")) {
84
+ const bundler = parseValue(arg, "--bundler=")
85
+ if (bundler !== "webpack" && bundler !== "rspack") {
86
+ throw new Error(
87
+ `Invalid bundler '${bundler}'. Must be 'webpack' or 'rspack'.`
88
+ )
89
+ }
90
+ options.bundler = bundler
91
+ } else if (arg.startsWith("--env=")) {
92
+ const env = parseValue(arg, "--env=")
93
+ if (env !== "development" && env !== "production" && env !== "test") {
94
+ throw new Error(
95
+ `Invalid environment '${env}'. Must be 'development', 'production', or 'test'.`
96
+ )
97
+ }
98
+ options.env = env
99
+ } else if (arg === "--client-only") {
100
+ options.clientOnly = true
101
+ } else if (arg === "--server-only") {
102
+ options.serverOnly = true
103
+ } else if (arg.startsWith("--output=")) {
104
+ options.output = parseValue(arg, "--output=")
105
+ } else if (arg.startsWith("--depth=")) {
106
+ const depth = parseValue(arg, "--depth=")
107
+ options.depth = depth === "null" ? null : parseInt(depth, 10)
108
+ } else if (arg.startsWith("--format=")) {
109
+ const format = parseValue(arg, "--format=")
110
+ if (format !== "yaml" && format !== "json" && format !== "inspect") {
111
+ throw new Error(
112
+ `Invalid format '${format}'. Must be 'yaml', 'json', or 'inspect'.`
113
+ )
114
+ }
115
+ options.format = format
116
+ } else if (arg === "--no-annotate") {
117
+ options.annotate = false
118
+ } else if (arg === "--verbose") {
119
+ options.verbose = true
120
+ }
121
+ }
122
+
123
+ return options
124
+ }
125
+
126
+ function applyDefaults(options: ExportOptions): void {
127
+ if (options.doctor) {
128
+ options.save = true
129
+ if (options.format === undefined) options.format = "yaml"
130
+ if (options.annotate === undefined) options.annotate = true
131
+ } else if (options.save) {
132
+ if (options.format === undefined) options.format = "yaml"
133
+ if (options.annotate === undefined) options.annotate = true
134
+ } else {
135
+ if (options.format === undefined) options.format = "inspect"
136
+ if (options.annotate === undefined) options.annotate = false
137
+ }
138
+ }
139
+
140
+ function validateOptions(options: ExportOptions): void {
141
+ if (options.clientOnly && options.serverOnly) {
142
+ throw new Error(
143
+ "--client-only and --server-only are mutually exclusive. Please specify only one."
144
+ )
145
+ }
146
+
147
+ if (options.saveDir && !options.save && !options.doctor) {
148
+ throw new Error("--save-dir requires --save or --doctor flag.")
149
+ }
150
+
151
+ if (options.output && options.saveDir) {
152
+ throw new Error(
153
+ "--output and --save-dir are mutually exclusive. Use one or the other."
154
+ )
155
+ }
156
+
157
+ if (options.annotate && options.format !== "yaml") {
158
+ throw new Error(
159
+ "--annotate (or default with --save/--doctor) requires --format=yaml. Use --no-annotate or --format=inspect/json."
160
+ )
161
+ }
162
+ }
163
+
164
+ async function runDoctorMode(
165
+ options: ExportOptions,
166
+ appRoot: string
167
+ ): Promise<void> {
168
+ console.log("\n" + "=".repeat(80))
169
+ console.log("🔍 Config Exporter - Doctor Mode")
170
+ console.log("=".repeat(80))
171
+ console.log("\nExporting development AND production configs...")
172
+ console.log("")
173
+
174
+ const environments: Array<"development" | "production"> = [
175
+ "development",
176
+ "production"
177
+ ]
178
+ const fileWriter = new FileWriter()
179
+ const defaultDir = resolve(process.cwd(), "shakapacker-config-exports")
180
+ const targetDir = options.saveDir || defaultDir
181
+
182
+ const createdFiles: string[] = []
183
+
184
+ for (const env of environments) {
185
+ console.log(`\n📦 Loading ${env} configuration...`)
186
+ const configs = await loadConfigsForEnv(env, options, appRoot)
187
+
188
+ for (const { config, metadata } of configs) {
189
+ const output = formatConfig(config, metadata, options, appRoot)
190
+ const filename = fileWriter.generateFilename(
191
+ metadata.bundler,
192
+ metadata.environment,
193
+ metadata.configType,
194
+ options.format!
195
+ )
196
+
197
+ const fullPath = resolve(targetDir, filename)
198
+ const fileOutput: FileOutput = { filename, content: output, metadata }
199
+ fileWriter.writeSingleFile(fullPath, output, true) // quiet mode
200
+ createdFiles.push(fullPath)
201
+ }
202
+ }
203
+
204
+ // Print summary
205
+ console.log("\n" + "=".repeat(80))
206
+ console.log("✅ Export Complete!")
207
+ console.log("=".repeat(80))
208
+ console.log(`\nCreated ${createdFiles.length} configuration file(s) in:`)
209
+ console.log(` ${targetDir}\n`)
210
+ console.log("Files:")
211
+ createdFiles.forEach((file) => {
212
+ console.log(` ✓ ${basename(file)}`)
213
+ })
214
+
215
+ // Check if directory should be added to .gitignore
216
+ const gitignorePath = resolve(process.cwd(), ".gitignore")
217
+ const dirName = basename(targetDir)
218
+ let shouldSuggestGitignore = false
219
+
220
+ if (existsSync(gitignorePath)) {
221
+ const gitignoreContent = readFileSync(gitignorePath, "utf8")
222
+ if (!gitignoreContent.includes(dirName)) {
223
+ shouldSuggestGitignore = true
224
+ }
225
+ }
226
+
227
+ if (shouldSuggestGitignore) {
228
+ console.log("\n" + "─".repeat(80))
229
+ console.log(
230
+ "💡 Tip: Add the export directory to .gitignore to avoid committing config files:"
231
+ )
232
+ console.log(`\n echo "${dirName}/" >> .gitignore\n`)
233
+ }
234
+
235
+ console.log("\n" + "=".repeat(80) + "\n")
236
+ }
237
+
238
+ async function runSaveMode(
239
+ options: ExportOptions,
240
+ appRoot: string
241
+ ): Promise<void> {
242
+ console.log(`[Config Exporter] Save mode: Exporting ${options.env} configs`)
243
+
244
+ const fileWriter = new FileWriter()
245
+ const targetDir = options.saveDir || process.cwd()
246
+ const configs = await loadConfigsForEnv(options.env!, options, appRoot)
247
+
248
+ if (options.output) {
249
+ // Single file output
250
+ const combined = configs.map((c) => c.config)
251
+ const metadata = configs[0].metadata
252
+ metadata.configCount = combined.length
253
+
254
+ const output = formatConfig(
255
+ combined.length === 1 ? combined[0] : combined,
256
+ metadata,
257
+ options,
258
+ appRoot
259
+ )
260
+ fileWriter.writeSingleFile(resolve(options.output), output)
261
+ } else {
262
+ // Multi-file output (one per config)
263
+ for (const { config, metadata } of configs) {
264
+ const output = formatConfig(config, metadata, options, appRoot)
265
+ const filename = fileWriter.generateFilename(
266
+ metadata.bundler,
267
+ metadata.environment,
268
+ metadata.configType,
269
+ options.format!
270
+ )
271
+ fileWriter.writeSingleFile(resolve(targetDir, filename), output)
272
+ }
273
+ }
274
+ }
275
+
276
+ async function runStdoutMode(
277
+ options: ExportOptions,
278
+ appRoot: string
279
+ ): Promise<void> {
280
+ const configs = await loadConfigsForEnv(options.env!, options, appRoot)
281
+ const combined = configs.map((c) => c.config)
282
+ const metadata = configs[0].metadata
283
+ metadata.configCount = combined.length
284
+
285
+ const config = combined.length === 1 ? combined[0] : combined
286
+ const output = formatConfig(config, metadata, options, appRoot)
287
+
288
+ console.log("\n" + "=".repeat(80) + "\n")
289
+ console.log(output)
290
+ }
291
+
292
+ async function loadConfigsForEnv(
293
+ env: "development" | "production" | "test",
294
+ options: ExportOptions,
295
+ appRoot: string
296
+ ): Promise<Array<{ config: any; metadata: ConfigMetadata }>> {
297
+ // Auto-detect bundler if not specified
298
+ const bundler = options.bundler || (await autoDetectBundler(env, appRoot))
299
+
300
+ // Set environment variables
301
+ process.env.NODE_ENV = env
302
+ process.env.RAILS_ENV = env
303
+
304
+ if (options.clientOnly) {
305
+ process.env.CLIENT_BUNDLE_ONLY = "yes"
306
+ } else if (options.serverOnly) {
307
+ process.env.SERVER_BUNDLE_ONLY = "yes"
308
+ }
309
+
310
+ // Find and load config file
311
+ const configFile = findConfigFile(bundler, appRoot)
312
+ // Quiet mode for cleaner output - only show if verbose or errors
313
+ if (process.env.VERBOSE) {
314
+ console.log(`[Config Exporter] Loading config: ${configFile}`)
315
+ console.log(`[Config Exporter] Environment: ${env}`)
316
+ console.log(`[Config Exporter] Bundler: ${bundler}`)
317
+ }
318
+
319
+ // Load the config
320
+ // Register ts-node for TypeScript config files
321
+ if (configFile.endsWith(".ts")) {
322
+ try {
323
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
324
+ require("ts-node/register/transpile-only")
325
+ } catch (error) {
326
+ throw new Error(
327
+ "TypeScript config detected but ts-node is not available. " +
328
+ "Install ts-node as a dev dependency: npm install --save-dev ts-node"
329
+ )
330
+ }
331
+ }
332
+
333
+ // Clear require cache for config file and all related modules
334
+ // This is critical for loading different environments in the same process
335
+ // MUST clear shakapacker env module cache so env.nodeEnv is re-read!
336
+ const configDir = dirname(configFile)
337
+ Object.keys(require.cache).forEach((key) => {
338
+ if (
339
+ key.includes("webpack.config") ||
340
+ key.includes("rspack.config") ||
341
+ key.startsWith(configDir) ||
342
+ key.includes("/shakapacker/") || // npm installed shakapacker
343
+ key.includes("\\shakapacker\\") || // Windows path
344
+ key.includes("/package/env") || // shakapacker env module (local dev)
345
+ key.includes("\\package\\env") || // Windows env module
346
+ key.includes("/package/index") || // shakapacker main module
347
+ key.includes("\\package\\index") || // Windows main module
348
+ key === configFile
349
+ ) {
350
+ delete require.cache[key]
351
+ }
352
+ })
353
+
354
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
355
+ let loadedConfig = require(configFile)
356
+
357
+ // Handle ES module default export
358
+ if (typeof loadedConfig === "object" && "default" in loadedConfig) {
359
+ loadedConfig = loadedConfig.default
360
+ }
361
+
362
+ // Determine config type and split if array
363
+ const configs: any[] = Array.isArray(loadedConfig)
364
+ ? loadedConfig
365
+ : [loadedConfig]
366
+ const results: Array<{ config: any; metadata: ConfigMetadata }> = []
367
+
368
+ configs.forEach((cfg, index) => {
369
+ let configType: "client" | "server" | "all" = "all"
370
+
371
+ // Try to infer config type from the config itself
372
+ if (configs.length === 2) {
373
+ // Likely client and server configs
374
+ configType = index === 0 ? "client" : "server"
375
+ } else if (options.clientOnly) {
376
+ configType = "client"
377
+ } else if (options.serverOnly) {
378
+ configType = "server"
379
+ }
380
+
381
+ const metadata: ConfigMetadata = {
382
+ exportedAt: new Date().toISOString(),
383
+ bundler,
384
+ environment: env,
385
+ configFile,
386
+ configType,
387
+ configCount: configs.length,
388
+ environmentVariables: {
389
+ NODE_ENV: process.env.NODE_ENV,
390
+ RAILS_ENV: process.env.RAILS_ENV,
391
+ CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
392
+ SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY
393
+ }
394
+ }
395
+
396
+ // Clean config if not verbose
397
+ let cleanedConfig = cfg
398
+ if (!options.verbose) {
399
+ cleanedConfig = cleanConfig(cfg, appRoot)
400
+ }
401
+
402
+ results.push({ config: cleanedConfig, metadata })
403
+ })
404
+
405
+ return results
406
+ }
407
+
408
+ function formatConfig(
409
+ config: any,
410
+ metadata: ConfigMetadata,
411
+ options: ExportOptions,
412
+ appRoot: string
413
+ ): string {
414
+ if (options.format === "yaml") {
415
+ const serializer = new YamlSerializer({
416
+ annotate: options.annotate!,
417
+ appRoot
418
+ })
419
+ return serializer.serialize(config, metadata)
420
+ } else if (options.format === "json") {
421
+ const jsonReplacer = (key: string, value: any): any => {
422
+ if (typeof value === "function") {
423
+ return `[Function: ${value.name || "anonymous"}]`
424
+ }
425
+ if (value instanceof RegExp) {
426
+ return `[RegExp: ${value.toString()}]`
427
+ }
428
+ if (
429
+ value &&
430
+ typeof value === "object" &&
431
+ value.constructor &&
432
+ value.constructor.name !== "Object" &&
433
+ value.constructor.name !== "Array"
434
+ ) {
435
+ return `[${value.constructor.name}]`
436
+ }
437
+ return value
438
+ }
439
+ return JSON.stringify({ metadata, config }, jsonReplacer, 2)
440
+ } else {
441
+ // inspect format
442
+ const inspectOptions = {
443
+ depth: options.depth,
444
+ colors: false,
445
+ maxArrayLength: null,
446
+ maxStringLength: null,
447
+ breakLength: 120,
448
+ compact: false
449
+ }
450
+
451
+ let output =
452
+ "=== METADATA ===\n\n" + inspect(metadata, inspectOptions) + "\n\n"
453
+ output += "=== CONFIG ===\n\n"
454
+
455
+ if (Array.isArray(config)) {
456
+ output += `Total configs: ${config.length}\n\n`
457
+ config.forEach((cfg, index) => {
458
+ output += `--- Config [${index}] ---\n\n`
459
+ output += inspect(cfg, inspectOptions) + "\n\n"
460
+ })
461
+ } else {
462
+ output += inspect(config, inspectOptions) + "\n"
463
+ }
464
+
465
+ return output
466
+ }
467
+ }
468
+
469
+ function cleanConfig(obj: any, rootPath: string): any {
470
+ const makePathRelative = (str: string): string => {
471
+ if (typeof str === "string" && str.startsWith(rootPath)) {
472
+ return "./" + str.substring(rootPath.length + 1)
473
+ }
474
+ return str
475
+ }
476
+
477
+ function clean(value: any, key?: string, parent?: any): any {
478
+ // Remove EnvironmentPlugin keys and defaultValues
479
+ if (
480
+ parent &&
481
+ parent.constructor &&
482
+ parent.constructor.name === "EnvironmentPlugin"
483
+ ) {
484
+ if (key === "keys" || key === "defaultValues") {
485
+ return undefined
486
+ }
487
+ }
488
+
489
+ if (typeof value === "function") {
490
+ // Show function source
491
+ const source = value.toString()
492
+ const compacted = source
493
+ .split("\n")
494
+ .map((line: string) => line.trim())
495
+ .filter((line: string) => line.length > 0)
496
+ .join(" ")
497
+ return compacted
498
+ }
499
+
500
+ if (typeof value === "string") {
501
+ return makePathRelative(value)
502
+ }
503
+
504
+ if (Array.isArray(value)) {
505
+ return value
506
+ .map((item, i) => clean(item, String(i), value))
507
+ .filter((v) => v !== undefined)
508
+ }
509
+
510
+ if (value && typeof value === "object") {
511
+ const cleaned: any = {}
512
+ for (const k in value) {
513
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
514
+ const cleanedValue = clean(value[k], k, value)
515
+ if (cleanedValue !== undefined) {
516
+ cleaned[k] = cleanedValue
517
+ }
518
+ }
519
+ }
520
+ return cleaned
521
+ }
522
+
523
+ return value
524
+ }
525
+
526
+ return clean(obj)
527
+ }
528
+
529
+ async function autoDetectBundler(
530
+ env: string,
531
+ appRoot: string
532
+ ): Promise<"webpack" | "rspack"> {
533
+ try {
534
+ const configPath =
535
+ process.env.SHAKAPACKER_CONFIG ||
536
+ resolve(appRoot, "config/shakapacker.yml")
537
+
538
+ if (existsSync(configPath)) {
539
+ const config: any = loadYaml(readFileSync(configPath, "utf8"))
540
+ const envConfig = config[env] || config.default || {}
541
+ const bundler = envConfig.assets_bundler || "webpack"
542
+ if (bundler !== "webpack" && bundler !== "rspack") {
543
+ console.warn(
544
+ `[Config Exporter] Invalid bundler '${bundler}' in shakapacker.yml, defaulting to webpack`
545
+ )
546
+ return "webpack"
547
+ }
548
+ console.log(`[Config Exporter] Auto-detected bundler: ${bundler}`)
549
+ return bundler
550
+ }
551
+ } catch (error: any) {
552
+ console.warn(
553
+ `[Config Exporter] Error detecting bundler, defaulting to webpack`
554
+ )
555
+ }
556
+
557
+ return "webpack"
558
+ }
559
+
560
+ function findConfigFile(
561
+ bundler: "webpack" | "rspack",
562
+ appRoot: string
563
+ ): string {
564
+ const extensions = ["ts", "js"]
565
+
566
+ if (bundler === "rspack") {
567
+ for (const ext of extensions) {
568
+ const rspackPath = resolve(appRoot, `config/rspack/rspack.config.${ext}`)
569
+ if (existsSync(rspackPath)) {
570
+ return rspackPath
571
+ }
572
+ }
573
+ }
574
+
575
+ // Fall back to webpack config
576
+ for (const ext of extensions) {
577
+ const webpackPath = resolve(appRoot, `config/webpack/webpack.config.${ext}`)
578
+ if (existsSync(webpackPath)) {
579
+ return webpackPath
580
+ }
581
+ }
582
+
583
+ throw new Error(
584
+ `Could not find ${bundler} config file. Expected: config/${bundler}/${bundler}.config.{js,ts}`
585
+ )
586
+ }
587
+
588
+ function findAppRoot(): string {
589
+ let currentDir = process.cwd()
590
+ const root = dirname(currentDir).split(sep)[0] + sep
591
+
592
+ while (currentDir !== root && currentDir !== dirname(currentDir)) {
593
+ if (
594
+ existsSync(resolve(currentDir, "package.json")) ||
595
+ existsSync(resolve(currentDir, "config/shakapacker.yml"))
596
+ ) {
597
+ return currentDir
598
+ }
599
+ currentDir = dirname(currentDir)
600
+ }
601
+
602
+ return process.cwd()
603
+ }
604
+
605
+ function setupNodePath(appRoot: string): void {
606
+ const nodePaths = [
607
+ resolve(appRoot, "node_modules"),
608
+ resolve(appRoot, "..", "..", "node_modules"),
609
+ resolve(appRoot, "..", "..", "package"),
610
+ ...(appRoot.includes("/spec/dummy")
611
+ ? [resolve(appRoot, "../../node_modules")]
612
+ : [])
613
+ ].filter((p) => existsSync(p))
614
+
615
+ if (nodePaths.length > 0) {
616
+ const existingNodePath = process.env.NODE_PATH || ""
617
+ process.env.NODE_PATH = existingNodePath
618
+ ? `${nodePaths.join(delimiter)}${delimiter}${existingNodePath}`
619
+ : nodePaths.join(delimiter)
620
+
621
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
622
+ require("module").Module._initPaths()
623
+ }
624
+ }
625
+
626
+ function showHelp(): void {
627
+ console.log(`
628
+ Shakapacker Config Exporter
629
+
630
+ Exports webpack or rspack configuration in a verbose, human-readable format
631
+ for comparison and analysis.
632
+
633
+ QUICK START (for troubleshooting):
634
+ bin/export-bundler-config --doctor
635
+
636
+ Exports annotated YAML configs for both development and production.
637
+ Creates separate files for client and server bundles.
638
+ Best for debugging, AI analysis, and comparing configurations.
639
+
640
+ Usage:
641
+ bin/export-bundler-config [options]
642
+
643
+ Options:
644
+ --doctor Export all configs for troubleshooting (dev + prod, annotated YAML)
645
+ --save Save to auto-generated file(s) (default: YAML format)
646
+ --save-dir=<directory> Directory for output files (requires --save)
647
+ --bundler=webpack|rspack Specify bundler (auto-detected if not provided)
648
+ --env=development|production|test Node environment (default: development, ignored with --doctor)
649
+ --client-only Generate only client config (sets CLIENT_BUNDLE_ONLY=yes)
650
+ --server-only Generate only server config (sets SERVER_BUNDLE_ONLY=yes)
651
+ --output=<filename> Output to specific file (default: stdout)
652
+ --depth=<number> Inspection depth (default: 20, use 'null' for unlimited)
653
+ --format=yaml|json|inspect Output format (default: inspect for stdout, yaml for --save/--doctor)
654
+ --no-annotate Disable inline documentation (YAML only)
655
+ --verbose Show full output without compact mode
656
+ --help, -h Show this help message
657
+
658
+ Note: --client-only and --server-only are mutually exclusive.
659
+ --save-dir requires --save.
660
+ --output and --save-dir are mutually exclusive.
661
+ If neither --client-only nor --server-only specified, both configs are generated.
662
+
663
+ Examples:
664
+ # RECOMMENDED: Export everything for troubleshooting
665
+ bin/export-bundler-config --doctor
666
+ # Creates: webpack-development-client.yaml, webpack-development-server.yaml,
667
+ # webpack-production-client.yaml, webpack-production-server.yaml
668
+
669
+ # Save current environment configs
670
+ bin/export-bundler-config --save
671
+ # Creates: webpack-development-client.yaml, webpack-development-server.yaml
672
+
673
+ # Save to specific directory
674
+ bin/export-bundler-config --save --save-dir=./debug
675
+
676
+ # Export only client config for production
677
+ bin/export-bundler-config --save --env=production --client-only
678
+ # Creates: webpack-production-client.yaml
679
+
680
+ # View config in terminal (stdout)
681
+ bin/export-bundler-config
682
+ `)
683
+ }