shakapacker 9.2.0 → 9.3.0.beta.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/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
- data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
- data/.github/workflows/claude-code-review.yml +4 -5
- data/.github/workflows/claude.yml +1 -2
- data/.github/workflows/dummy.yml +4 -4
- data/.github/workflows/generator.yml +9 -9
- data/.github/workflows/node.yml +11 -2
- data/.github/workflows/ruby.yml +16 -16
- data/.github/workflows/test-bundlers.yml +9 -9
- data/.gitignore +4 -0
- data/CHANGELOG.md +19 -4
- data/CLAUDE.md +6 -1
- data/CONTRIBUTING.md +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +14 -14
- data/TODO.md +10 -2
- data/TODO_v9.md +13 -3
- data/bin/export-bundler-config +1 -1
- data/conductor-setup.sh +1 -1
- data/conductor.json +1 -1
- data/docs/cdn_setup.md +13 -8
- data/docs/common-upgrades.md +2 -1
- data/docs/configuration.md +630 -0
- data/docs/css-modules-export-mode.md +120 -100
- data/docs/customizing_babel_config.md +16 -16
- data/docs/deployment.md +18 -0
- data/docs/developing_shakapacker.md +6 -0
- data/docs/optional-peer-dependencies.md +9 -4
- data/docs/peer-dependencies.md +17 -6
- data/docs/precompile_hook.md +342 -0
- data/docs/react.md +57 -47
- data/docs/releasing.md +0 -2
- data/docs/rspack.md +25 -21
- data/docs/rspack_migration_guide.md +335 -8
- data/docs/sprockets.md +1 -0
- data/docs/style_loader_vs_mini_css.md +12 -12
- data/docs/subresource_integrity.md +13 -7
- data/docs/transpiler-performance.md +40 -19
- data/docs/troubleshooting.md +0 -2
- data/docs/typescript-migration.md +48 -39
- data/docs/typescript.md +12 -8
- data/docs/using_esbuild_loader.md +10 -10
- data/docs/v6_upgrade.md +33 -20
- data/docs/v7_upgrade.md +8 -6
- data/docs/v8_upgrade.md +13 -12
- data/docs/v9_upgrade.md +2 -1
- data/eslint.config.fast.js +134 -0
- data/eslint.config.js +140 -0
- data/knip.ts +54 -0
- data/lib/install/bin/export-bundler-config +1 -1
- data/lib/install/config/shakapacker.yml +16 -5
- data/lib/shakapacker/compiler.rb +80 -0
- data/lib/shakapacker/configuration.rb +33 -5
- data/lib/shakapacker/dev_server_runner.rb +140 -1
- data/lib/shakapacker/doctor.rb +294 -65
- data/lib/shakapacker/instance.rb +8 -3
- data/lib/shakapacker/runner.rb +244 -8
- data/lib/shakapacker/version.rb +1 -1
- data/lib/tasks/shakapacker/doctor.rake +42 -2
- data/package/babel/preset.ts +7 -4
- data/package/config.ts +42 -30
- data/package/configExporter/cli.ts +799 -208
- data/package/configExporter/configFile.ts +520 -0
- data/package/configExporter/fileWriter.ts +12 -8
- data/package/configExporter/index.ts +9 -1
- data/package/configExporter/types.ts +36 -2
- data/package/configExporter/yamlSerializer.ts +22 -8
- data/package/dev_server.ts +1 -1
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
- data/package/environments/base.ts +18 -13
- data/package/environments/development.ts +1 -1
- data/package/environments/production.ts +4 -1
- data/package/index.d.ts +50 -3
- data/package/index.d.ts.template +50 -0
- data/package/index.ts +7 -7
- data/package/loaders.d.ts +2 -2
- data/package/optimization/rspack.ts +1 -1
- data/package/plugins/rspack.ts +15 -4
- data/package/plugins/webpack.ts +7 -3
- data/package/rspack/index.ts +10 -2
- data/package/rules/raw.ts +3 -2
- data/package/rules/sass.ts +1 -1
- data/package/types/README.md +15 -13
- data/package/types/index.ts +5 -5
- data/package/types.ts +0 -1
- data/package/utils/defaultConfigPath.ts +4 -1
- data/package/utils/errorCodes.ts +129 -100
- data/package/utils/errorHelpers.ts +34 -29
- data/package/utils/getStyleRule.ts +5 -2
- data/package/utils/helpers.ts +21 -11
- data/package/utils/pathValidation.ts +43 -35
- data/package/utils/requireOrError.ts +1 -1
- data/package/utils/snakeToCamelCase.ts +1 -1
- data/package/utils/typeGuards.ts +132 -83
- data/package/utils/validateDependencies.ts +1 -1
- data/package/webpack-types.d.ts +3 -3
- data/package/webpackDevServerConfig.ts +22 -10
- data/package-lock.json +2 -2
- data/package.json +36 -28
- data/scripts/type-check-no-emit.js +1 -1
- data/test/configExporter/configFile.test.js +392 -0
- data/test/configExporter/integration.test.js +275 -0
- data/test/helpers.js +1 -1
- data/test/package/configExporter.test.js +154 -0
- data/test/package/helpers.test.js +2 -2
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +2 -4
- data/test/package/rules/sass1.test.js +1 -3
- data/test/package/rules/sass16.test.js +23 -0
- data/tools/README.md +15 -5
- data/tsconfig.eslint.json +2 -9
- data/yarn.lock +1894 -1492
- metadata +19 -3
- data/.eslintignore +0 -5
@@ -1,22 +1,92 @@
|
|
1
1
|
// This will be a substantial file - the main CLI entry point
|
2
2
|
// Migrating from bin/export-bundler-config but streamlined for TypeScript
|
3
3
|
|
4
|
-
import { existsSync, readFileSync } from "fs"
|
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
|
+
import yargs from "yargs"
|
8
9
|
import { ExportOptions, ConfigMetadata, FileOutput } from "./types"
|
9
10
|
import { YamlSerializer } from "./yamlSerializer"
|
10
11
|
import { FileWriter } from "./fileWriter"
|
12
|
+
import { ConfigFileLoader, generateSampleConfigFile } from "./configFile"
|
13
|
+
|
14
|
+
// Read version from package.json
|
15
|
+
const packageJson = JSON.parse(
|
16
|
+
readFileSync(resolve(__dirname, "../../package.json"), "utf8")
|
17
|
+
)
|
18
|
+
const VERSION = packageJson.version
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Environment variable names that can be set by build configurations
|
22
|
+
*/
|
23
|
+
const BUILD_ENV_VARS = [
|
24
|
+
"NODE_ENV",
|
25
|
+
"RAILS_ENV",
|
26
|
+
"NODE_OPTIONS",
|
27
|
+
"BABEL_ENV",
|
28
|
+
"WEBPACK_SERVE",
|
29
|
+
"CLIENT_BUNDLE_ONLY",
|
30
|
+
"SERVER_BUNDLE_ONLY"
|
31
|
+
] as const
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Saves current values of build environment variables for later restoration
|
35
|
+
* @returns Object mapping variable names to their current values (or undefined)
|
36
|
+
*/
|
37
|
+
function saveBuildEnvironmentVariables(): Record<string, string | undefined> {
|
38
|
+
const saved: Record<string, string | undefined> = {}
|
39
|
+
BUILD_ENV_VARS.forEach((varName) => {
|
40
|
+
saved[varName] = process.env[varName]
|
41
|
+
})
|
42
|
+
return saved
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Restores previously saved environment variable values
|
47
|
+
* @param saved - Object mapping variable names to their original values
|
48
|
+
*/
|
49
|
+
function restoreBuildEnvironmentVariables(
|
50
|
+
saved: Record<string, string | undefined>
|
51
|
+
): void {
|
52
|
+
BUILD_ENV_VARS.forEach((varName) => {
|
53
|
+
const originalValue = saved[varName]
|
54
|
+
if (originalValue === undefined) {
|
55
|
+
delete process.env[varName]
|
56
|
+
} else {
|
57
|
+
process.env[varName] = originalValue
|
58
|
+
}
|
59
|
+
})
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Clears all whitelisted build environment variables from process.env
|
64
|
+
* to prevent environment variable leakage between builds
|
65
|
+
*/
|
66
|
+
function clearBuildEnvironmentVariables(): void {
|
67
|
+
BUILD_ENV_VARS.forEach((varName) => {
|
68
|
+
delete process.env[varName]
|
69
|
+
})
|
70
|
+
}
|
11
71
|
|
12
72
|
// Main CLI entry point
|
13
73
|
export async function run(args: string[]): Promise<number> {
|
14
74
|
try {
|
15
75
|
const options = parseArguments(args)
|
16
76
|
|
17
|
-
|
18
|
-
|
19
|
-
return
|
77
|
+
// Handle --init command
|
78
|
+
if (options.init) {
|
79
|
+
return runInitCommand(options)
|
80
|
+
}
|
81
|
+
|
82
|
+
// Handle --list-builds command
|
83
|
+
if (options.listBuilds) {
|
84
|
+
return runListBuildsCommand(options)
|
85
|
+
}
|
86
|
+
|
87
|
+
// Handle --all-builds command
|
88
|
+
if (options.allBuilds) {
|
89
|
+
return runAllBuildsCommand(options)
|
20
90
|
}
|
21
91
|
|
22
92
|
// Set up environment
|
@@ -27,16 +97,36 @@ export async function run(args: string[]): Promise<number> {
|
|
27
97
|
// Apply defaults
|
28
98
|
applyDefaults(options)
|
29
99
|
|
30
|
-
// Validate
|
31
|
-
|
100
|
+
// Validate after defaults are applied
|
101
|
+
if (options.annotate && options.format !== "yaml") {
|
102
|
+
throw new Error(
|
103
|
+
"Annotation requires YAML format. Use --no-annotate or --format=yaml."
|
104
|
+
)
|
105
|
+
}
|
106
|
+
|
107
|
+
// Validate --build requires config file
|
108
|
+
if (options.build) {
|
109
|
+
const loader = new ConfigFileLoader(options.configFile)
|
110
|
+
if (!loader.exists()) {
|
111
|
+
const configPath = options.configFile || ".bundler-config.yml"
|
112
|
+
throw new Error(
|
113
|
+
`--build requires a config file but ${configPath} not found. Run --init to create it.`
|
114
|
+
)
|
115
|
+
}
|
116
|
+
}
|
32
117
|
|
33
118
|
// Execute based on mode
|
34
119
|
if (options.doctor) {
|
35
120
|
await runDoctorMode(options, appRoot)
|
36
|
-
} else if (options.
|
37
|
-
|
38
|
-
} else {
|
121
|
+
} else if (options.stdout) {
|
122
|
+
// Explicit stdout mode
|
39
123
|
await runStdoutMode(options, appRoot)
|
124
|
+
} else if (options.output) {
|
125
|
+
// Save to single file
|
126
|
+
await runSingleFileMode(options, appRoot)
|
127
|
+
} else {
|
128
|
+
// Default: save to directory
|
129
|
+
await runSaveMode(options, appRoot)
|
40
130
|
}
|
41
131
|
|
42
132
|
return 0
|
@@ -47,117 +137,346 @@ export async function run(args: string[]): Promise<number> {
|
|
47
137
|
}
|
48
138
|
|
49
139
|
function parseArguments(args: string[]): ExportOptions {
|
50
|
-
const
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
}
|
140
|
+
const argv = yargs(args)
|
141
|
+
.version(VERSION)
|
142
|
+
.usage(
|
143
|
+
`Shakapacker Config Exporter
|
65
144
|
|
66
|
-
|
67
|
-
|
68
|
-
if (value.length === 0) {
|
69
|
-
throw new Error(`${prefix} requires a value`)
|
70
|
-
}
|
71
|
-
return value
|
72
|
-
}
|
145
|
+
Exports webpack or rspack configuration in a verbose, human-readable format
|
146
|
+
for comparison and analysis.
|
73
147
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
148
|
+
QUICK START (for troubleshooting):
|
149
|
+
bin/export-bundler-config --doctor
|
150
|
+
|
151
|
+
Exports annotated YAML configs for both development and production.
|
152
|
+
Creates separate files for client and server bundles.
|
153
|
+
Best for debugging, AI analysis, and comparing configurations.`
|
154
|
+
)
|
155
|
+
.option("doctor", {
|
156
|
+
type: "boolean",
|
157
|
+
default: false,
|
158
|
+
description:
|
159
|
+
"Export all configs for troubleshooting (dev + prod, annotated YAML)"
|
160
|
+
})
|
161
|
+
.option("save-dir", {
|
162
|
+
type: "string",
|
163
|
+
description:
|
164
|
+
"Directory for output files (default: shakapacker-config-exports)"
|
165
|
+
})
|
166
|
+
.option("stdout", {
|
167
|
+
type: "boolean",
|
168
|
+
default: false,
|
169
|
+
description: "Output to stdout instead of saving to files"
|
170
|
+
})
|
171
|
+
.option("bundler", {
|
172
|
+
type: "string",
|
173
|
+
choices: ["webpack", "rspack"] as const,
|
174
|
+
description: "Specify bundler (auto-detected if not provided)"
|
175
|
+
})
|
176
|
+
.option("env", {
|
177
|
+
type: "string",
|
178
|
+
choices: ["development", "production", "test"] as const,
|
179
|
+
description:
|
180
|
+
"Node environment (default: development, ignored with --doctor or --build)"
|
181
|
+
})
|
182
|
+
.option("client-only", {
|
183
|
+
type: "boolean",
|
184
|
+
default: false,
|
185
|
+
description: "Generate only client config (sets CLIENT_BUNDLE_ONLY=yes)"
|
186
|
+
})
|
187
|
+
.option("server-only", {
|
188
|
+
type: "boolean",
|
189
|
+
default: false,
|
190
|
+
description: "Generate only server config (sets SERVER_BUNDLE_ONLY=yes)"
|
191
|
+
})
|
192
|
+
.option("output", {
|
193
|
+
type: "string",
|
194
|
+
description: "Output to specific file instead of directory"
|
195
|
+
})
|
196
|
+
.option("depth", {
|
197
|
+
type: "number",
|
198
|
+
default: 20,
|
199
|
+
coerce: (value: number | string) => {
|
200
|
+
if (value === "null" || value === null) return null
|
201
|
+
return typeof value === "number" ? value : parseInt(String(value), 10)
|
202
|
+
},
|
203
|
+
description: "Inspection depth (use 'null' for unlimited)"
|
204
|
+
})
|
205
|
+
.option("format", {
|
206
|
+
type: "string",
|
207
|
+
choices: ["yaml", "json", "inspect"] as const,
|
208
|
+
description: "Output format (default: yaml for files, inspect for stdout)"
|
209
|
+
})
|
210
|
+
.option("annotate", {
|
211
|
+
type: "boolean",
|
212
|
+
description:
|
213
|
+
"Enable inline documentation (YAML only, default with --doctor or file output)"
|
214
|
+
})
|
215
|
+
.option("verbose", {
|
216
|
+
type: "boolean",
|
217
|
+
default: false,
|
218
|
+
description: "Show full output without compact mode"
|
219
|
+
})
|
220
|
+
.option("init", {
|
221
|
+
type: "boolean",
|
222
|
+
default: false,
|
223
|
+
description: "Generate sample .bundler-config.yml with examples"
|
224
|
+
})
|
225
|
+
.option("config-file", {
|
226
|
+
type: "string",
|
227
|
+
description: "Path to config file (default: .bundler-config.yml)"
|
228
|
+
})
|
229
|
+
.option("build", {
|
230
|
+
type: "string",
|
231
|
+
description: "Export config for specific build from config file"
|
232
|
+
})
|
233
|
+
.option("list-builds", {
|
234
|
+
type: "boolean",
|
235
|
+
default: false,
|
236
|
+
description: "List all available builds from config file"
|
237
|
+
})
|
238
|
+
.option("all-builds", {
|
239
|
+
type: "boolean",
|
240
|
+
default: false,
|
241
|
+
description: "Export all builds from config file"
|
242
|
+
})
|
243
|
+
.option("webpack", {
|
244
|
+
type: "boolean",
|
245
|
+
default: false,
|
246
|
+
description: "Use webpack (overrides config file)"
|
247
|
+
})
|
248
|
+
.option("rspack", {
|
249
|
+
type: "boolean",
|
250
|
+
default: false,
|
251
|
+
description: "Use rspack (overrides config file)"
|
252
|
+
})
|
253
|
+
.check((argv) => {
|
254
|
+
if (argv.webpack && argv.rspack) {
|
86
255
|
throw new Error(
|
87
|
-
|
256
|
+
"--webpack and --rspack are mutually exclusive. Please specify only one."
|
88
257
|
)
|
89
258
|
}
|
90
|
-
|
91
|
-
} else if (arg.startsWith("--env=")) {
|
92
|
-
const env = parseValue(arg, "--env=")
|
93
|
-
if (env !== "development" && env !== "production" && env !== "test") {
|
259
|
+
if (argv["client-only"] && argv["server-only"]) {
|
94
260
|
throw new Error(
|
95
|
-
|
261
|
+
"--client-only and --server-only are mutually exclusive. Please specify only one."
|
96
262
|
)
|
97
263
|
}
|
98
|
-
|
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") {
|
264
|
+
if (argv.output && argv["save-dir"]) {
|
111
265
|
throw new Error(
|
112
|
-
|
266
|
+
"--output and --save-dir are mutually exclusive. Use one or the other."
|
113
267
|
)
|
114
268
|
}
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
269
|
+
if (argv.stdout && argv["save-dir"]) {
|
270
|
+
throw new Error(
|
271
|
+
"--stdout and --save-dir are mutually exclusive. Use one or the other."
|
272
|
+
)
|
273
|
+
}
|
274
|
+
if (argv.build && argv["all-builds"]) {
|
275
|
+
throw new Error(
|
276
|
+
"--build and --all-builds are mutually exclusive. Use one or the other."
|
277
|
+
)
|
278
|
+
}
|
279
|
+
return true
|
280
|
+
})
|
281
|
+
.help("help")
|
282
|
+
.alias("help", "h")
|
283
|
+
.epilogue(
|
284
|
+
`Examples:
|
285
|
+
|
286
|
+
# Config File Workflow
|
287
|
+
bin/export-bundler-config --init
|
288
|
+
bin/export-bundler-config --list-builds
|
289
|
+
bin/export-bundler-config --build=dev
|
290
|
+
bin/export-bundler-config --all-builds --save-dir=./configs
|
291
|
+
bin/export-bundler-config --build=dev --rspack
|
292
|
+
|
293
|
+
# Traditional Workflow (without config file)
|
294
|
+
bin/export-bundler-config --doctor
|
295
|
+
# Creates: webpack-development-client-hmr.yaml, webpack-development-client.yaml,
|
296
|
+
# webpack-development-server.yaml, webpack-production-client.yaml,
|
297
|
+
# webpack-production-server.yaml
|
298
|
+
|
299
|
+
bin/export-bundler-config --env=production --client-only
|
300
|
+
bin/export-bundler-config --save-dir=./debug
|
301
|
+
bin/export-bundler-config # Saves to shakapacker-config-exports/
|
122
302
|
|
123
|
-
|
303
|
+
# View config in terminal (stdout)
|
304
|
+
bin/export-bundler-config --stdout
|
305
|
+
bin/export-bundler-config --output=config.yaml # Save to specific file`
|
306
|
+
)
|
307
|
+
.strict()
|
308
|
+
.parseSync()
|
309
|
+
|
310
|
+
// Type assertions are safe here because yargs validates choices at runtime
|
311
|
+
// Handle --webpack and --rspack flags
|
312
|
+
let bundler: "webpack" | "rspack" | undefined = argv.bundler as
|
313
|
+
| "webpack"
|
314
|
+
| "rspack"
|
315
|
+
| undefined
|
316
|
+
if (argv.webpack) bundler = "webpack"
|
317
|
+
if (argv.rspack) bundler = "rspack"
|
318
|
+
|
319
|
+
return {
|
320
|
+
bundler,
|
321
|
+
env: argv.env as "development" | "production" | "test" | undefined,
|
322
|
+
clientOnly: argv["client-only"],
|
323
|
+
serverOnly: argv["server-only"],
|
324
|
+
output: argv.output,
|
325
|
+
depth: argv.depth as number | null,
|
326
|
+
format: argv.format as "yaml" | "json" | "inspect" | undefined,
|
327
|
+
help: false, // yargs handles help internally
|
328
|
+
verbose: argv.verbose,
|
329
|
+
doctor: argv.doctor,
|
330
|
+
saveDir: argv["save-dir"],
|
331
|
+
stdout: argv.stdout,
|
332
|
+
annotate: argv.annotate,
|
333
|
+
init: argv.init,
|
334
|
+
configFile: argv["config-file"],
|
335
|
+
build: argv.build,
|
336
|
+
listBuilds: argv["list-builds"],
|
337
|
+
allBuilds: argv["all-builds"]
|
338
|
+
}
|
124
339
|
}
|
125
340
|
|
126
341
|
function applyDefaults(options: ExportOptions): void {
|
127
342
|
if (options.doctor) {
|
128
|
-
options.save = true
|
129
343
|
if (options.format === undefined) options.format = "yaml"
|
130
344
|
if (options.annotate === undefined) options.annotate = true
|
131
|
-
} else if (options.
|
345
|
+
} else if (!options.stdout && !options.output) {
|
346
|
+
// Default mode: save to directory
|
132
347
|
if (options.format === undefined) options.format = "yaml"
|
133
348
|
if (options.annotate === undefined) options.annotate = true
|
134
349
|
} else {
|
135
350
|
if (options.format === undefined) options.format = "inspect"
|
136
351
|
if (options.annotate === undefined) options.annotate = false
|
137
352
|
}
|
353
|
+
|
354
|
+
// Set default save directory for file output modes
|
355
|
+
if (!options.stdout && !options.output && !options.saveDir) {
|
356
|
+
options.saveDir = resolve(process.cwd(), "shakapacker-config-exports")
|
357
|
+
}
|
138
358
|
}
|
139
359
|
|
140
|
-
function
|
141
|
-
|
142
|
-
|
143
|
-
|
360
|
+
function runInitCommand(options: ExportOptions): number {
|
361
|
+
const configPath = options.configFile || ".bundler-config.yml"
|
362
|
+
const fullPath = resolve(process.cwd(), configPath)
|
363
|
+
|
364
|
+
if (existsSync(fullPath)) {
|
365
|
+
console.error(
|
366
|
+
`[Config Exporter] Error: Config file already exists: ${fullPath}`
|
367
|
+
)
|
368
|
+
console.error(
|
369
|
+
`Remove it first or use --config-file=<path> for a different location.`
|
144
370
|
)
|
371
|
+
return 1
|
145
372
|
}
|
146
373
|
|
147
|
-
|
148
|
-
|
149
|
-
}
|
374
|
+
const sampleConfig = generateSampleConfigFile()
|
375
|
+
writeFileSync(fullPath, sampleConfig, "utf8")
|
150
376
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
377
|
+
console.log(`[Config Exporter] ✅ Created config file: ${fullPath}`)
|
378
|
+
console.log(`\nNext steps:`)
|
379
|
+
console.log(` 1. Edit the config file to match your build setup`)
|
380
|
+
console.log(
|
381
|
+
` 2. List available builds: bin/export-bundler-config --list-builds`
|
382
|
+
)
|
383
|
+
console.log(
|
384
|
+
` 3. Export a build: bin/export-bundler-config --build=<name> --save\n`
|
385
|
+
)
|
386
|
+
|
387
|
+
return 0
|
388
|
+
}
|
389
|
+
|
390
|
+
function runListBuildsCommand(options: ExportOptions): number {
|
391
|
+
try {
|
392
|
+
const loader = new ConfigFileLoader(options.configFile)
|
393
|
+
loader.listBuilds()
|
394
|
+
return 0
|
395
|
+
} catch (error: any) {
|
396
|
+
console.error(`[Config Exporter] Error: ${error.message}`)
|
397
|
+
return 1
|
155
398
|
}
|
399
|
+
}
|
400
|
+
|
401
|
+
async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
|
402
|
+
// Save original environment to restore after all builds
|
403
|
+
const savedEnv = saveBuildEnvironmentVariables()
|
404
|
+
|
405
|
+
try {
|
406
|
+
// Set up environment
|
407
|
+
const appRoot = findAppRoot()
|
408
|
+
process.chdir(appRoot)
|
409
|
+
setupNodePath(appRoot)
|
410
|
+
|
411
|
+
// Apply defaults
|
412
|
+
applyDefaults(options)
|
413
|
+
|
414
|
+
const loader = new ConfigFileLoader(options.configFile)
|
415
|
+
if (!loader.exists()) {
|
416
|
+
const configPath = options.configFile || ".bundler-config.yml"
|
417
|
+
throw new Error(
|
418
|
+
`Config file ${configPath} not found. Run --init to create it.`
|
419
|
+
)
|
420
|
+
}
|
156
421
|
|
157
|
-
|
158
|
-
|
159
|
-
|
422
|
+
const config = loader.load()
|
423
|
+
const buildNames = Object.keys(config.builds)
|
424
|
+
|
425
|
+
console.log(
|
426
|
+
`\n📦 Exporting ${buildNames.length} builds from config file...\n`
|
160
427
|
)
|
428
|
+
|
429
|
+
const fileWriter = new FileWriter()
|
430
|
+
const targetDir = options.saveDir! // Set by applyDefaults
|
431
|
+
const createdFiles: string[] = []
|
432
|
+
|
433
|
+
// Export each build
|
434
|
+
for (const buildName of buildNames) {
|
435
|
+
console.log(`\n📦 Exporting build: ${buildName}`)
|
436
|
+
|
437
|
+
// Clear and restore environment to prevent leakage between builds
|
438
|
+
clearBuildEnvironmentVariables()
|
439
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
440
|
+
|
441
|
+
// Create a modified options object for this build
|
442
|
+
const buildOptions = { ...options, build: buildName }
|
443
|
+
const configs = await loadConfigsForEnv(undefined, buildOptions, appRoot)
|
444
|
+
|
445
|
+
for (const { config: cfg, metadata } of configs) {
|
446
|
+
const output = formatConfig(cfg, metadata, options, appRoot)
|
447
|
+
const filename = fileWriter.generateFilename(
|
448
|
+
metadata.bundler,
|
449
|
+
metadata.environment,
|
450
|
+
metadata.configType,
|
451
|
+
options.format!,
|
452
|
+
metadata.buildName
|
453
|
+
)
|
454
|
+
|
455
|
+
const fullPath = resolve(targetDir, filename)
|
456
|
+
fileWriter.writeSingleFile(fullPath, output)
|
457
|
+
createdFiles.push(fullPath)
|
458
|
+
}
|
459
|
+
}
|
460
|
+
|
461
|
+
// Print summary
|
462
|
+
console.log("\n" + "=".repeat(80))
|
463
|
+
console.log("✅ All Builds Exported!")
|
464
|
+
console.log("=".repeat(80))
|
465
|
+
console.log(`\nCreated ${createdFiles.length} configuration file(s) in:`)
|
466
|
+
console.log(` ${targetDir}\n`)
|
467
|
+
console.log("Files:")
|
468
|
+
createdFiles.forEach((file) => {
|
469
|
+
console.log(` ✓ ${basename(file)}`)
|
470
|
+
})
|
471
|
+
console.log("\n" + "=".repeat(80) + "\n")
|
472
|
+
|
473
|
+
return 0
|
474
|
+
} catch (error: any) {
|
475
|
+
console.error(`[Config Exporter] Error: ${error.message}`)
|
476
|
+
return 1
|
477
|
+
} finally {
|
478
|
+
// Restore original environment
|
479
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
161
480
|
}
|
162
481
|
}
|
163
482
|
|
@@ -165,42 +484,146 @@ async function runDoctorMode(
|
|
165
484
|
options: ExportOptions,
|
166
485
|
appRoot: string
|
167
486
|
): Promise<void> {
|
168
|
-
|
169
|
-
|
170
|
-
console.log("=".repeat(80))
|
171
|
-
console.log("\nExporting development AND production configs...")
|
172
|
-
console.log("")
|
487
|
+
// Save original environment to restore after all builds
|
488
|
+
const savedEnv = saveBuildEnvironmentVariables()
|
173
489
|
|
174
|
-
|
175
|
-
"
|
176
|
-
"
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
490
|
+
try {
|
491
|
+
console.log("\n" + "=".repeat(80))
|
492
|
+
console.log("🔍 Config Exporter - Doctor Mode")
|
493
|
+
console.log("=".repeat(80))
|
494
|
+
|
495
|
+
const fileWriter = new FileWriter()
|
496
|
+
const targetDir = options.saveDir! // Set by applyDefaults
|
497
|
+
|
498
|
+
const createdFiles: string[] = []
|
499
|
+
|
500
|
+
// Check if config file exists with shakapacker_doctor_default_builds_here flag
|
501
|
+
const configFilePath = options.configFile || ".bundler-config.yml"
|
502
|
+
const loader = new ConfigFileLoader(configFilePath)
|
503
|
+
|
504
|
+
if (loader.exists()) {
|
505
|
+
try {
|
506
|
+
const configData = loader.load()
|
507
|
+
if (configData.shakapacker_doctor_default_builds_here) {
|
508
|
+
console.log(
|
509
|
+
"\nUsing builds from config file (shakapacker_doctor_default_builds_here: true)...\n"
|
510
|
+
)
|
511
|
+
// Use config file builds
|
512
|
+
const buildNames = Object.keys(configData.builds)
|
513
|
+
|
514
|
+
for (const buildName of buildNames) {
|
515
|
+
console.log(`\n📦 Loading build: ${buildName}`)
|
516
|
+
|
517
|
+
// Clear and restore environment to prevent leakage between builds
|
518
|
+
clearBuildEnvironmentVariables()
|
519
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
520
|
+
|
521
|
+
const configs = await loadConfigsForEnv(
|
522
|
+
undefined,
|
523
|
+
{ ...options, build: buildName },
|
524
|
+
appRoot
|
525
|
+
)
|
526
|
+
|
527
|
+
for (const { config, metadata } of configs) {
|
528
|
+
const output = formatConfig(config, metadata, options, appRoot)
|
529
|
+
const filename = fileWriter.generateFilename(
|
530
|
+
metadata.bundler,
|
531
|
+
metadata.environment,
|
532
|
+
metadata.configType,
|
533
|
+
options.format!,
|
534
|
+
metadata.buildName
|
535
|
+
)
|
536
|
+
const fullPath = resolve(targetDir, filename)
|
537
|
+
fileWriter.writeSingleFile(fullPath, output)
|
538
|
+
createdFiles.push(fullPath)
|
539
|
+
}
|
540
|
+
}
|
181
541
|
|
182
|
-
|
542
|
+
// Print summary and exit early
|
543
|
+
printDoctorSummary(createdFiles, targetDir)
|
544
|
+
return
|
545
|
+
}
|
546
|
+
} catch (error: any) {
|
547
|
+
// If config file exists but is invalid, warn and fall through to default behavior
|
548
|
+
console.log(`\n⚠️ Config file found but invalid: ${error.message}`)
|
549
|
+
console.log("Falling back to default doctor mode...\n")
|
550
|
+
}
|
551
|
+
}
|
183
552
|
|
184
|
-
|
185
|
-
console.log(
|
186
|
-
|
553
|
+
// Default behavior: hardcoded configs
|
554
|
+
console.log("\nExporting all development and production configs...")
|
555
|
+
console.log("")
|
187
556
|
|
188
|
-
|
189
|
-
|
190
|
-
const
|
191
|
-
|
192
|
-
|
193
|
-
metadata.configType,
|
194
|
-
options.format!
|
195
|
-
)
|
557
|
+
const configsToExport = [
|
558
|
+
{ label: "development (HMR)", env: "development" as const, hmr: true },
|
559
|
+
{ label: "development", env: "development" as const, hmr: false },
|
560
|
+
{ label: "production", env: "production" as const, hmr: false }
|
561
|
+
]
|
196
562
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
563
|
+
for (const { label, env, hmr } of configsToExport) {
|
564
|
+
console.log(`\n📦 Loading ${label} configuration...`)
|
565
|
+
|
566
|
+
// Clear and restore environment to prevent leakage between builds
|
567
|
+
clearBuildEnvironmentVariables()
|
568
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
569
|
+
|
570
|
+
// Set WEBPACK_SERVE for HMR config
|
571
|
+
if (hmr) {
|
572
|
+
process.env.WEBPACK_SERVE = "true"
|
573
|
+
}
|
574
|
+
|
575
|
+
const configs = await loadConfigsForEnv(env, options, appRoot)
|
576
|
+
|
577
|
+
for (const { config, metadata } of configs) {
|
578
|
+
const output = formatConfig(config, metadata, options, appRoot)
|
579
|
+
|
580
|
+
// Adjust filename for HMR config
|
581
|
+
let filename: string
|
582
|
+
if (
|
583
|
+
hmr &&
|
584
|
+
(metadata.configType === "client" || metadata.configType === "all")
|
585
|
+
) {
|
586
|
+
/**
|
587
|
+
* HMR Mode Filename Logic:
|
588
|
+
* - When WEBPACK_SERVE=true, webpack-dev-server runs and HMR is enabled
|
589
|
+
* - HMR only applies to client bundles (server bundles don't use HMR)
|
590
|
+
* - If configType is "all", we still only generate client file for HMR
|
591
|
+
* because the server bundle is identical to non-HMR development
|
592
|
+
* - Filename uses "client" type and "development-hmr" build name to
|
593
|
+
* distinguish it from regular development client bundle
|
594
|
+
*/
|
595
|
+
filename = fileWriter.generateFilename(
|
596
|
+
metadata.bundler,
|
597
|
+
metadata.environment,
|
598
|
+
"client",
|
599
|
+
options.format!,
|
600
|
+
"development-hmr"
|
601
|
+
)
|
602
|
+
} else {
|
603
|
+
filename = fileWriter.generateFilename(
|
604
|
+
metadata.bundler,
|
605
|
+
metadata.environment,
|
606
|
+
metadata.configType,
|
607
|
+
options.format!,
|
608
|
+
metadata.buildName
|
609
|
+
)
|
610
|
+
}
|
611
|
+
|
612
|
+
const fullPath = resolve(targetDir, filename)
|
613
|
+
const fileOutput: FileOutput = { filename, content: output, metadata }
|
614
|
+
fileWriter.writeSingleFile(fullPath, output)
|
615
|
+
createdFiles.push(fullPath)
|
616
|
+
}
|
201
617
|
}
|
618
|
+
|
619
|
+
printDoctorSummary(createdFiles, targetDir)
|
620
|
+
} finally {
|
621
|
+
// Restore original environment
|
622
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
202
623
|
}
|
624
|
+
}
|
203
625
|
|
626
|
+
function printDoctorSummary(createdFiles: string[], targetDir: string): void {
|
204
627
|
// Print summary
|
205
628
|
console.log("\n" + "=".repeat(80))
|
206
629
|
console.log("✅ Export Complete!")
|
@@ -239,11 +662,13 @@ async function runSaveMode(
|
|
239
662
|
options: ExportOptions,
|
240
663
|
appRoot: string
|
241
664
|
): Promise<void> {
|
242
|
-
|
665
|
+
const env = options.env || "development"
|
666
|
+
console.log(`[Config Exporter] Exporting ${env} configs`)
|
243
667
|
|
244
668
|
const fileWriter = new FileWriter()
|
245
|
-
const targetDir = options.saveDir
|
246
|
-
const configs = await loadConfigsForEnv(options.env
|
669
|
+
const targetDir = options.saveDir! // Set by applyDefaults
|
670
|
+
const configs = await loadConfigsForEnv(options.env, options, appRoot)
|
671
|
+
const createdFiles: string[] = []
|
247
672
|
|
248
673
|
if (options.output) {
|
249
674
|
// Single file output
|
@@ -257,7 +682,9 @@ async function runSaveMode(
|
|
257
682
|
options,
|
258
683
|
appRoot
|
259
684
|
)
|
260
|
-
|
685
|
+
const fullPath = resolve(options.output)
|
686
|
+
fileWriter.writeSingleFile(fullPath, output)
|
687
|
+
createdFiles.push(fullPath)
|
261
688
|
} else {
|
262
689
|
// Multi-file output (one per config)
|
263
690
|
for (const { config, metadata } of configs) {
|
@@ -266,11 +693,20 @@ async function runSaveMode(
|
|
266
693
|
metadata.bundler,
|
267
694
|
metadata.environment,
|
268
695
|
metadata.configType,
|
269
|
-
options.format
|
696
|
+
options.format!,
|
697
|
+
metadata.buildName
|
270
698
|
)
|
271
|
-
|
699
|
+
const fullPath = resolve(targetDir, filename)
|
700
|
+
fileWriter.writeSingleFile(fullPath, output)
|
701
|
+
createdFiles.push(fullPath)
|
272
702
|
}
|
273
703
|
}
|
704
|
+
|
705
|
+
// Log all created files
|
706
|
+
console.log(`\n[Config Exporter] Created ${createdFiles.length} file(s):`)
|
707
|
+
createdFiles.forEach((file) => {
|
708
|
+
console.log(` ✓ ${file}`)
|
709
|
+
})
|
274
710
|
}
|
275
711
|
|
276
712
|
async function runStdoutMode(
|
@@ -289,17 +725,116 @@ async function runStdoutMode(
|
|
289
725
|
console.log(output)
|
290
726
|
}
|
291
727
|
|
728
|
+
async function runSingleFileMode(
|
729
|
+
options: ExportOptions,
|
730
|
+
appRoot: string
|
731
|
+
): Promise<void> {
|
732
|
+
const configs = await loadConfigsForEnv(options.env!, options, appRoot)
|
733
|
+
const combined = configs.map((c) => c.config)
|
734
|
+
const metadata = configs[0].metadata
|
735
|
+
metadata.configCount = combined.length
|
736
|
+
|
737
|
+
const config = combined.length === 1 ? combined[0] : combined
|
738
|
+
const output = formatConfig(config, metadata, options, appRoot)
|
739
|
+
|
740
|
+
const fileWriter = new FileWriter()
|
741
|
+
const filePath = resolve(process.cwd(), options.output!)
|
742
|
+
fileWriter.writeSingleFile(filePath, output)
|
743
|
+
}
|
744
|
+
|
292
745
|
async function loadConfigsForEnv(
|
293
|
-
env: "development" | "production" | "test",
|
746
|
+
env: "development" | "production" | "test" | undefined,
|
294
747
|
options: ExportOptions,
|
295
748
|
appRoot: string
|
296
749
|
): Promise<Array<{ config: any; metadata: ConfigMetadata }>> {
|
297
|
-
|
298
|
-
|
750
|
+
let bundler: "webpack" | "rspack"
|
751
|
+
let buildName: string | undefined
|
752
|
+
let buildOutputs: string[] = []
|
753
|
+
let customConfigFile: string | undefined
|
754
|
+
let bundlerEnvArgs: string[] = []
|
755
|
+
let finalEnv: "development" | "production" | "test"
|
756
|
+
|
757
|
+
// If using config file build
|
758
|
+
if (options.build) {
|
759
|
+
// Use a temporary env for auto-detection, will be overridden by build config
|
760
|
+
const tempEnv = env || "development"
|
761
|
+
const loader = new ConfigFileLoader(options.configFile)
|
762
|
+
const defaultBundler = await autoDetectBundler(tempEnv, appRoot)
|
763
|
+
const resolvedBuild = loader.resolveBuild(
|
764
|
+
options.build,
|
765
|
+
options,
|
766
|
+
defaultBundler
|
767
|
+
)
|
768
|
+
|
769
|
+
bundler = resolvedBuild.bundler
|
770
|
+
buildName = resolvedBuild.name
|
771
|
+
buildOutputs = resolvedBuild.outputs
|
772
|
+
customConfigFile = resolvedBuild.configFile
|
773
|
+
bundlerEnvArgs = resolvedBuild.bundlerEnvArgs
|
774
|
+
|
775
|
+
// Set environment variables from config
|
776
|
+
// Security: Only allow specific environment variables to prevent malicious configs
|
777
|
+
const DANGEROUS_ENV_VARS = [
|
778
|
+
"PATH",
|
779
|
+
"HOME",
|
780
|
+
"LD_PRELOAD",
|
781
|
+
"LD_LIBRARY_PATH",
|
782
|
+
"DYLD_LIBRARY_PATH",
|
783
|
+
"DYLD_INSERT_LIBRARIES"
|
784
|
+
]
|
785
|
+
|
786
|
+
for (const [key, value] of Object.entries(resolvedBuild.environment)) {
|
787
|
+
if (DANGEROUS_ENV_VARS.includes(key)) {
|
788
|
+
console.warn(
|
789
|
+
`[Config Exporter] Warning: Skipping dangerous environment variable: ${key}`
|
790
|
+
)
|
791
|
+
continue
|
792
|
+
}
|
793
|
+
if (!(BUILD_ENV_VARS as readonly string[]).includes(key)) {
|
794
|
+
console.warn(
|
795
|
+
`[Config Exporter] Warning: Skipping non-whitelisted environment variable: ${key}. ` +
|
796
|
+
`Allowed variables are: ${BUILD_ENV_VARS.join(", ")}`
|
797
|
+
)
|
798
|
+
continue
|
799
|
+
}
|
800
|
+
process.env[key] = value
|
801
|
+
}
|
802
|
+
|
803
|
+
// Determine final env: CLI flag > build config NODE_ENV > default
|
804
|
+
if (options.env) {
|
805
|
+
finalEnv = options.env
|
806
|
+
} else if (resolvedBuild.environment.NODE_ENV) {
|
807
|
+
const nodeEnv = resolvedBuild.environment.NODE_ENV
|
808
|
+
const allowedEnvs = ["development", "production", "test"]
|
809
|
+
if (allowedEnvs.includes(nodeEnv)) {
|
810
|
+
finalEnv = nodeEnv as "development" | "production" | "test"
|
811
|
+
} else {
|
812
|
+
throw new Error(
|
813
|
+
`Invalid NODE_ENV value in config: "${nodeEnv}". ` +
|
814
|
+
`Allowed values are: ${allowedEnvs.join(", ")}.`
|
815
|
+
)
|
816
|
+
}
|
817
|
+
} else {
|
818
|
+
finalEnv = "development"
|
819
|
+
}
|
820
|
+
|
821
|
+
// Sync process.env to reflect resolved environment
|
822
|
+
process.env.NODE_ENV = finalEnv
|
823
|
+
// Determine RAILS_ENV: CLI env option > build config RAILS_ENV > finalEnv
|
824
|
+
const railsEnv =
|
825
|
+
options.env || resolvedBuild.environment.RAILS_ENV || finalEnv
|
826
|
+
process.env.RAILS_ENV = railsEnv
|
827
|
+
} else {
|
828
|
+
// No build config - use CLI env or default
|
829
|
+
finalEnv = env || "development"
|
299
830
|
|
300
|
-
|
301
|
-
|
302
|
-
|
831
|
+
// Auto-detect bundler if not specified
|
832
|
+
bundler = options.bundler || (await autoDetectBundler(finalEnv, appRoot))
|
833
|
+
|
834
|
+
// Set environment variables
|
835
|
+
process.env.NODE_ENV = finalEnv
|
836
|
+
process.env.RAILS_ENV = finalEnv
|
837
|
+
}
|
303
838
|
|
304
839
|
if (options.clientOnly) {
|
305
840
|
process.env.CLIENT_BUNDLE_ONLY = "yes"
|
@@ -308,12 +843,16 @@ async function loadConfigsForEnv(
|
|
308
843
|
}
|
309
844
|
|
310
845
|
// Find and load config file
|
311
|
-
const configFile =
|
846
|
+
const configFile =
|
847
|
+
customConfigFile || findConfigFile(bundler, appRoot, finalEnv)
|
312
848
|
// Quiet mode for cleaner output - only show if verbose or errors
|
313
849
|
if (process.env.VERBOSE) {
|
314
850
|
console.log(`[Config Exporter] Loading config: ${configFile}`)
|
315
|
-
console.log(`[Config Exporter] Environment: ${
|
851
|
+
console.log(`[Config Exporter] Environment: ${finalEnv}`)
|
316
852
|
console.log(`[Config Exporter] Bundler: ${bundler}`)
|
853
|
+
if (buildName) {
|
854
|
+
console.log(`[Config Exporter] Build: ${buildName}`)
|
855
|
+
}
|
317
856
|
}
|
318
857
|
|
319
858
|
// Load the config
|
@@ -331,8 +870,26 @@ async function loadConfigsForEnv(
|
|
331
870
|
}
|
332
871
|
|
333
872
|
// Clear require cache for config file and all related modules
|
334
|
-
|
335
|
-
|
873
|
+
/**
|
874
|
+
* AGGRESSIVE REQUIRE CACHE CLEARING
|
875
|
+
*
|
876
|
+
* Why: This tool can load multiple environments (dev/prod) and builds in a
|
877
|
+
* single process. Node's require cache prevents modules from re-evaluating,
|
878
|
+
* which causes stale environment values (NODE_ENV, etc.) to persist.
|
879
|
+
*
|
880
|
+
* What: Clears cache for:
|
881
|
+
* - Webpack/rspack config files (they read process.env)
|
882
|
+
* - Shakapacker modules (env detection, config loading)
|
883
|
+
* - Config directory files (custom helpers that may read env)
|
884
|
+
*
|
885
|
+
* Trade-offs:
|
886
|
+
* - More reliable: Ensures each build gets fresh environment
|
887
|
+
* - Potentially brittle: String matching on paths (but comprehensive)
|
888
|
+
* - Performance: Minimal impact since this runs per-build, not per-file
|
889
|
+
*
|
890
|
+
* Maintenance: If adding new shakapacker modules that read env vars,
|
891
|
+
* ensure their paths are covered by the patterns below.
|
892
|
+
*/
|
336
893
|
const configDir = dirname(configFile)
|
337
894
|
Object.keys(require.cache).forEach((key) => {
|
338
895
|
if (
|
@@ -359,6 +916,38 @@ async function loadConfigsForEnv(
|
|
359
916
|
loadedConfig = loadedConfig.default
|
360
917
|
}
|
361
918
|
|
919
|
+
// Handle function exports (webpack config functions)
|
920
|
+
if (typeof loadedConfig === "function") {
|
921
|
+
// Webpack config functions receive (env, argv) parameters
|
922
|
+
// Build env object from bundler_env args if available
|
923
|
+
const envObject: Record<string, any> = {}
|
924
|
+
if (bundlerEnvArgs && bundlerEnvArgs.length > 0) {
|
925
|
+
// Parse --env key=value or --env key into object
|
926
|
+
for (let i = 0; i < bundlerEnvArgs.length; i += 2) {
|
927
|
+
if (bundlerEnvArgs[i] === "--env") {
|
928
|
+
const envArg = bundlerEnvArgs[i + 1]
|
929
|
+
if (envArg.includes("=")) {
|
930
|
+
const [key, value] = envArg.split("=")
|
931
|
+
envObject[key] = value
|
932
|
+
} else {
|
933
|
+
envObject[envArg] = true
|
934
|
+
}
|
935
|
+
}
|
936
|
+
}
|
937
|
+
}
|
938
|
+
|
939
|
+
const argv = { mode: finalEnv }
|
940
|
+
try {
|
941
|
+
loadedConfig = loadedConfig(envObject, argv)
|
942
|
+
} catch (error: any) {
|
943
|
+
throw new Error(
|
944
|
+
`Failed to execute config function: ${error.message}\n` +
|
945
|
+
`Config file: ${configFile}\n` +
|
946
|
+
`Environment: ${JSON.stringify(envObject)}`
|
947
|
+
)
|
948
|
+
}
|
949
|
+
}
|
950
|
+
|
362
951
|
// Determine config type and split if array
|
363
952
|
const configs: any[] = Array.isArray(loadedConfig)
|
364
953
|
? loadedConfig
|
@@ -368,8 +957,27 @@ async function loadConfigsForEnv(
|
|
368
957
|
configs.forEach((cfg, index) => {
|
369
958
|
let configType: "client" | "server" | "all" = "all"
|
370
959
|
|
371
|
-
//
|
372
|
-
if (
|
960
|
+
// Use outputs from build config if available
|
961
|
+
if (
|
962
|
+
buildOutputs.length > 0 &&
|
963
|
+
index < buildOutputs.length &&
|
964
|
+
buildOutputs[index]
|
965
|
+
) {
|
966
|
+
const outputValue = buildOutputs[index]
|
967
|
+
// Validate the output value is a valid config type
|
968
|
+
if (
|
969
|
+
outputValue === "client" ||
|
970
|
+
outputValue === "server" ||
|
971
|
+
outputValue === "all"
|
972
|
+
) {
|
973
|
+
configType = outputValue
|
974
|
+
} else {
|
975
|
+
throw new Error(
|
976
|
+
`Invalid output type '${outputValue}' at index ${index} in build '${buildName}'. ` +
|
977
|
+
`Allowed values are: client, server, all`
|
978
|
+
)
|
979
|
+
}
|
980
|
+
} else if (configs.length === 2) {
|
373
981
|
// Likely client and server configs
|
374
982
|
configType = index === 0 ? "client" : "server"
|
375
983
|
} else if (options.clientOnly) {
|
@@ -381,15 +989,17 @@ async function loadConfigsForEnv(
|
|
381
989
|
const metadata: ConfigMetadata = {
|
382
990
|
exportedAt: new Date().toISOString(),
|
383
991
|
bundler,
|
384
|
-
environment:
|
992
|
+
environment: finalEnv,
|
385
993
|
configFile,
|
386
994
|
configType,
|
387
995
|
configCount: configs.length,
|
996
|
+
buildName,
|
388
997
|
environmentVariables: {
|
389
998
|
NODE_ENV: process.env.NODE_ENV,
|
390
999
|
RAILS_ENV: process.env.RAILS_ENV,
|
391
1000
|
CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
|
392
|
-
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY
|
1001
|
+
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY,
|
1002
|
+
WEBPACK_SERVE: process.env.WEBPACK_SERVE
|
393
1003
|
}
|
394
1004
|
}
|
395
1005
|
|
@@ -526,46 +1136,86 @@ function cleanConfig(obj: any, rootPath: string): any {
|
|
526
1136
|
return clean(obj)
|
527
1137
|
}
|
528
1138
|
|
529
|
-
|
1139
|
+
/**
|
1140
|
+
* Loads and returns shakapacker.yml configuration
|
1141
|
+
*/
|
1142
|
+
function loadShakapackerConfig(
|
530
1143
|
env: string,
|
531
1144
|
appRoot: string
|
532
|
-
):
|
1145
|
+
): { bundler: "webpack" | "rspack"; configPath: string } {
|
533
1146
|
try {
|
534
|
-
const
|
1147
|
+
const configFilePath =
|
535
1148
|
process.env.SHAKAPACKER_CONFIG ||
|
536
1149
|
resolve(appRoot, "config/shakapacker.yml")
|
537
1150
|
|
538
|
-
if (existsSync(
|
539
|
-
const config: any = loadYaml(readFileSync(
|
1151
|
+
if (existsSync(configFilePath)) {
|
1152
|
+
const config: any = loadYaml(readFileSync(configFilePath, "utf8"))
|
540
1153
|
const envConfig = config[env] || config.default || {}
|
1154
|
+
|
1155
|
+
// Get bundler
|
541
1156
|
const bundler = envConfig.assets_bundler || "webpack"
|
542
1157
|
if (bundler !== "webpack" && bundler !== "rspack") {
|
543
1158
|
console.warn(
|
544
1159
|
`[Config Exporter] Invalid bundler '${bundler}' in shakapacker.yml, defaulting to webpack`
|
545
1160
|
)
|
546
|
-
return
|
1161
|
+
return {
|
1162
|
+
bundler: "webpack",
|
1163
|
+
configPath: bundler === "rspack" ? "config/rspack" : "config/webpack"
|
1164
|
+
}
|
547
1165
|
}
|
548
|
-
|
549
|
-
|
1166
|
+
|
1167
|
+
// Get config path
|
1168
|
+
const customConfigPath = envConfig.assets_bundler_config_path
|
1169
|
+
const configPath =
|
1170
|
+
customConfigPath ||
|
1171
|
+
(bundler === "rspack" ? "config/rspack" : "config/webpack")
|
1172
|
+
|
1173
|
+
console.log(
|
1174
|
+
`[Config Exporter] Auto-detected bundler: ${bundler}, config path: ${configPath}`
|
1175
|
+
)
|
1176
|
+
return { bundler, configPath }
|
550
1177
|
}
|
551
1178
|
} catch (error: any) {
|
552
1179
|
console.warn(
|
553
|
-
`[Config Exporter] Error
|
1180
|
+
`[Config Exporter] Error loading shakapacker config, defaulting to webpack`
|
554
1181
|
)
|
555
1182
|
}
|
556
1183
|
|
557
|
-
return "webpack"
|
1184
|
+
return { bundler: "webpack", configPath: "config/webpack" }
|
1185
|
+
}
|
1186
|
+
|
1187
|
+
/**
|
1188
|
+
* Auto-detects bundler from shakapacker.yml
|
1189
|
+
*
|
1190
|
+
* Error Handling Strategy:
|
1191
|
+
* - Invalid bundler → warns and defaults to webpack (graceful fallback)
|
1192
|
+
* - Config read errors → warns and defaults to webpack (graceful fallback)
|
1193
|
+
*
|
1194
|
+
* Rationale for warnings vs errors:
|
1195
|
+
* - This reads shakapacker.yml (infrastructure config), not user build config
|
1196
|
+
* - Failures here should not block the tool; defaulting to webpack is safe
|
1197
|
+
* - Contrast with NODE_ENV validation in build configs, which throws errors
|
1198
|
+
* because invalid NODE_ENV would produce incorrect builds
|
1199
|
+
*/
|
1200
|
+
async function autoDetectBundler(
|
1201
|
+
env: string,
|
1202
|
+
appRoot: string
|
1203
|
+
): Promise<"webpack" | "rspack"> {
|
1204
|
+
const { bundler } = loadShakapackerConfig(env, appRoot)
|
1205
|
+
return bundler
|
558
1206
|
}
|
559
1207
|
|
560
1208
|
function findConfigFile(
|
561
1209
|
bundler: "webpack" | "rspack",
|
562
|
-
appRoot: string
|
1210
|
+
appRoot: string,
|
1211
|
+
env: string
|
563
1212
|
): string {
|
1213
|
+
const { configPath } = loadShakapackerConfig(env, appRoot)
|
564
1214
|
const extensions = ["ts", "js"]
|
565
1215
|
|
566
1216
|
if (bundler === "rspack") {
|
567
1217
|
for (const ext of extensions) {
|
568
|
-
const rspackPath = resolve(appRoot, `
|
1218
|
+
const rspackPath = resolve(appRoot, configPath, `rspack.config.${ext}`)
|
569
1219
|
if (existsSync(rspackPath)) {
|
570
1220
|
return rspackPath
|
571
1221
|
}
|
@@ -574,14 +1224,14 @@ function findConfigFile(
|
|
574
1224
|
|
575
1225
|
// Fall back to webpack config
|
576
1226
|
for (const ext of extensions) {
|
577
|
-
const webpackPath = resolve(appRoot, `
|
1227
|
+
const webpackPath = resolve(appRoot, configPath, `webpack.config.${ext}`)
|
578
1228
|
if (existsSync(webpackPath)) {
|
579
1229
|
return webpackPath
|
580
1230
|
}
|
581
1231
|
}
|
582
1232
|
|
583
1233
|
throw new Error(
|
584
|
-
`Could not find ${bundler} config file. Expected:
|
1234
|
+
`Could not find ${bundler} config file. Expected: ${configPath}/${bundler}.config.{js,ts}`
|
585
1235
|
)
|
586
1236
|
}
|
587
1237
|
|
@@ -622,62 +1272,3 @@ function setupNodePath(appRoot: string): void {
|
|
622
1272
|
require("module").Module._initPaths()
|
623
1273
|
}
|
624
1274
|
}
|
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
|
-
}
|