shakapacker 9.3.0.beta.7 → 9.3.1
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/.claude/commands/update-changelog.md +224 -0
- data/.github/actionlint-matcher.json +17 -0
- data/.github/workflows/dummy.yml +9 -0
- data/.github/workflows/generator.yml +13 -0
- data/.github/workflows/node.yml +83 -0
- data/.github/workflows/ruby.yml +11 -0
- data/.github/workflows/test-bundlers.yml +10 -0
- data/CHANGELOG.md +55 -111
- data/CLAUDE.md +6 -10
- data/CONTRIBUTING.md +57 -0
- data/Gemfile.lock +1 -1
- data/README.md +84 -8
- data/docs/api-reference.md +519 -0
- data/docs/configuration.md +38 -4
- data/docs/css-modules-export-mode.md +40 -6
- data/docs/rspack_migration_guide.md +238 -2
- data/docs/transpiler-migration.md +12 -9
- data/docs/troubleshooting.md +21 -21
- data/docs/using_swc_loader.md +13 -10
- data/docs/v9_upgrade.md +11 -2
- data/eslint.config.fast.js +128 -8
- data/eslint.config.js +89 -33
- data/knip.ts +8 -1
- data/lib/install/config/shakapacker.yml +20 -7
- data/lib/shakapacker/configuration.rb +274 -8
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/dev_server_runner.rb +4 -0
- data/lib/shakapacker/doctor.rb +5 -5
- 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 +2 -4
- data/package/configExporter/buildValidator.ts +53 -29
- data/package/configExporter/cli.ts +106 -76
- 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 +3 -2
- data/package/env.ts +2 -2
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +6 -6
- data/package/environments/base.ts +6 -6
- data/package/environments/development.ts +7 -9
- data/package/environments/production.ts +7 -8
- data/package/environments/test.ts +4 -2
- data/package/esbuild/index.ts +0 -2
- data/package/index.d.ts +1 -0
- data/package/index.d.ts.template +1 -0
- data/package/index.ts +28 -5
- data/package/loaders.d.ts +2 -2
- data/package/optimization/webpack.ts +29 -31
- data/package/plugins/rspack.ts +3 -1
- data/package/plugins/webpack.ts +5 -3
- data/package/rspack/index.ts +5 -4
- data/package/rules/file.ts +2 -1
- data/package/rules/jscommon.ts +1 -0
- data/package/rules/raw.ts +3 -1
- data/package/rules/rspack.ts +0 -2
- data/package/rules/sass.ts +0 -2
- data/package/rules/webpack.ts +0 -1
- data/package/swc/index.ts +0 -2
- data/package/types.ts +8 -11
- data/package/utils/debug.ts +0 -4
- data/package/utils/getStyleRule.ts +17 -9
- data/package/utils/helpers.ts +8 -4
- data/package/utils/pathValidation.ts +78 -18
- data/package/utils/requireOrError.ts +14 -5
- data/package/utils/typeGuards.ts +43 -46
- data/package/webpack-types.d.ts +2 -2
- data/package/webpackDevServerConfig.ts +5 -4
- 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/transpiler-defaults.test.js +42 -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 +14 -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
|
})
|
|
@@ -283,43 +309,61 @@ QUICK START (for troubleshooting):
|
|
|
283
309
|
description:
|
|
284
310
|
"Generate only server config (fallback when no config file exists)"
|
|
285
311
|
})
|
|
286
|
-
.check((
|
|
287
|
-
if (
|
|
312
|
+
.check((parsedArgs) => {
|
|
313
|
+
if (parsedArgs.webpack && parsedArgs.rspack) {
|
|
288
314
|
throw new Error(
|
|
289
315
|
"--webpack and --rspack are mutually exclusive. Please specify only one."
|
|
290
316
|
)
|
|
291
317
|
}
|
|
292
|
-
if (
|
|
318
|
+
if (parsedArgs["client-only"] && parsedArgs["server-only"]) {
|
|
293
319
|
throw new Error(
|
|
294
320
|
"--client-only and --server-only are mutually exclusive. Please specify only one."
|
|
295
321
|
)
|
|
296
322
|
}
|
|
297
|
-
if (
|
|
323
|
+
if (parsedArgs.output && parsedArgs["save-dir"]) {
|
|
298
324
|
throw new Error(
|
|
299
325
|
"--output and --save-dir are mutually exclusive. Use one or the other."
|
|
300
326
|
)
|
|
301
327
|
}
|
|
302
|
-
if (
|
|
328
|
+
if (parsedArgs.stdout && parsedArgs["save-dir"]) {
|
|
303
329
|
throw new Error(
|
|
304
330
|
"--stdout and --save-dir are mutually exclusive. Use one or the other."
|
|
305
331
|
)
|
|
306
332
|
}
|
|
307
|
-
if (
|
|
333
|
+
if (parsedArgs.build && parsedArgs["all-builds"]) {
|
|
308
334
|
throw new Error(
|
|
309
335
|
"--build and --all-builds are mutually exclusive. Use one or the other."
|
|
310
336
|
)
|
|
311
337
|
}
|
|
312
|
-
if (
|
|
338
|
+
if (parsedArgs.validate && parsedArgs["validate-build"]) {
|
|
313
339
|
throw new Error(
|
|
314
340
|
"--validate and --validate-build are mutually exclusive. Use one or the other."
|
|
315
341
|
)
|
|
316
342
|
}
|
|
317
|
-
if (
|
|
343
|
+
if (
|
|
344
|
+
parsedArgs.validate &&
|
|
345
|
+
(parsedArgs.build || parsedArgs["all-builds"])
|
|
346
|
+
) {
|
|
318
347
|
throw new Error(
|
|
319
348
|
"--validate cannot be used with --build or --all-builds."
|
|
320
349
|
)
|
|
321
350
|
}
|
|
322
|
-
if (
|
|
351
|
+
if (parsedArgs["all-builds"] && parsedArgs.output) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
"--all-builds and --output are mutually exclusive. Use --save-dir instead."
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
if (parsedArgs["all-builds"] && parsedArgs.stdout) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
"--all-builds and --stdout are mutually exclusive. Use --save-dir instead."
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
if (parsedArgs.stdout && parsedArgs.output) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"--stdout and --output are mutually exclusive. Use one or the other."
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
if (parsedArgs.ssr && !parsedArgs.init) {
|
|
323
367
|
throw new Error(
|
|
324
368
|
"--ssr can only be used with --init. Use: bin/shakapacker-config --init --ssr"
|
|
325
369
|
)
|
|
@@ -358,21 +402,18 @@ QUICK START (for troubleshooting):
|
|
|
358
402
|
|
|
359
403
|
// Type assertions are safe here because yargs validates choices at runtime
|
|
360
404
|
// Handle --webpack and --rspack flags
|
|
361
|
-
let bundler
|
|
362
|
-
| "webpack"
|
|
363
|
-
| "rspack"
|
|
364
|
-
| undefined
|
|
405
|
+
let { bundler } = argv
|
|
365
406
|
if (argv.webpack) bundler = "webpack"
|
|
366
407
|
if (argv.rspack) bundler = "rspack"
|
|
367
408
|
|
|
368
409
|
return {
|
|
369
410
|
bundler,
|
|
370
|
-
env: argv.env
|
|
411
|
+
env: argv.env,
|
|
371
412
|
clientOnly: argv["client-only"],
|
|
372
413
|
serverOnly: argv["server-only"],
|
|
373
414
|
output: argv.output,
|
|
374
|
-
depth: argv.depth
|
|
375
|
-
format: argv.format
|
|
415
|
+
depth: argv.depth,
|
|
416
|
+
format: argv.format,
|
|
376
417
|
help: false, // yargs handles help internally
|
|
377
418
|
verbose: argv.verbose,
|
|
378
419
|
doctor: argv.doctor,
|
|
@@ -411,17 +452,14 @@ function applyDefaults(options: ExportOptions): ExportOptions {
|
|
|
411
452
|
!updatedOptions.output &&
|
|
412
453
|
!updatedOptions.saveDir
|
|
413
454
|
) {
|
|
414
|
-
updatedOptions.saveDir = resolve(
|
|
415
|
-
process.cwd(),
|
|
416
|
-
"shakapacker-config-exports"
|
|
417
|
-
)
|
|
455
|
+
updatedOptions.saveDir = resolve(process.cwd(), DEFAULT_EXPORT_DIR)
|
|
418
456
|
}
|
|
419
457
|
|
|
420
458
|
return updatedOptions
|
|
421
459
|
}
|
|
422
460
|
|
|
423
461
|
function runInitCommand(options: ExportOptions): number {
|
|
424
|
-
const configPath = options.configFile ||
|
|
462
|
+
const configPath = options.configFile || DEFAULT_CONFIG_FILE
|
|
425
463
|
const fullPath = resolve(process.cwd(), configPath)
|
|
426
464
|
|
|
427
465
|
// Check if SSR variant is requested via --ssr flag
|
|
@@ -504,7 +542,7 @@ end
|
|
|
504
542
|
// Make executable
|
|
505
543
|
try {
|
|
506
544
|
chmodSync(binStubPath, 0o755)
|
|
507
|
-
} catch (
|
|
545
|
+
} catch (_e) {
|
|
508
546
|
// chmod might fail on some systems, but mode in writeFileSync should handle it
|
|
509
547
|
}
|
|
510
548
|
}
|
|
@@ -528,7 +566,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
528
566
|
// Validate that config file exists
|
|
529
567
|
const loader = new ConfigFileLoader(options.configFile)
|
|
530
568
|
if (!loader.exists()) {
|
|
531
|
-
const configPath = options.configFile ||
|
|
569
|
+
const configPath = options.configFile || DEFAULT_CONFIG_FILE
|
|
532
570
|
throw new Error(
|
|
533
571
|
`Config file ${configPath} not found. Run --init to create it.`
|
|
534
572
|
)
|
|
@@ -561,7 +599,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
561
599
|
// Handle empty builds edge case
|
|
562
600
|
if (buildsToValidate.length === 0) {
|
|
563
601
|
throw new Error(
|
|
564
|
-
`No builds found in config file. Add at least one build to
|
|
602
|
+
`No builds found in config file. Add at least one build to ${DEFAULT_CONFIG_FILE} or run --init to see examples.`
|
|
565
603
|
)
|
|
566
604
|
}
|
|
567
605
|
}
|
|
@@ -611,6 +649,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
611
649
|
"development"
|
|
612
650
|
|
|
613
651
|
// Auto-detect bundler using the build's environment
|
|
652
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
|
|
614
653
|
const defaultBundler = await autoDetectBundler(
|
|
615
654
|
buildEnv,
|
|
616
655
|
appRoot,
|
|
@@ -625,6 +664,7 @@ async function runValidateCommand(options: ExportOptions): Promise<number> {
|
|
|
625
664
|
)
|
|
626
665
|
|
|
627
666
|
// Validate the build
|
|
667
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
|
|
628
668
|
const result = await validator.validateBuild(resolvedBuild, appRoot)
|
|
629
669
|
results.push(result)
|
|
630
670
|
|
|
@@ -674,8 +714,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
|
|
|
674
714
|
|
|
675
715
|
const loader = new ConfigFileLoader(resolvedOptions.configFile)
|
|
676
716
|
if (!loader.exists()) {
|
|
677
|
-
const configPath =
|
|
678
|
-
resolvedOptions.configFile || "config/shakapacker-builds.yml"
|
|
717
|
+
const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
|
|
679
718
|
throw new Error(
|
|
680
719
|
`Config file ${configPath} not found. Run --init to create it.`
|
|
681
720
|
)
|
|
@@ -704,6 +743,7 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
|
|
|
704
743
|
|
|
705
744
|
// Create a modified options object for this build
|
|
706
745
|
const buildOptions = { ...resolvedOptions, build: buildName }
|
|
746
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
|
|
707
747
|
const configs = await loadConfigsForEnv(undefined, buildOptions, appRoot)
|
|
708
748
|
|
|
709
749
|
for (const { config: cfg, metadata } of configs) {
|
|
@@ -762,7 +802,7 @@ async function runDoctorMode(
|
|
|
762
802
|
const createdFiles: string[] = []
|
|
763
803
|
|
|
764
804
|
// Check if config file exists - always use it for doctor mode
|
|
765
|
-
const configFilePath = options.configFile ||
|
|
805
|
+
const configFilePath = options.configFile || DEFAULT_CONFIG_FILE
|
|
766
806
|
const loader = new ConfigFileLoader(configFilePath)
|
|
767
807
|
|
|
768
808
|
if (loader.exists()) {
|
|
@@ -783,6 +823,7 @@ async function runDoctorMode(
|
|
|
783
823
|
// Clear shakapacker config cache between builds
|
|
784
824
|
shakapackerConfigCache = null
|
|
785
825
|
|
|
826
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential execution required: each build modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
|
|
786
827
|
const configs = await loadConfigsForEnv(
|
|
787
828
|
undefined,
|
|
788
829
|
{ ...options, build: buildName },
|
|
@@ -850,6 +891,7 @@ async function runDoctorMode(
|
|
|
850
891
|
process.env.WEBPACK_SERVE = "true"
|
|
851
892
|
}
|
|
852
893
|
|
|
894
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential execution required: each config modifies shared global state (env vars, config cache) that must be cleared/restored between iterations
|
|
853
895
|
const configs = await loadConfigsForEnv(env, options, appRoot)
|
|
854
896
|
|
|
855
897
|
for (const { config, metadata } of configs) {
|
|
@@ -888,7 +930,6 @@ async function runDoctorMode(
|
|
|
888
930
|
}
|
|
889
931
|
|
|
890
932
|
const fullPath = resolve(targetDir, filename)
|
|
891
|
-
const fileOutput: FileOutput = { filename, content: output, metadata }
|
|
892
933
|
FileWriter.writeSingleFile(fullPath, output)
|
|
893
934
|
createdFiles.push(fullPath)
|
|
894
935
|
}
|
|
@@ -1054,15 +1095,6 @@ async function loadConfigsForEnv(
|
|
|
1054
1095
|
|
|
1055
1096
|
// Set environment variables from config
|
|
1056
1097
|
// 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
1098
|
if (options.verbose) {
|
|
1067
1099
|
console.log(
|
|
1068
1100
|
`[Config Exporter] Setting environment variables from build config...`
|
|
@@ -1070,23 +1102,21 @@ async function loadConfigsForEnv(
|
|
|
1070
1102
|
}
|
|
1071
1103
|
|
|
1072
1104
|
for (const [key, value] of Object.entries(resolvedBuild.environment)) {
|
|
1073
|
-
if (
|
|
1105
|
+
if (isDangerousEnvVar(key)) {
|
|
1074
1106
|
console.warn(
|
|
1075
1107
|
`[Config Exporter] Warning: Skipping dangerous environment variable: ${key}`
|
|
1076
1108
|
)
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
if (!(BUILD_ENV_VARS as readonly string[]).includes(key)) {
|
|
1109
|
+
} else if (!isBuildEnvVar(key)) {
|
|
1080
1110
|
console.warn(
|
|
1081
1111
|
`[Config Exporter] Warning: Skipping non-whitelisted environment variable: ${key}. ` +
|
|
1082
1112
|
`Allowed variables are: ${BUILD_ENV_VARS.join(", ")}`
|
|
1083
1113
|
)
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1114
|
+
} else {
|
|
1115
|
+
if (options.verbose) {
|
|
1116
|
+
console.log(`[Config Exporter] ${key}=${value}`)
|
|
1117
|
+
}
|
|
1118
|
+
process.env[key] = value
|
|
1088
1119
|
}
|
|
1089
|
-
process.env[key] = value
|
|
1090
1120
|
}
|
|
1091
1121
|
|
|
1092
1122
|
// Determine final env: CLI flag > build config NODE_ENV > default
|
|
@@ -1168,7 +1198,7 @@ async function loadConfigsForEnv(
|
|
|
1168
1198
|
if (configFile.endsWith(".ts")) {
|
|
1169
1199
|
try {
|
|
1170
1200
|
require("ts-node/register/transpile-only")
|
|
1171
|
-
} catch (
|
|
1201
|
+
} catch (_error) {
|
|
1172
1202
|
throw new Error(
|
|
1173
1203
|
"TypeScript config detected but ts-node is not available. " +
|
|
1174
1204
|
"Install ts-node as a dev dependency: npm install --save-dev ts-node"
|
|
@@ -1605,7 +1635,7 @@ function loadShakapackerConfig(
|
|
|
1605
1635
|
|
|
1606
1636
|
return result
|
|
1607
1637
|
}
|
|
1608
|
-
} catch (
|
|
1638
|
+
} catch (_error: unknown) {
|
|
1609
1639
|
console.warn(
|
|
1610
1640
|
`[Config Exporter] Error loading shakapacker config, defaulting to webpack`
|
|
1611
1641
|
)
|