shakapacker 9.2.0 → 9.3.0.beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
- data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
- data/.github/workflows/claude-code-review.yml +4 -5
- data/.github/workflows/claude.yml +1 -2
- data/.github/workflows/dummy.yml +4 -4
- data/.github/workflows/generator.yml +9 -9
- data/.github/workflows/node.yml +11 -2
- data/.github/workflows/ruby.yml +16 -16
- data/.github/workflows/test-bundlers.yml +9 -9
- data/.gitignore +4 -0
- data/CHANGELOG.md +19 -4
- data/CLAUDE.md +6 -1
- data/CONTRIBUTING.md +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +14 -14
- data/TODO.md +10 -2
- data/TODO_v9.md +13 -3
- data/bin/export-bundler-config +1 -1
- data/conductor-setup.sh +1 -1
- data/conductor.json +1 -1
- data/docs/cdn_setup.md +13 -8
- data/docs/common-upgrades.md +2 -1
- data/docs/configuration.md +630 -0
- data/docs/css-modules-export-mode.md +120 -100
- data/docs/customizing_babel_config.md +16 -16
- data/docs/deployment.md +18 -0
- data/docs/developing_shakapacker.md +6 -0
- data/docs/optional-peer-dependencies.md +9 -4
- data/docs/peer-dependencies.md +17 -6
- data/docs/precompile_hook.md +342 -0
- data/docs/react.md +57 -47
- data/docs/releasing.md +0 -2
- data/docs/rspack.md +25 -21
- data/docs/rspack_migration_guide.md +335 -8
- data/docs/sprockets.md +1 -0
- data/docs/style_loader_vs_mini_css.md +12 -12
- data/docs/subresource_integrity.md +13 -7
- data/docs/transpiler-performance.md +40 -19
- data/docs/troubleshooting.md +0 -2
- data/docs/typescript-migration.md +48 -39
- data/docs/typescript.md +12 -8
- data/docs/using_esbuild_loader.md +10 -10
- data/docs/v6_upgrade.md +33 -20
- data/docs/v7_upgrade.md +8 -6
- data/docs/v8_upgrade.md +13 -12
- data/docs/v9_upgrade.md +2 -1
- data/eslint.config.fast.js +134 -0
- data/eslint.config.js +140 -0
- data/knip.ts +54 -0
- data/lib/install/bin/export-bundler-config +1 -1
- data/lib/install/config/shakapacker.yml +16 -5
- data/lib/shakapacker/compiler.rb +80 -0
- data/lib/shakapacker/configuration.rb +33 -5
- data/lib/shakapacker/dev_server_runner.rb +140 -1
- data/lib/shakapacker/doctor.rb +294 -65
- data/lib/shakapacker/instance.rb +8 -3
- data/lib/shakapacker/runner.rb +244 -8
- data/lib/shakapacker/version.rb +1 -1
- data/lib/tasks/shakapacker/doctor.rake +42 -2
- data/package/babel/preset.ts +7 -4
- data/package/config.ts +42 -30
- data/package/configExporter/cli.ts +799 -208
- data/package/configExporter/configFile.ts +520 -0
- data/package/configExporter/fileWriter.ts +12 -8
- data/package/configExporter/index.ts +9 -1
- data/package/configExporter/types.ts +36 -2
- data/package/configExporter/yamlSerializer.ts +22 -8
- data/package/dev_server.ts +1 -1
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +11 -5
- data/package/environments/base.ts +18 -13
- data/package/environments/development.ts +1 -1
- data/package/environments/production.ts +4 -1
- data/package/index.d.ts +50 -3
- data/package/index.d.ts.template +50 -0
- data/package/index.ts +7 -7
- data/package/loaders.d.ts +2 -2
- data/package/optimization/rspack.ts +1 -1
- data/package/plugins/rspack.ts +15 -4
- data/package/plugins/webpack.ts +7 -3
- data/package/rspack/index.ts +10 -2
- data/package/rules/raw.ts +3 -2
- data/package/rules/sass.ts +1 -1
- data/package/types/README.md +15 -13
- data/package/types/index.ts +5 -5
- data/package/types.ts +0 -1
- data/package/utils/defaultConfigPath.ts +4 -1
- data/package/utils/errorCodes.ts +129 -100
- data/package/utils/errorHelpers.ts +34 -29
- data/package/utils/getStyleRule.ts +5 -2
- data/package/utils/helpers.ts +21 -11
- data/package/utils/pathValidation.ts +43 -35
- data/package/utils/requireOrError.ts +1 -1
- data/package/utils/snakeToCamelCase.ts +1 -1
- data/package/utils/typeGuards.ts +132 -83
- data/package/utils/validateDependencies.ts +1 -1
- data/package/webpack-types.d.ts +3 -3
- data/package/webpackDevServerConfig.ts +22 -10
- data/package-lock.json +2 -2
- data/package.json +36 -28
- data/scripts/type-check-no-emit.js +1 -1
- data/test/configExporter/configFile.test.js +392 -0
- data/test/configExporter/integration.test.js +275 -0
- data/test/helpers.js +1 -1
- data/test/package/configExporter.test.js +154 -0
- data/test/package/helpers.test.js +2 -2
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +2 -4
- data/test/package/rules/sass1.test.js +1 -3
- data/test/package/rules/sass16.test.js +23 -0
- data/tools/README.md +15 -5
- data/tsconfig.eslint.json +2 -9
- data/yarn.lock +1894 -1492
- metadata +19 -3
- data/.eslintignore +0 -5
@@ -0,0 +1,520 @@
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "fs"
|
2
|
+
import { resolve, relative, isAbsolute } from "path"
|
3
|
+
import { load as loadYaml, FAILSAFE_SCHEMA } from "js-yaml"
|
4
|
+
import {
|
5
|
+
BundlerConfigFile,
|
6
|
+
BuildConfig,
|
7
|
+
ResolvedBuildConfig,
|
8
|
+
ExportOptions
|
9
|
+
} from "./types"
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Loads and validates bundler configuration files
|
13
|
+
* @example
|
14
|
+
* const loader = new ConfigFileLoader('.bundler-config.yml')
|
15
|
+
* const config = loader.load()
|
16
|
+
*/
|
17
|
+
export class ConfigFileLoader {
|
18
|
+
private configFilePath: string
|
19
|
+
|
20
|
+
/**
|
21
|
+
* @param configFilePath - Path to config file (defaults to .bundler-config.yml in cwd)
|
22
|
+
* @throws Error if path is outside project directory
|
23
|
+
*/
|
24
|
+
constructor(configFilePath?: string) {
|
25
|
+
this.configFilePath =
|
26
|
+
configFilePath || resolve(process.cwd(), ".bundler-config.yml")
|
27
|
+
this.validateConfigPath()
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Validates that the config file path is within the project directory
|
32
|
+
* to prevent path traversal attacks (including symlink traversal)
|
33
|
+
* @throws Error if path traversal is detected
|
34
|
+
*/
|
35
|
+
private validateConfigPath(): void {
|
36
|
+
const absPath = resolve(this.configFilePath)
|
37
|
+
const cwd = process.cwd()
|
38
|
+
|
39
|
+
// Resolve symlinks to get the real path
|
40
|
+
let realPath: string
|
41
|
+
try {
|
42
|
+
// Only resolve symlinks if the file exists
|
43
|
+
if (existsSync(absPath)) {
|
44
|
+
realPath = realpathSync(absPath)
|
45
|
+
} else {
|
46
|
+
// If file doesn't exist yet, just use the resolved path
|
47
|
+
realPath = absPath
|
48
|
+
}
|
49
|
+
} catch (error) {
|
50
|
+
// If we can't resolve the path, use the original
|
51
|
+
realPath = absPath
|
52
|
+
}
|
53
|
+
|
54
|
+
const rel = relative(cwd, realPath)
|
55
|
+
|
56
|
+
if (
|
57
|
+
rel.startsWith("..") ||
|
58
|
+
(isAbsolute(rel) && !realPath.startsWith(cwd))
|
59
|
+
) {
|
60
|
+
throw new Error(
|
61
|
+
`Config file must be within project directory. Attempted path: ${this.configFilePath}`
|
62
|
+
)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Checks if the config file exists
|
68
|
+
* @returns true if file exists, false otherwise
|
69
|
+
*/
|
70
|
+
exists(): boolean {
|
71
|
+
return existsSync(this.configFilePath)
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Loads and validates the config file
|
76
|
+
* @returns Parsed and validated config file
|
77
|
+
* @throws Error if file doesn't exist, is invalid YAML, or fails validation
|
78
|
+
*/
|
79
|
+
load(): BundlerConfigFile {
|
80
|
+
if (!this.exists()) {
|
81
|
+
throw new Error(
|
82
|
+
`Config file not found: ${this.configFilePath}\n` +
|
83
|
+
`Run 'bin/export-bundler-config --init' to generate a sample config file.`
|
84
|
+
)
|
85
|
+
}
|
86
|
+
|
87
|
+
try {
|
88
|
+
const content = readFileSync(this.configFilePath, "utf8")
|
89
|
+
// Use FAILSAFE_SCHEMA to prevent code execution via YAML parsing
|
90
|
+
const parsed = loadYaml(content, {
|
91
|
+
schema: FAILSAFE_SCHEMA,
|
92
|
+
json: true
|
93
|
+
}) as BundlerConfigFile
|
94
|
+
|
95
|
+
this.validate(parsed)
|
96
|
+
return parsed
|
97
|
+
} catch (error: any) {
|
98
|
+
throw new Error(
|
99
|
+
`Failed to load config file ${this.configFilePath}: ${error.message}`
|
100
|
+
)
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
private validate(config: BundlerConfigFile): void {
|
105
|
+
if (!config.builds || typeof config.builds !== "object") {
|
106
|
+
throw new Error("Config file must contain a 'builds' object")
|
107
|
+
}
|
108
|
+
|
109
|
+
if (Object.keys(config.builds).length === 0) {
|
110
|
+
throw new Error("Config file must contain at least one build")
|
111
|
+
}
|
112
|
+
|
113
|
+
if (
|
114
|
+
config.default_bundler &&
|
115
|
+
config.default_bundler !== "webpack" &&
|
116
|
+
config.default_bundler !== "rspack"
|
117
|
+
) {
|
118
|
+
throw new Error(
|
119
|
+
`Invalid default_bundler '${config.default_bundler}'. Must be 'webpack' or 'rspack'.`
|
120
|
+
)
|
121
|
+
}
|
122
|
+
|
123
|
+
// Validate each build
|
124
|
+
for (const [name, build] of Object.entries(config.builds)) {
|
125
|
+
// Guard: ensure build is a non-null plain object
|
126
|
+
if (build == null || typeof build !== "object" || Array.isArray(build)) {
|
127
|
+
throw new Error(
|
128
|
+
`Invalid build '${name}': must be an object, got ${build === null ? "null" : Array.isArray(build) ? "array" : typeof build}`
|
129
|
+
)
|
130
|
+
}
|
131
|
+
|
132
|
+
if (
|
133
|
+
build.bundler &&
|
134
|
+
build.bundler !== "webpack" &&
|
135
|
+
build.bundler !== "rspack"
|
136
|
+
) {
|
137
|
+
throw new Error(
|
138
|
+
`Invalid bundler '${build.bundler}' in build '${name}'. Must be 'webpack' or 'rspack'.`
|
139
|
+
)
|
140
|
+
}
|
141
|
+
|
142
|
+
if (build.bundler_env && typeof build.bundler_env !== "object") {
|
143
|
+
throw new Error(
|
144
|
+
`Invalid bundler_env in build '${name}'. Must be an object.`
|
145
|
+
)
|
146
|
+
}
|
147
|
+
|
148
|
+
if (build.environment && typeof build.environment !== "object") {
|
149
|
+
throw new Error(
|
150
|
+
`Invalid environment in build '${name}'. Must be an object.`
|
151
|
+
)
|
152
|
+
}
|
153
|
+
|
154
|
+
if (build.outputs && !Array.isArray(build.outputs)) {
|
155
|
+
throw new Error(
|
156
|
+
`Invalid outputs in build '${name}'. Must be an array of strings.`
|
157
|
+
)
|
158
|
+
}
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
/**
|
163
|
+
* Resolves a build configuration by name
|
164
|
+
* @param buildName - Name of the build from config file
|
165
|
+
* @param options - CLI options that may override build settings
|
166
|
+
* @param defaultBundler - Fallback bundler if not specified
|
167
|
+
* @returns Resolved build configuration with all settings applied
|
168
|
+
* @throws Error if build name not found
|
169
|
+
*/
|
170
|
+
resolveBuild(
|
171
|
+
buildName: string,
|
172
|
+
options: ExportOptions,
|
173
|
+
defaultBundler: "webpack" | "rspack"
|
174
|
+
): ResolvedBuildConfig {
|
175
|
+
const config = this.load()
|
176
|
+
const build = config.builds[buildName]
|
177
|
+
|
178
|
+
if (!build) {
|
179
|
+
const available = Object.keys(config.builds).join(", ")
|
180
|
+
throw new Error(
|
181
|
+
`Build '${buildName}' not found in config file.\n` +
|
182
|
+
`Available builds: ${available}\n` +
|
183
|
+
`Use --list-builds to see all available builds.`
|
184
|
+
)
|
185
|
+
}
|
186
|
+
|
187
|
+
// Resolve bundler with precedence
|
188
|
+
const bundler = this.resolveBundler(
|
189
|
+
options.bundler,
|
190
|
+
build.bundler,
|
191
|
+
config.default_bundler,
|
192
|
+
defaultBundler
|
193
|
+
)
|
194
|
+
|
195
|
+
// Expand environment variables
|
196
|
+
const environment = this.expandEnvironmentVariables(
|
197
|
+
build.environment || {},
|
198
|
+
bundler
|
199
|
+
)
|
200
|
+
|
201
|
+
// Convert bundler_env to CLI args
|
202
|
+
const bundlerEnvArgs = this.convertBundlerEnvToArgs(build.bundler_env || {})
|
203
|
+
|
204
|
+
// Resolve and validate outputs
|
205
|
+
const outputs = build.outputs || []
|
206
|
+
|
207
|
+
// Validate edge cases
|
208
|
+
if (outputs.length === 0) {
|
209
|
+
throw new Error(
|
210
|
+
`Build '${buildName}' has empty outputs array. ` +
|
211
|
+
`Please specify at least one output type (client, server, or all).`
|
212
|
+
)
|
213
|
+
}
|
214
|
+
|
215
|
+
// Check for duplicates
|
216
|
+
const uniqueOutputs = new Set(outputs)
|
217
|
+
if (uniqueOutputs.size !== outputs.length) {
|
218
|
+
throw new Error(
|
219
|
+
`Build '${buildName}' has duplicate output types. ` +
|
220
|
+
`Each output type should appear only once.`
|
221
|
+
)
|
222
|
+
}
|
223
|
+
|
224
|
+
// Resolve config file
|
225
|
+
let configFile: string | undefined
|
226
|
+
if (build.config) {
|
227
|
+
configFile = this.expandEnvironmentVariables(
|
228
|
+
{ config: build.config },
|
229
|
+
bundler
|
230
|
+
).config
|
231
|
+
|
232
|
+
// Validate config file path (prevent path traversal)
|
233
|
+
if (configFile) {
|
234
|
+
// Normalize Windows backslashes for validation
|
235
|
+
const configFileNormalized = configFile.replace(/\\/g, "/")
|
236
|
+
if (
|
237
|
+
configFileNormalized.includes("..") ||
|
238
|
+
!configFileNormalized.startsWith("config/")
|
239
|
+
) {
|
240
|
+
throw new Error(
|
241
|
+
`Invalid config file path in build '${buildName}': "${configFile}". ` +
|
242
|
+
`Config files must be within the config/ directory.`
|
243
|
+
)
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
return {
|
249
|
+
name: buildName,
|
250
|
+
description: build.description,
|
251
|
+
bundler,
|
252
|
+
environment,
|
253
|
+
bundlerEnvArgs,
|
254
|
+
outputs,
|
255
|
+
configFile
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
private resolveBundler(
|
260
|
+
cliFlag?: "webpack" | "rspack",
|
261
|
+
buildBundler?: "webpack" | "rspack",
|
262
|
+
defaultBundler?: "webpack" | "rspack",
|
263
|
+
fallback: "webpack" | "rspack" = "webpack"
|
264
|
+
): "webpack" | "rspack" {
|
265
|
+
return cliFlag || buildBundler || defaultBundler || fallback
|
266
|
+
}
|
267
|
+
|
268
|
+
private expandEnvironmentVariables(
|
269
|
+
vars: Record<string, string>,
|
270
|
+
bundler: string
|
271
|
+
): Record<string, string> {
|
272
|
+
const expanded: Record<string, string> = {}
|
273
|
+
|
274
|
+
for (const [key, value] of Object.entries(vars)) {
|
275
|
+
expanded[key] = this.expandString(value, bundler)
|
276
|
+
}
|
277
|
+
|
278
|
+
return expanded
|
279
|
+
}
|
280
|
+
|
281
|
+
private expandString(str: string, bundler: string): string {
|
282
|
+
// Replace \${BUNDLER} with actual bundler
|
283
|
+
let expanded = str.replace(/\$\{BUNDLER\}/g, bundler)
|
284
|
+
|
285
|
+
// Replace ${VAR:-default} with VAR value or default
|
286
|
+
expanded = expanded.replace(
|
287
|
+
/\$\{([^}:]+):-([^}]*)\}/g,
|
288
|
+
(_, varName, defaultValue) => {
|
289
|
+
// Validate env var name to prevent regex injection
|
290
|
+
if (!this.isValidEnvVarName(varName)) {
|
291
|
+
console.warn(
|
292
|
+
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
293
|
+
)
|
294
|
+
return `\${${varName}:-${defaultValue}}`
|
295
|
+
}
|
296
|
+
return process.env[varName] || defaultValue
|
297
|
+
}
|
298
|
+
)
|
299
|
+
|
300
|
+
// Replace ${VAR} with VAR value
|
301
|
+
expanded = expanded.replace(/\$\{([^}:]+)\}/g, (_, varName) => {
|
302
|
+
// Validate env var name to prevent regex injection
|
303
|
+
if (!this.isValidEnvVarName(varName)) {
|
304
|
+
console.warn(
|
305
|
+
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
306
|
+
)
|
307
|
+
return `\${${varName}}`
|
308
|
+
}
|
309
|
+
return process.env[varName] || ""
|
310
|
+
})
|
311
|
+
|
312
|
+
return expanded
|
313
|
+
}
|
314
|
+
|
315
|
+
/**
|
316
|
+
* Validates that an environment variable name matches the standard format
|
317
|
+
* Must start with letter or underscore, followed by letters, numbers, or underscores
|
318
|
+
* @param name - The variable name to validate
|
319
|
+
* @returns true if valid, false otherwise
|
320
|
+
*/
|
321
|
+
private isValidEnvVarName(name: string): boolean {
|
322
|
+
return /^[A-Z_][A-Z0-9_]*$/i.test(name)
|
323
|
+
}
|
324
|
+
|
325
|
+
private convertBundlerEnvToArgs(
|
326
|
+
bundlerEnv: Record<string, string | boolean>
|
327
|
+
): string[] {
|
328
|
+
const args: string[] = []
|
329
|
+
|
330
|
+
for (const [key, value] of Object.entries(bundlerEnv)) {
|
331
|
+
// YAML parser converts boolean true to string "true", so check both
|
332
|
+
if (value === true || value === "true") {
|
333
|
+
// Boolean true becomes --env key
|
334
|
+
args.push("--env", key)
|
335
|
+
} else if (typeof value === "string" && value !== "false") {
|
336
|
+
// String value becomes --env key=value (skip "false" strings)
|
337
|
+
args.push("--env", `${key}=${value}`)
|
338
|
+
}
|
339
|
+
// false or "false" are ignored
|
340
|
+
}
|
341
|
+
|
342
|
+
return args
|
343
|
+
}
|
344
|
+
|
345
|
+
/**
|
346
|
+
* Lists all available builds from the config file
|
347
|
+
* Prints formatted output to console
|
348
|
+
* @throws Error if config file doesn't exist or is invalid
|
349
|
+
*/
|
350
|
+
listBuilds(): void {
|
351
|
+
const config = this.load()
|
352
|
+
const builds = config.builds
|
353
|
+
|
354
|
+
console.log(`\nAvailable builds in ${this.configFilePath}:\n`)
|
355
|
+
|
356
|
+
for (const [name, build] of Object.entries(builds)) {
|
357
|
+
const bundler =
|
358
|
+
build.bundler || config.default_bundler || "webpack (default)"
|
359
|
+
const outputs = build.outputs ? build.outputs.join(", ") : "auto-detect"
|
360
|
+
|
361
|
+
console.log(` ${name}`)
|
362
|
+
if (build.description) {
|
363
|
+
console.log(` Description: ${build.description}`)
|
364
|
+
}
|
365
|
+
console.log(` Bundler: ${bundler}`)
|
366
|
+
console.log(` Outputs: ${outputs}`)
|
367
|
+
console.log()
|
368
|
+
}
|
369
|
+
}
|
370
|
+
}
|
371
|
+
|
372
|
+
/**
|
373
|
+
* Generates a sample configuration file with examples and documentation
|
374
|
+
* @returns YAML content as string ready to be written to file
|
375
|
+
*/
|
376
|
+
export function generateSampleConfigFile(): string {
|
377
|
+
// Using ${'$'} to escape template literal substitution in comments
|
378
|
+
return `# Bundler Build Configurations
|
379
|
+
# Generated by: bin/export-bundler-config --init
|
380
|
+
#
|
381
|
+
# This file defines build configurations for exporting bundler configs.
|
382
|
+
# You can define multiple builds with different environments and settings.
|
383
|
+
|
384
|
+
# Use these builds as defaults for --doctor mode (optional)
|
385
|
+
# When set to true, --doctor will export ALL builds defined below instead of hardcoded defaults
|
386
|
+
# shakapacker_doctor_default_builds_here: true
|
387
|
+
|
388
|
+
# Default bundler for all builds (can be overridden per-build or with --webpack/--rspack flags)
|
389
|
+
default_bundler: rspack # Options: webpack | rspack
|
390
|
+
|
391
|
+
builds:
|
392
|
+
# ============================================================================
|
393
|
+
# DEVELOPMENT WITH HMR (Hot Module Replacement)
|
394
|
+
# ============================================================================
|
395
|
+
# For Procfile.dev: WEBPACK_SERVE=true bin/shakapacker-dev-server
|
396
|
+
# Creates client bundle with React Fast Refresh enabled
|
397
|
+
|
398
|
+
dev-hmr:
|
399
|
+
description: Client bundle with HMR (React Fast Refresh)
|
400
|
+
environment:
|
401
|
+
NODE_ENV: development
|
402
|
+
RAILS_ENV: development
|
403
|
+
WEBPACK_SERVE: "true"
|
404
|
+
outputs:
|
405
|
+
- client
|
406
|
+
|
407
|
+
# ============================================================================
|
408
|
+
# DEVELOPMENT (Standard)
|
409
|
+
# ============================================================================
|
410
|
+
# For Procfile.dev-static-assets: bin/shakapacker --watch
|
411
|
+
# Creates both client and server bundles without HMR
|
412
|
+
|
413
|
+
dev:
|
414
|
+
description: Development client and server bundles (no HMR)
|
415
|
+
environment:
|
416
|
+
NODE_ENV: development
|
417
|
+
RAILS_ENV: development
|
418
|
+
outputs:
|
419
|
+
- client
|
420
|
+
- server
|
421
|
+
|
422
|
+
# ============================================================================
|
423
|
+
# PRODUCTION
|
424
|
+
# ============================================================================
|
425
|
+
# For asset precompilation: RAILS_ENV=production bin/shakapacker
|
426
|
+
# Creates optimized production bundles
|
427
|
+
|
428
|
+
prod:
|
429
|
+
description: Production client and server bundles
|
430
|
+
environment:
|
431
|
+
NODE_ENV: production
|
432
|
+
RAILS_ENV: production
|
433
|
+
outputs:
|
434
|
+
- client
|
435
|
+
- server
|
436
|
+
|
437
|
+
# ============================================================================
|
438
|
+
# ADDITIONAL EXAMPLES
|
439
|
+
# ============================================================================
|
440
|
+
|
441
|
+
# Example: Single bundle only (client or server)
|
442
|
+
# dev-client-only:
|
443
|
+
# description: Development client bundle only
|
444
|
+
# environment:
|
445
|
+
# NODE_ENV: development
|
446
|
+
# RAILS_ENV: development
|
447
|
+
# CLIENT_BUNDLE_ONLY: "yes"
|
448
|
+
# outputs:
|
449
|
+
# - client
|
450
|
+
|
451
|
+
# Example: Using bundler --env flags
|
452
|
+
# prod-modern:
|
453
|
+
# description: Production with custom webpack/rspack --env flags
|
454
|
+
# environment:
|
455
|
+
# NODE_ENV: production
|
456
|
+
# RAILS_ENV: production
|
457
|
+
# bundler_env:
|
458
|
+
# target: modern # Becomes: --env target=modern
|
459
|
+
# instrumented: true # Becomes: --env instrumented
|
460
|
+
# outputs:
|
461
|
+
# - client
|
462
|
+
# - server
|
463
|
+
|
464
|
+
# Example: Variable substitution with defaults
|
465
|
+
# staging:
|
466
|
+
# description: Staging environment with variable substitution
|
467
|
+
# environment:
|
468
|
+
# NODE_ENV: production
|
469
|
+
# RAILS_ENV: ${"$"}{RAILS_ENV:-staging} # Use env var or default to 'staging'
|
470
|
+
# outputs:
|
471
|
+
# - client
|
472
|
+
# - server
|
473
|
+
|
474
|
+
# Example: Custom config file path (uses ${"$"}{BUNDLER} substitution)
|
475
|
+
# custom-config:
|
476
|
+
# description: Using custom config file location
|
477
|
+
# environment:
|
478
|
+
# NODE_ENV: development
|
479
|
+
# config: config/${"$"}{BUNDLER}/${"$"}{BUNDLER}.config.js
|
480
|
+
# outputs:
|
481
|
+
# - client
|
482
|
+
# - server
|
483
|
+
|
484
|
+
# ============================================================================
|
485
|
+
# USAGE EXAMPLES
|
486
|
+
# ============================================================================
|
487
|
+
#
|
488
|
+
# Initialize this config file:
|
489
|
+
# bin/export-bundler-config --init
|
490
|
+
#
|
491
|
+
# List all available builds:
|
492
|
+
# bin/export-bundler-config --list-builds
|
493
|
+
#
|
494
|
+
# Export development build configs:
|
495
|
+
# bin/export-bundler-config --build=dev-hmr --save
|
496
|
+
# Creates: rspack-dev-hmr-client.yml
|
497
|
+
#
|
498
|
+
# bin/export-bundler-config --build=dev --save
|
499
|
+
# Creates: rspack-dev-client.yml, rspack-dev-server.yml
|
500
|
+
#
|
501
|
+
# Export production build:
|
502
|
+
# bin/export-bundler-config --build=prod --save
|
503
|
+
# Creates: rspack-prod-client.yml, rspack-prod-server.yml
|
504
|
+
#
|
505
|
+
# Use webpack instead of default rspack:
|
506
|
+
# bin/export-bundler-config --build=prod --save --webpack
|
507
|
+
# Creates: webpack-prod-client.yml, webpack-prod-server.yml
|
508
|
+
#
|
509
|
+
# Export to stdout for inspection (no files created):
|
510
|
+
# bin/export-bundler-config --build=dev
|
511
|
+
#
|
512
|
+
# Export to custom directory:
|
513
|
+
# bin/export-bundler-config --build=prod --save-dir=./debug
|
514
|
+
#
|
515
|
+
# Doctor mode (comprehensive troubleshooting):
|
516
|
+
# bin/export-bundler-config --doctor
|
517
|
+
# Creates files in: shakapacker-config-exports/
|
518
|
+
#
|
519
|
+
`
|
520
|
+
}
|
@@ -32,34 +32,38 @@ export class FileWriter {
|
|
32
32
|
/**
|
33
33
|
* Write a single file
|
34
34
|
*/
|
35
|
-
writeSingleFile(filePath: string, content: string
|
35
|
+
writeSingleFile(filePath: string, content: string): void {
|
36
36
|
// Ensure parent directory exists
|
37
37
|
const dir = dirname(filePath)
|
38
38
|
this.ensureDirectory(dir)
|
39
39
|
|
40
40
|
this.validateOutputPath(filePath)
|
41
41
|
this.writeFile(filePath, content)
|
42
|
-
|
43
|
-
console.log(`[Config Exporter] Config exported to: ${filePath}`)
|
44
|
-
}
|
42
|
+
console.log(`[Config Exporter] Created: ${filePath}`)
|
45
43
|
}
|
46
44
|
|
47
45
|
/**
|
48
46
|
* Generate filename for a config export
|
49
|
-
* Format: {bundler}-{env}-{type}.{ext}
|
47
|
+
* Format without build: {bundler}-{env}-{type}.{ext}
|
48
|
+
* Format with build: {bundler}-{build}-{type}.{ext}
|
50
49
|
* Examples:
|
51
50
|
* webpack-development-client.yaml
|
52
51
|
* rspack-production-server.yaml
|
53
52
|
* webpack-test-all.json
|
53
|
+
* webpack-development-client-hmr.yaml
|
54
|
+
* webpack-dev-client.yaml (with build name)
|
55
|
+
* rspack-cypress-dev-server.yaml (with build name)
|
54
56
|
*/
|
55
57
|
generateFilename(
|
56
58
|
bundler: string,
|
57
59
|
env: string,
|
58
|
-
configType: "client" | "server" | "all",
|
59
|
-
format: "yaml" | "json" | "inspect"
|
60
|
+
configType: "client" | "server" | "all" | "client-hmr",
|
61
|
+
format: "yaml" | "json" | "inspect",
|
62
|
+
buildName?: string
|
60
63
|
): string {
|
61
64
|
const ext = format === "yaml" ? "yaml" : format === "json" ? "json" : "txt"
|
62
|
-
|
65
|
+
const name = buildName || env
|
66
|
+
return `${bundler}-${name}-${configType}.${ext}`
|
63
67
|
}
|
64
68
|
|
65
69
|
private writeFile(filePath: string, content: string): void {
|
@@ -1,5 +1,13 @@
|
|
1
1
|
export { run } from "./cli"
|
2
|
-
export type {
|
2
|
+
export type {
|
3
|
+
ExportOptions,
|
4
|
+
ConfigMetadata,
|
5
|
+
FileOutput,
|
6
|
+
BundlerConfigFile,
|
7
|
+
BuildConfig,
|
8
|
+
ResolvedBuildConfig
|
9
|
+
} from "./types"
|
3
10
|
export { YamlSerializer } from "./yamlSerializer"
|
4
11
|
export { FileWriter } from "./fileWriter"
|
5
12
|
export { getDocForKey } from "./configDocs"
|
13
|
+
export { ConfigFileLoader, generateSampleConfigFile } from "./configFile"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
export interface ExportOptions {
|
2
2
|
doctor?: boolean
|
3
|
-
save?: boolean
|
4
3
|
saveDir?: string
|
4
|
+
stdout?: boolean
|
5
5
|
bundler?: "webpack" | "rspack"
|
6
6
|
env?: "development" | "production" | "test"
|
7
7
|
clientOnly?: boolean
|
@@ -12,6 +12,12 @@ export interface ExportOptions {
|
|
12
12
|
verbose?: boolean
|
13
13
|
depth?: number | null
|
14
14
|
help?: boolean
|
15
|
+
// New config file options
|
16
|
+
init?: boolean
|
17
|
+
configFile?: string
|
18
|
+
build?: string
|
19
|
+
listBuilds?: boolean
|
20
|
+
allBuilds?: boolean
|
15
21
|
}
|
16
22
|
|
17
23
|
export interface ConfigMetadata {
|
@@ -19,13 +25,15 @@ export interface ConfigMetadata {
|
|
19
25
|
bundler: string
|
20
26
|
environment: string
|
21
27
|
configFile: string
|
22
|
-
configType: "client" | "server" | "all"
|
28
|
+
configType: "client" | "server" | "all" | "client-hmr"
|
23
29
|
configCount: number
|
30
|
+
buildName?: string // New: name of the build from config file
|
24
31
|
environmentVariables: {
|
25
32
|
NODE_ENV?: string
|
26
33
|
RAILS_ENV?: string
|
27
34
|
CLIENT_BUNDLE_ONLY?: string
|
28
35
|
SERVER_BUNDLE_ONLY?: string
|
36
|
+
WEBPACK_SERVE?: string
|
29
37
|
}
|
30
38
|
}
|
31
39
|
|
@@ -34,3 +42,29 @@ export interface FileOutput {
|
|
34
42
|
content: string
|
35
43
|
metadata: ConfigMetadata
|
36
44
|
}
|
45
|
+
|
46
|
+
// Config file schema types
|
47
|
+
export interface BundlerConfigFile {
|
48
|
+
default_bundler?: "webpack" | "rspack"
|
49
|
+
shakapacker_doctor_default_builds_here?: boolean
|
50
|
+
builds: Record<string, BuildConfig>
|
51
|
+
}
|
52
|
+
|
53
|
+
export interface BuildConfig {
|
54
|
+
description?: string
|
55
|
+
bundler?: "webpack" | "rspack"
|
56
|
+
environment?: Record<string, string>
|
57
|
+
bundler_env?: Record<string, string | boolean>
|
58
|
+
outputs?: string[]
|
59
|
+
config?: string
|
60
|
+
}
|
61
|
+
|
62
|
+
export interface ResolvedBuildConfig {
|
63
|
+
name: string
|
64
|
+
description?: string
|
65
|
+
bundler: "webpack" | "rspack"
|
66
|
+
environment: Record<string, string>
|
67
|
+
bundlerEnvArgs: string[] // Converted bundler_env to CLI args
|
68
|
+
outputs: string[]
|
69
|
+
configFile?: string
|
70
|
+
}
|
@@ -160,21 +160,35 @@ export class YamlSerializer {
|
|
160
160
|
if (typeof item === "object" && !Array.isArray(item) && item !== null) {
|
161
161
|
// For objects in arrays, emit marker on its own line and indent content
|
162
162
|
lines.push(`${itemIndent}-`)
|
163
|
-
serialized
|
163
|
+
const nonEmptyLines = serialized
|
164
164
|
.split("\n")
|
165
165
|
.filter((line: string) => line.trim().length > 0)
|
166
|
-
|
167
|
-
|
168
|
-
|
166
|
+
// Compute minimum leading whitespace to preserve relative indentation
|
167
|
+
const minIndent = Math.min(
|
168
|
+
...nonEmptyLines.map(
|
169
|
+
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
170
|
+
)
|
171
|
+
)
|
172
|
+
nonEmptyLines.forEach((line: string) => {
|
173
|
+
// Remove only the common indent, preserving relative indentation
|
174
|
+
lines.push(contentIndent + line.substring(minIndent))
|
175
|
+
})
|
169
176
|
} else if (serialized.includes("\n")) {
|
170
177
|
// For multiline values, emit marker on its own line and indent content
|
171
178
|
lines.push(`${itemIndent}-`)
|
172
|
-
serialized
|
179
|
+
const nonEmptyLines = serialized
|
173
180
|
.split("\n")
|
174
181
|
.filter((line: string) => line.trim().length > 0)
|
175
|
-
|
176
|
-
|
177
|
-
|
182
|
+
// Compute minimum leading whitespace to preserve relative indentation
|
183
|
+
const minIndent = Math.min(
|
184
|
+
...nonEmptyLines.map(
|
185
|
+
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
186
|
+
)
|
187
|
+
)
|
188
|
+
nonEmptyLines.forEach((line: string) => {
|
189
|
+
// Remove only the common indent, preserving relative indentation
|
190
|
+
lines.push(contentIndent + line.substring(minIndent))
|
191
|
+
})
|
178
192
|
} else {
|
179
193
|
// For simple values, keep on same line
|
180
194
|
lines.push(`${itemIndent}- ${serialized}`)
|
data/package/dev_server.ts
CHANGED
@@ -18,7 +18,7 @@ if (devServerConfig) {
|
|
18
18
|
const envValue = envFetch(`${envPrefix}_${key.toUpperCase()}`)
|
19
19
|
if (envValue !== undefined) {
|
20
20
|
// Use bracket notation to avoid ASI issues
|
21
|
-
(devServerConfig as Record<string, unknown>)[key] = envValue
|
21
|
+
;(devServerConfig as Record<string, unknown>)[key] = envValue
|
22
22
|
}
|
23
23
|
})
|
24
24
|
}
|