shakapacker 9.3.0.beta.7 → 9.3.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/update-changelog.md +224 -0
  3. data/.github/actionlint-matcher.json +17 -0
  4. data/.github/workflows/dummy.yml +9 -0
  5. data/.github/workflows/generator.yml +13 -0
  6. data/.github/workflows/node.yml +83 -0
  7. data/.github/workflows/ruby.yml +11 -0
  8. data/.github/workflows/test-bundlers.yml +10 -0
  9. data/CHANGELOG.md +55 -111
  10. data/CLAUDE.md +6 -10
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile.lock +1 -1
  13. data/README.md +84 -8
  14. data/docs/api-reference.md +519 -0
  15. data/docs/configuration.md +38 -4
  16. data/docs/css-modules-export-mode.md +40 -6
  17. data/docs/rspack_migration_guide.md +238 -2
  18. data/docs/transpiler-migration.md +12 -9
  19. data/docs/troubleshooting.md +21 -21
  20. data/docs/using_swc_loader.md +13 -10
  21. data/docs/v9_upgrade.md +11 -2
  22. data/eslint.config.fast.js +128 -8
  23. data/eslint.config.js +89 -33
  24. data/knip.ts +8 -1
  25. data/lib/install/config/shakapacker.yml +20 -7
  26. data/lib/shakapacker/configuration.rb +274 -8
  27. data/lib/shakapacker/dev_server.rb +88 -1
  28. data/lib/shakapacker/dev_server_runner.rb +4 -0
  29. data/lib/shakapacker/doctor.rb +5 -5
  30. data/lib/shakapacker/instance.rb +85 -1
  31. data/lib/shakapacker/manifest.rb +85 -11
  32. data/lib/shakapacker/version.rb +1 -1
  33. data/lib/shakapacker.rb +143 -3
  34. data/lib/tasks/shakapacker/doctor.rake +1 -1
  35. data/lib/tasks/shakapacker/export_bundler_config.rake +4 -4
  36. data/package/config.ts +2 -4
  37. data/package/configExporter/buildValidator.ts +53 -29
  38. data/package/configExporter/cli.ts +106 -76
  39. data/package/configExporter/configFile.ts +33 -26
  40. data/package/configExporter/types.ts +64 -0
  41. data/package/configExporter/yamlSerializer.ts +118 -43
  42. data/package/dev_server.ts +3 -2
  43. data/package/env.ts +2 -2
  44. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +6 -6
  45. data/package/environments/base.ts +6 -6
  46. data/package/environments/development.ts +7 -9
  47. data/package/environments/production.ts +7 -8
  48. data/package/environments/test.ts +4 -2
  49. data/package/esbuild/index.ts +0 -2
  50. data/package/index.d.ts +1 -0
  51. data/package/index.d.ts.template +1 -0
  52. data/package/index.ts +28 -5
  53. data/package/loaders.d.ts +2 -2
  54. data/package/optimization/webpack.ts +29 -31
  55. data/package/plugins/rspack.ts +3 -1
  56. data/package/plugins/webpack.ts +5 -3
  57. data/package/rspack/index.ts +5 -4
  58. data/package/rules/file.ts +2 -1
  59. data/package/rules/jscommon.ts +1 -0
  60. data/package/rules/raw.ts +3 -1
  61. data/package/rules/rspack.ts +0 -2
  62. data/package/rules/sass.ts +0 -2
  63. data/package/rules/webpack.ts +0 -1
  64. data/package/swc/index.ts +0 -2
  65. data/package/types.ts +8 -11
  66. data/package/utils/debug.ts +0 -4
  67. data/package/utils/getStyleRule.ts +17 -9
  68. data/package/utils/helpers.ts +8 -4
  69. data/package/utils/pathValidation.ts +78 -18
  70. data/package/utils/requireOrError.ts +14 -5
  71. data/package/utils/typeGuards.ts +43 -46
  72. data/package/webpack-types.d.ts +2 -2
  73. data/package/webpackDevServerConfig.ts +5 -4
  74. data/package.json +2 -3
  75. data/test/package/configExporter/cli.test.js +440 -0
  76. data/test/package/configExporter/types.test.js +163 -0
  77. data/test/package/configExporter.test.js +264 -0
  78. data/test/package/transpiler-defaults.test.js +42 -0
  79. data/test/package/yamlSerializer.test.js +204 -0
  80. data/test/typescript/pathValidation.test.js +44 -0
  81. data/test/typescript/requireOrError.test.js +49 -0
  82. data/yarn.lock +0 -32
  83. metadata +14 -5
  84. data/.eslintrc.fast.js +0 -40
  85. data/.eslintrc.js +0 -84
@@ -3,19 +3,20 @@
3
3
  * @module environments/production
4
4
  */
5
5
 
6
- /* eslint global-require: 0 */
7
6
  /* eslint import/no-dynamic-require: 0 */
8
7
 
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
8
  import type {
15
9
  Configuration as WebpackConfiguration,
16
10
  WebpackPluginInstance
17
11
  } from "webpack"
18
12
  import type { CompressionPluginConstructor } from "./types"
13
+ import type { Config } from "../types"
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") as Config
19
20
 
20
21
  const optimizationPath = resolve(
21
22
  __dirname,
@@ -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,12 @@
3
3
  * @module environments/test
4
4
  */
5
5
 
6
+ import type { Configuration as WebpackConfiguration } from "webpack"
7
+ import type { Config } from "../types"
8
+
6
9
  const { merge } = require("webpack-merge")
7
- const config = require("../config")
10
+ const config = require("../config") as Config
8
11
  const baseConfig = require("./base")
9
- import type { Configuration as WebpackConfiguration } from "webpack"
10
12
 
11
13
  interface TestConfig {
12
14
  mode: "development" | "production" | "none"
@@ -1,4 +1,3 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
4
3
  import { resolve } from "path"
@@ -21,7 +20,6 @@ const getLoaderExtension = (filename: string): string => {
21
20
  const getCustomConfig = (): Partial<RuleSetRule> => {
22
21
  const path = resolve("config", "esbuild.config.js")
23
22
  if (existsSync(path)) {
24
- // eslint-disable-next-line @typescript-eslint/no-require-imports
25
23
  return require(path)
26
24
  }
27
25
  return {}
data/package/index.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * When adding/modifying exports in index.ts, update this file accordingly.
8
8
  */
9
9
 
10
+ // @ts-expect-error: webpack is an optional peer dependency (using type-only import)
10
11
  import type { Configuration, RuleSetRule } from "webpack"
11
12
  import type { Config, DevServerConfig, Env } from "./types"
12
13
 
@@ -7,6 +7,7 @@
7
7
  * When adding/modifying exports in index.ts, update this file accordingly.
8
8
  */
9
9
 
10
+ // @ts-expect-error: webpack is an optional peer dependency (using type-only import)
10
11
  import type { Configuration, RuleSetRule } from "webpack"
11
12
  import type { Config, DevServerConfig, Env } from "./types"
12
13
 
data/package/index.ts CHANGED
@@ -1,11 +1,9 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
4
3
  import * as webpackMerge from "webpack-merge"
5
4
  import { resolve } from "path"
6
5
  import { existsSync } from "fs"
7
- // @ts-ignore: webpack is an optional peer dependency (using type-only import)
8
- import type { Configuration } from "webpack"
6
+ import type { Configuration, RuleSetRule } from "webpack"
9
7
  import config from "./config"
10
8
  import baseConfig from "./environments/base"
11
9
  import devServer from "./dev_server"
@@ -14,8 +12,16 @@ import { moduleExists, canProcess } from "./utils/helpers"
14
12
  import inliningCss from "./utils/inliningCss"
15
13
 
16
14
  const rulesPath = resolve(__dirname, "rules", `${config.assets_bundler}.js`)
17
- const rules = require(rulesPath)
15
+ /** Array of webpack/rspack loader rules */
16
+ const rules = require(rulesPath) as RuleSetRule[]
18
17
 
18
+ /**
19
+ * Generate webpack configuration with optional custom config.
20
+ *
21
+ * @param extraConfig - Optional webpack configuration to merge with base config
22
+ * @returns Final webpack configuration
23
+ * @throws {Error} If more than one argument is provided
24
+ */
19
25
  const generateWebpackConfig = (
20
26
  extraConfig: Configuration = {},
21
27
  ...extraArgs: unknown[]
@@ -41,15 +47,32 @@ const generateWebpackConfig = (
41
47
  return webpackMerge.merge({}, environmentConfig, extraConfig)
42
48
  }
43
49
 
50
+ /**
51
+ * The Shakapacker module exports.
52
+ * This object is exported via CommonJS `export =`.
53
+ *
54
+ * NOTE: This pattern is temporary and will be replaced with named exports
55
+ * once issue #641 is resolved.
56
+ */
44
57
  export = {
45
- config, // shakapacker.yml
58
+ /** Shakapacker configuration from shakapacker.yml */
59
+ config,
60
+ /** Development server configuration */
46
61
  devServer,
62
+ /** Generate webpack configuration with optional custom config */
47
63
  generateWebpackConfig,
64
+ /** Base webpack/rspack configuration */
48
65
  baseConfig,
66
+ /** Environment configuration (railsEnv, nodeEnv, etc.) */
49
67
  env,
68
+ /** Array of webpack/rspack loader rules */
50
69
  rules,
70
+ /** Check if a module exists in node_modules */
51
71
  moduleExists,
72
+ /** Process a file if a specific loader is available */
52
73
  canProcess,
74
+ /** Whether CSS should be inlined (dev server with HMR) */
53
75
  inliningCss,
76
+ /** webpack-merge functions (merge, mergeWithCustomize, mergeWithRules, unique) */
54
77
  ...webpackMerge
55
78
  }
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
@@ -1,8 +1,10 @@
1
+ import type { Config } from "../types"
2
+
1
3
  const { requireOrError } = require("../utils/requireOrError")
2
4
 
3
5
  const { RspackManifestPlugin } = requireOrError("rspack-manifest-plugin")
4
6
  const rspack = requireOrError("@rspack/core")
5
- const config = require("../config")
7
+ const config = require("../config") as Config
6
8
  const { isProduction } = require("../env")
7
9
  const { moduleExists } = require("../utils/helpers")
8
10
 
@@ -1,9 +1,11 @@
1
+ import type { Config } from "../types"
2
+
1
3
  const { requireOrError } = require("../utils/requireOrError")
2
4
  // TODO: Change to `const { WebpackAssetsManifest }` when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
3
5
  const WebpackAssetsManifest = requireOrError("webpack-assets-manifest")
4
6
  const webpack = requireOrError("webpack")
5
- const config = require("../config")
6
- const { isProduction, isDevelopment } = require("../env")
7
+ const config = require("../config") as Config
8
+ const { isProduction } = require("../env")
7
9
  const { moduleExists } = require("../utils/helpers")
8
10
 
9
11
  const getPlugins = (): unknown[] => {
@@ -15,7 +17,7 @@ const getPlugins = (): unknown[] => {
15
17
  const plugins = [
16
18
  new webpack.EnvironmentPlugin(process.env),
17
19
  new WebpackAssetsManifestConstructor({
18
- merge: isDevelopment,
20
+ merge: true,
19
21
  entrypoints: true,
20
22
  writeToDisk: true,
21
23
  output: config.manifestPath,
@@ -1,14 +1,15 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
4
3
  // Mixed require/import syntax:
5
4
  // - Using require() for compiled JS modules that may not have proper ES module exports
6
5
  // - Using import for type-only imports and Node.js built-in modules
7
- const webpackMerge = require("webpack-merge")
8
6
  import { resolve } from "path"
9
7
  import { existsSync } from "fs"
10
8
  import type { RspackConfigWithDevServer } from "../environments/types"
11
- const config = require("../config")
9
+ import type { Config } from "../types"
10
+
11
+ const webpackMerge = require("webpack-merge")
12
+ const config = require("../config") as Config
12
13
  const baseConfig = require("../environments/base")
13
14
  const devServer = require("../dev_server")
14
15
  const env = require("../env")
@@ -35,7 +36,7 @@ const generateRspackConfig = (
35
36
 
36
37
  const { nodeEnv } = env
37
38
  const path = resolve(__dirname, "../environments", `${nodeEnv}.js`)
38
- // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+
39
40
  const environmentConfig = existsSync(path) ? require(path) : baseConfig
40
41
 
41
42
  // Create base rspack config
@@ -1,4 +1,5 @@
1
- import { dirname, sep, normalize } from "path"
1
+ import { dirname, normalize } from "path"
2
+
2
3
  const {
3
4
  additional_paths: additionalPaths,
4
5
  source_path: sourcePath
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "path"
2
2
  import { realpathSync } from "fs"
3
+
3
4
  const {
4
5
  source_path: sourcePath,
5
6
  additional_paths: additionalPaths
data/package/rules/raw.ts CHANGED
@@ -1,4 +1,6 @@
1
- const config = require("../config")
1
+ import type { Config } from "../types"
2
+
3
+ const config = require("../config") as Config
2
4
 
3
5
  const rspackRawConfig = () => ({
4
6
  resourceQuery: /raw/,
@@ -1,5 +1,3 @@
1
- /* eslint global-require: 0 */
2
-
3
1
  const { moduleExists } = require("../utils/helpers")
4
2
  const { debug, info, warn } = require("../utils/debug")
5
3
 
@@ -1,5 +1,3 @@
1
- /* eslint global-require: 0 */
2
-
3
1
  const { getStyleRule } = require("../utils/getStyleRule")
4
2
  const { canProcess, packageMajorVersion } = require("../utils/helpers")
5
3
  const { additional_paths: extraPaths } = require("../config")
@@ -1,4 +1,3 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
4
3
  export = [
data/package/swc/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint global-require: 0 */
2
1
  /* eslint import/no-dynamic-require: 0 */
3
2
 
4
3
  import { resolve } from "path"
@@ -18,7 +17,6 @@ const isTypescriptFile = (filename: string): boolean =>
18
17
  const getCustomConfig = (): Partial<RuleSetRule> => {
19
18
  const path = resolve("config", "swc.config.js")
20
19
  if (existsSync(path)) {
21
- // eslint-disable-next-line @typescript-eslint/no-require-imports
22
20
  return require(path)
23
21
  }
24
22
  return {}
data/package/types.ts CHANGED
@@ -15,6 +15,7 @@ export interface Config {
15
15
  source_entry_path: string
16
16
  nested_entries: boolean
17
17
  css_extract_ignore_order_warnings: boolean
18
+ css_modules_export_mode?: "named" | "default"
18
19
  public_root_path: string
19
20
  public_output_path: string
20
21
  private_output_path?: string
@@ -53,8 +54,6 @@ export interface Env {
53
54
  type Header =
54
55
  | Array<{ key: string; value: string }>
55
56
  | Record<string, string | string[]>
56
- type ServerType = "http" | "https" | "spdy"
57
- type WebSocketType = "sockjs" | "ws"
58
57
 
59
58
  /**
60
59
  * This has the same keys and behavior as https://webpack.js.org/configuration/dev-server/ except:
@@ -63,7 +62,7 @@ type WebSocketType = "sockjs" | "ws"
63
62
  * @see {import('webpack-dev-server').Configuration}
64
63
  */
65
64
  export interface DevServerConfig {
66
- allowed_hosts?: "all" | "auto" | string | string[]
65
+ allowed_hosts?: string | string[]
67
66
  bonjour?: boolean | Record<string, unknown> // bonjour.BonjourOptions
68
67
  client?: Record<string, unknown> // Client
69
68
  compress?: boolean
@@ -71,7 +70,7 @@ export interface DevServerConfig {
71
70
  headers?: Header | (() => Header)
72
71
  history_api_fallback?: boolean | Record<string, unknown> // HistoryApiFallbackOptions
73
72
  hmr?: "only" | boolean
74
- host?: "local-ip" | "local-ipv4" | "local-ipv6" | string
73
+ host?: string
75
74
  http2?: boolean
76
75
  https?: boolean | https.ServerOptions
77
76
  ipc?: boolean | string
@@ -85,23 +84,21 @@ export interface DevServerConfig {
85
84
  | string[]
86
85
  | Record<string, unknown>
87
86
  | Record<string, unknown>[]
88
- port?: "auto" | string | number
87
+ port?: string | number
89
88
  proxy?: unknown // ProxyConfigMap | ProxyConfigArray
90
89
  setup_exit_signals?: boolean
91
- static?: boolean | string | unknown // Static | Array<string | Static>
92
- watch_files?: string | string[] | unknown // WatchFiles | Array<WatchFiles | string>
90
+ static?: unknown // Static | Array<string | Static>
91
+ watch_files?: unknown // WatchFiles | Array<WatchFiles | string>
93
92
  web_socket_server?:
94
93
  | string
95
94
  | boolean
96
- | WebSocketType
97
95
  | {
98
- type?: string | boolean | WebSocketType
96
+ type?: string | boolean
99
97
  options?: Record<string, unknown>
100
98
  }
101
99
  server?:
102
100
  | string
103
101
  | boolean
104
- | ServerType
105
- | { type?: string | boolean | ServerType; options?: https.ServerOptions }
102
+ | { type?: string | boolean; options?: https.ServerOptions }
106
103
  [otherWebpackDevServerConfigKey: string]: unknown
107
104
  }
@@ -18,24 +18,20 @@ const isDebugMode = (): boolean => {
18
18
 
19
19
  const debug = (message: string, ...args: unknown[]): void => {
20
20
  if (isDebugMode()) {
21
- // eslint-disable-next-line no-console
22
21
  console.log(`[Shakapacker] ${message}`, ...args)
23
22
  }
24
23
  }
25
24
 
26
25
  const warn = (message: string, ...args: unknown[]): void => {
27
- // eslint-disable-next-line no-console
28
26
  console.warn(`[Shakapacker] WARNING: ${message}`, ...args)
29
27
  }
30
28
 
31
29
  const error = (message: string, ...args: unknown[]): void => {
32
- // eslint-disable-next-line no-console
33
30
  console.error(`[Shakapacker] ERROR: ${message}`, ...args)
34
31
  }
35
32
 
36
33
  const info = (message: string, ...args: unknown[]): void => {
37
34
  if (isDebugMode()) {
38
- // eslint-disable-next-line no-console
39
35
  console.info(`[Shakapacker] INFO: ${message}`, ...args)
40
36
  }
41
37
  }
@@ -1,18 +1,19 @@
1
- /* eslint global-require: 0 */
1
+ import type { Config } from "../types"
2
+
2
3
  const { canProcess, moduleExists } = require("./helpers")
3
4
  const { requireOrError } = require("./requireOrError")
4
- const config = require("../config")
5
+ const config = require("../config") as Config
5
6
  const inliningCss = require("./inliningCss")
6
7
 
7
8
  interface StyleRule {
8
9
  test: RegExp
9
- use: any[]
10
+ use: unknown[]
10
11
  type?: string
11
12
  }
12
13
 
13
14
  const getStyleRule = (
14
15
  test: RegExp,
15
- preprocessors: any[] = []
16
+ preprocessors: unknown[] = []
16
17
  ): StyleRule | null => {
17
18
  if (moduleExists("css-loader")) {
18
19
  const tryPostcss = () =>
@@ -28,6 +29,11 @@ const getStyleRule = (
28
29
  ? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
29
30
  : requireOrError("mini-css-extract-plugin").loader
30
31
 
32
+ // Determine CSS Modules export mode based on configuration
33
+ // 'named' (default): Use named exports with camelCaseOnly (v9 behavior)
34
+ // 'default': Use default exports with camelCase (v8 behavior)
35
+ const useNamedExports = config.css_modules_export_mode !== "default"
36
+
31
37
  const use = [
32
38
  inliningCss ? "style-loader" : extractionPlugin,
33
39
  {
@@ -37,11 +43,13 @@ const getStyleRule = (
37
43
  importLoaders: 2,
38
44
  modules: {
39
45
  auto: true,
40
- // v9 defaults: Use named exports with camelCase conversion
41
- // Note: css-loader requires 'camelCaseOnly' or 'dashesOnly' when namedExport is true
42
- // Using 'camelCase' with namedExport: true causes a build error
43
- namedExport: true,
44
- exportLocalsConvention: "camelCaseOnly"
46
+ // Use named exports for v9 (default), or default exports for v8 compatibility
47
+ namedExport: useNamedExports,
48
+ // 'camelCaseOnly' with namedExport: true (v9 default)
49
+ // 'camelCase' with namedExport: false (v8 behavior - exports both original and camelCase)
50
+ exportLocalsConvention: useNamedExports
51
+ ? "camelCaseOnly"
52
+ : "camelCase"
45
53
  }
46
54
  }
47
55
  },
@@ -38,6 +38,11 @@ const loaderMatches = <T = unknown>(
38
38
  loaderToCheck: string,
39
39
  fn: () => T
40
40
  ): T | null => {
41
+ // If transpiler is set to 'none', skip all transpiler rules (for custom webpack configs)
42
+ if (configLoader === "none") {
43
+ return null
44
+ }
45
+
41
46
  if (configLoader !== loaderToCheck) {
42
47
  return null
43
48
  }
@@ -59,15 +64,14 @@ const loaderMatches = <T = unknown>(
59
64
 
60
65
  const packageFullVersion = (packageName: string): string => {
61
66
  try {
62
- // eslint-disable-next-line import/no-dynamic-require
63
67
  const packageJsonPath = require.resolve(`${packageName}/package.json`)
64
- // eslint-disable-next-line import/no-dynamic-require, global-require
68
+ // eslint-disable-next-line import/no-dynamic-require
65
69
  const packageJson = require(packageJsonPath) as { version: string }
66
70
  return packageJson.version
67
- } catch (error: any) {
71
+ } catch (error: unknown) {
68
72
  // Re-throw the error with proper code to maintain compatibility with babel preset
69
73
  // The preset expects MODULE_NOT_FOUND errors to handle missing core-js gracefully
70
- if (error.code === "MODULE_NOT_FOUND") {
74
+ if ((error as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") {
71
75
  throw error
72
76
  }
73
77
  // For other errors, warn and re-throw
@@ -28,26 +28,87 @@ export function isPathTraversalSafe(inputPath: string): boolean {
28
28
  /**
29
29
  * Resolves and validates a path within a base directory
30
30
  * Prevents directory traversal attacks by ensuring the resolved path
31
- * stays within the base directory
31
+ * stays within the base directory.
32
+ * Also resolves symlinks to prevent symlink-based path traversal attacks.
33
+ *
34
+ * @param basePath - The base directory to validate against
35
+ * @param userPath - The user-provided path to validate
36
+ * @param resolveSymlinks - Whether to resolve symlinks (default: true for security)
37
+ * @returns The validated absolute path
38
+ * @throws Error if path is outside base directory
32
39
  */
33
- export function safeResolvePath(basePath: string, userPath: string): string {
34
- // Normalize the base path
35
- const normalizedBase = path.resolve(basePath)
40
+ export function safeResolvePath(
41
+ basePath: string,
42
+ userPath: string,
43
+ resolveSymlinks = true
44
+ ): string {
45
+ // Resolve the base path through symlinks if enabled
46
+ let normalizedBase: string
47
+ try {
48
+ normalizedBase = resolveSymlinks
49
+ ? fs.realpathSync(basePath)
50
+ : path.resolve(basePath)
51
+ } catch (error: unknown) {
52
+ // If basePath doesn't exist (ENOENT), fall back to path.resolve
53
+ // Rethrow other errors (e.g., permission issues) as they indicate real problems
54
+ const nodeError = error as NodeJS.ErrnoException
55
+ if (nodeError?.code === "ENOENT") {
56
+ normalizedBase = path.resolve(basePath)
57
+ } else {
58
+ throw error
59
+ }
60
+ }
61
+
62
+ // For paths that may not exist yet, validate the parent directory
63
+ const absolutePath = path.resolve(basePath, userPath)
64
+ const parentDir = path.dirname(absolutePath)
65
+ const fileName = path.basename(absolutePath)
66
+
67
+ // Resolve parent directory through symlinks if it exists and symlink resolution is enabled
68
+ let resolvedParent: string
69
+ try {
70
+ resolvedParent = resolveSymlinks
71
+ ? fs.realpathSync(parentDir)
72
+ : path.resolve(parentDir)
73
+ } catch (error: unknown) {
74
+ // If parent doesn't exist (ENOENT), validate the absolute path as-is
75
+ // Rethrow other errors (e.g., permission issues) as they indicate real problems
76
+ const nodeError = error as NodeJS.ErrnoException
77
+ if (nodeError?.code === "ENOENT") {
78
+ if (
79
+ !absolutePath.startsWith(normalizedBase + path.sep) &&
80
+ absolutePath !== normalizedBase
81
+ ) {
82
+ throw new Error(
83
+ `[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
84
+ `Requested path would resolve outside of allowed directory.\n` +
85
+ `Base: ${normalizedBase}\n` +
86
+ `Attempted: ${userPath}\n` +
87
+ `Resolved to: ${absolutePath}`
88
+ )
89
+ }
90
+ return absolutePath
91
+ }
92
+ throw error
93
+ }
36
94
 
37
- // Resolve the user path relative to base
38
- const resolved = path.resolve(normalizedBase, userPath)
95
+ // Reconstruct the full path with the resolved (symlink-free) parent
96
+ const resolved = path.resolve(resolvedParent, fileName)
39
97
 
40
98
  // Ensure the resolved path is within the base directory
41
99
  if (
42
100
  !resolved.startsWith(normalizedBase + path.sep) &&
43
101
  resolved !== normalizedBase
44
102
  ) {
103
+ const symlinkNote = resolveSymlinks
104
+ ? ` (symlink-resolved from ${userPath})`
105
+ : ""
45
106
  throw new Error(
46
107
  `[SHAKAPACKER SECURITY] Path traversal attempt detected.\n` +
47
108
  `Requested path would resolve outside of allowed directory.\n` +
48
109
  `Base: ${normalizedBase}\n` +
49
110
  `Attempted: ${userPath}\n` +
50
- `Resolved to: ${resolved}`
111
+ `Resolved to: ${resolved}${symlinkNote}`
51
112
  )
52
113
  }
53
114
 
@@ -77,17 +138,16 @@ export function validatePaths(paths: string[], basePath: string): string[] {
77
138
  console.warn(
78
139
  `[SHAKAPACKER WARNING] Skipping potentially unsafe path: ${userPath}`
79
140
  )
80
- continue
81
- }
82
-
83
- try {
84
- const safePath = safeResolvePath(basePath, userPath)
85
- validatedPaths.push(safePath)
86
- } catch (error) {
87
- console.warn(
88
- `[SHAKAPACKER WARNING] Invalid path configuration: ${userPath}\n` +
89
- `Error: ${error instanceof Error ? error.message : String(error)}`
90
- )
141
+ } else {
142
+ try {
143
+ const safePath = safeResolvePath(basePath, userPath)
144
+ validatedPaths.push(safePath)
145
+ } catch (error) {
146
+ console.warn(
147
+ `[SHAKAPACKER WARNING] Invalid path configuration: ${userPath}\n` +
148
+ `Error: ${error instanceof Error ? error.message : String(error)}`
149
+ )
150
+ }
91
151
  }
92
152
  }
93
153