shakapacker 9.1.0 → 9.2.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.
@@ -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,92 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs"
2
+ import { resolve, dirname, relative, isAbsolute, basename } from "path"
3
+ import { tmpdir } from "os"
4
+ import { FileOutput } from "./types"
5
+
6
+ /**
7
+ * Handles writing config exports to files.
8
+ * Supports single file output or multiple files (one per config).
9
+ */
10
+ export class FileWriter {
11
+ /**
12
+ * Write multiple config files (one per config in array)
13
+ */
14
+ writeMultipleFiles(outputs: FileOutput[], targetDir: string): void {
15
+ // Ensure directory exists
16
+ this.ensureDirectory(targetDir)
17
+
18
+ // Write each file
19
+ outputs.forEach((output) => {
20
+ const safeName = basename(output.filename)
21
+ const filePath = resolve(targetDir, safeName)
22
+ this.validateOutputPath(filePath)
23
+ this.writeFile(filePath, output.content)
24
+ console.log(`[Config Exporter] Created: ${filePath}`)
25
+ })
26
+
27
+ console.log(
28
+ `[Config Exporter] Exported ${outputs.length} config file(s) to ${targetDir}`
29
+ )
30
+ }
31
+
32
+ /**
33
+ * Write a single file
34
+ */
35
+ writeSingleFile(filePath: string, content: string, quiet = false): void {
36
+ // Ensure parent directory exists
37
+ const dir = dirname(filePath)
38
+ this.ensureDirectory(dir)
39
+
40
+ this.validateOutputPath(filePath)
41
+ this.writeFile(filePath, content)
42
+ if (!quiet && process.env.VERBOSE) {
43
+ console.log(`[Config Exporter] Config exported to: ${filePath}`)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Generate filename for a config export
49
+ * Format: {bundler}-{env}-{type}.{ext}
50
+ * Examples:
51
+ * webpack-development-client.yaml
52
+ * rspack-production-server.yaml
53
+ * webpack-test-all.json
54
+ */
55
+ generateFilename(
56
+ bundler: string,
57
+ env: string,
58
+ configType: "client" | "server" | "all",
59
+ format: "yaml" | "json" | "inspect"
60
+ ): string {
61
+ const ext = format === "yaml" ? "yaml" : format === "json" ? "json" : "txt"
62
+ return `${bundler}-${env}-${configType}.${ext}`
63
+ }
64
+
65
+ private writeFile(filePath: string, content: string): void {
66
+ writeFileSync(filePath, content, "utf8")
67
+ }
68
+
69
+ private ensureDirectory(dir: string): void {
70
+ if (!existsSync(dir)) {
71
+ mkdirSync(dir, { recursive: true })
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Validate output path and warn if writing outside cwd
77
+ */
78
+ validateOutputPath(outputPath: string): void {
79
+ const absPath = resolve(outputPath)
80
+ const cwd = process.cwd()
81
+
82
+ const isWithin = (base: string, target: string) => {
83
+ const rel = relative(base, target)
84
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))
85
+ }
86
+ if (!isWithin(cwd, absPath) && !isWithin(tmpdir(), absPath)) {
87
+ console.warn(
88
+ `[Config Exporter] Warning: Writing to ${absPath} which is outside current directory (${cwd}) or temp (${tmpdir()})`
89
+ )
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,5 @@
1
+ export { run } from "./cli"
2
+ export type { ExportOptions, ConfigMetadata, FileOutput } from "./types"
3
+ export { YamlSerializer } from "./yamlSerializer"
4
+ export { FileWriter } from "./fileWriter"
5
+ export { getDocForKey } from "./configDocs"
@@ -0,0 +1,36 @@
1
+ export interface ExportOptions {
2
+ doctor?: boolean
3
+ save?: boolean
4
+ saveDir?: string
5
+ bundler?: "webpack" | "rspack"
6
+ env?: "development" | "production" | "test"
7
+ clientOnly?: boolean
8
+ serverOnly?: boolean
9
+ output?: string
10
+ format?: "yaml" | "json" | "inspect"
11
+ annotate?: boolean
12
+ verbose?: boolean
13
+ depth?: number | null
14
+ help?: boolean
15
+ }
16
+
17
+ export interface ConfigMetadata {
18
+ exportedAt: string
19
+ bundler: string
20
+ environment: string
21
+ configFile: string
22
+ configType: "client" | "server" | "all"
23
+ configCount: number
24
+ environmentVariables: {
25
+ NODE_ENV?: string
26
+ RAILS_ENV?: string
27
+ CLIENT_BUNDLE_ONLY?: string
28
+ SERVER_BUNDLE_ONLY?: string
29
+ }
30
+ }
31
+
32
+ export interface FileOutput {
33
+ filename: string
34
+ content: string
35
+ metadata: ConfigMetadata
36
+ }
@@ -0,0 +1,266 @@
1
+ import { ConfigMetadata } from "./types"
2
+ import { getDocForKey } from "./configDocs"
3
+ import { relative, isAbsolute } from "path"
4
+
5
+ /**
6
+ * Serializes webpack/rspack config to YAML format with optional inline documentation.
7
+ * Handles functions, RegExp, and special objects that don't serialize well to standard YAML.
8
+ */
9
+ export class YamlSerializer {
10
+ private annotate: boolean
11
+ private appRoot: string
12
+
13
+ constructor(options: { annotate: boolean; appRoot: string }) {
14
+ this.annotate = options.annotate
15
+ this.appRoot = options.appRoot
16
+ }
17
+
18
+ /**
19
+ * Serialize a config object to YAML string with metadata header
20
+ */
21
+ serialize(config: any, metadata: ConfigMetadata): string {
22
+ const output: string[] = []
23
+
24
+ // Add metadata header
25
+ output.push(this.createHeader(metadata))
26
+ output.push("")
27
+
28
+ // Serialize the config
29
+ output.push(this.serializeValue(config, 0, ""))
30
+
31
+ return output.join("\n")
32
+ }
33
+
34
+ private createHeader(metadata: ConfigMetadata): string {
35
+ const lines: string[] = []
36
+ lines.push("# " + "=".repeat(77))
37
+ lines.push("# Webpack/Rspack Configuration Export")
38
+ lines.push(`# Generated: ${metadata.exportedAt}`)
39
+ lines.push(`# Environment: ${metadata.environment}`)
40
+ lines.push(`# Bundler: ${metadata.bundler}`)
41
+ lines.push(`# Config Type: ${metadata.configType}`)
42
+ if (metadata.configCount > 1) {
43
+ lines.push(`# Total Configs: ${metadata.configCount}`)
44
+ }
45
+ lines.push("# " + "=".repeat(77))
46
+ return lines.join("\n")
47
+ }
48
+
49
+ private serializeValue(value: any, indent: number, keyPath: string): string {
50
+ if (value === null || value === undefined) {
51
+ return "null"
52
+ }
53
+
54
+ if (typeof value === "boolean") {
55
+ return value.toString()
56
+ }
57
+
58
+ if (typeof value === "number") {
59
+ return value.toString()
60
+ }
61
+
62
+ if (typeof value === "string") {
63
+ return this.serializeString(value, indent)
64
+ }
65
+
66
+ if (typeof value === "function") {
67
+ return this.serializeFunction(value)
68
+ }
69
+
70
+ if (value instanceof RegExp) {
71
+ return this.serializeString(value.toString())
72
+ }
73
+
74
+ if (Array.isArray(value)) {
75
+ return this.serializeArray(value, indent, keyPath)
76
+ }
77
+
78
+ if (typeof value === "object") {
79
+ return this.serializeObject(value, indent, keyPath)
80
+ }
81
+
82
+ return String(value)
83
+ }
84
+
85
+ private serializeString(str: string, indent: number = 0): string {
86
+ // Make absolute paths relative for cleaner output
87
+ const cleaned = this.makePathRelative(str)
88
+
89
+ // Handle multiline strings
90
+ if (cleaned.includes("\n")) {
91
+ const lines = cleaned.split("\n")
92
+ const lineIndent = " ".repeat(indent + 2)
93
+ return "|\n" + lines.map((line) => lineIndent + line).join("\n")
94
+ }
95
+
96
+ // Escape strings that need quoting
97
+ if (
98
+ cleaned.includes(":") ||
99
+ cleaned.includes("#") ||
100
+ cleaned.includes("'") ||
101
+ cleaned.includes('"') ||
102
+ cleaned.startsWith(" ") ||
103
+ cleaned.endsWith(" ")
104
+ ) {
105
+ // Escape backslashes first, then quotes to avoid double-escaping
106
+ return `"${cleaned.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
107
+ }
108
+
109
+ return cleaned
110
+ }
111
+
112
+ private serializeFunction(fn: Function): string {
113
+ // Get function source code
114
+ const source = fn.toString()
115
+
116
+ // Pretty-print function: maintain readable formatting
117
+ const lines = source.split("\n")
118
+
119
+ // For very long functions, truncate
120
+ const maxLines = 50
121
+ const truncated = lines.length > maxLines
122
+ const displayLines = truncated ? lines.slice(0, maxLines) : lines
123
+
124
+ // Clean up indentation while preserving structure
125
+ const minIndent = Math.min(
126
+ ...displayLines
127
+ .filter((l) => l.trim().length > 0)
128
+ .map((l) => l.match(/^\s*/)?.[0].length || 0)
129
+ )
130
+
131
+ const formatted =
132
+ displayLines.map((line) => line.substring(minIndent)).join("\n") +
133
+ (truncated ? "\n..." : "")
134
+
135
+ // Use serializeString to properly handle multiline
136
+ return this.serializeString(formatted)
137
+ }
138
+
139
+ private serializeArray(arr: any[], indent: number, keyPath: string): string {
140
+ if (arr.length === 0) {
141
+ return "[]"
142
+ }
143
+
144
+ const lines: string[] = []
145
+ const itemIndent = " ".repeat(indent + 2)
146
+ const contentIndent = " ".repeat(indent + 4)
147
+
148
+ arr.forEach((item, index) => {
149
+ const itemPath = `${keyPath}[${index}]`
150
+ const serialized = this.serializeValue(item, indent + 4, itemPath)
151
+
152
+ // Add documentation for array items if available
153
+ if (this.annotate) {
154
+ const doc = getDocForKey(itemPath)
155
+ if (doc) {
156
+ lines.push(`${itemIndent}# ${doc}`)
157
+ }
158
+ }
159
+
160
+ if (typeof item === "object" && !Array.isArray(item) && item !== null) {
161
+ // For objects in arrays, emit marker on its own line and indent content
162
+ lines.push(`${itemIndent}-`)
163
+ serialized
164
+ .split("\n")
165
+ .filter((line: string) => line.trim().length > 0)
166
+ .forEach((line: string) => {
167
+ lines.push(contentIndent + line)
168
+ })
169
+ } else if (serialized.includes("\n")) {
170
+ // For multiline values, emit marker on its own line and indent content
171
+ lines.push(`${itemIndent}-`)
172
+ serialized
173
+ .split("\n")
174
+ .filter((line: string) => line.trim().length > 0)
175
+ .forEach((line: string) => {
176
+ lines.push(contentIndent + line)
177
+ })
178
+ } else {
179
+ // For simple values, keep on same line
180
+ lines.push(`${itemIndent}- ${serialized}`)
181
+ }
182
+ })
183
+
184
+ return "\n" + lines.join("\n")
185
+ }
186
+
187
+ private serializeObject(obj: any, indent: number, keyPath: string): string {
188
+ const keys = Object.keys(obj)
189
+ if (keys.length === 0) {
190
+ return "{}"
191
+ }
192
+
193
+ const lines: string[] = []
194
+ const keyIndent = " ".repeat(indent)
195
+ const valueIndent = " ".repeat(indent + 2)
196
+
197
+ keys.forEach((key) => {
198
+ const value = obj[key]
199
+ const fullKeyPath = keyPath ? `${keyPath}.${key}` : key
200
+
201
+ // Add documentation comment if available and annotation is enabled
202
+ if (this.annotate) {
203
+ const doc = getDocForKey(fullKeyPath)
204
+ if (doc) {
205
+ lines.push(`${keyIndent}# ${doc}`)
206
+ }
207
+ }
208
+
209
+ // Handle multiline strings specially with block scalar
210
+ if (typeof value === "string" && value.includes("\n")) {
211
+ lines.push(`${keyIndent}${key}: |`)
212
+ for (const line of value.split("\n")) {
213
+ lines.push(`${valueIndent}${line}`)
214
+ }
215
+ } else if (
216
+ typeof value === "object" &&
217
+ value !== null &&
218
+ !Array.isArray(value)
219
+ ) {
220
+ if (Object.keys(value).length === 0) {
221
+ lines.push(`${keyIndent}${key}: {}`)
222
+ } else {
223
+ lines.push(`${keyIndent}${key}:`)
224
+ const nestedLines = this.serializeObject(
225
+ value,
226
+ indent + 2,
227
+ fullKeyPath
228
+ )
229
+ lines.push(nestedLines)
230
+ }
231
+ } else if (Array.isArray(value)) {
232
+ if (value.length === 0) {
233
+ lines.push(`${keyIndent}${key}: []`)
234
+ } else {
235
+ lines.push(`${keyIndent}${key}:`)
236
+ const arrayLines = this.serializeArray(value, indent + 2, fullKeyPath)
237
+ lines.push(arrayLines)
238
+ }
239
+ } else {
240
+ const serialized = this.serializeValue(value, indent + 2, fullKeyPath)
241
+ lines.push(`${keyIndent}${key}: ${serialized}`)
242
+ }
243
+ })
244
+
245
+ return lines.join("\n")
246
+ }
247
+
248
+ private makePathRelative(str: string): string {
249
+ if (typeof str !== "string") return str
250
+ if (!isAbsolute(str)) return str
251
+
252
+ // Convert absolute paths to relative paths using path.relative
253
+ const rel = relative(this.appRoot, str)
254
+
255
+ if (rel === "") {
256
+ return "."
257
+ }
258
+
259
+ // If path is outside appRoot or already absolute, keep original
260
+ if (rel.startsWith("..") || isAbsolute(rel)) {
261
+ return str
262
+ }
263
+
264
+ return "./" + rel
265
+ }
266
+ }
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "shakapacker",
9
- "version": "9.1.0",
9
+ "version": "9.2.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "js-yaml": "^4.1.0",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "description": "Use webpack to manage app-like JavaScript modules in Rails",
5
5
  "homepage": "https://github.com/shakacode/shakapacker",
6
6
  "bugs": {
@@ -21,6 +21,7 @@
21
21
  "./rspack": "./package/rspack/index.js",
22
22
  "./swc": "./package/swc/index.js",
23
23
  "./esbuild": "./package/esbuild/index.js",
24
+ "./configExporter": "./package/configExporter/index.js",
24
25
  "./package.json": "./package.json",
25
26
  "./package/babel/preset.js": "./package/babel/preset.js",
26
27
  "./package/*": "./package/*"