Skip to content

Commit 99e042c

Browse files
committed
Migration to Rspack
1 parent ed679f6 commit 99e042c

40 files changed

+2099
-760
lines changed

README.md

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# React Proto - React TypeScript Boilerplate
22

3-
![node.js@22](https://img.shields.io/badge/node.js-22-339933?style=for-the-badge&logo=nodedotjs) ![typescript@5](https://img.shields.io/badge/typescript-5-3178C6?style=for-the-badge&logo=typescript) ![reactjs@18](https://img.shields.io/badge/Reactjs-18-61DAFB?style=for-the-badge&logo=react) ![webpack@5](https://img.shields.io/badge/webpack-5-8dd6f9?style=for-the-badge&logo=webpack) ![sass@1.7](https://img.shields.io/badge/sass-1.7-CC6699?style=for-the-badge&logo=sass) ![ts-standard](https://img.shields.io/badge/standard-ts-F3DF49?style=for-the-badge&logo=standardjs)
3+
![node.js@22](https://img.shields.io/badge/node.js-22-339933?style=for-the-badge&logo=nodedotjs) ![typescript@5](https://img.shields.io/badge/typescript-5-3178C6?style=for-the-badge&logo=typescript) ![reactjs@18](https://img.shields.io/badge/Reactjs-18-61DAFB?style=for-the-badge&logo=react) ![rspack@1](https://img.shields.io/badge/rspack-1-f0965b?style=for-the-badge) ![webpack@5](https://img.shields.io/badge/webpack-5-8dd6f9?style=for-the-badge&logo=webpack) ![sass@1.7](https://img.shields.io/badge/sass-1.7-CC6699?style=for-the-badge&logo=sass) ![ts-standard](https://img.shields.io/badge/standard-ts-F3DF49?style=for-the-badge&logo=standardjs)
44

55
<img align="right" width="100" src="src/assets/images/logo.png">
66

@@ -10,6 +10,14 @@ This project is a compilation of different approaches in React development that
1010

1111
You can also check a [React Proto Lite](https://github.com/StopNGo/react-proto-lite) - Template React project for fast SPA prototyping. It contains only everything necessary for Single Page Application projects without any server side parts.
1212

13+
## Huge Update: Migrating to Rspack
14+
15+
Starting from version `2.0.0`, this project uses [Rspack](https://rspack.dev/) as the primary bundler.
16+
17+
Rspack is a high performance JavaScript bundler written in Rust. It offers strong compatibility with the webpack ecosystem, allowing for seamless replacement of webpack, and provides lightning fast build speeds.
18+
19+
Webpack is still available as an option ([rspack vs webpack](#rspack-vs-webpack) and [switching back to webpack](#switching-back-to-webpack)).
20+
1321
---
1422
- [Issue](#issue)
1523
- [What's Inside](#whats-inside)
@@ -26,7 +34,7 @@ You can also check a [React Proto Lite](https://github.com/StopNGo/react-proto-l
2634

2735
Every new React developer knows that React is a library, not a complete framework. Thus, it provides maximum flexibility. However, a lot of knowledge is required to create a fully functional web application powered with React.
2836

29-
That is why there exist such a famous framework as [Next.js](https://nextjs.org/) as well as a tool [Create React App (CRA)](https://create-react-app.dev/).
37+
That is why there exist such a famous framework as [Next.js](https://nextjs.org/) as well as a tool [Create React App (CRA)](https://create-react-app.dev/) or [Rsbuild for React](https://rsbuild.dev/guide/framework/react).
3038

3139
Despite the advantages that such tools have, there are some cons that their user may face:
3240

@@ -46,7 +54,7 @@ Thus, the goal of this project is to **collect in one place all the most common
4654
Core:
4755

4856
- **React** 18+ (**Preact** 10+ as an option, see [comparison](#react-vs-preact) below)
49-
- **webpack** 5+ (with optional **SWC** support and SSR or static build; [why not Vite?](#why-not-vite))
57+
- **Rspack** 1 (**webpack** 5+ as an option) with **SWC** support and SSR or static build ([why not Vite?](#why-not-vite), [rspack vs webpack](#rspack-vs-webpack) and [switching back to webpack](#switching-back-to-webpack))
5058
- **TypeScript** (with strict rules, including webpack configuration)
5159

5260
SSR:
@@ -130,15 +138,17 @@ Live preview:
130138

131139
`git clone https://github.com/StopNGo/react-proto`
132140

133-
2. Install all packages:
141+
2. Delete the `_webpack` folder if you are not going to use webpack bundler or [switch to it](#switching-back-to-webpack) before installing the packages.
142+
143+
3. Install all packages:
134144

135145
`npm i`
136146

137-
3. Run project in a development mode:
147+
4. Run project in a development mode:
138148

139149
`npm start`
140150

141-
4. Open your browser with the next address:
151+
5. Open your browser with the next address:
142152

143153
`http://localhost:8080/`
144154

@@ -176,6 +186,20 @@ Live preview:
176186

177187
`npm run build:static:report`
178188

189+
### Switching back to webpack
190+
191+
- Copy the contents of `_webpack` folder (except `README.md`) to the root of the project.
192+
193+
- Delete the `rspack.config.ts` file.
194+
195+
- Delete the `rspack` and `_webpack` folders.
196+
197+
- If you have a previous installation, clean the `node_modules` folder.
198+
199+
- Then install the packages:
200+
201+
`npm i`
202+
179203
### Updating packages
180204

181205
All packages in this project are pinned to latest versions at the time of publishing to exclude version-based conflicts and errors and to guarantee proper work of the code in this repository.
@@ -203,19 +227,43 @@ Vite is an excellent new generation bundler that could speed up your development
203227

204228
As for the speed: you can check this article - [Storybook Performance: Vite vs Webpack](https://storybook.js.org/blog/storybook-performance-from-webpack-to-vite/). As you can see - Webpack could still be fast enough. React Proto has such configurations. In `webpack\constants.ts` you can switch on SWC and Lazy Compilation.
205229

206-
Also, I'm looking forward to [Turbopack](https://turbo.build/pack) - the Rust-powered successor to Webpack. Now it is available only in Next.js, but I hope the future migration from the Wepback will be smooth because the principle of configuration should be the same.
230+
Starting from version `2.0.0`, this project uses [Rspack](https://rspack.dev/) as the primary bundler. This bundler written in Rust and offers strong compatibility with the webpack ecosystem, so, performance should be much better ([rspack vs webpack](#rspack-vs-webpack)).
231+
232+
I'm also looking forward to [Turbopack](https://turbo.build/pack) — another Rust-powered successor to Webpack. Currently, it's available only in Next.js, but it might be released as a standalone CLI tool in the future.
233+
234+
### Rspack vs webpack
207235

236+
Rspack is a high performance JavaScript bundler written in Rust. It offers strong compatibility with the webpack ecosystem, allowing for seamless replacement of webpack, and provides lightning fast build speeds.
237+
238+
Here is a comparison between Rspack 1 with the built-in SWC loader and Webpack 5+ with the external SWC loader, while building the SSR version of the sample application on the same hardware configuration:
239+
240+
| | Rspack | webpack |
241+
| ------- | --------- | -------- |
242+
| Server | 5.35 s | 6.83 s |
243+
| Client | 5.32 s | 7.50 s |
244+
245+
Of course, the larger the project, the greater the performance advantage. However, if you need more webpack compatibility or hot module reloading while developing with [Preact](#react-vs-preact), you can always [switch back to the webpack bundler](#switching-back-to-webpack).
246+
247+
Also, the optimization process of Rspack is currently slightly worse. Check the bundle size comparison for the non-SSR version of the sample application in this repository:
248+
249+
| | Rspack | webpack |
250+
| ------- | --------- | -------- |
251+
| Parsed | 284.47 KB | 262.9 KB |
252+
| Gzipped | 90.29 KB | 86.84 KB |
208253
### React vs Preact
209254

210255
In `webpack\constants.ts` you can choose to use [Preact](https://preactjs.com/) library instead React itself (`IS_PREACT` boolean constant).
211256

212-
Preact is a fast and compact React-compatible Virtual DOM library. But because its community is much smaller, you can face with some incompatibility with React API and functionality, especially with new ones. Also some tests show some frame drops during moving a lot of elements. Below you can see a bundle size comparison of no-SSR version of the sample application of this repository (according to Webpack Bundle Analyzer):
257+
Preact is a fast and compact React-compatible Virtual DOM library. But because its community is much smaller, you can face with some incompatibility with React API and functionality, especially with new ones. Also some tests show some frame drops during moving a lot of elements. Below you can see a bundle size comparison of no-SSR version of the sample application of this repository that was built with Rspack (according to Webpack Bundle Analyzer):
213258

214259
| | React | Preact |
215260
| ------- | --------- | -------- |
216261
| Parsed | 262.9 KB | 150.55 KB |
217262
| Gzipped | 86.84 KB | 52.09 KB |
218263

264+
**Important Note**
265+
At the moment, the Rspack version of the project does not support hot module reloading during development with Preact. Compared to Webpack, it requires some additional tricky configuration, which I will probably add in the near future. However, if you need HMR (and you definitely do!), you can develop your project using React and then build it with Preact. Or just [switch your project back to the webpack](#switching-back-to-webpack).
266+
219267
### Why not any common i18n package?
220268
You can freely integrate any React compatible i18n solution. But if React Proto already uses Redux and RTK, why just not use them for this task? Therefore, I have created a custom internationalization solution with a minimum additional code. It supports translations dynamic loading, server side rendering based on user acceptable languages, strict typing, etc. At the moment it just does not support string processing like pluralization, but it could easily be added later.
221269

_webpack/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Webpack Version
2+
3+
To use the webpack version of the project:
4+
5+
1. Copy the contents of this folder (except `README.md`) to the root of the project.
6+
7+
2. Delete the `rspack.config.ts` file.
8+
9+
3. Delete the `rspack` and `_webpack` folders.
10+
11+
4. If you have a previous installation, clean the `node_modules` folder.
12+
13+
5. Then install the packages:
14+
15+
`npm i`
16+
17+
That's it!

_webpack/package.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"name": "react-proto",
3+
"version": "2.0.0",
4+
"description": "React TypeScript Boilerplate",
5+
"author": "Max L Stop&Go",
6+
"license": "ISC",
7+
"sideEffects": [
8+
"*.css",
9+
"*.scss"
10+
],
11+
"scripts": {
12+
"clean": "rimraf dist",
13+
"start:webpack": "cross-env NODE_ENV=development webpack --mode=development",
14+
"start:server": "wait-on dist/server.js && node dist/server.js",
15+
"dev": "npm run clean && npm-run-all --print-label --parallel start:webpack start:server",
16+
"start": "nodemon --exec npm run dev --watch src/server --ext ts,tsx,json ",
17+
"build": "npm run clean && cross-env NODE_ENV=production webpack --mode production",
18+
"build:report": "npm run build --withReport",
19+
"run": "node dist/server.js",
20+
"start:static": "npm run clean && cross-env NODE_ENV=development NO_SSR=true webpack serve --mode development --open",
21+
"build:static": "npm run clean && cross-env NODE_ENV=production NO_SSR=true webpack --mode production",
22+
"build:static:report": "npm run build:static --withReport",
23+
"prettier": "prettier \"src/**/*\" --write --single-quote --no-semi --ignore-unknown --trailing-comma none --jsx-single-quote",
24+
"lint": "ts-standard . && stylelint **/*.{css,scss}",
25+
"lint:fix": "npm run prettier && ts-standard . --fix && stylelint --fix **/*.{css,scss}",
26+
"test": "jest",
27+
"add-comp": "node scripts/add-comp.js"
28+
},
29+
"devDependencies": {
30+
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
31+
"@svgr/webpack": "8.1.0",
32+
"@swc/core": "1.7.26",
33+
"@testing-library/jest-dom": "6.5.0",
34+
"@testing-library/preact": "3.2.4",
35+
"@testing-library/react": "16.0.1",
36+
"@testing-library/user-event": "14.5.2",
37+
"@types/compression": "1.7.5",
38+
"@types/cookie-parser": "1.4.7",
39+
"@types/express": "5.0.0",
40+
"@types/jest": "29.5.13",
41+
"@types/loadable__component": "5.13.9",
42+
"@types/loadable__server": "5.12.11",
43+
"@types/node": "22.7.5",
44+
"@types/react": "18.3.11",
45+
"@types/react-dom": "18.3.0",
46+
"@types/react-helmet": "6.1.11",
47+
"@types/react-router-dom": "5.3.3",
48+
"@types/serialize-javascript": "5.0.4",
49+
"@types/webpack-bundle-analyzer": "4.7.0",
50+
"@types/webpack-env": "1.18.5",
51+
"@types/webpack-hot-middleware": "2.25.9",
52+
"@types/webpack-node-externals": "3.0.4",
53+
"classnames": "2.5.1",
54+
"copy-webpack-plugin": "12.0.2",
55+
"cross-env": "7.0.3",
56+
"css-hot-loader": "1.4.4",
57+
"css-loader": "7.1.2",
58+
"csso-webpack-plugin": "2.0.0-beta.3",
59+
"express": "4.21.1",
60+
"fork-ts-checker-webpack-plugin": "9.0.2",
61+
"html-webpack-plugin": "5.6.0",
62+
"jest": "29.7.0",
63+
"jest-environment-jsdom": "29.7.0",
64+
"mini-css-extract-plugin": "2.9.1",
65+
"nodemon": "3.1.7",
66+
"npm-run-all": "4.1.5",
67+
"null-loader": "4.0.1",
68+
"postcss": "8.4.47",
69+
"postcss-scss": "4.0.9",
70+
"prettier": "3.3.3",
71+
"rimraf": "6.0.1",
72+
"sass": "1.79.4",
73+
"sass-loader": "16.0.2",
74+
"stylelint": "16.9.0",
75+
"stylelint-config-clean-order": "6.1.0",
76+
"stylelint-config-standard-scss": "13.1.0",
77+
"swc-loader": "0.2.6",
78+
"ts-jest": "29.2.5",
79+
"ts-loader": "9.5.1",
80+
"ts-node": "10.9.2",
81+
"ts-standard": "12.0.2",
82+
"typescript": "5.6.2",
83+
"typescript-plugin-css-modules": "5.1.0",
84+
"wait-on": "8.0.1",
85+
"webpack": "5.95.0",
86+
"webpack-bundle-analyzer": "4.10.2",
87+
"webpack-cli": "5.1.4",
88+
"webpack-dev-middleware": "7.4.2",
89+
"webpack-dev-server": "5.1.0",
90+
"webpack-hot-middleware": "2.26.1",
91+
"webpack-node-externals": "3.0.0"
92+
},
93+
"dependencies": {
94+
"@apollo/react-ssr": "4.0.0",
95+
"@loadable/server": "5.16.5",
96+
"@loadable/webpack-plugin": "5.15.2",
97+
"@reduxjs/toolkit": "2.2.8",
98+
"@types/loadable__webpack-plugin": "5.7.6",
99+
"compression": "1.7.4",
100+
"cookie-parser": "1.4.7",
101+
"cross-fetch": "4.0.0",
102+
"helmet": "8.0.0",
103+
"identity-obj-proxy": "3.0.0",
104+
"preact": "10.24.2",
105+
"react": "18.3.1",
106+
"react-dom": "18.3.1",
107+
"react-helmet-async": "2.0.5",
108+
"react-redux": "9.1.2",
109+
"react-router-dom": "6.26.2",
110+
"serialize-javascript": "6.0.2"
111+
}
112+
}

_webpack/src/server/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { DEV_SERVER_PORT, IS_DEV } from '_webpack/constants'
2+
3+
export const SERVER_PORT: number = IS_DEV ? DEV_SERVER_PORT : 3000
4+
5+
export const IS_RENDER_TO_STREAM: boolean = true
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import helmet from 'helmet'
2+
import { randomUUID } from 'crypto'
3+
import { Response, Request, NextFunction } from 'express'
4+
import { IS_DEV } from '_webpack/constants'
5+
6+
const nonce = (_req: Request, res: Response, next: NextFunction): void => {
7+
res.locals.cspNonce = Buffer.from(randomUUID()).toString('base64')
8+
next()
9+
}
10+
11+
const csp = (req: Request, res: Response, next: NextFunction): void => {
12+
const middleware = helmet({
13+
contentSecurityPolicy: {
14+
useDefaults: true,
15+
directives: {
16+
defaultSrc: ["'self'", 'pokeapi.co', 'localhost:*'],
17+
imgSrc: ["'self'", 'raw.githubusercontent.com'],
18+
scriptSrc: [
19+
"'self'",
20+
`'nonce-${String(res.locals.cspNonce)}'`,
21+
IS_DEV ? "'unsafe-eval'" : ''
22+
]
23+
}
24+
},
25+
crossOriginEmbedderPolicy: { policy: 'credentialless' },
26+
noSniff: false,
27+
originAgentCluster: false
28+
})
29+
30+
return middleware(req, res, next)
31+
}
32+
33+
export { nonce, csp }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import webpack from 'webpack'
2+
import { RequestHandler } from 'express'
3+
import devMiddleware from 'webpack-dev-middleware'
4+
import hotMiddleware from 'webpack-hot-middleware'
5+
6+
import { clientConfig as config } from '_webpack/client.config'
7+
8+
const compiler = webpack({ ...config, mode: 'development' })
9+
10+
export const devMiddlewareInstance = devMiddleware(compiler, {
11+
serverSideRender: true,
12+
writeToDisk: true,
13+
publicPath:
14+
config.output?.publicPath != null ? String(config.output.publicPath) : '/'
15+
})
16+
17+
export function hotReload (): RequestHandler[] {
18+
return [devMiddlewareInstance, hotMiddleware(compiler)]
19+
}
20+
21+
export default hotReload

_webpack/src/server/server.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import path from 'path'
2+
import express, { RequestHandler } from 'express'
3+
import compression from 'compression'
4+
import cookieParser from 'cookie-parser'
5+
import { ChunkExtractor } from '@loadable/server'
6+
7+
import { csp, serverRenderer, nonce } from 'server/middlewares'
8+
import { IS_RENDER_TO_STREAM, SERVER_PORT } from 'server/constants'
9+
import { DIST_DIR, IS_DEV, SRC_DIR } from '_webpack/constants'
10+
11+
const { PORT = SERVER_PORT } = process.env
12+
13+
const runServer = (hotReload?: () => RequestHandler[]): void => {
14+
const app = express()
15+
const statsFile = path.resolve('./dist/stats.json')
16+
const chunkExtractor = new ChunkExtractor({ statsFile })
17+
18+
app
19+
.use(nonce)
20+
.use(csp)
21+
.use(express.json())
22+
.use(compression())
23+
.use(express.static(path.resolve(DIST_DIR)))
24+
.use(cookieParser())
25+
26+
if (IS_DEV) {
27+
if (hotReload != null) {
28+
app.get('/*', [...hotReload()])
29+
}
30+
} else {
31+
app.get('/sw.js', (_req, res) => {
32+
res.sendFile(path.join(SRC_DIR, 'sw.js'))
33+
})
34+
}
35+
36+
app.get('/*', serverRenderer(chunkExtractor))
37+
38+
app.listen(PORT, () => {
39+
console.log(
40+
`App listening on port ${PORT}! (render to ${
41+
IS_RENDER_TO_STREAM ? 'stream' : 'string'
42+
})`
43+
)
44+
})
45+
}
46+
47+
if (IS_DEV) {
48+
;(async () => {
49+
const { hotReload, devMiddlewareInstance } = await import(
50+
'./middlewares/hotReload'
51+
)
52+
devMiddlewareInstance.waitUntilValid(() => {
53+
runServer(hotReload)
54+
})
55+
})()
56+
.then(() => {})
57+
.catch((er) => console.log(er))
58+
} else {
59+
runServer()
60+
}

0 commit comments

Comments
 (0)