shakapacker 9.1.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 +7 -0
- data/CHANGELOG.md +50 -4
- data/CLAUDE.md +6 -1
- data/CONTRIBUTING.md +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +35 -14
- data/TODO.md +10 -2
- data/TODO_v9.md +13 -3
- data/bin/export-bundler-config +11 -0
- 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 +68 -6
- 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 +195 -0
- data/docs/rspack.md +25 -21
- data/docs/rspack_migration_guide.md +363 -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 +122 -23
- 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 +11 -0
- data/lib/install/bin/shakapacker +1 -1
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/install/config/shakapacker.yml +16 -5
- data/lib/shakapacker/bundler_switcher.rb +7 -0
- data/lib/shakapacker/compiler.rb +80 -0
- data/lib/shakapacker/configuration.rb +56 -2
- data/lib/shakapacker/dev_server_runner.rb +140 -1
- data/lib/shakapacker/doctor.rb +302 -57
- data/lib/shakapacker/instance.rb +8 -3
- data/lib/shakapacker/rspack_runner.rb +1 -1
- data/lib/shakapacker/runner.rb +245 -9
- 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/doctor.rake +42 -2
- data/lib/tasks/shakapacker/export_bundler_config.rake +72 -0
- data/package/babel/preset.ts +7 -4
- data/package/config.ts +42 -30
- data/package/configExporter/cli.ts +1274 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/configFile.ts +520 -0
- data/package/configExporter/fileWriter.ts +96 -0
- data/package/configExporter/index.ts +13 -0
- data/package/configExporter/types.ts +70 -0
- data/package/configExporter/yamlSerializer.ts +280 -0
- 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 +37 -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 +1635 -1442
- metadata +29 -3
- data/.eslintignore +0 -5
|
@@ -0,0 +1,1274 @@
|
|
|
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, writeFileSync } 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 yargs from "yargs"
|
|
9
|
+
import { ExportOptions, ConfigMetadata, FileOutput } from "./types"
|
|
10
|
+
import { YamlSerializer } from "./yamlSerializer"
|
|
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
|
+
}
|
|
71
|
+
|
|
72
|
+
// Main CLI entry point
|
|
73
|
+
export async function run(args: string[]): Promise<number> {
|
|
74
|
+
try {
|
|
75
|
+
const options = parseArguments(args)
|
|
76
|
+
|
|
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)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Set up environment
|
|
93
|
+
const appRoot = findAppRoot()
|
|
94
|
+
process.chdir(appRoot)
|
|
95
|
+
setupNodePath(appRoot)
|
|
96
|
+
|
|
97
|
+
// Apply defaults
|
|
98
|
+
applyDefaults(options)
|
|
99
|
+
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
// Execute based on mode
|
|
119
|
+
if (options.doctor) {
|
|
120
|
+
await runDoctorMode(options, appRoot)
|
|
121
|
+
} else if (options.stdout) {
|
|
122
|
+
// Explicit stdout mode
|
|
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)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 0
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
console.error(`[Config Exporter] Error: ${error.message}`)
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseArguments(args: string[]): ExportOptions {
|
|
140
|
+
const argv = yargs(args)
|
|
141
|
+
.version(VERSION)
|
|
142
|
+
.usage(
|
|
143
|
+
`Shakapacker Config Exporter
|
|
144
|
+
|
|
145
|
+
Exports webpack or rspack configuration in a verbose, human-readable format
|
|
146
|
+
for comparison and analysis.
|
|
147
|
+
|
|
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) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"--webpack and --rspack are mutually exclusive. Please specify only one."
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
if (argv["client-only"] && argv["server-only"]) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"--client-only and --server-only are mutually exclusive. Please specify only one."
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
if (argv.output && argv["save-dir"]) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"--output and --save-dir are mutually exclusive. Use one or the other."
|
|
267
|
+
)
|
|
268
|
+
}
|
|
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/
|
|
302
|
+
|
|
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
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function applyDefaults(options: ExportOptions): void {
|
|
342
|
+
if (options.doctor) {
|
|
343
|
+
if (options.format === undefined) options.format = "yaml"
|
|
344
|
+
if (options.annotate === undefined) options.annotate = true
|
|
345
|
+
} else if (!options.stdout && !options.output) {
|
|
346
|
+
// Default mode: save to directory
|
|
347
|
+
if (options.format === undefined) options.format = "yaml"
|
|
348
|
+
if (options.annotate === undefined) options.annotate = true
|
|
349
|
+
} else {
|
|
350
|
+
if (options.format === undefined) options.format = "inspect"
|
|
351
|
+
if (options.annotate === undefined) options.annotate = false
|
|
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
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
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.`
|
|
370
|
+
)
|
|
371
|
+
return 1
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const sampleConfig = generateSampleConfigFile()
|
|
375
|
+
writeFileSync(fullPath, sampleConfig, "utf8")
|
|
376
|
+
|
|
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
|
|
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
|
+
}
|
|
421
|
+
|
|
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`
|
|
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)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function runDoctorMode(
|
|
484
|
+
options: ExportOptions,
|
|
485
|
+
appRoot: string
|
|
486
|
+
): Promise<void> {
|
|
487
|
+
// Save original environment to restore after all builds
|
|
488
|
+
const savedEnv = saveBuildEnvironmentVariables()
|
|
489
|
+
|
|
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
|
+
}
|
|
541
|
+
|
|
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
|
+
}
|
|
552
|
+
|
|
553
|
+
// Default behavior: hardcoded configs
|
|
554
|
+
console.log("\nExporting all development and production configs...")
|
|
555
|
+
console.log("")
|
|
556
|
+
|
|
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
|
+
]
|
|
562
|
+
|
|
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
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
printDoctorSummary(createdFiles, targetDir)
|
|
620
|
+
} finally {
|
|
621
|
+
// Restore original environment
|
|
622
|
+
restoreBuildEnvironmentVariables(savedEnv)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function printDoctorSummary(createdFiles: string[], targetDir: string): void {
|
|
627
|
+
// Print summary
|
|
628
|
+
console.log("\n" + "=".repeat(80))
|
|
629
|
+
console.log("✅ Export Complete!")
|
|
630
|
+
console.log("=".repeat(80))
|
|
631
|
+
console.log(`\nCreated ${createdFiles.length} configuration file(s) in:`)
|
|
632
|
+
console.log(` ${targetDir}\n`)
|
|
633
|
+
console.log("Files:")
|
|
634
|
+
createdFiles.forEach((file) => {
|
|
635
|
+
console.log(` ✓ ${basename(file)}`)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// Check if directory should be added to .gitignore
|
|
639
|
+
const gitignorePath = resolve(process.cwd(), ".gitignore")
|
|
640
|
+
const dirName = basename(targetDir)
|
|
641
|
+
let shouldSuggestGitignore = false
|
|
642
|
+
|
|
643
|
+
if (existsSync(gitignorePath)) {
|
|
644
|
+
const gitignoreContent = readFileSync(gitignorePath, "utf8")
|
|
645
|
+
if (!gitignoreContent.includes(dirName)) {
|
|
646
|
+
shouldSuggestGitignore = true
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (shouldSuggestGitignore) {
|
|
651
|
+
console.log("\n" + "─".repeat(80))
|
|
652
|
+
console.log(
|
|
653
|
+
"💡 Tip: Add the export directory to .gitignore to avoid committing config files:"
|
|
654
|
+
)
|
|
655
|
+
console.log(`\n echo "${dirName}/" >> .gitignore\n`)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
console.log("\n" + "=".repeat(80) + "\n")
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function runSaveMode(
|
|
662
|
+
options: ExportOptions,
|
|
663
|
+
appRoot: string
|
|
664
|
+
): Promise<void> {
|
|
665
|
+
const env = options.env || "development"
|
|
666
|
+
console.log(`[Config Exporter] Exporting ${env} configs`)
|
|
667
|
+
|
|
668
|
+
const fileWriter = new FileWriter()
|
|
669
|
+
const targetDir = options.saveDir! // Set by applyDefaults
|
|
670
|
+
const configs = await loadConfigsForEnv(options.env, options, appRoot)
|
|
671
|
+
const createdFiles: string[] = []
|
|
672
|
+
|
|
673
|
+
if (options.output) {
|
|
674
|
+
// Single file output
|
|
675
|
+
const combined = configs.map((c) => c.config)
|
|
676
|
+
const metadata = configs[0].metadata
|
|
677
|
+
metadata.configCount = combined.length
|
|
678
|
+
|
|
679
|
+
const output = formatConfig(
|
|
680
|
+
combined.length === 1 ? combined[0] : combined,
|
|
681
|
+
metadata,
|
|
682
|
+
options,
|
|
683
|
+
appRoot
|
|
684
|
+
)
|
|
685
|
+
const fullPath = resolve(options.output)
|
|
686
|
+
fileWriter.writeSingleFile(fullPath, output)
|
|
687
|
+
createdFiles.push(fullPath)
|
|
688
|
+
} else {
|
|
689
|
+
// Multi-file output (one per config)
|
|
690
|
+
for (const { config, metadata } of configs) {
|
|
691
|
+
const output = formatConfig(config, metadata, options, appRoot)
|
|
692
|
+
const filename = fileWriter.generateFilename(
|
|
693
|
+
metadata.bundler,
|
|
694
|
+
metadata.environment,
|
|
695
|
+
metadata.configType,
|
|
696
|
+
options.format!,
|
|
697
|
+
metadata.buildName
|
|
698
|
+
)
|
|
699
|
+
const fullPath = resolve(targetDir, filename)
|
|
700
|
+
fileWriter.writeSingleFile(fullPath, output)
|
|
701
|
+
createdFiles.push(fullPath)
|
|
702
|
+
}
|
|
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
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function runStdoutMode(
|
|
713
|
+
options: ExportOptions,
|
|
714
|
+
appRoot: string
|
|
715
|
+
): Promise<void> {
|
|
716
|
+
const configs = await loadConfigsForEnv(options.env!, options, appRoot)
|
|
717
|
+
const combined = configs.map((c) => c.config)
|
|
718
|
+
const metadata = configs[0].metadata
|
|
719
|
+
metadata.configCount = combined.length
|
|
720
|
+
|
|
721
|
+
const config = combined.length === 1 ? combined[0] : combined
|
|
722
|
+
const output = formatConfig(config, metadata, options, appRoot)
|
|
723
|
+
|
|
724
|
+
console.log("\n" + "=".repeat(80) + "\n")
|
|
725
|
+
console.log(output)
|
|
726
|
+
}
|
|
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
|
+
|
|
745
|
+
async function loadConfigsForEnv(
|
|
746
|
+
env: "development" | "production" | "test" | undefined,
|
|
747
|
+
options: ExportOptions,
|
|
748
|
+
appRoot: string
|
|
749
|
+
): Promise<Array<{ config: any; metadata: ConfigMetadata }>> {
|
|
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"
|
|
830
|
+
|
|
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
|
+
}
|
|
838
|
+
|
|
839
|
+
if (options.clientOnly) {
|
|
840
|
+
process.env.CLIENT_BUNDLE_ONLY = "yes"
|
|
841
|
+
} else if (options.serverOnly) {
|
|
842
|
+
process.env.SERVER_BUNDLE_ONLY = "yes"
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Find and load config file
|
|
846
|
+
const configFile =
|
|
847
|
+
customConfigFile || findConfigFile(bundler, appRoot, finalEnv)
|
|
848
|
+
// Quiet mode for cleaner output - only show if verbose or errors
|
|
849
|
+
if (process.env.VERBOSE) {
|
|
850
|
+
console.log(`[Config Exporter] Loading config: ${configFile}`)
|
|
851
|
+
console.log(`[Config Exporter] Environment: ${finalEnv}`)
|
|
852
|
+
console.log(`[Config Exporter] Bundler: ${bundler}`)
|
|
853
|
+
if (buildName) {
|
|
854
|
+
console.log(`[Config Exporter] Build: ${buildName}`)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Load the config
|
|
859
|
+
// Register ts-node for TypeScript config files
|
|
860
|
+
if (configFile.endsWith(".ts")) {
|
|
861
|
+
try {
|
|
862
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
863
|
+
require("ts-node/register/transpile-only")
|
|
864
|
+
} catch (error) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
"TypeScript config detected but ts-node is not available. " +
|
|
867
|
+
"Install ts-node as a dev dependency: npm install --save-dev ts-node"
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Clear require cache for config file and all related modules
|
|
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
|
+
*/
|
|
893
|
+
const configDir = dirname(configFile)
|
|
894
|
+
Object.keys(require.cache).forEach((key) => {
|
|
895
|
+
if (
|
|
896
|
+
key.includes("webpack.config") ||
|
|
897
|
+
key.includes("rspack.config") ||
|
|
898
|
+
key.startsWith(configDir) ||
|
|
899
|
+
key.includes("/shakapacker/") || // npm installed shakapacker
|
|
900
|
+
key.includes("\\shakapacker\\") || // Windows path
|
|
901
|
+
key.includes("/package/env") || // shakapacker env module (local dev)
|
|
902
|
+
key.includes("\\package\\env") || // Windows env module
|
|
903
|
+
key.includes("/package/index") || // shakapacker main module
|
|
904
|
+
key.includes("\\package\\index") || // Windows main module
|
|
905
|
+
key === configFile
|
|
906
|
+
) {
|
|
907
|
+
delete require.cache[key]
|
|
908
|
+
}
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
912
|
+
let loadedConfig = require(configFile)
|
|
913
|
+
|
|
914
|
+
// Handle ES module default export
|
|
915
|
+
if (typeof loadedConfig === "object" && "default" in loadedConfig) {
|
|
916
|
+
loadedConfig = loadedConfig.default
|
|
917
|
+
}
|
|
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
|
+
|
|
951
|
+
// Determine config type and split if array
|
|
952
|
+
const configs: any[] = Array.isArray(loadedConfig)
|
|
953
|
+
? loadedConfig
|
|
954
|
+
: [loadedConfig]
|
|
955
|
+
const results: Array<{ config: any; metadata: ConfigMetadata }> = []
|
|
956
|
+
|
|
957
|
+
configs.forEach((cfg, index) => {
|
|
958
|
+
let configType: "client" | "server" | "all" = "all"
|
|
959
|
+
|
|
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) {
|
|
981
|
+
// Likely client and server configs
|
|
982
|
+
configType = index === 0 ? "client" : "server"
|
|
983
|
+
} else if (options.clientOnly) {
|
|
984
|
+
configType = "client"
|
|
985
|
+
} else if (options.serverOnly) {
|
|
986
|
+
configType = "server"
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const metadata: ConfigMetadata = {
|
|
990
|
+
exportedAt: new Date().toISOString(),
|
|
991
|
+
bundler,
|
|
992
|
+
environment: finalEnv,
|
|
993
|
+
configFile,
|
|
994
|
+
configType,
|
|
995
|
+
configCount: configs.length,
|
|
996
|
+
buildName,
|
|
997
|
+
environmentVariables: {
|
|
998
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
999
|
+
RAILS_ENV: process.env.RAILS_ENV,
|
|
1000
|
+
CLIENT_BUNDLE_ONLY: process.env.CLIENT_BUNDLE_ONLY,
|
|
1001
|
+
SERVER_BUNDLE_ONLY: process.env.SERVER_BUNDLE_ONLY,
|
|
1002
|
+
WEBPACK_SERVE: process.env.WEBPACK_SERVE
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Clean config if not verbose
|
|
1007
|
+
let cleanedConfig = cfg
|
|
1008
|
+
if (!options.verbose) {
|
|
1009
|
+
cleanedConfig = cleanConfig(cfg, appRoot)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
results.push({ config: cleanedConfig, metadata })
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
return results
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function formatConfig(
|
|
1019
|
+
config: any,
|
|
1020
|
+
metadata: ConfigMetadata,
|
|
1021
|
+
options: ExportOptions,
|
|
1022
|
+
appRoot: string
|
|
1023
|
+
): string {
|
|
1024
|
+
if (options.format === "yaml") {
|
|
1025
|
+
const serializer = new YamlSerializer({
|
|
1026
|
+
annotate: options.annotate!,
|
|
1027
|
+
appRoot
|
|
1028
|
+
})
|
|
1029
|
+
return serializer.serialize(config, metadata)
|
|
1030
|
+
} else if (options.format === "json") {
|
|
1031
|
+
const jsonReplacer = (key: string, value: any): any => {
|
|
1032
|
+
if (typeof value === "function") {
|
|
1033
|
+
return `[Function: ${value.name || "anonymous"}]`
|
|
1034
|
+
}
|
|
1035
|
+
if (value instanceof RegExp) {
|
|
1036
|
+
return `[RegExp: ${value.toString()}]`
|
|
1037
|
+
}
|
|
1038
|
+
if (
|
|
1039
|
+
value &&
|
|
1040
|
+
typeof value === "object" &&
|
|
1041
|
+
value.constructor &&
|
|
1042
|
+
value.constructor.name !== "Object" &&
|
|
1043
|
+
value.constructor.name !== "Array"
|
|
1044
|
+
) {
|
|
1045
|
+
return `[${value.constructor.name}]`
|
|
1046
|
+
}
|
|
1047
|
+
return value
|
|
1048
|
+
}
|
|
1049
|
+
return JSON.stringify({ metadata, config }, jsonReplacer, 2)
|
|
1050
|
+
} else {
|
|
1051
|
+
// inspect format
|
|
1052
|
+
const inspectOptions = {
|
|
1053
|
+
depth: options.depth,
|
|
1054
|
+
colors: false,
|
|
1055
|
+
maxArrayLength: null,
|
|
1056
|
+
maxStringLength: null,
|
|
1057
|
+
breakLength: 120,
|
|
1058
|
+
compact: false
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
let output =
|
|
1062
|
+
"=== METADATA ===\n\n" + inspect(metadata, inspectOptions) + "\n\n"
|
|
1063
|
+
output += "=== CONFIG ===\n\n"
|
|
1064
|
+
|
|
1065
|
+
if (Array.isArray(config)) {
|
|
1066
|
+
output += `Total configs: ${config.length}\n\n`
|
|
1067
|
+
config.forEach((cfg, index) => {
|
|
1068
|
+
output += `--- Config [${index}] ---\n\n`
|
|
1069
|
+
output += inspect(cfg, inspectOptions) + "\n\n"
|
|
1070
|
+
})
|
|
1071
|
+
} else {
|
|
1072
|
+
output += inspect(config, inspectOptions) + "\n"
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return output
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function cleanConfig(obj: any, rootPath: string): any {
|
|
1080
|
+
const makePathRelative = (str: string): string => {
|
|
1081
|
+
if (typeof str === "string" && str.startsWith(rootPath)) {
|
|
1082
|
+
return "./" + str.substring(rootPath.length + 1)
|
|
1083
|
+
}
|
|
1084
|
+
return str
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function clean(value: any, key?: string, parent?: any): any {
|
|
1088
|
+
// Remove EnvironmentPlugin keys and defaultValues
|
|
1089
|
+
if (
|
|
1090
|
+
parent &&
|
|
1091
|
+
parent.constructor &&
|
|
1092
|
+
parent.constructor.name === "EnvironmentPlugin"
|
|
1093
|
+
) {
|
|
1094
|
+
if (key === "keys" || key === "defaultValues") {
|
|
1095
|
+
return undefined
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (typeof value === "function") {
|
|
1100
|
+
// Show function source
|
|
1101
|
+
const source = value.toString()
|
|
1102
|
+
const compacted = source
|
|
1103
|
+
.split("\n")
|
|
1104
|
+
.map((line: string) => line.trim())
|
|
1105
|
+
.filter((line: string) => line.length > 0)
|
|
1106
|
+
.join(" ")
|
|
1107
|
+
return compacted
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (typeof value === "string") {
|
|
1111
|
+
return makePathRelative(value)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (Array.isArray(value)) {
|
|
1115
|
+
return value
|
|
1116
|
+
.map((item, i) => clean(item, String(i), value))
|
|
1117
|
+
.filter((v) => v !== undefined)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (value && typeof value === "object") {
|
|
1121
|
+
const cleaned: any = {}
|
|
1122
|
+
for (const k in value) {
|
|
1123
|
+
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
1124
|
+
const cleanedValue = clean(value[k], k, value)
|
|
1125
|
+
if (cleanedValue !== undefined) {
|
|
1126
|
+
cleaned[k] = cleanedValue
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return cleaned
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return value
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return clean(obj)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Loads and returns shakapacker.yml configuration
|
|
1141
|
+
*/
|
|
1142
|
+
function loadShakapackerConfig(
|
|
1143
|
+
env: string,
|
|
1144
|
+
appRoot: string
|
|
1145
|
+
): { bundler: "webpack" | "rspack"; configPath: string } {
|
|
1146
|
+
try {
|
|
1147
|
+
const configFilePath =
|
|
1148
|
+
process.env.SHAKAPACKER_CONFIG ||
|
|
1149
|
+
resolve(appRoot, "config/shakapacker.yml")
|
|
1150
|
+
|
|
1151
|
+
if (existsSync(configFilePath)) {
|
|
1152
|
+
const config: any = loadYaml(readFileSync(configFilePath, "utf8"))
|
|
1153
|
+
const envConfig = config[env] || config.default || {}
|
|
1154
|
+
|
|
1155
|
+
// Get bundler
|
|
1156
|
+
const bundler = envConfig.assets_bundler || "webpack"
|
|
1157
|
+
if (bundler !== "webpack" && bundler !== "rspack") {
|
|
1158
|
+
console.warn(
|
|
1159
|
+
`[Config Exporter] Invalid bundler '${bundler}' in shakapacker.yml, defaulting to webpack`
|
|
1160
|
+
)
|
|
1161
|
+
return {
|
|
1162
|
+
bundler: "webpack",
|
|
1163
|
+
configPath: bundler === "rspack" ? "config/rspack" : "config/webpack"
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
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 }
|
|
1177
|
+
}
|
|
1178
|
+
} catch (error: any) {
|
|
1179
|
+
console.warn(
|
|
1180
|
+
`[Config Exporter] Error loading shakapacker config, defaulting to webpack`
|
|
1181
|
+
)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
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
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function findConfigFile(
|
|
1209
|
+
bundler: "webpack" | "rspack",
|
|
1210
|
+
appRoot: string,
|
|
1211
|
+
env: string
|
|
1212
|
+
): string {
|
|
1213
|
+
const { configPath } = loadShakapackerConfig(env, appRoot)
|
|
1214
|
+
const extensions = ["ts", "js"]
|
|
1215
|
+
|
|
1216
|
+
if (bundler === "rspack") {
|
|
1217
|
+
for (const ext of extensions) {
|
|
1218
|
+
const rspackPath = resolve(appRoot, configPath, `rspack.config.${ext}`)
|
|
1219
|
+
if (existsSync(rspackPath)) {
|
|
1220
|
+
return rspackPath
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Fall back to webpack config
|
|
1226
|
+
for (const ext of extensions) {
|
|
1227
|
+
const webpackPath = resolve(appRoot, configPath, `webpack.config.${ext}`)
|
|
1228
|
+
if (existsSync(webpackPath)) {
|
|
1229
|
+
return webpackPath
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`Could not find ${bundler} config file. Expected: ${configPath}/${bundler}.config.{js,ts}`
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function findAppRoot(): string {
|
|
1239
|
+
let currentDir = process.cwd()
|
|
1240
|
+
const root = dirname(currentDir).split(sep)[0] + sep
|
|
1241
|
+
|
|
1242
|
+
while (currentDir !== root && currentDir !== dirname(currentDir)) {
|
|
1243
|
+
if (
|
|
1244
|
+
existsSync(resolve(currentDir, "package.json")) ||
|
|
1245
|
+
existsSync(resolve(currentDir, "config/shakapacker.yml"))
|
|
1246
|
+
) {
|
|
1247
|
+
return currentDir
|
|
1248
|
+
}
|
|
1249
|
+
currentDir = dirname(currentDir)
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return process.cwd()
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function setupNodePath(appRoot: string): void {
|
|
1256
|
+
const nodePaths = [
|
|
1257
|
+
resolve(appRoot, "node_modules"),
|
|
1258
|
+
resolve(appRoot, "..", "..", "node_modules"),
|
|
1259
|
+
resolve(appRoot, "..", "..", "package"),
|
|
1260
|
+
...(appRoot.includes("/spec/dummy")
|
|
1261
|
+
? [resolve(appRoot, "../../node_modules")]
|
|
1262
|
+
: [])
|
|
1263
|
+
].filter((p) => existsSync(p))
|
|
1264
|
+
|
|
1265
|
+
if (nodePaths.length > 0) {
|
|
1266
|
+
const existingNodePath = process.env.NODE_PATH || ""
|
|
1267
|
+
process.env.NODE_PATH = existingNodePath
|
|
1268
|
+
? `${nodePaths.join(delimiter)}${delimiter}${existingNodePath}`
|
|
1269
|
+
: nodePaths.join(delimiter)
|
|
1270
|
+
|
|
1271
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1272
|
+
require("module").Module._initPaths()
|
|
1273
|
+
}
|
|
1274
|
+
}
|