shakapacker 9.3.0.beta.6 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -105
- data/ESLINT_TECHNICAL_DEBT.md +8 -2
- data/Gemfile.lock +1 -1
- data/README.md +53 -2
- data/docs/configuration.md +28 -0
- data/docs/rspack_migration_guide.md +238 -2
- data/docs/troubleshooting.md +21 -21
- data/eslint.config.fast.js +8 -0
- data/eslint.config.js +47 -10
- data/knip.ts +8 -1
- data/lib/install/config/shakapacker.yml +6 -6
- data/lib/shakapacker/configuration.rb +227 -4
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/doctor.rb +129 -72
- data/lib/shakapacker/instance.rb +85 -1
- data/lib/shakapacker/manifest.rb +85 -11
- data/lib/shakapacker/runner.rb +12 -8
- data/lib/shakapacker/swc_migrator.rb +7 -7
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker.rb +143 -3
- data/lib/tasks/shakapacker/doctor.rake +1 -1
- data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
- data/package/config.ts +0 -1
- data/package/configExporter/buildValidator.ts +53 -29
- data/package/configExporter/cli.ts +152 -118
- data/package/configExporter/configFile.ts +33 -26
- data/package/configExporter/fileWriter.ts +3 -3
- data/package/configExporter/types.ts +64 -0
- data/package/configExporter/yamlSerializer.ts +147 -36
- data/package/dev_server.ts +2 -1
- data/package/env.ts +1 -1
- data/package/environments/base.ts +4 -4
- data/package/environments/development.ts +7 -6
- data/package/environments/production.ts +6 -7
- data/package/environments/test.ts +2 -1
- data/package/index.ts +28 -4
- data/package/loaders.d.ts +2 -2
- data/package/optimization/webpack.ts +29 -31
- data/package/plugins/webpack.ts +2 -1
- data/package/rspack/index.ts +2 -1
- data/package/rules/file.ts +1 -0
- data/package/rules/jscommon.ts +1 -0
- data/package/utils/helpers.ts +0 -1
- data/package/utils/pathValidation.ts +68 -7
- data/package/utils/requireOrError.ts +10 -2
- data/package/utils/typeGuards.ts +43 -46
- data/package/webpack-types.d.ts +2 -2
- data/package/webpackDevServerConfig.ts +1 -0
- data/package.json +2 -3
- data/test/configExporter/integration.test.js +8 -8
- data/test/package/configExporter/cli.test.js +440 -0
- data/test/package/configExporter/types.test.js +163 -0
- data/test/package/configExporter.test.js +271 -7
- data/test/package/yamlSerializer.test.js +204 -0
- data/test/typescript/pathValidation.test.js +44 -0
- data/test/typescript/requireOrError.test.js +49 -0
- data/yarn.lock +0 -32
- metadata +11 -6
- data/.eslintrc.fast.js +0 -40
- data/.eslintrc.js +0 -84
- data/package-lock.json +0 -13047
|
@@ -3,9 +3,9 @@ import { resolve, relative, isAbsolute } from "path"
|
|
|
3
3
|
import { load as loadYaml, FAILSAFE_SCHEMA } from "js-yaml"
|
|
4
4
|
import {
|
|
5
5
|
BundlerConfigFile,
|
|
6
|
-
BuildConfig,
|
|
7
6
|
ResolvedBuildConfig,
|
|
8
|
-
ExportOptions
|
|
7
|
+
ExportOptions,
|
|
8
|
+
DEFAULT_CONFIG_FILE
|
|
9
9
|
} from "./types"
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,12 +18,12 @@ export class ConfigFileLoader {
|
|
|
18
18
|
private configFilePath: string
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @param configFilePath - Path to config file (defaults to
|
|
21
|
+
* @param configFilePath - Path to config file (defaults to DEFAULT_CONFIG_FILE in cwd)
|
|
22
22
|
* @throws Error if path is outside project directory
|
|
23
23
|
*/
|
|
24
24
|
constructor(configFilePath?: string) {
|
|
25
25
|
this.configFilePath =
|
|
26
|
-
configFilePath || resolve(process.cwd(),
|
|
26
|
+
configFilePath || resolve(process.cwd(), DEFAULT_CONFIG_FILE)
|
|
27
27
|
this.validateConfigPath()
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -46,7 +46,7 @@ export class ConfigFileLoader {
|
|
|
46
46
|
// If file doesn't exist yet, just use the resolved path
|
|
47
47
|
realPath = absPath
|
|
48
48
|
}
|
|
49
|
-
} catch
|
|
49
|
+
} catch {
|
|
50
50
|
// If we can't resolve the path, use the original
|
|
51
51
|
realPath = absPath
|
|
52
52
|
}
|
|
@@ -92,16 +92,18 @@ export class ConfigFileLoader {
|
|
|
92
92
|
json: true
|
|
93
93
|
}) as BundlerConfigFile
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
ConfigFileLoader.validate(parsed)
|
|
96
96
|
return parsed
|
|
97
|
-
} catch (error:
|
|
97
|
+
} catch (error: unknown) {
|
|
98
|
+
const errorMessage =
|
|
99
|
+
error instanceof Error ? error.message : String(error)
|
|
98
100
|
throw new Error(
|
|
99
|
-
`Failed to load config file ${this.configFilePath}: ${
|
|
101
|
+
`Failed to load config file ${this.configFilePath}: ${errorMessage}`
|
|
100
102
|
)
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
private validate(config: BundlerConfigFile): void {
|
|
106
|
+
private static validate(config: BundlerConfigFile): void {
|
|
105
107
|
if (!config.builds || typeof config.builds !== "object") {
|
|
106
108
|
throw new Error("Config file must contain a 'builds' object")
|
|
107
109
|
}
|
|
@@ -193,7 +195,7 @@ export class ConfigFileLoader {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// Resolve bundler with precedence
|
|
196
|
-
const bundler =
|
|
198
|
+
const bundler = ConfigFileLoader.resolveBundler(
|
|
197
199
|
options.bundler,
|
|
198
200
|
build.bundler,
|
|
199
201
|
config.default_bundler,
|
|
@@ -207,7 +209,9 @@ export class ConfigFileLoader {
|
|
|
207
209
|
)
|
|
208
210
|
|
|
209
211
|
// Convert bundler_env to CLI args
|
|
210
|
-
const bundlerEnvArgs =
|
|
212
|
+
const bundlerEnvArgs = ConfigFileLoader.convertBundlerEnvToArgs(
|
|
213
|
+
build.bundler_env || {}
|
|
214
|
+
)
|
|
211
215
|
|
|
212
216
|
// Resolve and validate outputs
|
|
213
217
|
const outputs = build.outputs || []
|
|
@@ -264,7 +268,7 @@ export class ConfigFileLoader {
|
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
private resolveBundler(
|
|
271
|
+
private static resolveBundler(
|
|
268
272
|
cliFlag?: "webpack" | "rspack",
|
|
269
273
|
buildBundler?: "webpack" | "rspack",
|
|
270
274
|
defaultBundler?: "webpack" | "rspack",
|
|
@@ -293,9 +297,9 @@ export class ConfigFileLoader {
|
|
|
293
297
|
// Replace ${VAR:-default} with VAR value or default
|
|
294
298
|
expanded = expanded.replace(
|
|
295
299
|
/\$\{([^}:]+):-([^}]*)\}/g,
|
|
296
|
-
(_, varName, defaultValue) => {
|
|
300
|
+
(_: string, varName: string, defaultValue: string) => {
|
|
297
301
|
// Validate env var name to prevent regex injection
|
|
298
|
-
if (!
|
|
302
|
+
if (!ConfigFileLoader.isValidEnvVarName(varName)) {
|
|
299
303
|
console.warn(
|
|
300
304
|
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
|
301
305
|
)
|
|
@@ -306,16 +310,19 @@ export class ConfigFileLoader {
|
|
|
306
310
|
)
|
|
307
311
|
|
|
308
312
|
// Replace ${VAR} with VAR value
|
|
309
|
-
expanded = expanded.replace(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
expanded = expanded.replace(
|
|
314
|
+
/\$\{([^}:]+)\}/g,
|
|
315
|
+
(_: string, varName: string) => {
|
|
316
|
+
// Validate env var name to prevent regex injection
|
|
317
|
+
if (!ConfigFileLoader.isValidEnvVarName(varName)) {
|
|
318
|
+
console.warn(
|
|
319
|
+
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
|
|
320
|
+
)
|
|
321
|
+
return `\${${varName}}`
|
|
322
|
+
}
|
|
323
|
+
return process.env[varName] || ""
|
|
316
324
|
}
|
|
317
|
-
|
|
318
|
-
})
|
|
325
|
+
)
|
|
319
326
|
|
|
320
327
|
return expanded
|
|
321
328
|
}
|
|
@@ -326,11 +333,11 @@ export class ConfigFileLoader {
|
|
|
326
333
|
* @param name - The variable name to validate
|
|
327
334
|
* @returns true if valid, false otherwise
|
|
328
335
|
*/
|
|
329
|
-
private isValidEnvVarName(name: string): boolean {
|
|
336
|
+
private static isValidEnvVarName(name: string): boolean {
|
|
330
337
|
return /^[A-Z_][A-Z0-9_]*$/i.test(name)
|
|
331
338
|
}
|
|
332
339
|
|
|
333
|
-
private convertBundlerEnvToArgs(
|
|
340
|
+
private static convertBundlerEnvToArgs(
|
|
334
341
|
bundlerEnv: Record<string, string | boolean>
|
|
335
342
|
): string[] {
|
|
336
343
|
const args: string[] = []
|
|
@@ -357,7 +364,7 @@ export class ConfigFileLoader {
|
|
|
357
364
|
*/
|
|
358
365
|
listBuilds(): void {
|
|
359
366
|
const config = this.load()
|
|
360
|
-
const builds = config
|
|
367
|
+
const { builds } = config
|
|
361
368
|
|
|
362
369
|
console.log(`\nAvailable builds in ${this.configFilePath}:\n`)
|
|
363
370
|
|
|
@@ -56,12 +56,12 @@ export class FileWriter {
|
|
|
56
56
|
* @example
|
|
57
57
|
* // Built-in types
|
|
58
58
|
* generateFilename("webpack", "development", "client", "yaml")
|
|
59
|
-
* // => "webpack-development-client.
|
|
59
|
+
* // => "webpack-development-client.yml"
|
|
60
60
|
*
|
|
61
61
|
* @example
|
|
62
62
|
* // Custom output names
|
|
63
63
|
* generateFilename("webpack", "development", "client-modern", "yaml", "dev-hmr")
|
|
64
|
-
* // => "webpack-dev-hmr-client-modern.
|
|
64
|
+
* // => "webpack-dev-hmr-client-modern.yml"
|
|
65
65
|
*/
|
|
66
66
|
static generateFilename(
|
|
67
67
|
bundler: string,
|
|
@@ -72,7 +72,7 @@ export class FileWriter {
|
|
|
72
72
|
): string {
|
|
73
73
|
let ext: string
|
|
74
74
|
if (format === "yaml") {
|
|
75
|
-
ext = "
|
|
75
|
+
ext = "yml"
|
|
76
76
|
} else if (format === "json") {
|
|
77
77
|
ext = "json"
|
|
78
78
|
} else {
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable names that can be set by build configurations.
|
|
3
|
+
* These are the only environment variables that build configs are allowed to set.
|
|
4
|
+
* This whitelist prevents malicious configs from modifying critical system variables.
|
|
5
|
+
*/
|
|
6
|
+
export const BUILD_ENV_VARS = [
|
|
7
|
+
"NODE_ENV",
|
|
8
|
+
"RAILS_ENV",
|
|
9
|
+
"NODE_OPTIONS",
|
|
10
|
+
"BABEL_ENV",
|
|
11
|
+
"WEBPACK_SERVE",
|
|
12
|
+
"CLIENT_BUNDLE_ONLY",
|
|
13
|
+
"SERVER_BUNDLE_ONLY"
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Environment variables that must never be set by build configurations.
|
|
18
|
+
* Setting these could compromise system security or cause unexpected behavior.
|
|
19
|
+
*/
|
|
20
|
+
export const DANGEROUS_ENV_VARS = [
|
|
21
|
+
"PATH",
|
|
22
|
+
"HOME",
|
|
23
|
+
"LD_PRELOAD",
|
|
24
|
+
"LD_LIBRARY_PATH",
|
|
25
|
+
"DYLD_LIBRARY_PATH",
|
|
26
|
+
"DYLD_INSERT_LIBRARIES"
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Type predicate to check if a string is in the BUILD_ENV_VARS whitelist
|
|
31
|
+
*
|
|
32
|
+
* Note: The type assertion is necessary because TypeScript's type system cannot
|
|
33
|
+
* infer that .includes() on a readonly const array will properly narrow the type.
|
|
34
|
+
* The assertion is safe because we're only widening the type for the includes() check.
|
|
35
|
+
*/
|
|
36
|
+
export function isBuildEnvVar(
|
|
37
|
+
key: string
|
|
38
|
+
): key is (typeof BUILD_ENV_VARS)[number] {
|
|
39
|
+
return (BUILD_ENV_VARS as readonly string[]).includes(key)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type predicate to check if a string is in the DANGEROUS_ENV_VARS blacklist
|
|
44
|
+
*
|
|
45
|
+
* Note: The type assertion is necessary because TypeScript's type system cannot
|
|
46
|
+
* infer that .includes() on a readonly const array will properly narrow the type.
|
|
47
|
+
* The assertion is safe because we're only widening the type for the includes() check.
|
|
48
|
+
*/
|
|
49
|
+
export function isDangerousEnvVar(
|
|
50
|
+
key: string
|
|
51
|
+
): key is (typeof DANGEROUS_ENV_VARS)[number] {
|
|
52
|
+
return (DANGEROUS_ENV_VARS as readonly string[]).includes(key)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default directory for config exports when using --doctor or file output modes.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_EXPORT_DIR = "shakapacker-config-exports"
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default config file path for bundler build configurations.
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_CONFIG_FILE = "config/shakapacker-builds.yml"
|
|
64
|
+
|
|
1
65
|
export interface ExportOptions {
|
|
2
66
|
doctor?: boolean
|
|
3
67
|
saveDir?: string
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { relative, isAbsolute } from "path"
|
|
1
2
|
import { ConfigMetadata } from "./types"
|
|
2
3
|
import { getDocForKey } from "./configDocs"
|
|
3
|
-
import { relative, isAbsolute } from "path"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Serializes webpack/rspack config to YAML format with optional inline documentation.
|
|
@@ -8,6 +8,7 @@ import { relative, isAbsolute } from "path"
|
|
|
8
8
|
*/
|
|
9
9
|
export class YamlSerializer {
|
|
10
10
|
private annotate: boolean
|
|
11
|
+
|
|
11
12
|
private appRoot: string
|
|
12
13
|
|
|
13
14
|
constructor(options: { annotate: boolean; appRoot: string }) {
|
|
@@ -18,11 +19,11 @@ export class YamlSerializer {
|
|
|
18
19
|
/**
|
|
19
20
|
* Serialize a config object to YAML string with metadata header
|
|
20
21
|
*/
|
|
21
|
-
serialize(config:
|
|
22
|
+
serialize(config: unknown, metadata: ConfigMetadata): string {
|
|
22
23
|
const output: string[] = []
|
|
23
24
|
|
|
24
25
|
// Add metadata header
|
|
25
|
-
output.push(
|
|
26
|
+
output.push(YamlSerializer.createHeader(metadata))
|
|
26
27
|
output.push("")
|
|
27
28
|
|
|
28
29
|
// Serialize the config
|
|
@@ -31,9 +32,9 @@ export class YamlSerializer {
|
|
|
31
32
|
return output.join("\n")
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
private createHeader(metadata: ConfigMetadata): string {
|
|
35
|
+
private static createHeader(metadata: ConfigMetadata): string {
|
|
35
36
|
const lines: string[] = []
|
|
36
|
-
lines.push(
|
|
37
|
+
lines.push(`# ${"=".repeat(77)}`)
|
|
37
38
|
lines.push("# Webpack/Rspack Configuration Export")
|
|
38
39
|
lines.push(`# Generated: ${metadata.exportedAt}`)
|
|
39
40
|
lines.push(`# Environment: ${metadata.environment}`)
|
|
@@ -42,11 +43,15 @@ export class YamlSerializer {
|
|
|
42
43
|
if (metadata.configCount > 1) {
|
|
43
44
|
lines.push(`# Total Configs: ${metadata.configCount}`)
|
|
44
45
|
}
|
|
45
|
-
lines.push(
|
|
46
|
+
lines.push(`# ${"=".repeat(77)}`)
|
|
46
47
|
return lines.join("\n")
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
private serializeValue(
|
|
50
|
+
private serializeValue(
|
|
51
|
+
value: unknown,
|
|
52
|
+
indent: number,
|
|
53
|
+
keyPath: string
|
|
54
|
+
): string {
|
|
50
55
|
if (value === null || value === undefined) {
|
|
51
56
|
return "null"
|
|
52
57
|
}
|
|
@@ -64,11 +69,28 @@ export class YamlSerializer {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
if (typeof value === "function") {
|
|
67
|
-
return this.serializeFunction(
|
|
72
|
+
return this.serializeFunction(
|
|
73
|
+
value as (...args: unknown[]) => unknown,
|
|
74
|
+
indent
|
|
75
|
+
)
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
if (value instanceof RegExp) {
|
|
71
|
-
|
|
79
|
+
// Extract pattern without surrounding slashes: "/pattern/flags" -> "pattern"
|
|
80
|
+
// Include flags as inline comment when present for semantic clarity
|
|
81
|
+
const regexStr = value.toString()
|
|
82
|
+
const lastSlash = regexStr.lastIndexOf("/")
|
|
83
|
+
const pattern = regexStr.slice(1, lastSlash)
|
|
84
|
+
const flags = regexStr.slice(lastSlash + 1)
|
|
85
|
+
|
|
86
|
+
const serializedPattern = this.serializeString(pattern, indent)
|
|
87
|
+
|
|
88
|
+
// Add flags as inline comment if present (e.g., "pattern" # flags: gi)
|
|
89
|
+
if (flags) {
|
|
90
|
+
return `${serializedPattern} # flags: ${flags}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return serializedPattern
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
if (Array.isArray(value)) {
|
|
@@ -76,10 +98,20 @@ export class YamlSerializer {
|
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
if (typeof value === "object") {
|
|
79
|
-
return this.serializeObject(
|
|
101
|
+
return this.serializeObject(
|
|
102
|
+
value as Record<string, unknown>,
|
|
103
|
+
indent,
|
|
104
|
+
keyPath
|
|
105
|
+
)
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
|
|
108
|
+
// Handle remaining types explicitly
|
|
109
|
+
if (typeof value === "symbol") return value.toString()
|
|
110
|
+
if (typeof value === "bigint") return value.toString()
|
|
111
|
+
|
|
112
|
+
// All remaining types are primitives (string, number, boolean, null, undefined)
|
|
113
|
+
// that String() handles safely - cast to exclude objects since we've already handled them
|
|
114
|
+
return String(value as string | number | boolean | null | undefined)
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
private serializeString(str: string, indent: number = 0): string {
|
|
@@ -90,15 +122,28 @@ export class YamlSerializer {
|
|
|
90
122
|
if (cleaned.includes("\n")) {
|
|
91
123
|
const lines = cleaned.split("\n")
|
|
92
124
|
const lineIndent = " ".repeat(indent + 2)
|
|
93
|
-
return
|
|
125
|
+
return `|\n${lines.map((line) => lineIndent + line).join("\n")}`
|
|
94
126
|
}
|
|
95
127
|
|
|
96
|
-
// Escape strings that need quoting
|
|
128
|
+
// Escape strings that need quoting in YAML
|
|
129
|
+
// YAML has many special characters that can cause parsing errors:
|
|
130
|
+
// : # ' " (basic delimiters)
|
|
131
|
+
// [ ] { } (flow collections)
|
|
132
|
+
// * & ! @ ` (special constructs: aliases, anchors, tags)
|
|
97
133
|
if (
|
|
98
134
|
cleaned.includes(":") ||
|
|
99
135
|
cleaned.includes("#") ||
|
|
100
136
|
cleaned.includes("'") ||
|
|
101
137
|
cleaned.includes('"') ||
|
|
138
|
+
cleaned.includes("[") ||
|
|
139
|
+
cleaned.includes("]") ||
|
|
140
|
+
cleaned.includes("{") ||
|
|
141
|
+
cleaned.includes("}") ||
|
|
142
|
+
cleaned.includes("*") ||
|
|
143
|
+
cleaned.includes("&") ||
|
|
144
|
+
cleaned.includes("!") ||
|
|
145
|
+
cleaned.includes("@") ||
|
|
146
|
+
cleaned.includes("`") ||
|
|
102
147
|
cleaned.startsWith(" ") ||
|
|
103
148
|
cleaned.endsWith(" ")
|
|
104
149
|
) {
|
|
@@ -109,7 +154,10 @@ export class YamlSerializer {
|
|
|
109
154
|
return cleaned
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
private serializeFunction(
|
|
157
|
+
private serializeFunction(
|
|
158
|
+
fn: (...args: unknown[]) => unknown,
|
|
159
|
+
indent: number = 0
|
|
160
|
+
): string {
|
|
113
161
|
// Get function source code
|
|
114
162
|
const source = fn.toString()
|
|
115
163
|
|
|
@@ -122,21 +170,24 @@ export class YamlSerializer {
|
|
|
122
170
|
const displayLines = truncated ? lines.slice(0, maxLines) : lines
|
|
123
171
|
|
|
124
172
|
// Clean up indentation while preserving structure
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
173
|
+
const indentLevels = displayLines
|
|
174
|
+
.filter((l) => l.trim().length > 0)
|
|
175
|
+
.map((l) => l.match(/^\s*/)?.[0].length || 0)
|
|
176
|
+
const minIndent = indentLevels.length > 0 ? Math.min(...indentLevels) : 0
|
|
130
177
|
|
|
131
178
|
const formatted =
|
|
132
179
|
displayLines.map((line) => line.substring(minIndent)).join("\n") +
|
|
133
180
|
(truncated ? "\n..." : "")
|
|
134
181
|
|
|
135
|
-
// Use serializeString to properly handle multiline
|
|
136
|
-
return this.serializeString(formatted)
|
|
182
|
+
// Use serializeString to properly handle multiline with correct indentation
|
|
183
|
+
return this.serializeString(formatted, indent)
|
|
137
184
|
}
|
|
138
185
|
|
|
139
|
-
private serializeArray(
|
|
186
|
+
private serializeArray(
|
|
187
|
+
arr: unknown[],
|
|
188
|
+
indent: number,
|
|
189
|
+
keyPath: string
|
|
190
|
+
): string {
|
|
140
191
|
if (arr.length === 0) {
|
|
141
192
|
return "[]"
|
|
142
193
|
}
|
|
@@ -147,6 +198,22 @@ export class YamlSerializer {
|
|
|
147
198
|
|
|
148
199
|
arr.forEach((item, index) => {
|
|
149
200
|
const itemPath = `${keyPath}[${index}]`
|
|
201
|
+
|
|
202
|
+
// Check if this is a plugin object and add its name as a comment
|
|
203
|
+
const pluginName = YamlSerializer.getConstructorName(item)
|
|
204
|
+
const isPlugin = pluginName && /(^|\.)plugins\[\d+\]/.test(itemPath)
|
|
205
|
+
const isEmpty =
|
|
206
|
+
typeof item === "object" &&
|
|
207
|
+
item !== null &&
|
|
208
|
+
!Array.isArray(item) &&
|
|
209
|
+
Object.keys(item).length === 0
|
|
210
|
+
|
|
211
|
+
// For non-empty plugins, add comment before the plugin
|
|
212
|
+
// For empty plugins, the name will be shown inline
|
|
213
|
+
if (isPlugin && !isEmpty) {
|
|
214
|
+
lines.push(`${itemIndent}# ${pluginName}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
150
217
|
const serialized = this.serializeValue(item, indent + 4, itemPath)
|
|
151
218
|
|
|
152
219
|
// Add documentation for array items if available
|
|
@@ -164,11 +231,11 @@ export class YamlSerializer {
|
|
|
164
231
|
.split("\n")
|
|
165
232
|
.filter((line: string) => line.trim().length > 0)
|
|
166
233
|
// Compute minimum leading whitespace to preserve relative indentation
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
|
170
|
-
)
|
|
234
|
+
const indentLevels = nonEmptyLines.map(
|
|
235
|
+
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
|
171
236
|
)
|
|
237
|
+
const minIndent =
|
|
238
|
+
indentLevels.length > 0 ? Math.min(...indentLevels) : 0
|
|
172
239
|
nonEmptyLines.forEach((line: string) => {
|
|
173
240
|
// Remove only the common indent, preserving relative indentation
|
|
174
241
|
lines.push(contentIndent + line.substring(minIndent))
|
|
@@ -180,11 +247,11 @@ export class YamlSerializer {
|
|
|
180
247
|
.split("\n")
|
|
181
248
|
.filter((line: string) => line.trim().length > 0)
|
|
182
249
|
// Compute minimum leading whitespace to preserve relative indentation
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
|
186
|
-
)
|
|
250
|
+
const indentLevels = nonEmptyLines.map(
|
|
251
|
+
(line: string) => line.match(/^\s*/)?.[0].length || 0
|
|
187
252
|
)
|
|
253
|
+
const minIndent =
|
|
254
|
+
indentLevels.length > 0 ? Math.min(...indentLevels) : 0
|
|
188
255
|
nonEmptyLines.forEach((line: string) => {
|
|
189
256
|
// Remove only the common indent, preserving relative indentation
|
|
190
257
|
lines.push(contentIndent + line.substring(minIndent))
|
|
@@ -195,12 +262,22 @@ export class YamlSerializer {
|
|
|
195
262
|
}
|
|
196
263
|
})
|
|
197
264
|
|
|
198
|
-
return
|
|
265
|
+
return `\n${lines.join("\n")}`
|
|
199
266
|
}
|
|
200
267
|
|
|
201
|
-
private serializeObject(
|
|
202
|
-
|
|
268
|
+
private serializeObject(
|
|
269
|
+
obj: Record<string, unknown>,
|
|
270
|
+
indent: number,
|
|
271
|
+
keyPath: string
|
|
272
|
+
): string {
|
|
273
|
+
const keys = Object.keys(obj).sort()
|
|
274
|
+
const constructorName = YamlSerializer.getConstructorName(obj)
|
|
275
|
+
|
|
276
|
+
// For empty objects, show constructor name if available
|
|
203
277
|
if (keys.length === 0) {
|
|
278
|
+
if (constructorName) {
|
|
279
|
+
return `{} # ${constructorName}`
|
|
280
|
+
}
|
|
204
281
|
return "{}"
|
|
205
282
|
}
|
|
206
283
|
|
|
@@ -226,6 +303,12 @@ export class YamlSerializer {
|
|
|
226
303
|
for (const line of value.split("\n")) {
|
|
227
304
|
lines.push(`${valueIndent}${line}`)
|
|
228
305
|
}
|
|
306
|
+
} else if (value instanceof RegExp || typeof value === "function") {
|
|
307
|
+
// Handle RegExp and functions explicitly before the generic object check
|
|
308
|
+
// to prevent them from being treated as empty objects (RegExp/functions
|
|
309
|
+
// have no enumerable keys but should serialize as their string representation)
|
|
310
|
+
const serialized = this.serializeValue(value, indent + 2, fullKeyPath)
|
|
311
|
+
lines.push(`${keyIndent}${key}: ${serialized}`)
|
|
229
312
|
} else if (
|
|
230
313
|
typeof value === "object" &&
|
|
231
314
|
value !== null &&
|
|
@@ -236,7 +319,7 @@ export class YamlSerializer {
|
|
|
236
319
|
} else {
|
|
237
320
|
lines.push(`${keyIndent}${key}:`)
|
|
238
321
|
const nestedLines = this.serializeObject(
|
|
239
|
-
value,
|
|
322
|
+
value as Record<string, unknown>,
|
|
240
323
|
indent + 2,
|
|
241
324
|
fullKeyPath
|
|
242
325
|
)
|
|
@@ -260,7 +343,6 @@ export class YamlSerializer {
|
|
|
260
343
|
}
|
|
261
344
|
|
|
262
345
|
private makePathRelative(str: string): string {
|
|
263
|
-
if (typeof str !== "string") return str
|
|
264
346
|
if (!isAbsolute(str)) return str
|
|
265
347
|
|
|
266
348
|
// Convert absolute paths to relative paths using path.relative
|
|
@@ -275,6 +357,35 @@ export class YamlSerializer {
|
|
|
275
357
|
return str
|
|
276
358
|
}
|
|
277
359
|
|
|
278
|
-
return
|
|
360
|
+
return `./${rel}`
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Extracts the constructor name from an object
|
|
365
|
+
* Returns null for plain objects (Object constructor) or objects without prototypes
|
|
366
|
+
*/
|
|
367
|
+
private static getConstructorName(obj: unknown): string | null {
|
|
368
|
+
if (!obj || typeof obj !== "object") return null
|
|
369
|
+
if (Array.isArray(obj)) return null
|
|
370
|
+
|
|
371
|
+
// Use Object.getPrototypeOf for safer access to constructor
|
|
372
|
+
// This handles Object.create(null) and unusual prototypes correctly
|
|
373
|
+
try {
|
|
374
|
+
const proto = Object.getPrototypeOf(obj) as {
|
|
375
|
+
constructor?: { name?: string }
|
|
376
|
+
} | null
|
|
377
|
+
if (!proto || proto === Object.prototype) return null
|
|
378
|
+
|
|
379
|
+
const { constructor } = proto
|
|
380
|
+
if (!constructor || typeof constructor !== "function") return null
|
|
381
|
+
|
|
382
|
+
const constructorName = constructor.name
|
|
383
|
+
if (!constructorName || constructorName === "Object") return null
|
|
384
|
+
|
|
385
|
+
return constructorName
|
|
386
|
+
} catch {
|
|
387
|
+
// Handle frozen objects or other edge cases
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
279
390
|
}
|
|
280
391
|
}
|
data/package/dev_server.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// These are the raw shakapacker dev server config settings from the YML file with ENV overrides applied.
|
|
2
|
+
import { DevServerConfig } from "./types"
|
|
3
|
+
|
|
2
4
|
const { isBoolean } = require("./utils/helpers")
|
|
3
5
|
const config = require("./config")
|
|
4
|
-
import { DevServerConfig } from "./types"
|
|
5
6
|
|
|
6
7
|
const envFetch = (key: string): string | boolean | undefined => {
|
|
7
8
|
const value = process.env[key]
|
data/package/env.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { load } from "js-yaml"
|
|
2
2
|
import { readFileSync } from "fs"
|
|
3
|
+
|
|
3
4
|
const defaultConfigPath = require("./utils/defaultConfigPath")
|
|
4
5
|
const configPath = require("./utils/configPath")
|
|
5
6
|
const { isFileNotFoundError } = require("./utils/errorHelpers")
|
|
@@ -76,7 +77,6 @@ const validatedRailsEnv =
|
|
|
76
77
|
initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
|
|
77
78
|
|
|
78
79
|
if (initialRailsEnv && validatedRailsEnv !== initialRailsEnv) {
|
|
79
|
-
/* eslint no-console:0 */
|
|
80
80
|
console.warn(
|
|
81
81
|
`[SHAKAPACKER WARNING] Environment '${initialRailsEnv}' not found in the configuration.\n` +
|
|
82
82
|
`Using '${DEFAULT}' configuration as a fallback.`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint global-require: 0 */
|
|
2
2
|
/* eslint import/no-dynamic-require: 0 */
|
|
3
3
|
|
|
4
|
+
import { Dirent } from "fs"
|
|
5
|
+
import type { Configuration, Entry } from "webpack"
|
|
6
|
+
|
|
4
7
|
const { basename, dirname, join, relative, resolve } = require("path")
|
|
5
8
|
const { existsSync, readdirSync } = require("fs")
|
|
6
|
-
import { Dirent } from "fs"
|
|
7
9
|
const extname = require("path-complete-extname")
|
|
8
|
-
// @ts-ignore: webpack is an optional peer dependency (using type-only import)
|
|
9
|
-
import type { Configuration, Entry } from "webpack"
|
|
10
10
|
const config = require("../config")
|
|
11
11
|
const { isProduction } = require("../env")
|
|
12
12
|
|
|
@@ -73,7 +73,7 @@ const getEntryObject = (): Entry => {
|
|
|
73
73
|
const previousPaths = entries[name]
|
|
74
74
|
if (previousPaths) {
|
|
75
75
|
const pathArray = Array.isArray(previousPaths)
|
|
76
|
-
?
|
|
76
|
+
? previousPaths
|
|
77
77
|
: [previousPaths as string]
|
|
78
78
|
pathArray.push(assetPath)
|
|
79
79
|
entries[name] = pathArray
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
* @module environments/development
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { merge } = require("webpack-merge")
|
|
7
|
-
const config = require("../config")
|
|
8
|
-
const baseConfig = require("./base")
|
|
9
|
-
const webpackDevServerConfig = require("../webpackDevServerConfig")
|
|
10
|
-
const { runningWebpackDevServer } = require("../env")
|
|
11
|
-
const { moduleExists } = require("../utils/helpers")
|
|
12
6
|
import type {
|
|
13
7
|
WebpackConfigWithDevServer,
|
|
14
8
|
RspackConfigWithDevServer,
|
|
@@ -16,6 +10,13 @@ import type {
|
|
|
16
10
|
ReactRefreshRspackPlugin
|
|
17
11
|
} from "./types"
|
|
18
12
|
|
|
13
|
+
const { merge } = require("webpack-merge")
|
|
14
|
+
const config = require("../config")
|
|
15
|
+
const baseConfig = require("./base")
|
|
16
|
+
const webpackDevServerConfig = require("../webpackDevServerConfig")
|
|
17
|
+
const { runningWebpackDevServer } = require("../env")
|
|
18
|
+
const { moduleExists } = require("../utils/helpers")
|
|
19
|
+
|
|
19
20
|
/**
|
|
20
21
|
* Base development configuration shared between webpack and rspack
|
|
21
22
|
*/
|