shakapacker 9.3.0.beta.7 → 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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -109
  3. data/Gemfile.lock +1 -1
  4. data/README.md +53 -2
  5. data/docs/configuration.md +28 -0
  6. data/docs/rspack_migration_guide.md +238 -2
  7. data/docs/troubleshooting.md +21 -21
  8. data/eslint.config.fast.js +8 -0
  9. data/eslint.config.js +47 -10
  10. data/knip.ts +8 -1
  11. data/lib/install/config/shakapacker.yml +6 -6
  12. data/lib/shakapacker/configuration.rb +227 -4
  13. data/lib/shakapacker/dev_server.rb +88 -1
  14. data/lib/shakapacker/doctor.rb +4 -4
  15. data/lib/shakapacker/instance.rb +85 -1
  16. data/lib/shakapacker/manifest.rb +85 -11
  17. data/lib/shakapacker/version.rb +1 -1
  18. data/lib/shakapacker.rb +143 -3
  19. data/lib/tasks/shakapacker/doctor.rake +1 -1
  20. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  21. data/package/config.ts +0 -1
  22. data/package/configExporter/buildValidator.ts +53 -29
  23. data/package/configExporter/cli.ts +81 -56
  24. data/package/configExporter/configFile.ts +33 -26
  25. data/package/configExporter/types.ts +64 -0
  26. data/package/configExporter/yamlSerializer.ts +118 -43
  27. data/package/dev_server.ts +2 -1
  28. data/package/env.ts +1 -1
  29. data/package/environments/base.ts +4 -4
  30. data/package/environments/development.ts +7 -6
  31. data/package/environments/production.ts +6 -7
  32. data/package/environments/test.ts +2 -1
  33. data/package/index.ts +28 -4
  34. data/package/loaders.d.ts +2 -2
  35. data/package/optimization/webpack.ts +29 -31
  36. data/package/rspack/index.ts +2 -1
  37. data/package/rules/file.ts +1 -0
  38. data/package/rules/jscommon.ts +1 -0
  39. data/package/utils/helpers.ts +0 -1
  40. data/package/utils/pathValidation.ts +68 -7
  41. data/package/utils/requireOrError.ts +10 -2
  42. data/package/utils/typeGuards.ts +43 -46
  43. data/package/webpack-types.d.ts +2 -2
  44. data/package/webpackDevServerConfig.ts +1 -0
  45. data/package.json +2 -3
  46. data/test/package/configExporter/cli.test.js +440 -0
  47. data/test/package/configExporter/types.test.js +163 -0
  48. data/test/package/configExporter.test.js +264 -0
  49. data/test/package/yamlSerializer.test.js +204 -0
  50. data/test/typescript/pathValidation.test.js +44 -0
  51. data/test/typescript/requireOrError.test.js +49 -0
  52. data/yarn.lock +0 -32
  53. metadata +11 -5
  54. data/.eslintrc.fast.js +0 -40
  55. data/.eslintrc.js +0 -84
@@ -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: any, metadata: ConfigMetadata): string {
22
+ serialize(config: unknown, metadata: ConfigMetadata): string {
22
23
  const output: string[] = []
23
24
 
24
25
  // Add metadata header
25
- output.push(this.createHeader(metadata))
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("# " + "=".repeat(77))
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("# " + "=".repeat(77))
46
+ lines.push(`# ${"=".repeat(77)}`)
46
47
  return lines.join("\n")
47
48
  }
48
49
 
49
- private serializeValue(value: any, indent: number, keyPath: string): string {
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(value)
72
+ return this.serializeFunction(
73
+ value as (...args: unknown[]) => unknown,
74
+ indent
75
+ )
68
76
  }
69
77
 
70
78
  if (value instanceof RegExp) {
71
- return this.serializeString(value.toString())
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(value, indent, keyPath)
101
+ return this.serializeObject(
102
+ value as Record<string, unknown>,
103
+ indent,
104
+ keyPath
105
+ )
80
106
  }
81
107
 
82
- return String(value)
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 "|\n" + lines.map((line) => lineIndent + line).join("\n")
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(fn: Function): string {
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 minIndent = Math.min(
126
- ...displayLines
127
- .filter((l) => l.trim().length > 0)
128
- .map((l) => l.match(/^\s*/)?.[0].length || 0)
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(arr: any[], indent: number, keyPath: string): string {
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
  }
@@ -149,7 +200,7 @@ export class YamlSerializer {
149
200
  const itemPath = `${keyPath}[${index}]`
150
201
 
151
202
  // Check if this is a plugin object and add its name as a comment
152
- const pluginName = this.getConstructorName(item)
203
+ const pluginName = YamlSerializer.getConstructorName(item)
153
204
  const isPlugin = pluginName && /(^|\.)plugins\[\d+\]/.test(itemPath)
154
205
  const isEmpty =
155
206
  typeof item === "object" &&
@@ -180,11 +231,11 @@ export class YamlSerializer {
180
231
  .split("\n")
181
232
  .filter((line: string) => line.trim().length > 0)
182
233
  // 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
- )
234
+ const indentLevels = nonEmptyLines.map(
235
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
187
236
  )
237
+ const minIndent =
238
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
188
239
  nonEmptyLines.forEach((line: string) => {
189
240
  // Remove only the common indent, preserving relative indentation
190
241
  lines.push(contentIndent + line.substring(minIndent))
@@ -196,11 +247,11 @@ export class YamlSerializer {
196
247
  .split("\n")
197
248
  .filter((line: string) => line.trim().length > 0)
198
249
  // Compute minimum leading whitespace to preserve relative indentation
199
- const minIndent = Math.min(
200
- ...nonEmptyLines.map(
201
- (line: string) => line.match(/^\s*/)?.[0].length || 0
202
- )
250
+ const indentLevels = nonEmptyLines.map(
251
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
203
252
  )
253
+ const minIndent =
254
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
204
255
  nonEmptyLines.forEach((line: string) => {
205
256
  // Remove only the common indent, preserving relative indentation
206
257
  lines.push(contentIndent + line.substring(minIndent))
@@ -211,12 +262,16 @@ export class YamlSerializer {
211
262
  }
212
263
  })
213
264
 
214
- return "\n" + lines.join("\n")
265
+ return `\n${lines.join("\n")}`
215
266
  }
216
267
 
217
- private serializeObject(obj: any, indent: number, keyPath: string): string {
218
- const keys = Object.keys(obj)
219
- const constructorName = this.getConstructorName(obj)
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)
220
275
 
221
276
  // For empty objects, show constructor name if available
222
277
  if (keys.length === 0) {
@@ -248,6 +303,12 @@ export class YamlSerializer {
248
303
  for (const line of value.split("\n")) {
249
304
  lines.push(`${valueIndent}${line}`)
250
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}`)
251
312
  } else if (
252
313
  typeof value === "object" &&
253
314
  value !== null &&
@@ -258,7 +319,7 @@ export class YamlSerializer {
258
319
  } else {
259
320
  lines.push(`${keyIndent}${key}:`)
260
321
  const nestedLines = this.serializeObject(
261
- value,
322
+ value as Record<string, unknown>,
262
323
  indent + 2,
263
324
  fullKeyPath
264
325
  )
@@ -282,7 +343,6 @@ export class YamlSerializer {
282
343
  }
283
344
 
284
345
  private makePathRelative(str: string): string {
285
- if (typeof str !== "string") return str
286
346
  if (!isAbsolute(str)) return str
287
347
 
288
348
  // Convert absolute paths to relative paths using path.relative
@@ -297,20 +357,35 @@ export class YamlSerializer {
297
357
  return str
298
358
  }
299
359
 
300
- return "./" + rel
360
+ return `./${rel}`
301
361
  }
302
362
 
303
363
  /**
304
364
  * Extracts the constructor name from an object
305
- * Returns null for plain objects (Object constructor)
365
+ * Returns null for plain objects (Object constructor) or objects without prototypes
306
366
  */
307
- private getConstructorName(obj: any): string | null {
367
+ private static getConstructorName(obj: unknown): string | null {
308
368
  if (!obj || typeof obj !== "object") return null
309
369
  if (Array.isArray(obj)) return null
310
370
 
311
- const constructorName = obj.constructor?.name
312
- if (!constructorName || constructorName === "Object") return null
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
313
378
 
314
- return constructorName
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
+ }
315
390
  }
316
391
  }
@@ -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
- ? (previousPaths as string[])
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
  */
@@ -6,17 +6,18 @@
6
6
  /* eslint global-require: 0 */
7
7
  /* eslint import/no-dynamic-require: 0 */
8
8
 
9
- const { resolve } = require("path")
10
- const { merge } = require("webpack-merge")
11
- const baseConfig = require("./base")
12
- const { moduleExists } = require("../utils/helpers")
13
- const config = require("../config")
14
9
  import type {
15
10
  Configuration as WebpackConfiguration,
16
11
  WebpackPluginInstance
17
12
  } from "webpack"
18
13
  import type { CompressionPluginConstructor } from "./types"
19
14
 
15
+ const { resolve } = require("path")
16
+ const { merge } = require("webpack-merge")
17
+ const baseConfig = require("./base")
18
+ const { moduleExists } = require("../utils/helpers")
19
+ const config = require("../config")
20
+
20
21
  const optimizationPath = resolve(
21
22
  __dirname,
22
23
  "..",
@@ -27,7 +28,6 @@ const { getOptimization } = require(optimizationPath)
27
28
 
28
29
  let CompressionPlugin: CompressionPluginConstructor | null = null
29
30
  if (moduleExists("compression-webpack-plugin")) {
30
- // eslint-disable-next-line global-require
31
31
  CompressionPlugin = require("compression-webpack-plugin")
32
32
  }
33
33
 
@@ -73,7 +73,6 @@ const productionConfig: Partial<WebpackConfiguration> = {
73
73
  }
74
74
 
75
75
  if (config.useContentHash === false) {
76
- // eslint-disable-next-line no-console
77
76
  console.warn(`⚠️ WARNING
78
77
  Setting 'useContentHash' to 'false' in the production environment (specified by NODE_ENV environment variable) is not allowed!
79
78
  Content hashes get added to the filenames regardless of setting useContentHash in 'shakapacker.yml' to false.
@@ -3,10 +3,11 @@
3
3
  * @module environments/test
4
4
  */
5
5
 
6
+ import type { Configuration as WebpackConfiguration } from "webpack"
7
+
6
8
  const { merge } = require("webpack-merge")
7
9
  const config = require("../config")
8
10
  const baseConfig = require("./base")
9
- import type { Configuration as WebpackConfiguration } from "webpack"
10
11
 
11
12
  interface TestConfig {
12
13
  mode: "development" | "production" | "none"
data/package/index.ts CHANGED
@@ -4,8 +4,7 @@
4
4
  import * as webpackMerge from "webpack-merge"
5
5
  import { resolve } from "path"
6
6
  import { existsSync } from "fs"
7
- // @ts-ignore: webpack is an optional peer dependency (using type-only import)
8
- import type { Configuration } from "webpack"
7
+ import type { Configuration, RuleSetRule } from "webpack"
9
8
  import config from "./config"
10
9
  import baseConfig from "./environments/base"
11
10
  import devServer from "./dev_server"
@@ -14,8 +13,16 @@ import { moduleExists, canProcess } from "./utils/helpers"
14
13
  import inliningCss from "./utils/inliningCss"
15
14
 
16
15
  const rulesPath = resolve(__dirname, "rules", `${config.assets_bundler}.js`)
17
- const rules = require(rulesPath)
16
+ /** Array of webpack/rspack loader rules */
17
+ const rules = require(rulesPath) as RuleSetRule[]
18
18
 
19
+ /**
20
+ * Generate webpack configuration with optional custom config.
21
+ *
22
+ * @param extraConfig - Optional webpack configuration to merge with base config
23
+ * @returns Final webpack configuration
24
+ * @throws {Error} If more than one argument is provided
25
+ */
19
26
  const generateWebpackConfig = (
20
27
  extraConfig: Configuration = {},
21
28
  ...extraArgs: unknown[]
@@ -41,15 +48,32 @@ const generateWebpackConfig = (
41
48
  return webpackMerge.merge({}, environmentConfig, extraConfig)
42
49
  }
43
50
 
51
+ /**
52
+ * The Shakapacker module exports.
53
+ * This object is exported via CommonJS `export =`.
54
+ *
55
+ * NOTE: This pattern is temporary and will be replaced with named exports
56
+ * once issue #641 is resolved.
57
+ */
44
58
  export = {
45
- config, // shakapacker.yml
59
+ /** Shakapacker configuration from shakapacker.yml */
60
+ config,
61
+ /** Development server configuration */
46
62
  devServer,
63
+ /** Generate webpack configuration with optional custom config */
47
64
  generateWebpackConfig,
65
+ /** Base webpack/rspack configuration */
48
66
  baseConfig,
67
+ /** Environment configuration (railsEnv, nodeEnv, etc.) */
49
68
  env,
69
+ /** Array of webpack/rspack loader rules */
50
70
  rules,
71
+ /** Check if a module exists in node_modules */
51
72
  moduleExists,
73
+ /** Process a file if a specific loader is available */
52
74
  canProcess,
75
+ /** Whether CSS should be inlined (dev server with HMR) */
53
76
  inliningCss,
77
+ /** webpack-merge functions (merge, mergeWithCustomize, mergeWithRules, unique) */
54
78
  ...webpackMerge
55
79
  }
data/package/loaders.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- // @ts-ignore: webpack is an optional peer dependency (using type-only import)
1
+ // @ts-expect-error: webpack is an optional peer dependency (using type-only import)
2
2
  import type { LoaderDefinitionFunction } from "webpack"
3
3
 
4
4
  export interface ShakapackerLoaderOptions {
5
- [key: string]: any
5
+ [key: string]: unknown
6
6
  }
7
7
 
8
8
  export interface ShakapackerLoader {
@@ -19,38 +19,36 @@ interface OptimizationConfig {
19
19
  minimizer: unknown[]
20
20
  }
21
21
 
22
- const getOptimization = (): OptimizationConfig => {
23
- return {
24
- minimizer: [
25
- tryCssMinimizer(),
26
- new TerserPlugin({
27
- // SHAKAPACKER_PARALLEL env var: number of parallel workers, or true for auto (os.cpus().length - 1)
28
- // If not set or invalid, defaults to true (automatic parallelization)
29
- parallel: process.env.SHAKAPACKER_PARALLEL
30
- ? Number.parseInt(process.env.SHAKAPACKER_PARALLEL, 10) || true
31
- : true,
32
- terserOptions: {
33
- parse: {
34
- // Let terser parse ecma 8 code but always output
35
- // ES5 compliant code for older browsers
36
- ecma: 8
37
- },
38
- compress: {
39
- ecma: 5,
40
- warnings: false,
41
- comparisons: false
42
- },
43
- mangle: { safari10: true },
44
- output: {
45
- ecma: 5,
46
- comments: false,
47
- ascii_only: true
48
- }
22
+ const getOptimization = (): OptimizationConfig => ({
23
+ minimizer: [
24
+ tryCssMinimizer(),
25
+ new TerserPlugin({
26
+ // SHAKAPACKER_PARALLEL env var: number of parallel workers, or true for auto (os.cpus().length - 1)
27
+ // If not set or invalid, defaults to true (automatic parallelization)
28
+ parallel: process.env.SHAKAPACKER_PARALLEL
29
+ ? Number.parseInt(process.env.SHAKAPACKER_PARALLEL, 10) || true
30
+ : true,
31
+ terserOptions: {
32
+ parse: {
33
+ // Let terser parse ecma 8 code but always output
34
+ // ES5 compliant code for older browsers
35
+ ecma: 8
36
+ },
37
+ compress: {
38
+ ecma: 5,
39
+ warnings: false,
40
+ comparisons: false
41
+ },
42
+ mangle: { safari10: true },
43
+ output: {
44
+ ecma: 5,
45
+ comments: false,
46
+ ascii_only: true
49
47
  }
50
- })
51
- ].filter(Boolean)
52
- }
53
- }
48
+ }
49
+ })
50
+ ].filter(Boolean)
51
+ })
54
52
 
55
53
  export = {
56
54
  getOptimization
@@ -4,10 +4,11 @@
4
4
  // Mixed require/import syntax:
5
5
  // - Using require() for compiled JS modules that may not have proper ES module exports
6
6
  // - Using import for type-only imports and Node.js built-in modules
7
- const webpackMerge = require("webpack-merge")
8
7
  import { resolve } from "path"
9
8
  import { existsSync } from "fs"
10
9
  import type { RspackConfigWithDevServer } from "../environments/types"
10
+
11
+ const webpackMerge = require("webpack-merge")
11
12
  const config = require("../config")
12
13
  const baseConfig = require("../environments/base")
13
14
  const devServer = require("../dev_server")