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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +32 -1
- data/Gemfile.lock +1 -1
- data/README.md +21 -0
- data/bin/export-bundler-config +11 -0
- data/docs/deployment.md +52 -8
- data/docs/releasing.md +197 -0
- data/docs/rspack_migration_guide.md +28 -0
- data/docs/troubleshooting.md +124 -23
- data/lib/install/bin/export-bundler-config +11 -0
- data/lib/install/bin/shakapacker +1 -1
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/shakapacker/bundler_switcher.rb +7 -0
- data/lib/shakapacker/configuration.rb +28 -2
- data/lib/shakapacker/doctor.rb +16 -0
- data/lib/shakapacker/rspack_runner.rb +1 -1
- data/lib/shakapacker/runner.rb +1 -1
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/webpack_runner.rb +1 -1
- data/lib/shakapacker.rb +10 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
- data/package/configExporter/cli.ts +683 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/fileWriter.ts +92 -0
- data/package/configExporter/index.ts +5 -0
- data/package/configExporter/types.ts +36 -0
- data/package/configExporter/yamlSerializer.ts +266 -0
- data/package-lock.json +2 -2
- data/package.json +2 -1
- data/yarn.lock +151 -360
- metadata +12 -2
@@ -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
|
+
}
|