shakapacker 9.3.0.beta.7 → 9.3.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/CHANGELOG.md +46 -109
- data/Gemfile.lock +1 -1
- data/README.md +53 -2
- data/docs/configuration.md +28 -0
- data/docs/rspack_migration_guide.md +238 -2
- data/docs/troubleshooting.md +21 -21
- data/eslint.config.fast.js +8 -0
- data/eslint.config.js +47 -10
- data/knip.ts +8 -1
- data/lib/install/config/shakapacker.yml +6 -6
- data/lib/shakapacker/configuration.rb +227 -4
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/doctor.rb +4 -4
- data/lib/shakapacker/instance.rb +85 -1
- data/lib/shakapacker/manifest.rb +85 -11
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker.rb +143 -3
- data/lib/tasks/shakapacker/doctor.rake +1 -1
- data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
- data/package/config.ts +0 -1
- data/package/configExporter/buildValidator.ts +53 -29
- data/package/configExporter/cli.ts +81 -56
- data/package/configExporter/configFile.ts +33 -26
- data/package/configExporter/types.ts +64 -0
- data/package/configExporter/yamlSerializer.ts +118 -43
- data/package/dev_server.ts +2 -1
- data/package/env.ts +1 -1
- data/package/environments/base.ts +4 -4
- data/package/environments/development.ts +7 -6
- data/package/environments/production.ts +6 -7
- data/package/environments/test.ts +2 -1
- data/package/index.ts +28 -4
- data/package/loaders.d.ts +2 -2
- data/package/optimization/webpack.ts +29 -31
- data/package/rspack/index.ts +2 -1
- data/package/rules/file.ts +1 -0
- data/package/rules/jscommon.ts +1 -0
- data/package/utils/helpers.ts +0 -1
- data/package/utils/pathValidation.ts +68 -7
- data/package/utils/requireOrError.ts +10 -2
- data/package/utils/typeGuards.ts +43 -46
- data/package/webpack-types.d.ts +2 -2
- data/package/webpackDevServerConfig.ts +1 -0
- data/package.json +2 -3
- data/test/package/configExporter/cli.test.js +440 -0
- data/test/package/configExporter/types.test.js +163 -0
- data/test/package/configExporter.test.js +264 -0
- data/test/package/yamlSerializer.test.js +204 -0
- data/test/typescript/pathValidation.test.js +44 -0
- data/test/typescript/requireOrError.test.js +49 -0
- data/yarn.lock +0 -32
- metadata +11 -5
- data/.eslintrc.fast.js +0 -40
- data/.eslintrc.js +0 -84
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process"
|
|
2
2
|
import { existsSync } from "fs"
|
|
3
|
-
import { resolve, relative
|
|
3
|
+
import { resolve, relative } from "path"
|
|
4
4
|
import { ResolvedBuildConfig, BuildValidationResult } from "./types"
|
|
5
5
|
|
|
6
6
|
export interface ValidatorOptions {
|
|
@@ -173,7 +173,7 @@ export class BuildValidator {
|
|
|
173
173
|
* @returns The resolved absolute path to the config file
|
|
174
174
|
* @throws Error if the config file does not exist or is outside appRoot
|
|
175
175
|
*/
|
|
176
|
-
private validateConfigPath(
|
|
176
|
+
private static validateConfigPath(
|
|
177
177
|
configFile: string,
|
|
178
178
|
appRoot: string,
|
|
179
179
|
buildName: string
|
|
@@ -222,7 +222,7 @@ export class BuildValidator {
|
|
|
222
222
|
const isHMR =
|
|
223
223
|
build.environment.WEBPACK_SERVE === "true" ||
|
|
224
224
|
build.environment.HMR === "true"
|
|
225
|
-
const bundler = build
|
|
225
|
+
const { bundler } = build
|
|
226
226
|
|
|
227
227
|
if (isHMR) {
|
|
228
228
|
return this.validateHMRBuild(build, appRoot, bundler)
|
|
@@ -273,7 +273,7 @@ export class BuildValidator {
|
|
|
273
273
|
// Add config file if specified
|
|
274
274
|
if (build.configFile) {
|
|
275
275
|
try {
|
|
276
|
-
const configPath =
|
|
276
|
+
const configPath = BuildValidator.validateConfigPath(
|
|
277
277
|
build.configFile,
|
|
278
278
|
appRoot,
|
|
279
279
|
build.name
|
|
@@ -301,7 +301,7 @@ export class BuildValidator {
|
|
|
301
301
|
args.push(...build.bundlerEnvArgs)
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
return new Promise((
|
|
304
|
+
return new Promise((resolvePromise) => {
|
|
305
305
|
const child = spawn(devServerBin, args, {
|
|
306
306
|
cwd: appRoot,
|
|
307
307
|
env: this.filterEnvironment(build.environment),
|
|
@@ -316,7 +316,7 @@ export class BuildValidator {
|
|
|
316
316
|
const resolveOnce = (res: BuildValidationResult) => {
|
|
317
317
|
if (!resolved) {
|
|
318
318
|
resolved = true
|
|
319
|
-
|
|
319
|
+
resolvePromise(res)
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -397,8 +397,8 @@ export class BuildValidator {
|
|
|
397
397
|
})
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
child.stdout?.on("data", (data) => processOutput(data))
|
|
401
|
-
child.stderr?.on("data", (data) => processOutput(data))
|
|
400
|
+
child.stdout?.on("data", (data: Buffer) => processOutput(data))
|
|
401
|
+
child.stderr?.on("data", (data: Buffer) => processOutput(data))
|
|
402
402
|
|
|
403
403
|
child.on("exit", (code) => {
|
|
404
404
|
clearTimeout(timeoutId)
|
|
@@ -428,7 +428,7 @@ export class BuildValidator {
|
|
|
428
428
|
|
|
429
429
|
// Check for specific error codes and provide actionable guidance
|
|
430
430
|
if ("code" in err) {
|
|
431
|
-
const code =
|
|
431
|
+
const { code } = err as NodeJS.ErrnoException
|
|
432
432
|
if (code === "ENOENT") {
|
|
433
433
|
errorMessage += `. Binary not found. Install with: npm install -D ${devServerCmd}`
|
|
434
434
|
} else if (code === "EMFILE" || code === "ENFILE") {
|
|
@@ -484,7 +484,7 @@ export class BuildValidator {
|
|
|
484
484
|
// Add config file if specified
|
|
485
485
|
if (build.configFile) {
|
|
486
486
|
try {
|
|
487
|
-
const configPath =
|
|
487
|
+
const configPath = BuildValidator.validateConfigPath(
|
|
488
488
|
build.configFile,
|
|
489
489
|
appRoot,
|
|
490
490
|
build.name
|
|
@@ -515,7 +515,7 @@ export class BuildValidator {
|
|
|
515
515
|
// Add --json for structured output (helps parse errors)
|
|
516
516
|
args.push("--json")
|
|
517
517
|
|
|
518
|
-
return new Promise((
|
|
518
|
+
return new Promise((resolvePromise) => {
|
|
519
519
|
const child = spawn(bundlerBin, args, {
|
|
520
520
|
cwd: appRoot,
|
|
521
521
|
env: this.filterEnvironment(build.environment),
|
|
@@ -534,7 +534,7 @@ export class BuildValidator {
|
|
|
534
534
|
`Timeout: ${bundler} did not complete within ${this.options.timeout}ms.`
|
|
535
535
|
)
|
|
536
536
|
child.kill("SIGTERM")
|
|
537
|
-
|
|
537
|
+
resolvePromise(result)
|
|
538
538
|
}, this.options.timeout)
|
|
539
539
|
|
|
540
540
|
child.stdout?.on("data", (data: Buffer) => {
|
|
@@ -603,7 +603,7 @@ export class BuildValidator {
|
|
|
603
603
|
|
|
604
604
|
// Parse JSON output
|
|
605
605
|
try {
|
|
606
|
-
const jsonOutput
|
|
606
|
+
const jsonOutput = JSON.parse(stdoutData) as WebpackJsonOutput
|
|
607
607
|
|
|
608
608
|
// Extract output path if available
|
|
609
609
|
if (jsonOutput.outputPath) {
|
|
@@ -613,10 +613,22 @@ export class BuildValidator {
|
|
|
613
613
|
// Check for errors in webpack/rspack JSON output
|
|
614
614
|
if (jsonOutput.errors && jsonOutput.errors.length > 0) {
|
|
615
615
|
jsonOutput.errors.forEach((error) => {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
616
|
+
let errorMsg: string
|
|
617
|
+
if (typeof error === "string") {
|
|
618
|
+
errorMsg = error
|
|
619
|
+
} else if (error.message) {
|
|
620
|
+
errorMsg = error.message
|
|
621
|
+
} else {
|
|
622
|
+
// Attempt to extract useful info from malformed error using all enumerable props
|
|
623
|
+
try {
|
|
624
|
+
errorMsg = JSON.stringify(
|
|
625
|
+
error,
|
|
626
|
+
Object.getOwnPropertyNames(error)
|
|
627
|
+
)
|
|
628
|
+
} catch {
|
|
629
|
+
errorMsg = "[Error object with no message]"
|
|
630
|
+
}
|
|
631
|
+
}
|
|
620
632
|
result.errors.push(errorMsg)
|
|
621
633
|
// Also add to output for visibility
|
|
622
634
|
if (!this.options.verbose) {
|
|
@@ -628,10 +640,22 @@ export class BuildValidator {
|
|
|
628
640
|
// Check for warnings
|
|
629
641
|
if (jsonOutput.warnings && jsonOutput.warnings.length > 0) {
|
|
630
642
|
jsonOutput.warnings.forEach((warning) => {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
643
|
+
let warningMsg: string
|
|
644
|
+
if (typeof warning === "string") {
|
|
645
|
+
warningMsg = warning
|
|
646
|
+
} else if (warning.message) {
|
|
647
|
+
warningMsg = warning.message
|
|
648
|
+
} else {
|
|
649
|
+
// Attempt to extract useful info from malformed warning using all enumerable props
|
|
650
|
+
try {
|
|
651
|
+
warningMsg = JSON.stringify(
|
|
652
|
+
warning,
|
|
653
|
+
Object.getOwnPropertyNames(warning)
|
|
654
|
+
)
|
|
655
|
+
} catch {
|
|
656
|
+
warningMsg = "[Warning object with no message]"
|
|
657
|
+
}
|
|
658
|
+
}
|
|
635
659
|
result.warnings.push(warningMsg)
|
|
636
660
|
})
|
|
637
661
|
}
|
|
@@ -683,7 +707,7 @@ export class BuildValidator {
|
|
|
683
707
|
result.output.push(stderrData)
|
|
684
708
|
}
|
|
685
709
|
|
|
686
|
-
|
|
710
|
+
resolvePromise(result)
|
|
687
711
|
})
|
|
688
712
|
|
|
689
713
|
child.on("error", (err) => {
|
|
@@ -693,7 +717,7 @@ export class BuildValidator {
|
|
|
693
717
|
|
|
694
718
|
// Check for specific error codes and provide actionable guidance
|
|
695
719
|
if ("code" in err) {
|
|
696
|
-
const code =
|
|
720
|
+
const { code } = err as NodeJS.ErrnoException
|
|
697
721
|
if (code === "ENOENT") {
|
|
698
722
|
errorMessage += `. Binary not found. Install with: npm install -D ${bundler}`
|
|
699
723
|
} else if (code === "EMFILE" || code === "ENFILE") {
|
|
@@ -704,7 +728,7 @@ export class BuildValidator {
|
|
|
704
728
|
}
|
|
705
729
|
|
|
706
730
|
result.errors.push(errorMessage)
|
|
707
|
-
|
|
731
|
+
resolvePromise(result)
|
|
708
732
|
})
|
|
709
733
|
})
|
|
710
734
|
}
|
|
@@ -776,19 +800,19 @@ export class BuildValidator {
|
|
|
776
800
|
formatResults(results: BuildValidationResult[]): string {
|
|
777
801
|
const lines: string[] = []
|
|
778
802
|
|
|
779
|
-
lines.push(
|
|
803
|
+
lines.push(`\n${"=".repeat(80)}`)
|
|
780
804
|
lines.push("🔍 Build Validation Results")
|
|
781
|
-
lines.push("=".repeat(80)
|
|
805
|
+
lines.push(`${"=".repeat(80)}\n`)
|
|
782
806
|
|
|
783
|
-
|
|
807
|
+
const totalBuilds = results.length
|
|
784
808
|
let successCount = 0
|
|
785
809
|
let failureCount = 0
|
|
786
810
|
|
|
787
811
|
results.forEach((result) => {
|
|
788
812
|
if (result.success) {
|
|
789
|
-
successCount
|
|
813
|
+
successCount += 1
|
|
790
814
|
} else {
|
|
791
|
-
failureCount
|
|
815
|
+
failureCount += 1
|
|
792
816
|
}
|
|
793
817
|
|
|
794
818
|
const icon = result.success ? "✅" : "❌"
|
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
// This will be a substantial file - the main CLI entry point
|
|
2
|
-
//
|
|
2
|
+
// Originally migrated from bin/export-bundler-config, now bin/shakapacker-config
|
|
3
3
|
|
|
4
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
8
|
import yargs from "yargs"
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ExportOptions,
|
|
11
|
+
ConfigMetadata,
|
|
12
|
+
FileOutput,
|
|
13
|
+
BUILD_ENV_VARS,
|
|
14
|
+
isBuildEnvVar,
|
|
15
|
+
isDangerousEnvVar,
|
|
16
|
+
DEFAULT_EXPORT_DIR,
|
|
17
|
+
DEFAULT_CONFIG_FILE
|
|
18
|
+
} from "./types"
|
|
10
19
|
import { YamlSerializer } from "./yamlSerializer"
|
|
11
20
|
import { FileWriter } from "./fileWriter"
|
|
12
21
|
import { ConfigFileLoader, generateSampleConfigFile } from "./configFile"
|
|
13
22
|
import { BuildValidator } from "./buildValidator"
|
|
23
|
+
import { safeResolvePath } from "../utils/pathValidation"
|
|
14
24
|
|
|
15
25
|
// Read version from package.json
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"BABEL_ENV",
|
|
29
|
-
"WEBPACK_SERVE",
|
|
30
|
-
"CLIENT_BUNDLE_ONLY",
|
|
31
|
-
"SERVER_BUNDLE_ONLY"
|
|
32
|
-
] as const
|
|
26
|
+
let VERSION = "unknown"
|
|
27
|
+
try {
|
|
28
|
+
const packageJson = JSON.parse(
|
|
29
|
+
readFileSync(resolve(__dirname, "../../package.json"), "utf8")
|
|
30
|
+
) as { version?: string }
|
|
31
|
+
VERSION = packageJson.version || "unknown"
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(
|
|
34
|
+
"Could not read version from package.json:",
|
|
35
|
+
error instanceof Error ? error.message : String(error)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Saves current values of build environment variables for later restoration
|
|
@@ -103,6 +108,15 @@ export async function run(args: string[]): Promise<number> {
|
|
|
103
108
|
// Apply defaults
|
|
104
109
|
const resolvedOptions = applyDefaults(options)
|
|
105
110
|
|
|
111
|
+
// Validate paths for security AFTER defaults are applied
|
|
112
|
+
// Use safeResolvePath which validates and resolves symlinks
|
|
113
|
+
if (resolvedOptions.output) {
|
|
114
|
+
safeResolvePath(appRoot, resolvedOptions.output)
|
|
115
|
+
}
|
|
116
|
+
if (resolvedOptions.saveDir) {
|
|
117
|
+
safeResolvePath(appRoot, resolvedOptions.saveDir)
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
// Validate after defaults are applied
|
|
107
121
|
if (resolvedOptions.annotate && resolvedOptions.format !== "yaml") {
|
|
108
122
|
throw new Error(
|
|
@@ -114,8 +128,7 @@ export async function run(args: string[]): Promise<number> {
|
|
|
114
128
|
if (resolvedOptions.build) {
|
|
115
129
|
const loader = new ConfigFileLoader(resolvedOptions.configFile)
|
|
116
130
|
if (!loader.exists()) {
|
|
117
|
-
const configPath =
|
|
118
|
-
resolvedOptions.configFile || "config/shakapacker-builds.yml"
|
|
131
|
+
const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
|
|
119
132
|
throw new Error(
|
|
120
133
|
`--build requires a config file but ${configPath} not found. Run --init to create it.`
|
|
121
134
|
)
|
|
@@ -144,7 +157,7 @@ export async function run(args: string[]): Promise<number> {
|
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
function parseArguments(args: string[]): ExportOptions {
|
|
160
|
+
export function parseArguments(args: string[]): ExportOptions {
|
|
148
161
|
const argv = yargs(args)
|
|
149
162
|
.version(VERSION)
|
|
150
163
|
.usage(
|
|
@@ -164,8 +177,7 @@ QUICK START (for troubleshooting):
|
|
|
164
177
|
.option("init", {
|
|
165
178
|
type: "boolean",
|
|
166
179
|
default: false,
|
|
167
|
-
description:
|
|
168
|
-
"Generate config/shakapacker-builds.yml (use with --ssr for SSR builds)"
|
|
180
|
+
description: `Generate ${DEFAULT_CONFIG_FILE} (use with --ssr for SSR builds)`
|
|
169
181
|
})
|
|
170
182
|
.option("ssr", {
|
|
171
183
|
type: "boolean",
|
|
@@ -188,8 +200,7 @@ QUICK START (for troubleshooting):
|
|
|
188
200
|
})
|
|
189
201
|
.option("config-file", {
|
|
190
202
|
type: "string",
|
|
191
|
-
description:
|
|
192
|
-
"Path to config file (default: config/shakapacker-builds.yml)"
|
|
203
|
+
description: `Path to config file (default: ${DEFAULT_CONFIG_FILE})`
|
|
193
204
|
})
|
|
194
205
|
// Validation Options
|
|
195
206
|
.option("validate", {
|
|
@@ -235,11 +246,26 @@ QUICK START (for troubleshooting):
|
|
|
235
246
|
"Enable inline documentation (YAML only, default with --doctor or file output)"
|
|
236
247
|
})
|
|
237
248
|
.option("depth", {
|
|
238
|
-
|
|
249
|
+
// Note: type omitted to allow string "null" (yargs would reject it).
|
|
250
|
+
// Coerce function handles validation for both numbers and "null".
|
|
239
251
|
default: 20,
|
|
240
252
|
coerce: (value: number | string) => {
|
|
253
|
+
// Handle "null" string for unlimited depth
|
|
241
254
|
if (value === "null" || value === null) return null
|
|
242
|
-
|
|
255
|
+
|
|
256
|
+
// Reject non-numeric types (arrays, objects, etc.)
|
|
257
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`--depth must be a number or 'null', got: ${typeof value}`
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const parsed =
|
|
264
|
+
typeof value === "number" ? value : parseInt(String(value), 10)
|
|
265
|
+
if (Number.isNaN(parsed)) {
|
|
266
|
+
throw new Error(`--depth must be a number or 'null', got: ${value}`)
|
|
267
|
+
}
|
|
268
|
+
return parsed
|
|
243
269
|
},
|
|
244
270
|
description: "Inspection depth (use 'null' for unlimited)"
|
|
245
271
|
})
|
|
@@ -319,6 +345,21 @@ QUICK START (for troubleshooting):
|
|
|
319
345
|
"--validate cannot be used with --build or --all-builds."
|
|
320
346
|
)
|
|
321
347
|
}
|
|
348
|
+
if (argv["all-builds"] && argv.output) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
"--all-builds and --output are mutually exclusive. Use --save-dir instead."
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
if (argv["all-builds"] && argv.stdout) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"--all-builds and --stdout are mutually exclusive. Use --save-dir instead."
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
if (argv.stdout && argv.output) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
"--stdout and --output are mutually exclusive. Use one or the other."
|
|
361
|
+
)
|
|
362
|
+
}
|
|
322
363
|
if (argv.ssr && !argv.init) {
|
|
323
364
|
throw new Error(
|
|
324
365
|
"--ssr can only be used with --init. Use: bin/shakapacker-config --init --ssr"
|
|
@@ -358,21 +399,18 @@ QUICK START (for troubleshooting):
|
|
|
358
399
|
|
|
359
400
|
// Type assertions are safe here because yargs validates choices at runtime
|
|
360
401
|
// Handle --webpack and --rspack flags
|
|
361
|
-
let bundler
|
|
362
|
-
| "webpack"
|
|
363
|
-
| "rspack"
|
|
364
|
-
| undefined
|
|
402
|
+
let { bundler } = argv
|
|
365
403
|
if (argv.webpack) bundler = "webpack"
|
|
366
404
|
if (argv.rspack) bundler = "rspack"
|
|
367
405
|
|
|
368
406
|
return {
|
|
369
407
|
bundler,
|
|
370
|
-
env: argv.env
|
|
408
|
+
env: argv.env,
|
|
371
409
|
clientOnly: argv["client-only"],
|
|
372
410
|
serverOnly: argv["server-only"],
|
|
373
411
|
output: argv.output,
|
|
374
|
-
depth: argv.depth
|
|
375
|
-
format: argv.format
|
|
412
|
+
depth: argv.depth,
|
|
413
|
+
format: argv.format,
|
|
376
414
|
help: false, // yargs handles help internally
|
|
377
415
|
verbose: argv.verbose,
|
|
378
416
|
doctor: argv.doctor,
|
|
@@ -411,17 +449,14 @@ function applyDefaults(options: ExportOptions): ExportOptions {
|
|
|
411
449
|
!updatedOptions.output &&
|
|
412
450
|
!updatedOptions.saveDir
|
|
413
451
|
) {
|
|
414
|
-
updatedOptions.saveDir = resolve(
|
|
415
|
-
process.cwd(),
|
|
416
|
-
"shakapacker-config-exports"
|
|
417
|
-
)
|
|
452
|
+
updatedOptions.saveDir = resolve(process.cwd(), DEFAULT_EXPORT_DIR)
|
|
418
453
|
}
|
|
419
454
|
|
|
420
455
|
return updatedOptions
|
|
421
456
|
}
|
|
422
457
|
|
|
423
458
|
function runInitCommand(options: ExportOptions): number {
|
|
424
|
-
const configPath = options.configFile ||
|
|
459
|
+
const configPath = options.configFile || DEFAULT_CONFIG_FILE
|
|
425
460
|
const fullPath = resolve(process.cwd(), configPath)
|
|
426
461
|
|
|
427
462
|
// Check if SSR variant is requested via --ssr flag
|
|
@@ -528,7 +563,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
528
563
|
// Validate that config file exists
|
|
529
564
|
const loader = new ConfigFileLoader(options.configFile)
|
|
530
565
|
if (!loader.exists()) {
|
|
531
|
-
const configPath = options.configFile ||
|
|
566
|
+
const configPath = options.configFile || DEFAULT_CONFIG_FILE
|
|
532
567
|
throw new Error(
|
|
533
568
|
`Config file ${configPath} not found. Run --init to create it.`
|
|
534
569
|
)
|
|
@@ -561,7 +596,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
561
596
|
// Handle empty builds edge case
|
|
562
597
|
if (buildsToValidate.length === 0) {
|
|
563
598
|
throw new Error(
|
|
564
|
-
`No builds found in config file. Add at least one build to
|
|
599
|
+
`No builds found in config file. Add at least one build to ${DEFAULT_CONFIG_FILE} or run --init to see examples.`
|
|
565
600
|
)
|
|
566
601
|
}
|
|
567
602
|
}
|
|
@@ -674,8 +709,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
|
|
|
674
709
|
|
|
675
710
|
const loader = new ConfigFileLoader(resolvedOptions.configFile)
|
|
676
711
|
if (!loader.exists()) {
|
|
677
|
-
const configPath =
|
|
678
|
-
resolvedOptions.configFile || "config/shakapacker-builds.yml"
|
|
712
|
+
const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
|
|
679
713
|
throw new Error(
|
|
680
714
|
`Config file ${configPath} not found. Run --init to create it.`
|
|
681
715
|
)
|
|
@@ -762,7 +796,7 @@ async function runDoctorMode(
|
|
|
762
796
|
const createdFiles: string[] = []
|
|
763
797
|
|
|
764
798
|
// Check if config file exists - always use it for doctor mode
|
|
765
|
-
const configFilePath = options.configFile ||
|
|
799
|
+
const configFilePath = options.configFile || DEFAULT_CONFIG_FILE
|
|
766
800
|
const loader = new ConfigFileLoader(configFilePath)
|
|
767
801
|
|
|
768
802
|
if (loader.exists()) {
|
|
@@ -1054,15 +1088,6 @@ async function loadConfigsForEnv(
|
|
|
1054
1088
|
|
|
1055
1089
|
// Set environment variables from config
|
|
1056
1090
|
// Security: Only allow specific environment variables to prevent malicious configs
|
|
1057
|
-
const DANGEROUS_ENV_VARS = [
|
|
1058
|
-
"PATH",
|
|
1059
|
-
"HOME",
|
|
1060
|
-
"LD_PRELOAD",
|
|
1061
|
-
"LD_LIBRARY_PATH",
|
|
1062
|
-
"DYLD_LIBRARY_PATH",
|
|
1063
|
-
"DYLD_INSERT_LIBRARIES"
|
|
1064
|
-
]
|
|
1065
|
-
|
|
1066
1091
|
if (options.verbose) {
|
|
1067
1092
|
console.log(
|
|
1068
1093
|
`[Config Exporter] Setting environment variables from build config...`
|
|
@@ -1070,13 +1095,13 @@ async function loadConfigsForEnv(
|
|
|
1070
1095
|
}
|
|
1071
1096
|
|
|
1072
1097
|
for (const [key, value] of Object.entries(resolvedBuild.environment)) {
|
|
1073
|
-
if (
|
|
1098
|
+
if (isDangerousEnvVar(key)) {
|
|
1074
1099
|
console.warn(
|
|
1075
1100
|
`[Config Exporter] Warning: Skipping dangerous environment variable: ${key}`
|
|
1076
1101
|
)
|
|
1077
1102
|
continue
|
|
1078
1103
|
}
|
|
1079
|
-
if (!(
|
|
1104
|
+
if (!isBuildEnvVar(key)) {
|
|
1080
1105
|
console.warn(
|
|
1081
1106
|
`[Config Exporter] Warning: Skipping non-whitelisted environment variable: ${key}. ` +
|
|
1082
1107
|
`Allowed variables are: ${BUILD_ENV_VARS.join(", ")}`
|
|
@@ -3,9 +3,9 @@ import { resolve, relative, isAbsolute } from "path"
|
|
|
3
3
|
import { load as loadYaml, FAILSAFE_SCHEMA } from "js-yaml"
|
|
4
4
|
import {
|
|
5
5
|
BundlerConfigFile,
|
|
6
|
-
BuildConfig,
|
|
7
6
|
ResolvedBuildConfig,
|
|
8
|
-
ExportOptions
|
|
7
|
+
ExportOptions,
|
|
8
|
+
DEFAULT_CONFIG_FILE
|
|
9
9
|
} from "./types"
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,12 +18,12 @@ export class ConfigFileLoader {
|
|
|
18
18
|
private configFilePath: string
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @param configFilePath - Path to config file (defaults to
|
|
21
|
+
* @param configFilePath - Path to config file (defaults to DEFAULT_CONFIG_FILE in cwd)
|
|
22
22
|
* @throws Error if path is outside project directory
|
|
23
23
|
*/
|
|
24
24
|
constructor(configFilePath?: string) {
|
|
25
25
|
this.configFilePath =
|
|
26
|
-
configFilePath || resolve(process.cwd(),
|
|
26
|
+
configFilePath || resolve(process.cwd(), DEFAULT_CONFIG_FILE)
|
|
27
27
|
this.validateConfigPath()
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -46,7 +46,7 @@ export class ConfigFileLoader {
|
|
|
46
46
|
// If file doesn't exist yet, just use the resolved path
|
|
47
47
|
realPath = absPath
|
|
48
48
|
}
|
|
49
|
-
} catch
|
|
49
|
+
} catch {
|
|
50
50
|
// If we can't resolve the path, use the original
|
|
51
51
|
realPath = absPath
|
|
52
52
|
}
|
|
@@ -92,16 +92,18 @@ export class ConfigFileLoader {
|
|
|
92
92
|
json: true
|
|
93
93
|
}) as BundlerConfigFile
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
ConfigFileLoader.validate(parsed)
|
|
96
96
|
return parsed
|
|
97
|
-
} catch (error:
|
|
97
|
+
} catch (error: unknown) {
|
|
98
|
+
const errorMessage =
|
|
99
|
+
error instanceof Error ? error.message : String(error)
|
|
98
100
|
throw new Error(
|
|
99
|
-
`Failed to load config file ${this.configFilePath}: ${
|
|
101
|
+
`Failed to load config file ${this.configFilePath}: ${errorMessage}`
|
|
100
102
|
)
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
private validate(config: BundlerConfigFile): void {
|
|
106
|
+
private static validate(config: BundlerConfigFile): void {
|
|
105
107
|
if (!config.builds || typeof config.builds !== "object") {
|
|
106
108
|
throw new Error("Config file must contain a 'builds' object")
|
|
107
109
|
}
|
|
@@ -193,7 +195,7 @@ export class ConfigFileLoader {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// Resolve bundler with precedence
|
|
196
|
-
const bundler =
|
|
198
|
+
const bundler = ConfigFileLoader.resolveBundler(
|
|
197
199
|
options.bundler,
|
|
198
200
|
build.bundler,
|
|
199
201
|
config.default_bundler,
|
|
@@ -207,7 +209,9 @@ export class ConfigFileLoader {
|
|
|
207
209
|
)
|
|
208
210
|
|
|
209
211
|
// Convert bundler_env to CLI args
|
|
210
|
-
const bundlerEnvArgs =
|
|
212
|
+
const bundlerEnvArgs = ConfigFileLoader.convertBundlerEnvToArgs(
|
|
213
|
+
build.bundler_env || {}
|
|
214
|
+
)
|
|
211
215
|
|
|
212
216
|
// Resolve and validate outputs
|
|
213
217
|
const outputs = build.outputs || []
|
|
@@ -264,7 +268,7 @@ export class ConfigFileLoader {
|
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
private resolveBundler(
|
|
271
|
+
private static resolveBundler(
|
|
268
272
|
cliFlag?: "webpack" | "rspack",
|
|
269
273
|
buildBundler?: "webpack" | "rspack",
|
|
270
274
|
defaultBundler?: "webpack" | "rspack",
|
|
@@ -293,9 +297,9 @@ export class ConfigFileLoader {
|
|
|
293
297
|
// Replace ${VAR:-default} with VAR value or default
|
|
294
298
|
expanded = expanded.replace(
|
|
295
299
|
/\$\{([^}:]+):-([^}]*)\}/g,
|
|
296
|
-
(_, varName, defaultValue) => {
|
|
300
|
+
(_: string, varName: string, defaultValue: string) => {
|
|
297
301
|
// Validate env var name to prevent regex injection
|
|
298
|
-
if (!
|
|
302
|
+
if (!ConfigFileLoader.isValidEnvVarName(varName)) {
|
|
299
303
|
console.warn(
|
|
300
304
|
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
|
301
305
|
)
|
|
@@ -306,16 +310,19 @@ export class ConfigFileLoader {
|
|
|
306
310
|
)
|
|
307
311
|
|
|
308
312
|
// Replace ${VAR} with VAR value
|
|
309
|
-
expanded = expanded.replace(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
expanded = expanded.replace(
|
|
314
|
+
/\$\{([^}:]+)\}/g,
|
|
315
|
+
(_: string, varName: string) => {
|
|
316
|
+
// Validate env var name to prevent regex injection
|
|
317
|
+
if (!ConfigFileLoader.isValidEnvVarName(varName)) {
|
|
318
|
+
console.warn(
|
|
319
|
+
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
|
320
|
+
)
|
|
321
|
+
return `\${${varName}}`
|
|
322
|
+
}
|
|
323
|
+
return process.env[varName] || ""
|
|
316
324
|
}
|
|
317
|
-
|
|
318
|
-
})
|
|
325
|
+
)
|
|
319
326
|
|
|
320
327
|
return expanded
|
|
321
328
|
}
|
|
@@ -326,11 +333,11 @@ export class ConfigFileLoader {
|
|
|
326
333
|
* @param name - The variable name to validate
|
|
327
334
|
* @returns true if valid, false otherwise
|
|
328
335
|
*/
|
|
329
|
-
private isValidEnvVarName(name: string): boolean {
|
|
336
|
+
private static isValidEnvVarName(name: string): boolean {
|
|
330
337
|
return /^[A-Z_][A-Z0-9_]*$/i.test(name)
|
|
331
338
|
}
|
|
332
339
|
|
|
333
|
-
private convertBundlerEnvToArgs(
|
|
340
|
+
private static convertBundlerEnvToArgs(
|
|
334
341
|
bundlerEnv: Record<string, string | boolean>
|
|
335
342
|
): string[] {
|
|
336
343
|
const args: string[] = []
|
|
@@ -357,7 +364,7 @@ export class ConfigFileLoader {
|
|
|
357
364
|
*/
|
|
358
365
|
listBuilds(): void {
|
|
359
366
|
const config = this.load()
|
|
360
|
-
const builds = config
|
|
367
|
+
const { builds } = config
|
|
361
368
|
|
|
362
369
|
console.log(`\nAvailable builds in ${this.configFilePath}:\n`)
|
|
363
370
|
|