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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation mapping for webpack/rspack configuration keys.
|
|
3
|
+
* Used to add inline comments when exporting configs with --annotate flag.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CONFIG_DOCS: Record<string, string> = {
|
|
7
|
+
mode: "Controls webpack optimization: 'development' (fast builds, detailed errors), 'production' (optimized, minified), or 'none'",
|
|
8
|
+
output: "Configuration for output bundles",
|
|
9
|
+
"output.filename":
|
|
10
|
+
"Bundle name template. [name]=entry name, [contenthash]=content-based hash for caching, [chunkhash]=chunk hash",
|
|
11
|
+
"output.path": "Absolute directory path where bundles are written",
|
|
12
|
+
"output.publicPath":
|
|
13
|
+
"URL prefix for loading assets in the browser (used by webpack for code splitting and asset loading)",
|
|
14
|
+
"output.chunkFilename":
|
|
15
|
+
"Template for non-entry chunk files created by code splitting",
|
|
16
|
+
"output.assetModuleFilename":
|
|
17
|
+
"Template for asset module filenames (images, fonts, etc.)",
|
|
18
|
+
"output.crossOriginLoading":
|
|
19
|
+
"Cross-origin loading setting for script tags: 'anonymous', 'use-credentials', or false",
|
|
20
|
+
"output.globalObject":
|
|
21
|
+
"Global object reference for UMD builds (e.g., 'this', 'window', 'global')",
|
|
22
|
+
devtool:
|
|
23
|
+
"Source map style: 'source-map' (full, slow), 'eval-source-map' (full, fast rebuild), 'cheap-source-map' (fast, less detail), false (none)",
|
|
24
|
+
optimization: "Code optimization settings",
|
|
25
|
+
"optimization.minimize":
|
|
26
|
+
"Enable/disable minification (true in production mode)",
|
|
27
|
+
"optimization.minimizer":
|
|
28
|
+
"Array of minimizer plugins (e.g., TerserPlugin, CssMinimizerPlugin)",
|
|
29
|
+
"optimization.splitChunks":
|
|
30
|
+
"Code splitting configuration - extracts common dependencies into separate chunks",
|
|
31
|
+
"optimization.runtimeChunk":
|
|
32
|
+
"Extract webpack runtime into separate chunk: 'single' (one runtime for all), true (one per entry), false (inline)",
|
|
33
|
+
"optimization.moduleIds":
|
|
34
|
+
"Module ID generation strategy: 'deterministic' (stable), 'named' (readable), 'natural' (numeric order)",
|
|
35
|
+
"optimization.chunkIds":
|
|
36
|
+
"Chunk ID generation strategy: 'deterministic', 'named', 'natural'",
|
|
37
|
+
module: "Configures how different file types are processed",
|
|
38
|
+
"module.rules":
|
|
39
|
+
"Array of rules defining loaders and processing for different file types",
|
|
40
|
+
plugins:
|
|
41
|
+
"Array of webpack plugins to apply (e.g., HtmlWebpackPlugin, MiniCssExtractPlugin)",
|
|
42
|
+
resolve: "Module resolution configuration",
|
|
43
|
+
"resolve.extensions":
|
|
44
|
+
"File extensions to try when resolving modules (e.g., ['.js', '.jsx', '.ts', '.tsx'])",
|
|
45
|
+
"resolve.modules":
|
|
46
|
+
"Directories to search when resolving modules (e.g., ['node_modules', 'app/javascript'])",
|
|
47
|
+
"resolve.alias":
|
|
48
|
+
"Create import aliases for modules (e.g., @components -> ./src/components)",
|
|
49
|
+
resolveLoader: "Configuration for resolving loaders",
|
|
50
|
+
"resolveLoader.modules": "Directories to search for loaders",
|
|
51
|
+
entry:
|
|
52
|
+
"Entry points for the application - where webpack starts building the dependency graph",
|
|
53
|
+
devServer: "Webpack dev server configuration (HMR, proxying, HTTPS, etc.)",
|
|
54
|
+
"devServer.port": "Port number for dev server (default: 8080)",
|
|
55
|
+
"devServer.host": "Host for dev server (e.g., 'localhost', '0.0.0.0')",
|
|
56
|
+
"devServer.hot": "Enable Hot Module Replacement (HMR)",
|
|
57
|
+
"devServer.https": "Enable HTTPS for dev server",
|
|
58
|
+
stats:
|
|
59
|
+
"Controls bundle information display: 'normal', 'verbose', 'minimal', 'errors-only', 'none'",
|
|
60
|
+
bail: "Fail the build on first error (true) or continue and report all errors (false)",
|
|
61
|
+
performance: "Performance budget configuration",
|
|
62
|
+
"performance.maxAssetSize":
|
|
63
|
+
"Maximum size (in bytes) for individual assets before webpack warns",
|
|
64
|
+
"performance.maxEntrypointSize":
|
|
65
|
+
"Maximum size (in bytes) for entry point bundles before webpack warns",
|
|
66
|
+
target:
|
|
67
|
+
"Build target environment: 'web' (browser), 'node' (Node.js), 'webworker', etc.",
|
|
68
|
+
externals:
|
|
69
|
+
"Dependencies to exclude from bundle (assumed to be available in runtime environment)",
|
|
70
|
+
cache:
|
|
71
|
+
"Build caching configuration: false (disabled), { type: 'memory' }, or { type: 'filesystem' }",
|
|
72
|
+
watch: "Enable watch mode - rebuild on file changes",
|
|
73
|
+
watchOptions: "Watch mode configuration (polling, ignored files, etc.)"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get documentation for a specific config key path.
|
|
78
|
+
* Supports nested paths like 'output.filename'.
|
|
79
|
+
*/
|
|
80
|
+
export function getDocForKey(keyPath: string): string | undefined {
|
|
81
|
+
return CONFIG_DOCS[keyPath]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get documentation for a key, trying parent paths if exact match not found.
|
|
86
|
+
* E.g., 'output.filename' -> tries 'output.filename', then 'output'
|
|
87
|
+
*/
|
|
88
|
+
export function getDocForKeyWithFallback(keyPath: string): string | undefined {
|
|
89
|
+
// Try exact match first
|
|
90
|
+
if (CONFIG_DOCS[keyPath]) {
|
|
91
|
+
return CONFIG_DOCS[keyPath]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Try parent key
|
|
95
|
+
const parts = keyPath.split(".")
|
|
96
|
+
if (parts.length > 1) {
|
|
97
|
+
const parentKey = parts.slice(0, -1).join(".")
|
|
98
|
+
return CONFIG_DOCS[parentKey]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
@@ -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
|
+
}
|