shakapacker 9.0.0.beta.4 → 9.0.0.beta.6

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/dummy.yml +4 -0
  5. data/.github/workflows/generator.yml +7 -0
  6. data/.github/workflows/node.yml +22 -0
  7. data/.github/workflows/ruby.yml +11 -0
  8. data/.github/workflows/test-bundlers.yml +27 -9
  9. data/.gitignore +20 -0
  10. data/.yalcignore +26 -0
  11. data/CHANGELOG.md +58 -40
  12. data/CONTRIBUTING.md +64 -0
  13. data/Gemfile.lock +1 -1
  14. data/README.md +80 -1
  15. data/docs/optional-peer-dependencies.md +198 -0
  16. data/docs/typescript.md +99 -0
  17. data/docs/v9_upgrade.md +79 -2
  18. data/lib/install/template.rb +8 -1
  19. data/lib/shakapacker/configuration.rb +58 -1
  20. data/lib/shakapacker/doctor.rb +751 -0
  21. data/lib/shakapacker/swc_migrator.rb +292 -0
  22. data/lib/shakapacker/version.rb +1 -1
  23. data/lib/shakapacker.rb +1 -0
  24. data/lib/tasks/shakapacker/doctor.rake +8 -0
  25. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  26. data/lib/tasks/shakapacker.rake +1 -0
  27. data/package/config.ts +162 -0
  28. data/package/{dev_server.js → dev_server.ts} +8 -5
  29. data/package/env.ts +67 -0
  30. data/package/environments/base.js +94 -117
  31. data/package/environments/base.ts +138 -0
  32. data/package/index.d.ts +3 -150
  33. data/package/{index.js → index.ts} +18 -8
  34. data/package/loaders.d.ts +28 -0
  35. data/package/types.ts +108 -0
  36. data/package/utils/configPath.ts +6 -0
  37. data/package/utils/{debug.js → debug.ts} +7 -7
  38. data/package/utils/defaultConfigPath.ts +4 -0
  39. data/package/utils/errorHelpers.ts +77 -0
  40. data/package/utils/{getStyleRule.js → getStyleRule.ts} +17 -20
  41. data/package/utils/helpers.ts +85 -0
  42. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  43. data/package/utils/{requireOrError.js → requireOrError.ts} +2 -2
  44. data/package/utils/snakeToCamelCase.ts +5 -0
  45. data/package/utils/typeGuards.ts +228 -0
  46. data/package/utils/{validateDependencies.js → validateDependencies.ts} +4 -4
  47. data/package/webpack-types.d.ts +33 -0
  48. data/package/webpackDevServerConfig.ts +117 -0
  49. data/package.json +112 -4
  50. data/test/peer-dependencies.sh +85 -0
  51. data/test/typescript/build.test.js +117 -0
  52. data/tsconfig.json +39 -0
  53. data/yarn.lock +1 -1
  54. metadata +34 -17
  55. data/package/config.js +0 -80
  56. data/package/env.js +0 -48
  57. data/package/utils/configPath.js +0 -4
  58. data/package/utils/defaultConfigPath.js +0 -2
  59. data/package/utils/helpers.js +0 -127
  60. data/package/utils/snakeToCamelCase.js +0 -5
  61. data/package/utils/validateCssModulesConfig.js +0 -91
  62. data/package/webpackDevServerConfig.js +0 -73
data/package/types.ts ADDED
@@ -0,0 +1,108 @@
1
+ import * as https from "node:https"
2
+
3
+ // Type for the raw YAML config file
4
+ export interface YamlConfig {
5
+ [environment: string]: Partial<Config>
6
+ }
7
+
8
+ // Type for backward compatibility
9
+ export interface LegacyConfig extends Config {
10
+ webpack_loader?: string
11
+ }
12
+
13
+ export interface Config {
14
+ source_path: string
15
+ source_entry_path: string
16
+ nested_entries: boolean
17
+ css_extract_ignore_order_warnings: boolean
18
+ public_root_path: string
19
+ public_output_path: string
20
+ private_output_path?: string
21
+ cache_path: string
22
+ webpack_compile_output: boolean
23
+ shakapacker_precompile: boolean
24
+ additional_paths: string[]
25
+ cache_manifest: boolean
26
+ javascript_transpiler: string
27
+ ensure_consistent_versioning: boolean
28
+ compiler_strategy: string
29
+ useContentHash: boolean
30
+ compile: boolean
31
+ outputPath: string
32
+ publicPath: string
33
+ publicPathWithoutCDN: string
34
+ manifestPath: string
35
+ manifest_path?: string
36
+ assets_bundler?: string
37
+ dev_server?: DevServerConfig
38
+ integrity?: {
39
+ enabled: boolean
40
+ cross_origin: string
41
+ hash_functions?: string[]
42
+ }
43
+ }
44
+
45
+ export interface Env {
46
+ railsEnv: string
47
+ nodeEnv: string
48
+ isProduction: boolean
49
+ isDevelopment: boolean
50
+ runningWebpackDevServer: boolean
51
+ }
52
+
53
+ type Header =
54
+ | Array<{ key: string; value: string }>
55
+ | Record<string, string | string[]>
56
+ type ServerType = "http" | "https" | "spdy"
57
+ type WebSocketType = "sockjs" | "ws"
58
+
59
+ /**
60
+ * This has the same keys and behavior as https://webpack.js.org/configuration/dev-server/ except:
61
+ * 1. `hot` is replaced by `hmr`;
62
+ * 2. Camel-cased properties are replaced by snake-cased ones.
63
+ * @see {import('webpack-dev-server').Configuration}
64
+ */
65
+ export interface DevServerConfig {
66
+ allowed_hosts?: "all" | "auto" | string | string[]
67
+ bonjour?: boolean | Record<string, unknown> // bonjour.BonjourOptions
68
+ client?: Record<string, unknown> // Client
69
+ compress?: boolean
70
+ dev_middleware?: Record<string, unknown> // webpackDevMiddleware.Options
71
+ headers?: Header | (() => Header)
72
+ history_api_fallback?: boolean | Record<string, unknown> // HistoryApiFallbackOptions
73
+ hmr?: "only" | boolean
74
+ host?: "local-ip" | "local-ipv4" | "local-ipv6" | string
75
+ http2?: boolean
76
+ https?: boolean | https.ServerOptions
77
+ ipc?: boolean | string
78
+ magic_html?: boolean
79
+ live_reload?: boolean
80
+ inline_css?: boolean
81
+ env_prefix?: string
82
+ open?:
83
+ | boolean
84
+ | string
85
+ | string[]
86
+ | Record<string, unknown>
87
+ | Record<string, unknown>[]
88
+ port?: "auto" | string | number
89
+ proxy?: unknown // ProxyConfigMap | ProxyConfigArray
90
+ setup_exit_signals?: boolean
91
+ static?: boolean | string | unknown // Static | Array<string | Static>
92
+ watch_files?: string | string[] | unknown // WatchFiles | Array<WatchFiles | string>
93
+ web_socket_server?:
94
+ | string
95
+ | boolean
96
+ | WebSocketType
97
+ | {
98
+ type?: string | boolean | WebSocketType
99
+ options?: Record<string, unknown>
100
+ }
101
+ server?:
102
+ | string
103
+ | boolean
104
+ | ServerType
105
+ | { type?: string | boolean | ServerType; options?: https.ServerOptions }
106
+ [otherWebpackDevServerConfigKey: string]: unknown
107
+ }
108
+
@@ -0,0 +1,6 @@
1
+ import { resolve } from "path"
2
+
3
+ const configPath: string =
4
+ process.env.SHAKAPACKER_CONFIG || resolve("config", "shakapacker.yml")
5
+
6
+ export = configPath
@@ -3,7 +3,7 @@
3
3
  * Provides conditional logging based on environment variables
4
4
  */
5
5
 
6
- const isDebugMode = () => {
6
+ const isDebugMode = (): boolean => {
7
7
  // Explicitly check for debug mode being disabled
8
8
  if (process.env.SHAKAPACKER_DEBUG === "false") {
9
9
  return false
@@ -16,34 +16,34 @@ const isDebugMode = () => {
16
16
  )
17
17
  }
18
18
 
19
- const debug = (message, ...args) => {
19
+ const debug = (message: string, ...args: any[]): void => {
20
20
  if (isDebugMode()) {
21
21
  // eslint-disable-next-line no-console
22
22
  console.log(`[Shakapacker] ${message}`, ...args)
23
23
  }
24
24
  }
25
25
 
26
- const warn = (message, ...args) => {
26
+ const warn = (message: string, ...args: any[]): void => {
27
27
  // eslint-disable-next-line no-console
28
28
  console.warn(`[Shakapacker] WARNING: ${message}`, ...args)
29
29
  }
30
30
 
31
- const error = (message, ...args) => {
31
+ const error = (message: string, ...args: any[]): void => {
32
32
  // eslint-disable-next-line no-console
33
33
  console.error(`[Shakapacker] ERROR: ${message}`, ...args)
34
34
  }
35
35
 
36
- const info = (message, ...args) => {
36
+ const info = (message: string, ...args: any[]): void => {
37
37
  if (isDebugMode()) {
38
38
  // eslint-disable-next-line no-console
39
39
  console.info(`[Shakapacker] INFO: ${message}`, ...args)
40
40
  }
41
41
  }
42
42
 
43
- module.exports = {
43
+ export = {
44
44
  debug,
45
45
  warn,
46
46
  error,
47
47
  info,
48
48
  isDebugMode
49
- }
49
+ }
@@ -0,0 +1,4 @@
1
+ import { resolve } from "path"
2
+
3
+ const path: string = resolve(__dirname, "../../lib/install/config/shakapacker.yml")
4
+ export = path
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Error handling utilities for consistent error management
3
+ */
4
+
5
+ /**
6
+ * Checks if an error is a file not found error (ENOENT)
7
+ */
8
+ export function isFileNotFoundError(error: unknown): boolean {
9
+ return (
10
+ error !== null &&
11
+ typeof error === 'object' &&
12
+ 'code' in error &&
13
+ (error as NodeJS.ErrnoException).code === 'ENOENT'
14
+ )
15
+ }
16
+
17
+ /**
18
+ * Checks if an error is a module not found error
19
+ */
20
+ export function isModuleNotFoundError(error: unknown): boolean {
21
+ return (
22
+ error !== null &&
23
+ typeof error === 'object' &&
24
+ 'code' in error &&
25
+ (error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND'
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Creates a consistent error message for file operations
31
+ */
32
+ export function createFileOperationError(
33
+ operation: 'read' | 'write' | 'delete',
34
+ filePath: string,
35
+ details?: string
36
+ ): Error {
37
+ const baseMessage = `Failed to ${operation} file at path '${filePath}'`
38
+ const errorDetails = details ? ` - ${details}` : ''
39
+ const suggestion = operation === 'read'
40
+ ? ' (check if file exists and permissions are correct)'
41
+ : operation === 'write'
42
+ ? ' (check write permissions and disk space)'
43
+ : ' (check permissions)'
44
+ return new Error(`${baseMessage}${errorDetails}${suggestion}`)
45
+ }
46
+
47
+ /**
48
+ * Safely gets error message from unknown error type
49
+ */
50
+ export function getErrorMessage(error: unknown): string {
51
+ if (error instanceof Error) {
52
+ // Include stack trace for better debugging in development
53
+ const isDev = process.env.NODE_ENV === 'development'
54
+ return isDev && error.stack ? `${error.message}\n${error.stack}` : error.message
55
+ }
56
+ if (typeof error === 'string') {
57
+ return error
58
+ }
59
+ if (error && typeof error === 'object' && 'message' in error) {
60
+ return String((error as { message: unknown }).message)
61
+ }
62
+ // Provide more context for truly unknown errors
63
+ return `Unknown error occurred (type: ${typeof error}, value: ${JSON.stringify(error)})`
64
+ }
65
+
66
+ /**
67
+ * Type guard for NodeJS errors with errno
68
+ */
69
+ export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
70
+ return (
71
+ error instanceof Error &&
72
+ 'code' in error &&
73
+ typeof (error as any).code === 'string'
74
+ )
75
+ }
76
+
77
+
@@ -3,12 +3,17 @@ const { canProcess, moduleExists } = require("./helpers")
3
3
  const { requireOrError } = require("./requireOrError")
4
4
  const config = require("../config")
5
5
  const inliningCss = require("./inliningCss")
6
- const { validateCssModulesConfig } = require("./validateCssModulesConfig")
7
6
 
8
- const getStyleRule = (test, preprocessors = []) => {
7
+ interface StyleRule {
8
+ test: RegExp
9
+ use: any[]
10
+ type?: string
11
+ }
12
+
13
+ const getStyleRule = (test: RegExp, preprocessors: any[] = []): StyleRule | null => {
9
14
  if (moduleExists("css-loader")) {
10
15
  const tryPostcss = () =>
11
- canProcess("postcss-loader", (loaderPath) => ({
16
+ canProcess("postcss-loader", (loaderPath: string) => ({
12
17
  loader: loaderPath,
13
18
  options: { sourceMap: true }
14
19
  }))
@@ -20,31 +25,23 @@ const getStyleRule = (test, preprocessors = []) => {
20
25
  ? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
21
26
  : requireOrError("mini-css-extract-plugin").loader
22
27
 
23
- const cssLoaderOptions = {
24
- sourceMap: true,
25
- importLoaders: 2,
26
- modules: {
27
- auto: true,
28
- // v9 defaults: named exports with camelCase conversion
29
- namedExport: true,
30
- exportLocalsConvention: "camelCase"
31
- }
32
- }
33
-
34
- // Validate CSS modules configuration
35
- validateCssModulesConfig(cssLoaderOptions)
36
-
37
28
  const use = [
38
29
  inliningCss ? "style-loader" : extractionPlugin,
39
30
  {
40
31
  loader: require.resolve("css-loader"),
41
- options: cssLoaderOptions
32
+ options: {
33
+ sourceMap: true,
34
+ importLoaders: 2,
35
+ modules: {
36
+ auto: true
37
+ }
38
+ }
42
39
  },
43
40
  tryPostcss(),
44
41
  ...preprocessors
45
42
  ].filter(Boolean)
46
43
 
47
- const result = {
44
+ const result: StyleRule = {
48
45
  test,
49
46
  use
50
47
  }
@@ -59,4 +56,4 @@ const getStyleRule = (test, preprocessors = []) => {
59
56
  return null
60
57
  }
61
58
 
62
- module.exports = { getStyleRule }
59
+ export = { getStyleRule }
@@ -0,0 +1,85 @@
1
+ const { isModuleNotFoundError, getErrorMessage } = require("./errorHelpers")
2
+
3
+ const isBoolean = (str: string): boolean => /^true/.test(str) || /^false/.test(str)
4
+
5
+ const ensureTrailingSlash = (path: string): string => (path.endsWith("/") ? path : `${path}/`)
6
+
7
+ const resolvedPath = (packageName: string): string | null => {
8
+ try {
9
+ return require.resolve(packageName)
10
+ } catch (error: unknown) {
11
+ if (!isModuleNotFoundError(error)) {
12
+ throw error
13
+ }
14
+ return null
15
+ }
16
+ }
17
+
18
+ const moduleExists = (packageName: string): boolean => !!resolvedPath(packageName)
19
+
20
+ const canProcess = <T = unknown>(rule: string, fn: (modulePath: string) => T): T | null => {
21
+ const modulePath = resolvedPath(rule)
22
+
23
+ if (modulePath) {
24
+ return fn(modulePath)
25
+ }
26
+
27
+ return null
28
+ }
29
+
30
+ const loaderMatches = <T = unknown>(configLoader: string, loaderToCheck: string, fn: () => T): T | null => {
31
+ if (configLoader !== loaderToCheck) {
32
+ return null
33
+ }
34
+
35
+ const loaderName = `${configLoader}-loader`
36
+
37
+ if (!moduleExists(loaderName)) {
38
+ throw new Error(
39
+ `Your Shakapacker config specified using ${configLoader}, but ${loaderName} package is not installed.\n` +
40
+ `\nTo fix this issue, run one of the following commands:\n` +
41
+ ` npm install --save-dev ${loaderName}\n` +
42
+ ` yarn add --dev ${loaderName}\n` +
43
+ `\nOr change your 'javascript_transpiler' setting in shakapacker.yml to use a different loader.`
44
+ )
45
+ }
46
+
47
+ return fn()
48
+ }
49
+
50
+ const packageFullVersion = (packageName: string): string => {
51
+ try {
52
+ // eslint-disable-next-line import/no-dynamic-require
53
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
54
+ // eslint-disable-next-line import/no-dynamic-require, global-require
55
+ const packageJson = require(packageJsonPath) as { version: string }
56
+ return packageJson.version
57
+ } catch (error: any) {
58
+ // Re-throw the error with proper code to maintain compatibility with babel preset
59
+ // The preset expects MODULE_NOT_FOUND errors to handle missing core-js gracefully
60
+ if (error.code === "MODULE_NOT_FOUND") {
61
+ throw error
62
+ }
63
+ // For other errors, warn and re-throw
64
+ console.warn(
65
+ `[SHAKAPACKER WARNING] Failed to get version for package ${packageName}: ${getErrorMessage(error)}`
66
+ )
67
+ throw error
68
+ }
69
+ }
70
+
71
+ const packageMajorVersion = (packageName: string): string => {
72
+ const match = packageFullVersion(packageName).match(/^\d+/)
73
+ return match ? match[0] : "0"
74
+ }
75
+
76
+ export {
77
+ isBoolean,
78
+ ensureTrailingSlash,
79
+ canProcess,
80
+ moduleExists,
81
+ loaderMatches,
82
+ packageFullVersion,
83
+ packageMajorVersion,
84
+ resolvedPath
85
+ }
@@ -2,7 +2,7 @@ const { runningWebpackDevServer } = require("../env")
2
2
  const devServer = require("../dev_server")
3
3
 
4
4
  // This logic is tied to lib/shakapacker/instance.rb
5
- const inliningCss =
6
- runningWebpackDevServer && devServer.hmr && devServer.inline_css !== false
5
+ const inliningCss: boolean =
6
+ runningWebpackDevServer && !!devServer.hmr && devServer.inline_css !== false
7
7
 
8
- module.exports = inliningCss
8
+ export = inliningCss
@@ -2,7 +2,7 @@
2
2
  /* eslint import/no-dynamic-require: 0 */
3
3
  const config = require("../config")
4
4
 
5
- const requireOrError = (moduleName) => {
5
+ const requireOrError = (moduleName: string): any => {
6
6
  try {
7
7
  return require(moduleName)
8
8
  } catch (error) {
@@ -12,4 +12,4 @@ const requireOrError = (moduleName) => {
12
12
  }
13
13
  }
14
14
 
15
- module.exports = { requireOrError }
15
+ export = { requireOrError }
@@ -0,0 +1,5 @@
1
+ function snakeToCamelCase(s: string): string {
2
+ return s.replace(/(_\w)/g, (match) => match[1].toUpperCase())
3
+ }
4
+
5
+ export = snakeToCamelCase
@@ -0,0 +1,228 @@
1
+ import { Config, DevServerConfig, YamlConfig } from "../types"
2
+
3
+ // Cache for validated configs in production
4
+ const validatedConfigs = new WeakMap<object, boolean>()
5
+
6
+ // Only validate in development or when explicitly enabled
7
+ const shouldValidate = process.env.NODE_ENV !== 'production' || process.env.SHAKAPACKER_STRICT_VALIDATION === 'true'
8
+
9
+ /**
10
+ * Type guard to validate Config object at runtime
11
+ * In production, caches results for performance unless SHAKAPACKER_STRICT_VALIDATION is set
12
+ */
13
+ export function isValidConfig(obj: unknown): obj is Config {
14
+ if (typeof obj !== 'object' || obj === null) {
15
+ return false
16
+ }
17
+
18
+ // Quick return for production with cached results
19
+ if (!shouldValidate && validatedConfigs.has(obj as object)) {
20
+ return validatedConfigs.get(obj as object) as boolean
21
+ }
22
+
23
+ const config = obj as Record<string, unknown>
24
+
25
+ // Check required string fields
26
+ const requiredStringFields = [
27
+ 'source_path',
28
+ 'source_entry_path',
29
+ 'public_root_path',
30
+ 'public_output_path',
31
+ 'cache_path',
32
+ 'javascript_transpiler'
33
+ ]
34
+
35
+ for (const field of requiredStringFields) {
36
+ if (typeof config[field] !== 'string') {
37
+ // Cache negative result in production
38
+ if (!shouldValidate) {
39
+ validatedConfigs.set(obj as object, false)
40
+ }
41
+ return false
42
+ }
43
+ }
44
+
45
+ // Check required boolean fields
46
+ const requiredBooleanFields = [
47
+ 'nested_entries',
48
+ 'css_extract_ignore_order_warnings',
49
+ 'webpack_compile_output',
50
+ 'shakapacker_precompile',
51
+ 'cache_manifest',
52
+ 'ensure_consistent_versioning',
53
+ 'useContentHash',
54
+ 'compile'
55
+ ]
56
+
57
+ for (const field of requiredBooleanFields) {
58
+ if (typeof config[field] !== 'boolean') {
59
+ // Cache negative result in production
60
+ if (!shouldValidate) {
61
+ validatedConfigs.set(obj as object, false)
62
+ }
63
+ return false
64
+ }
65
+ }
66
+
67
+ // Check arrays
68
+ if (!Array.isArray(config.additional_paths)) {
69
+ // Cache negative result in production
70
+ if (!shouldValidate) {
71
+ validatedConfigs.set(obj as object, false)
72
+ }
73
+ return false
74
+ }
75
+
76
+ // Check optional fields
77
+ if (config.dev_server !== undefined && !isValidDevServerConfig(config.dev_server)) {
78
+ // Cache negative result in production
79
+ if (!shouldValidate) {
80
+ validatedConfigs.set(obj as object, false)
81
+ }
82
+ return false
83
+ }
84
+
85
+ if (config.integrity !== undefined) {
86
+ const integrity = config.integrity as Record<string, unknown>
87
+ if (typeof integrity.enabled !== 'boolean' ||
88
+ typeof integrity.cross_origin !== 'string') {
89
+ // Cache negative result in production
90
+ if (!shouldValidate) {
91
+ validatedConfigs.set(obj as object, false)
92
+ }
93
+ return false
94
+ }
95
+ }
96
+
97
+ const result = true
98
+
99
+ // Cache result in production
100
+ if (!shouldValidate) {
101
+ validatedConfigs.set(obj as object, result)
102
+ }
103
+
104
+ return result
105
+ }
106
+
107
+ /**
108
+ * Type guard to validate DevServerConfig object at runtime
109
+ * In production, performs minimal validation for performance
110
+ */
111
+ export function isValidDevServerConfig(obj: unknown): obj is DevServerConfig {
112
+ if (typeof obj !== 'object' || obj === null) {
113
+ return false
114
+ }
115
+
116
+ // In production, skip deep validation unless explicitly enabled
117
+ if (!shouldValidate) {
118
+ return true
119
+ }
120
+
121
+ const config = obj as Record<string, unknown>
122
+
123
+ // All fields are optional, just check types if present
124
+ if (config.hmr !== undefined &&
125
+ typeof config.hmr !== 'boolean' &&
126
+ config.hmr !== 'only') {
127
+ return false
128
+ }
129
+
130
+ if (config.port !== undefined &&
131
+ typeof config.port !== 'number' &&
132
+ typeof config.port !== 'string' &&
133
+ config.port !== 'auto') {
134
+ return false
135
+ }
136
+
137
+ return true
138
+ }
139
+
140
+ /**
141
+ * Type guard to validate YamlConfig structure
142
+ * In production, performs minimal validation for performance
143
+ */
144
+ export function isValidYamlConfig(obj: unknown): obj is YamlConfig {
145
+ if (typeof obj !== 'object' || obj === null) {
146
+ return false
147
+ }
148
+
149
+ // In production, skip deep validation unless explicitly enabled
150
+ if (!shouldValidate) {
151
+ return true
152
+ }
153
+
154
+ const config = obj as Record<string, unknown>
155
+
156
+ // Each key should map to an object
157
+ for (const env of Object.keys(config)) {
158
+ if (typeof config[env] !== 'object' || config[env] === null) {
159
+ return false
160
+ }
161
+ }
162
+
163
+ return true
164
+ }
165
+
166
+ /**
167
+ * Validates partial config used for merging
168
+ * Ensures that if fields are present, they have the correct types
169
+ * In production, performs minimal validation for performance
170
+ */
171
+ export function isPartialConfig(obj: unknown): obj is Partial<Config> {
172
+ if (typeof obj !== 'object' || obj === null) {
173
+ return false
174
+ }
175
+
176
+ // In production, skip deep validation unless explicitly enabled
177
+ if (!shouldValidate) {
178
+ return true
179
+ }
180
+
181
+ const config = obj as Record<string, unknown>
182
+
183
+ // Check string fields if present
184
+ const stringFields = [
185
+ 'source_path', 'source_entry_path', 'public_root_path',
186
+ 'public_output_path', 'cache_path', 'javascript_transpiler'
187
+ ]
188
+
189
+ for (const field of stringFields) {
190
+ if (field in config && typeof config[field] !== 'string') {
191
+ return false
192
+ }
193
+ }
194
+
195
+ // Check boolean fields if present
196
+ const booleanFields = [
197
+ 'nested_entries', 'css_extract_ignore_order_warnings',
198
+ 'webpack_compile_output', 'shakapacker_precompile',
199
+ 'cache_manifest', 'ensure_consistent_versioning'
200
+ ]
201
+
202
+ for (const field of booleanFields) {
203
+ if (field in config && typeof config[field] !== 'boolean') {
204
+ return false
205
+ }
206
+ }
207
+
208
+ // Check arrays if present
209
+ if ('additional_paths' in config && !Array.isArray(config.additional_paths)) {
210
+ return false
211
+ }
212
+
213
+ return true
214
+ }
215
+
216
+ /**
217
+ * Creates a validation error with helpful context
218
+ */
219
+ export function createConfigValidationError(
220
+ configPath: string,
221
+ environment: string,
222
+ details?: string
223
+ ): Error {
224
+ const message = `Invalid configuration in ${configPath} for environment '${environment}'`
225
+ return new Error(details ? `${message}: ${details}` : message)
226
+ }
227
+
228
+
@@ -5,7 +5,7 @@
5
5
  const { moduleExists } = require("./helpers")
6
6
  const { error } = require("./debug")
7
7
 
8
- const validateRspackDependencies = () => {
8
+ const validateRspackDependencies = (): void => {
9
9
  const requiredDependencies = ["@rspack/core", "rspack-manifest-plugin"]
10
10
 
11
11
  const missingDependencies = requiredDependencies.filter(
@@ -28,7 +28,7 @@ const validateRspackDependencies = () => {
28
28
  }
29
29
  }
30
30
 
31
- const validateWebpackDependencies = () => {
31
+ const validateWebpackDependencies = (): void => {
32
32
  const requiredDependencies = [
33
33
  "webpack",
34
34
  "webpack-cli",
@@ -55,7 +55,7 @@ const validateWebpackDependencies = () => {
55
55
  }
56
56
  }
57
57
 
58
- module.exports = {
58
+ export = {
59
59
  validateRspackDependencies,
60
60
  validateWebpackDependencies
61
- }
61
+ }