shakapacker 9.6.1 → 9.7.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.
data/docs/rspack.md CHANGED
@@ -4,9 +4,33 @@ Shakapacker supports [Rspack](https://rspack.rs) as an alternative assets bundle
4
4
 
5
5
  **📖 For configuration options, see the [Configuration Guide](./configuration.md)**
6
6
 
7
+ ## Version Compatibility
8
+
9
+ Shakapacker supports both Rspack v1 (`^1.0.0`) and Rspack v2 (`^2.0.0-0`). No configuration changes are needed when upgrading between rspack versions — shakapacker's generated config works with both.
10
+
11
+ **Rspack v2 note:** Rspack v2 ships as a pure ESM package and requires **Node.js 20.19.0+**.
12
+
13
+ **Rspack v1 note:** Rspack v1 itself supports older Node versions, but Shakapacker requires Node 20+.
14
+
15
+ **React refresh plugin note:** `@rspack/plugin-react-refresh` currently remains on the v1 line in Shakapacker peer deps.
16
+
17
+ **Current CI coverage note:** Shakapacker currently validates rspack v2 using `2.0.0-beta.6`. The rspack v2 dev dependencies are intentionally pinned while v2 is in beta and should be revisited when stable `2.0.0` is released.
18
+
19
+ ### Why upgrade to Rspack v2?
20
+
21
+ - **Persistent cache with proper invalidation** — Rspack v2 promotes persistent caching (`cache.type: 'filesystem'`) from experimental to stable, with portable cache support (`cache.portable`) and read-only cache for CI (`cache.readonly`). This means fast rebuilds that survive process restarts and are properly invalidated when dependencies change.
22
+ - **Incremental compilation (stable)** — The `incremental` option moves from `experiments` to a top-level config, signaling it's production-ready. Incremental builds skip unchanged work in the dependency graph.
23
+ - **Better tree shaking** — CJS `require()` destructuring and variable property access are now tree-shaken, and Module Federation shares can be tree-shaken.
24
+ - **Unified target configuration** — A single `target` setting now propagates defaults to SWC and LightningCSS automatically, eliminating redundant per-loader configuration.
25
+ - **Stricter export validation** — `exportsPresence` defaults to `'error'`, catching missing or misspelled exports at build time instead of silently producing broken bundles.
26
+ - **React Server Components** — Built-in RSC support for frameworks.
27
+ - **Performance** — Dozens of Rust-level optimizations across every beta release (hash caching, regex fast paths, reduced allocations, rayon parallelism).
28
+
29
+ See the [Rspack v2 breaking changes discussion](https://github.com/web-infra-dev/rspack/discussions/9270) for full details.
30
+
7
31
  ## Installation
8
32
 
9
- First, install the required Rspack dependencies:
33
+ Install the required Rspack dependencies:
10
34
 
11
35
  ```bash
12
36
  npm install @rspack/core @rspack/cli -D
@@ -296,7 +296,8 @@ module Shakapacker
296
296
  if child_pid
297
297
  Process.kill("TERM", child_pid)
298
298
  else
299
- raise SignalException, "TERM" # if there is no child_pid we never spawned the process and can quit as normal
299
+ # Signal arrived before spawn completed; re-raise so the process exits normally.
300
+ raise SignalException, "TERM"
300
301
  end
301
302
  rescue Errno::ESRCH
302
303
  nil
@@ -1,4 +1,4 @@
1
1
  module Shakapacker
2
2
  # Change the version in package.json too, please!
3
- VERSION = "9.6.1".freeze
3
+ VERSION = "9.7.0".freeze
4
4
  end
@@ -712,6 +712,17 @@ async function runAllBuildsCommand(options: ExportOptions): Promise<number> {
712
712
  // Apply defaults
713
713
  const resolvedOptions = applyDefaults(options)
714
714
 
715
+ // Validate paths for security in all-builds mode.
716
+ // saveDir is always set by applyDefaults(); --output is not used in --all-builds mode.
717
+ safeResolvePath(appRoot, resolvedOptions.saveDir!)
718
+
719
+ // Keep in sync with validation in run()
720
+ if (resolvedOptions.annotate && resolvedOptions.format !== "yaml") {
721
+ throw new Error(
722
+ "Annotation requires YAML format. Use --no-annotate or --format=yaml."
723
+ )
724
+ }
725
+
715
726
  const loader = new ConfigFileLoader(resolvedOptions.configFile)
716
727
  if (!loader.exists()) {
717
728
  const configPath = resolvedOptions.configFile || DEFAULT_CONFIG_FILE
@@ -58,9 +58,14 @@ const getPlugins = (): unknown[] => {
58
58
  config.integrity?.enabled &&
59
59
  moduleExists("webpack-subresource-integrity")
60
60
  ) {
61
- const SubresourceIntegrityPlugin = requireOrError(
61
+ // webpack-subresource-integrity v5+ exports the plugin as a named export.
62
+ const subresourceIntegrityModule = requireOrError(
62
63
  "webpack-subresource-integrity"
63
64
  )
65
+ const SubresourceIntegrityPlugin =
66
+ "SubresourceIntegrityPlugin" in subresourceIntegrityModule
67
+ ? subresourceIntegrityModule.SubresourceIntegrityPlugin
68
+ : subresourceIntegrityModule
64
69
  plugins.push(
65
70
  new SubresourceIntegrityPlugin({
66
71
  hashFuncNames: config.integrity.hash_functions,
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "9.6.1",
3
+ "version": "9.7.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": {
@@ -51,8 +51,8 @@
51
51
  "devDependencies": {
52
52
  "@eslint/eslintrc": "^3.2.0",
53
53
  "@eslint/js": "^9.37.0",
54
- "@rspack/cli": "^1.5.8",
55
- "@rspack/core": "^1.5.8",
54
+ "@rspack/cli": "2.0.0-beta.6",
55
+ "@rspack/core": "2.0.0-beta.6",
56
56
  "@swc/core": "^1.3.0",
57
57
  "@types/babel__core": "^7.20.5",
58
58
  "@types/js-yaml": "^4.0.9",
@@ -99,8 +99,8 @@
99
99
  "@babel/plugin-transform-runtime": "^7.17.0",
100
100
  "@babel/preset-env": "^7.16.11",
101
101
  "@babel/runtime": "^7.17.9",
102
- "@rspack/cli": "^1.0.0",
103
- "@rspack/core": "^1.0.0",
102
+ "@rspack/cli": "^1.0.0 || ^2.0.0-0",
103
+ "@rspack/core": "^1.0.0 || ^2.0.0-0",
104
104
  "@rspack/plugin-react-refresh": "^1.0.0",
105
105
  "@swc/core": "^1.3.0",
106
106
  "@types/babel__core": "^7.0.0",
@@ -5,6 +5,43 @@
5
5
  * then re-require bundlerUtils to exercise the rspack branches.
6
6
  */
7
7
 
8
+ // Mock requireOrError to provide a fake @rspack/core (v2 is pure ESM, can't be require()'d by Jest)
9
+ jest.mock("../../package/utils/requireOrError", () => {
10
+ const CssExtractRspackPlugin = jest.fn(function (options) {
11
+ this.options = options
12
+ })
13
+ CssExtractRspackPlugin.loader = "css-extract-rspack-loader"
14
+
15
+ return {
16
+ requireOrError: (moduleName) => {
17
+ if (moduleName === "@rspack/core") {
18
+ return {
19
+ DefinePlugin: jest.fn(function (definitions) {
20
+ this.definitions = definitions
21
+ }),
22
+ EnvironmentPlugin: jest.fn(function (env) {
23
+ this.env = env
24
+ }),
25
+ ProvidePlugin: jest.fn(function (definitions) {
26
+ this.definitions = definitions
27
+ }),
28
+ HotModuleReplacementPlugin: jest.fn(),
29
+ ProgressPlugin: jest.fn(),
30
+ CssExtractRspackPlugin,
31
+ SubresourceIntegrityPlugin: jest.fn(function (options) {
32
+ this.options = options
33
+ }),
34
+ SwcJsMinimizerRspackPlugin: jest.fn(),
35
+ LightningCssMinimizerRspackPlugin: jest.fn()
36
+ }
37
+ }
38
+ return jest
39
+ .requireActual("../../package/utils/requireOrError")
40
+ .requireOrError(moduleName)
41
+ }
42
+ }
43
+ })
44
+
8
45
  let bundlerUtils
9
46
 
10
47
  describe("bundlerUtils with rspack", () => {
@@ -80,7 +117,9 @@ describe("bundlerUtils with rspack", () => {
80
117
  test("returns rspack DefinePlugin", () => {
81
118
  const DefinePlugin = bundlerUtils.getDefinePlugin()
82
119
  expect(DefinePlugin).toBeDefined()
83
- expect(DefinePlugin).toBeInstanceOf(Function)
120
+
121
+ const instance = new DefinePlugin({ FOO: "bar" })
122
+ expect(instance.definitions).toStrictEqual({ FOO: "bar" })
84
123
  })
85
124
  })
86
125
 
@@ -88,7 +127,9 @@ describe("bundlerUtils with rspack", () => {
88
127
  test("returns rspack EnvironmentPlugin", () => {
89
128
  const EnvironmentPlugin = bundlerUtils.getEnvironmentPlugin()
90
129
  expect(EnvironmentPlugin).toBeDefined()
91
- expect(EnvironmentPlugin).toBeInstanceOf(Function)
130
+
131
+ const instance = new EnvironmentPlugin(["NODE_ENV"])
132
+ expect(instance.env).toStrictEqual(["NODE_ENV"])
92
133
  })
93
134
  })
94
135
 
@@ -96,7 +137,9 @@ describe("bundlerUtils with rspack", () => {
96
137
  test("returns rspack ProvidePlugin", () => {
97
138
  const ProvidePlugin = bundlerUtils.getProvidePlugin()
98
139
  expect(ProvidePlugin).toBeDefined()
99
- expect(ProvidePlugin).toBeInstanceOf(Function)
140
+
141
+ const instance = new ProvidePlugin({ React: "react" })
142
+ expect(instance.definitions).toStrictEqual({ React: "react" })
100
143
  })
101
144
  })
102
145
  })
@@ -455,5 +455,37 @@ describe("configExporter", () => {
455
455
  parseArguments(["--all-builds", "--save-dir=./configs"])
456
456
  }).not.toThrow()
457
457
  })
458
+
459
+ test("run rejects --all-builds with annotate and non-yaml format", async () => {
460
+ const { run } = require("../../package/configExporter/cli")
461
+ const mockConsoleError = jest
462
+ .spyOn(console, "error")
463
+ .mockImplementation(() => {})
464
+
465
+ const result = await run(["--all-builds", "--annotate", "--format=json"])
466
+
467
+ expect(result).toBe(1)
468
+ expect(mockConsoleError).toHaveBeenCalledWith(
469
+ expect.stringContaining("Annotation requires YAML format")
470
+ )
471
+
472
+ mockConsoleError.mockRestore()
473
+ })
474
+
475
+ test("run validates --all-builds save-dir path traversal", async () => {
476
+ const { run } = require("../../package/configExporter/cli")
477
+ const mockConsoleError = jest
478
+ .spyOn(console, "error")
479
+ .mockImplementation(() => {})
480
+
481
+ const result = await run(["--all-builds", "--save-dir=../outside"])
482
+
483
+ expect(result).toBe(1)
484
+ expect(mockConsoleError).toHaveBeenCalledWith(
485
+ expect.stringContaining("[SHAKAPACKER SECURITY] Path traversal attempt")
486
+ )
487
+
488
+ mockConsoleError.mockRestore()
489
+ })
458
490
  })
459
491
  })
@@ -0,0 +1,89 @@
1
+ const loadPluginsWithSriModule = (sriModule) => {
2
+ let getPlugins
3
+
4
+ jest.isolateModules(() => {
5
+ jest.doMock("../../../package/config", () => ({
6
+ manifestPath: "public/packs/manifest.json",
7
+ publicPathWithoutCDN: "/packs/",
8
+ integrity: {
9
+ enabled: true,
10
+ hash_functions: ["sha256"]
11
+ },
12
+ css_extract_ignore_order_warnings: false,
13
+ useContentHash: false
14
+ }))
15
+
16
+ jest.doMock("../../../package/env", () => ({
17
+ isProduction: true
18
+ }))
19
+
20
+ jest.doMock("../../../package/utils/helpers", () => ({
21
+ moduleExists: (moduleName) =>
22
+ moduleName === "webpack-subresource-integrity"
23
+ }))
24
+
25
+ jest.doMock("../../../package/utils/ensureManifestExists", () => ({
26
+ __esModule: true,
27
+ default: jest.fn()
28
+ }))
29
+
30
+ jest.doMock("../../../package/utils/requireOrError", () => ({
31
+ requireOrError: (moduleName) => {
32
+ if (moduleName === "webpack-assets-manifest") {
33
+ return function WebpackAssetsManifest() {}
34
+ }
35
+ if (moduleName === "webpack") {
36
+ return {
37
+ EnvironmentPlugin: function EnvironmentPlugin() {}
38
+ }
39
+ }
40
+ if (moduleName === "webpack-subresource-integrity") {
41
+ return sriModule
42
+ }
43
+
44
+ throw new Error(`Unexpected module request: ${moduleName}`)
45
+ }
46
+ }))
47
+ ;({ getPlugins } = require("../../../package/plugins/webpack"))
48
+ })
49
+
50
+ return getPlugins
51
+ }
52
+
53
+ describe("webpack plugins - webpack-subresource-integrity compatibility", () => {
54
+ afterEach(() => {
55
+ jest.clearAllMocks()
56
+ })
57
+
58
+ test("supports webpack-subresource-integrity v5 named export", () => {
59
+ const SubresourceIntegrityPlugin = jest.fn(
60
+ function SubresourceIntegrityPluginMock(options) {
61
+ this.options = options
62
+ }
63
+ )
64
+ const getPlugins = loadPluginsWithSriModule({ SubresourceIntegrityPlugin })
65
+
66
+ getPlugins()
67
+
68
+ expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
69
+ hashFuncNames: ["sha256"],
70
+ enabled: true
71
+ })
72
+ })
73
+
74
+ test("supports webpack-subresource-integrity default export", () => {
75
+ const SubresourceIntegrityPlugin = jest.fn(
76
+ function SubresourceIntegrityPluginMock(options) {
77
+ this.options = options
78
+ }
79
+ )
80
+ const getPlugins = loadPluginsWithSriModule(SubresourceIntegrityPlugin)
81
+
82
+ getPlugins()
83
+
84
+ expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
85
+ hashFuncNames: ["sha256"],
86
+ enabled: true
87
+ })
88
+ })
89
+ })
@@ -1,4 +1,4 @@
1
- /* eslint-disable jest/no-conditional-in-test */
1
+ /* eslint-disable func-names, jest/no-conditional-in-test */
2
2
 
3
3
  const { chdirTestApp } = require("../../helpers")
4
4
 
@@ -29,6 +29,48 @@ jest.mock("../../../package/utils/validateDependencies", () => ({
29
29
  validateRspackDependencies: jest.fn()
30
30
  }))
31
31
 
32
+ // Mock requireOrError to provide a fake @rspack/core (v2 is pure ESM, can't be require()'d by Jest)
33
+ jest.mock("../../../package/utils/requireOrError", () => ({
34
+ requireOrError: (moduleName) => {
35
+ if (moduleName === "@rspack/core") {
36
+ const CssExtractRspackPlugin = jest.fn(function (options) {
37
+ this.options = options
38
+ })
39
+ CssExtractRspackPlugin.loader = "css-extract-rspack-loader"
40
+
41
+ return {
42
+ DefinePlugin: jest.fn(function (definitions) {
43
+ this.definitions = definitions
44
+ }),
45
+ EnvironmentPlugin: jest.fn(function (env) {
46
+ this.env = env
47
+ }),
48
+ ProvidePlugin: jest.fn(function (definitions) {
49
+ this.definitions = definitions
50
+ }),
51
+ HotModuleReplacementPlugin: jest.fn(),
52
+ ProgressPlugin: jest.fn(),
53
+ CssExtractRspackPlugin,
54
+ SubresourceIntegrityPlugin: jest.fn(function (options) {
55
+ this.options = options
56
+ }),
57
+ SwcJsMinimizerRspackPlugin: jest.fn(),
58
+ LightningCssMinimizerRspackPlugin: jest.fn()
59
+ }
60
+ }
61
+ if (moduleName === "rspack-manifest-plugin") {
62
+ return {
63
+ RspackManifestPlugin: jest.fn(function (options) {
64
+ this.options = options
65
+ })
66
+ }
67
+ }
68
+ return jest
69
+ .requireActual("../../../package/utils/requireOrError")
70
+ .requireOrError(moduleName)
71
+ }
72
+ }))
73
+
32
74
  describe("rspack/index", () => {
33
75
  let rspackIndex
34
76
  let validateRspackDependencies