Skip to content

Commit

Permalink
Migration to Rspack
Browse files Browse the repository at this point in the history
  • Loading branch information
StopNGo committed Oct 14, 2024
1 parent ed679f6 commit 99e042c
Show file tree
Hide file tree
Showing 40 changed files with 2,099 additions and 760 deletions.
64 changes: 56 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# React Proto - React TypeScript Boilerplate

![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) ![[email protected]](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)
![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) ![[email protected]](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)

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

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

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.

## Huge Update: Migrating to Rspack

Starting from version `2.0.0`, this project uses [Rspack](https://rspack.dev/) as the primary bundler.

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.

Webpack is still available as an option ([rspack vs webpack](#rspack-vs-webpack) and [switching back to webpack](#switching-back-to-webpack)).

---
- [Issue](#issue)
- [What's Inside](#whats-inside)
Expand All @@ -26,7 +34,7 @@ You can also check a [React Proto Lite](https://github.com/StopNGo/react-proto-l

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.

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/).
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).

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

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

- **React** 18+ (**Preact** 10+ as an option, see [comparison](#react-vs-preact) below)
- **webpack** 5+ (with optional **SWC** support and SSR or static build; [why not Vite?](#why-not-vite))
- **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))
- **TypeScript** (with strict rules, including webpack configuration)

SSR:
Expand Down Expand Up @@ -130,15 +138,17 @@ Live preview:

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

2. Install all packages:
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.

3. Install all packages:

`npm i`

3. Run project in a development mode:
4. Run project in a development mode:

`npm start`

4. Open your browser with the next address:
5. Open your browser with the next address:

`http://localhost:8080/`

Expand Down Expand Up @@ -176,6 +186,20 @@ Live preview:

`npm run build:static:report`

### Switching back to webpack

- Copy the contents of `_webpack` folder (except `README.md`) to the root of the project.

- Delete the `rspack.config.ts` file.

- Delete the `rspack` and `_webpack` folders.

- If you have a previous installation, clean the `node_modules` folder.

- Then install the packages:

`npm i`

### Updating packages

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.
Expand Down Expand Up @@ -203,19 +227,43 @@ Vite is an excellent new generation bundler that could speed up your development

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.

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.
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)).

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.

### Rspack vs webpack

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.

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:

| | Rspack | webpack |
| ------- | --------- | -------- |
| Server | 5.35 s | 6.83 s |
| Client | 5.32 s | 7.50 s |

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).

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:

| | Rspack | webpack |
| ------- | --------- | -------- |
| Parsed | 284.47 KB | 262.9 KB |
| Gzipped | 90.29 KB | 86.84 KB |
### React vs Preact

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

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):
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):

| | React | Preact |
| ------- | --------- | -------- |
| Parsed | 262.9 KB | 150.55 KB |
| Gzipped | 86.84 KB | 52.09 KB |

**Important Note**
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).

### Why not any common i18n package?
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.

Expand Down
17 changes: 17 additions & 0 deletions _webpack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Webpack Version

To use the webpack version of the project:

1. Copy the contents of this folder (except `README.md`) to the root of the project.

2. Delete the `rspack.config.ts` file.

3. Delete the `rspack` and `_webpack` folders.

4. If you have a previous installation, clean the `node_modules` folder.

5. Then install the packages:

`npm i`

That's it!
112 changes: 112 additions & 0 deletions _webpack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"name": "react-proto",
"version": "2.0.0",
"description": "React TypeScript Boilerplate",
"author": "Max L Stop&Go",
"license": "ISC",
"sideEffects": [
"*.css",
"*.scss"
],
"scripts": {
"clean": "rimraf dist",
"start:webpack": "cross-env NODE_ENV=development webpack --mode=development",
"start:server": "wait-on dist/server.js && node dist/server.js",
"dev": "npm run clean && npm-run-all --print-label --parallel start:webpack start:server",
"start": "nodemon --exec npm run dev --watch src/server --ext ts,tsx,json ",
"build": "npm run clean && cross-env NODE_ENV=production webpack --mode production",
"build:report": "npm run build --withReport",
"run": "node dist/server.js",
"start:static": "npm run clean && cross-env NODE_ENV=development NO_SSR=true webpack serve --mode development --open",
"build:static": "npm run clean && cross-env NODE_ENV=production NO_SSR=true webpack --mode production",
"build:static:report": "npm run build:static --withReport",
"prettier": "prettier \"src/**/*\" --write --single-quote --no-semi --ignore-unknown --trailing-comma none --jsx-single-quote",
"lint": "ts-standard . && stylelint **/*.{css,scss}",
"lint:fix": "npm run prettier && ts-standard . --fix && stylelint --fix **/*.{css,scss}",
"test": "jest",
"add-comp": "node scripts/add-comp.js"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
"@svgr/webpack": "8.1.0",
"@swc/core": "1.7.26",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/preact": "3.2.4",
"@testing-library/react": "16.0.1",
"@testing-library/user-event": "14.5.2",
"@types/compression": "1.7.5",
"@types/cookie-parser": "1.4.7",
"@types/express": "5.0.0",
"@types/jest": "29.5.13",
"@types/loadable__component": "5.13.9",
"@types/loadable__server": "5.12.11",
"@types/node": "22.7.5",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.0",
"@types/react-helmet": "6.1.11",
"@types/react-router-dom": "5.3.3",
"@types/serialize-javascript": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-env": "1.18.5",
"@types/webpack-hot-middleware": "2.25.9",
"@types/webpack-node-externals": "3.0.4",
"classnames": "2.5.1",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-hot-loader": "1.4.4",
"css-loader": "7.1.2",
"csso-webpack-plugin": "2.0.0-beta.3",
"express": "4.21.1",
"fork-ts-checker-webpack-plugin": "9.0.2",
"html-webpack-plugin": "5.6.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"mini-css-extract-plugin": "2.9.1",
"nodemon": "3.1.7",
"npm-run-all": "4.1.5",
"null-loader": "4.0.1",
"postcss": "8.4.47",
"postcss-scss": "4.0.9",
"prettier": "3.3.3",
"rimraf": "6.0.1",
"sass": "1.79.4",
"sass-loader": "16.0.2",
"stylelint": "16.9.0",
"stylelint-config-clean-order": "6.1.0",
"stylelint-config-standard-scss": "13.1.0",
"swc-loader": "0.2.6",
"ts-jest": "29.2.5",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"ts-standard": "12.0.2",
"typescript": "5.6.2",
"typescript-plugin-css-modules": "5.1.0",
"wait-on": "8.0.1",
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
"webpack-dev-middleware": "7.4.2",
"webpack-dev-server": "5.1.0",
"webpack-hot-middleware": "2.26.1",
"webpack-node-externals": "3.0.0"
},
"dependencies": {
"@apollo/react-ssr": "4.0.0",
"@loadable/server": "5.16.5",
"@loadable/webpack-plugin": "5.15.2",
"@reduxjs/toolkit": "2.2.8",
"@types/loadable__webpack-plugin": "5.7.6",
"compression": "1.7.4",
"cookie-parser": "1.4.7",
"cross-fetch": "4.0.0",
"helmet": "8.0.0",
"identity-obj-proxy": "3.0.0",
"preact": "10.24.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet-async": "2.0.5",
"react-redux": "9.1.2",
"react-router-dom": "6.26.2",
"serialize-javascript": "6.0.2"
}
}
5 changes: 5 additions & 0 deletions _webpack/src/server/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DEV_SERVER_PORT, IS_DEV } from '_webpack/constants'

export const SERVER_PORT: number = IS_DEV ? DEV_SERVER_PORT : 3000

export const IS_RENDER_TO_STREAM: boolean = true
33 changes: 33 additions & 0 deletions _webpack/src/server/middlewares/csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import helmet from 'helmet'
import { randomUUID } from 'crypto'
import { Response, Request, NextFunction } from 'express'
import { IS_DEV } from '_webpack/constants'

const nonce = (_req: Request, res: Response, next: NextFunction): void => {
res.locals.cspNonce = Buffer.from(randomUUID()).toString('base64')
next()
}

const csp = (req: Request, res: Response, next: NextFunction): void => {
const middleware = helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
defaultSrc: ["'self'", 'pokeapi.co', 'localhost:*'],
imgSrc: ["'self'", 'raw.githubusercontent.com'],
scriptSrc: [
"'self'",
`'nonce-${String(res.locals.cspNonce)}'`,
IS_DEV ? "'unsafe-eval'" : ''
]
}
},
crossOriginEmbedderPolicy: { policy: 'credentialless' },
noSniff: false,
originAgentCluster: false
})

return middleware(req, res, next)
}

export { nonce, csp }
21 changes: 21 additions & 0 deletions _webpack/src/server/middlewares/hotReload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import webpack from 'webpack'
import { RequestHandler } from 'express'
import devMiddleware from 'webpack-dev-middleware'
import hotMiddleware from 'webpack-hot-middleware'

import { clientConfig as config } from '_webpack/client.config'

const compiler = webpack({ ...config, mode: 'development' })

export const devMiddlewareInstance = devMiddleware(compiler, {
serverSideRender: true,
writeToDisk: true,
publicPath:
config.output?.publicPath != null ? String(config.output.publicPath) : '/'
})

export function hotReload (): RequestHandler[] {
return [devMiddlewareInstance, hotMiddleware(compiler)]
}

export default hotReload
60 changes: 60 additions & 0 deletions _webpack/src/server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import path from 'path'
import express, { RequestHandler } from 'express'
import compression from 'compression'
import cookieParser from 'cookie-parser'
import { ChunkExtractor } from '@loadable/server'

import { csp, serverRenderer, nonce } from 'server/middlewares'
import { IS_RENDER_TO_STREAM, SERVER_PORT } from 'server/constants'
import { DIST_DIR, IS_DEV, SRC_DIR } from '_webpack/constants'

const { PORT = SERVER_PORT } = process.env

const runServer = (hotReload?: () => RequestHandler[]): void => {
const app = express()
const statsFile = path.resolve('./dist/stats.json')
const chunkExtractor = new ChunkExtractor({ statsFile })

app
.use(nonce)
.use(csp)
.use(express.json())
.use(compression())
.use(express.static(path.resolve(DIST_DIR)))
.use(cookieParser())

if (IS_DEV) {
if (hotReload != null) {
app.get('/*', [...hotReload()])
}
} else {
app.get('/sw.js', (_req, res) => {
res.sendFile(path.join(SRC_DIR, 'sw.js'))
})
}

app.get('/*', serverRenderer(chunkExtractor))

app.listen(PORT, () => {
console.log(
`App listening on port ${PORT}! (render to ${
IS_RENDER_TO_STREAM ? 'stream' : 'string'
})`
)
})
}

if (IS_DEV) {
;(async () => {
const { hotReload, devMiddlewareInstance } = await import(
'./middlewares/hotReload'
)
devMiddlewareInstance.waitUntilValid(() => {
runServer(hotReload)
})
})()
.then(() => {})
.catch((er) => console.log(er))
} else {
runServer()
}
Loading

0 comments on commit 99e042c

Please sign in to comment.