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.
- checksums.yaml +4 -4
- data/.claude/commands/address-review.md +74 -21
- data/.claude/commands/update-changelog.md +86 -49
- data/.github/workflows/claude-code-review.yml +1 -1
- data/CHANGELOG.md +13 -1
- data/Rakefile +5 -0
- data/docs/node_package_api.md +24 -25
- data/docs/releasing.md +23 -17
- data/docs/rspack.md +25 -1
- data/lib/shakapacker/runner.rb +2 -1
- data/lib/shakapacker/version.rb +1 -1
- data/package/configExporter/cli.ts +11 -0
- data/package/plugins/webpack.ts +6 -1
- data/package.json +5 -5
- data/test/package/bundlerUtils.rspack.test.js +46 -3
- data/test/package/configExporter.test.js +32 -0
- data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
- data/test/package/rspack/index.test.js +43 -1
- data/yarn.lock +92 -225
- metadata +4 -2
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
|
-
|
|
33
|
+
Install the required Rspack dependencies:
|
|
10
34
|
|
|
11
35
|
```bash
|
|
12
36
|
npm install @rspack/core @rspack/cli -D
|
data/lib/shakapacker/runner.rb
CHANGED
|
@@ -296,7 +296,8 @@ module Shakapacker
|
|
|
296
296
|
if child_pid
|
|
297
297
|
Process.kill("TERM", child_pid)
|
|
298
298
|
else
|
|
299
|
-
|
|
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
|
data/lib/shakapacker/version.rb
CHANGED
|
@@ -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
|
data/package/plugins/webpack.ts
CHANGED
|
@@ -58,9 +58,14 @@ const getPlugins = (): unknown[] => {
|
|
|
58
58
|
config.integrity?.enabled &&
|
|
59
59
|
moduleExists("webpack-subresource-integrity")
|
|
60
60
|
) {
|
|
61
|
-
|
|
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.
|
|
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": "
|
|
55
|
-
"@rspack/core": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|