Repository: pmmmwh/react-refresh-webpack-plugin
Branch: main
Commit: 5e4ce91c63e1
Files: 194
Total size: 293.4 KB
Directory structure:
gitextract_f1f9g2an/
├── .babelrc.json
├── .eslintignore
├── .eslintrc.json
├── .gitattributes
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── client/
│ ├── ErrorOverlayEntry.js
│ ├── ReactRefreshEntry.js
│ ├── package.json
│ └── utils/
│ ├── errorEventHandlers.js
│ ├── formatWebpackErrors.js
│ └── retry.js
├── docs/
│ ├── API.md
│ └── TROUBLESHOOTING.md
├── examples/
│ ├── flow-with-babel/
│ │ ├── .flowconfig
│ │ ├── babel.config.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── src/
│ │ │ ├── App.jsx
│ │ │ ├── ArrowFunction.jsx
│ │ │ ├── ClassDefault.jsx
│ │ │ ├── ClassNamed.jsx
│ │ │ ├── FunctionDefault.jsx
│ │ │ ├── FunctionNamed.jsx
│ │ │ ├── LazyComponent.jsx
│ │ │ └── index.js
│ │ └── webpack.config.js
│ ├── typescript-with-babel/
│ │ ├── babel.config.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── ArrowFunction.tsx
│ │ │ ├── ClassDefault.tsx
│ │ │ ├── ClassNamed.tsx
│ │ │ ├── FunctionDefault.tsx
│ │ │ ├── FunctionNamed.tsx
│ │ │ ├── LazyComponent.tsx
│ │ │ └── index.tsx
│ │ ├── tsconfig.json
│ │ └── webpack.config.js
│ ├── typescript-with-swc/
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── ArrowFunction.tsx
│ │ │ ├── ClassDefault.tsx
│ │ │ ├── ClassNamed.tsx
│ │ │ ├── FunctionDefault.tsx
│ │ │ ├── FunctionNamed.tsx
│ │ │ ├── LazyComponent.tsx
│ │ │ └── index.tsx
│ │ ├── tsconfig.json
│ │ └── webpack.config.js
│ ├── typescript-with-tsc/
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── ArrowFunction.tsx
│ │ │ ├── ClassDefault.tsx
│ │ │ ├── ClassNamed.tsx
│ │ │ ├── FunctionDefault.tsx
│ │ │ ├── FunctionNamed.tsx
│ │ │ ├── LazyComponent.tsx
│ │ │ └── index.tsx
│ │ ├── tsconfig.dev.json
│ │ ├── tsconfig.json
│ │ └── webpack.config.js
│ ├── webpack-dev-server/
│ │ ├── babel.config.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── src/
│ │ │ ├── App.jsx
│ │ │ ├── ArrowFunction.jsx
│ │ │ ├── ClassDefault.jsx
│ │ │ ├── ClassNamed.jsx
│ │ │ ├── FunctionDefault.jsx
│ │ │ ├── FunctionNamed.jsx
│ │ │ ├── LazyComponent.jsx
│ │ │ └── index.js
│ │ └── webpack.config.js
│ ├── webpack-hot-middleware/
│ │ ├── babel.config.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── index.html
│ │ ├── server.js
│ │ ├── src/
│ │ │ ├── App.jsx
│ │ │ ├── ArrowFunction.jsx
│ │ │ ├── ClassDefault.jsx
│ │ │ ├── ClassNamed.jsx
│ │ │ ├── FunctionDefault.jsx
│ │ │ ├── FunctionNamed.jsx
│ │ │ ├── LazyComponent.jsx
│ │ │ └── index.js
│ │ └── webpack.config.js
│ └── webpack-plugin-serve/
│ ├── babel.config.js
│ ├── package.json
│ ├── public/
│ │ └── index.html
│ ├── src/
│ │ ├── App.jsx
│ │ ├── ArrowFunction.jsx
│ │ ├── ClassDefault.jsx
│ │ ├── ClassNamed.jsx
│ │ ├── FunctionDefault.jsx
│ │ ├── FunctionNamed.jsx
│ │ ├── LazyComponent.jsx
│ │ └── index.js
│ └── webpack.config.js
├── jest.config.js
├── lib/
│ ├── globals.js
│ ├── index.js
│ ├── options.json
│ ├── runtime/
│ │ └── RefreshUtils.js
│ ├── types.js
│ └── utils/
│ ├── getAdditionalEntries.js
│ ├── getIntegrationEntry.js
│ ├── getSocketIntegration.js
│ ├── index.js
│ ├── injectRefreshLoader.js
│ ├── makeRefreshRuntimeModule.js
│ └── normalizeOptions.js
├── loader/
│ ├── index.js
│ ├── options.json
│ ├── types.js
│ └── utils/
│ ├── getIdentitySourceMap.js
│ ├── getModuleSystem.js
│ ├── getRefreshModuleRuntime.js
│ ├── index.js
│ ├── normalizeOptions.js
│ └── webpackGlobal.js
├── options/
│ └── index.js
├── overlay/
│ ├── components/
│ │ ├── CompileErrorTrace.js
│ │ ├── PageHeader.js
│ │ ├── RuntimeErrorFooter.js
│ │ ├── RuntimeErrorHeader.js
│ │ ├── RuntimeErrorStack.js
│ │ └── Spacer.js
│ ├── containers/
│ │ ├── CompileErrorContainer.js
│ │ └── RuntimeErrorContainer.js
│ ├── index.js
│ ├── package.json
│ ├── theme.js
│ └── utils.js
├── package.json
├── scripts/
│ └── test.js
├── sockets/
│ ├── WDSSocket.js
│ ├── WHMEventSource.js
│ ├── WPSSocket.js
│ └── package.json
├── test/
│ ├── conformance/
│ │ ├── ReactRefresh.test.js
│ │ └── environment.js
│ ├── helpers/
│ │ ├── compilation/
│ │ │ ├── fixtures/
│ │ │ │ └── source-map-loader.js
│ │ │ ├── index.js
│ │ │ └── normalizeErrors.js
│ │ └── sandbox/
│ │ ├── aliasWDSv4.js
│ │ ├── configs.js
│ │ ├── fixtures/
│ │ │ └── hmr-notifier.js
│ │ ├── index.js
│ │ └── spawn.js
│ ├── jest-environment.js
│ ├── jest-global-setup.js
│ ├── jest-global-teardown.js
│ ├── jest-resolver.js
│ ├── jest-test-setup.js
│ ├── loader/
│ │ ├── fixtures/
│ │ │ ├── auto/
│ │ │ │ └── package.json
│ │ │ ├── cjs/
│ │ │ │ ├── esm/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── package.json
│ │ │ │ ├── index.js
│ │ │ │ └── package.json
│ │ │ └── esm/
│ │ │ ├── cjs/
│ │ │ │ ├── index.js
│ │ │ │ └── package.json
│ │ │ ├── index.js
│ │ │ └── package.json
│ │ ├── loader.test.js
│ │ ├── unit/
│ │ │ ├── getIdentitySourceMap.test.js
│ │ │ ├── getModuleSystem.test.js
│ │ │ ├── getRefreshModuleRuntime.test.js
│ │ │ └── normalizeOptions.test.js
│ │ └── validateOptions.test.js
│ ├── mocks/
│ │ └── fetch.js
│ └── unit/
│ ├── fixtures/
│ │ └── socketIntegration.js
│ ├── getAdditionalEntries.test.js
│ ├── getIntegrationEntry.test.js
│ ├── getSocketIntegration.test.js
│ ├── globals.test.js
│ ├── makeRefreshRuntimeModule.test.js
│ ├── normalizeOptions.test.js
│ └── validateOptions.test.js
├── tsconfig.json
├── types/
│ ├── lib/
│ │ ├── index.d.ts
│ │ └── types.d.ts
│ ├── loader/
│ │ ├── index.d.ts
│ │ └── types.d.ts
│ └── options/
│ └── index.d.ts
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc.json
================================================
{
"env": {
"test": {
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
}
}
================================================
FILE: .eslintignore
================================================
*dist/
*node_modules/
*umd/
*__tmp__
# Ignore examples because they might have custom ESLint configurations
examples/*
================================================
FILE: .eslintrc.json
================================================
{
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"commonjs": true,
"es2022": true,
"node": true
},
"overrides": [
{
"files": ["client/**/*.js", "overlay/**/*.js", "lib/runtime/**/*.js", "sockets/**/*.js"],
"parserOptions": {
"ecmaVersion": 2015
},
"env": {
"browser": true,
"es6": true
}
},
{
"files": [
"scripts/test.mjs",
"test/jest-test-setup.js",
"test/helpers/{,!(fixtures)*/}*.js",
"test/mocks/**/*.js",
"test/**/*.test.js"
],
"env": {
"jest": true
},
"globals": {
"__DEBUG__": true,
"WDS_VERSION": true,
"browser": true,
"window": true
},
"parserOptions": {
"sourceType": "module"
}
},
{
"files": ["test/helpers/**/fixtures/*.js", "test/conformance/**/*.test.js"],
"env": {
"browser": true
}
},
{
"files": ["test/**/fixtures/esm/*.js"],
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
},
"env": {
"commonjs": false,
"es6": true
}
},
{
"files": ["test/**/fixtures/cjs/esm/*.js"],
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
},
"env": {
"commonjs": false,
"es6": true
}
},
{
"files": ["test/**/fixtures/esm/cjs/*.js"],
"env": {
"commonjs": true,
"es6": true
}
}
]
}
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
lint-and-format:
name: Lint and Format
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 22
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Check Linting
run: yarn lint
- name: Check Formatting
run: yarn format:check
test:
name: Tests (Node ${{ matrix.node-version }} - WDS ${{ matrix.wds-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- '18'
- '20'
- '22'
wds-version:
- '4'
- '5'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Run Tests
run: yarn test --testPathIgnorePatterns conformance
env:
BROWSER: false
WDS_VERSION: ${{ matrix.wds-version }}
conformance:
name: Conformance (Node ${{ matrix.node-version }} - WDS ${{ matrix.wds-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- '18'
- '20'
- '22'
wds-version:
- '4'
- '5'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Disable AppArmor
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Run Conformance Tests
run: yarn test conformance
env:
WDS_VERSION: ${{ matrix.wds-version }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
*node_modules
# distribution
*dist
*umd
# misc
.DS_Store
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# editor config
.idea
.vscode
# test artifacts
*__tmp__
================================================
FILE: .prettierignore
================================================
*dist/
*node_modules/
*umd/
*__tmp__
================================================
FILE: .prettierrc.json
================================================
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5"
}
================================================
FILE: CHANGELOG.md
================================================
## 0.6.2 (26 Nov 2025)
### Fixes
- Relaxed peer dependency requirement on `type-fest` to allow v5.x
([#934](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/934))
## 0.6.1 (26 Jun 2025)
### Fixes
- Ensure `this` propagates into module factory properly
([#921](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/921))
## 0.6.0 (28 Apr 2025)
### BREAKING
- Minimum required Node.js version has been bumped to `18.12.0`.
- Minimum required `webpack` version has been bumped to `5.2.0`.
- Minimum supported `webpack-dev-server` version has been bumped to `4.8.0`.
- Minimum supported `webpack-plugin-serve` version has been bumped to `1.0.0`.
- `overlay.sockHost`, `overlay.sockPath`, `overlay.sockPort`, `overlay.sockProtocol` and `overlay.useURLPolyfill` have all been removed.
([#850](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/850))
It was necessary to support WDS below `4.8.0` (published in April 2022).
It is no-longer necessary as a direct integration with WDS is now possible.
### Features
- Added helper script to better support use cases where React and/or React-DOM are externalized
([#852](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/852))
### Fixes
- Ensure plugin injected entries are no-op in production
([#900](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/900))
### Internal
- Dropped support for Webpack 4 / WDS 3
([#850](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/850),
[#904](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/904))
- Migrated from `ansi-html` to `anser` in error overlay
([#854](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/854))
- Bumped all development dependencies
([#905](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/905))
## 0.5.17 (26 Jun 2025)
### Fixes
- Ensure `this` propagates into module factory properly
([#922](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/922))
## 0.5.16 (31 Mar 2025)
### Fixes
- Fixed out of order cleanup when using top-level await
([#898](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/898))
## 0.5.15 (3 Jun 2024)
### Fixes
- Fixed wrong import in error overlay for `ansi-html`
([#853](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/853))
## 0.5.14 (1 Jun 2024)
### Fixes
- Moved to `ansi-html` `v0.0.9` and `schema-utils` `v4.x`
([#848](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/848))
### Internal
- Run tests on latest versions of Node.js 18, 20 and 22
([#848](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/848))
- Bumped `jest` to v29 and some other development dependencies
([#848](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/848))
- Removed `yalc`
([#849](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/849))
## 0.5.13 (28 Apr 2024)
### Fixes
- Fixed module system inferring (ESM vs CJS) to start from the point of each file
([#771](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/771))
## 0.5.12 (27 Apr 2024)
### Fixes
- Fixed incorrect `sockProtocol` override
([#835](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/835))
- Relaxed peer dependency requirement on `webpack-dev-server` to allow v5.x
([#837](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/837))
## 0.5.11 (15 Aug 2023)
### Features
- Added support to exclude dynamically generated modules from other loaders
([#769](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/769))
### Fixes
- Fixed unnecessary memory leaks due to `prevExports`
([#766](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/766))
- Relaxed peer dependency requirement on `type-fest` to allow v4.x
([#767](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/767))
- Fixed module type resolution when there is difference across contexts
([#768](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/768))
## 0.5.10 (24 Nov 2022)
### Fixes
- Bumped `loader-utils` to fix security vulnerability
([#700](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/700))
## 0.5.9 (10 Nov 2022)
### Fixes
- Bumped `loader-utils` to fix security vulnerability
([#685](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/685))
## 0.5.8 (9 Oct 2022)
### Fixes
- Fixed performance issue regarding `require.resolve` in loader injection
([#669](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/669))
- Bumped `core-js-pure` to not depend on deprecated versions
([#674](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/674))
## 0.5.7 (23 May 2022)
### Fixes
- Removed debug `console.log` statement
([#631](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/631))
### Internal
- Run tests on Node.js 18
([#631](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/631))
## 0.5.6 (10 May 2022)
### Fixes
- Fixed faulty `this` type import in loader
([#624](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/624))
- Made current script detection more robust for edge cases
([#630](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/630))
### Internal
- Swapped to new `ReactDOM.createRoot` API in examples
([#626](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/626))
## 0.5.5 (4 April 2022)
### Fixes
- Handle unknown `moduleId` for dynamically generated modules
([#547](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/547))
- Handle WDS `auto` value on `port`
([#574](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/574))
- Fixed `react-refresh@0.12.0` compatibility
([#576](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/576))
- Fixed crash when parsing compile errors in overlay
([#577](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/577))
- Respect virtual modules when injecting loader
([#593](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/593))
- Allow `port` to be missing for WDS, also some general refactoring
([#623](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/623))
### Internal
- A couple documentation changes in README
([#575](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/575),
[8c39623](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/8c39623dac237aa15795b60820babcffebb28c64),
[#597](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/597))
- Bumped dependencies for testing infrastructure
([#526](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/526),
[#564](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/564),
[#567](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/567),
[#581](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/581),
[#588](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/588),
[#591](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/591),
[#594](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/594),
[#616](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/616))
## 0.5.4 (22 December 2021)
### Fixes
- Skip loader injection for files referenced as assets
([#545](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/545))
- Changed failures of `exports` capturing to warn instead of throw
([#546](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/546))
## 0.5.3 (28 November 2021)
### Fixes
- Updated overlay for unsafe area in Safari
([#528](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/528))
- Fixed performance in large projects due to memory leak in loader
([#537](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/537))
## 0.5.2 (19 November 2021)
### Features
- Added support for WDS v4 `client.webSocketURL`
([#529](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/529))
### Fixes
- Fixed lost module context due to interceptor by always using regular functions
([#531](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/531))
- Relaxed peer dependency requirement on `react-refresh`
([#534](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/534))
## 0.5.1 (15 September 2021)
### Fixes
- Relaxed peer dependency requirement on `type-fest` to allow v2.x
([#507](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/507),
[#508](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/508))
### Internal
- Fixed typos in README
([#509](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/509))
## 0.5.0 (14 September 2021)
### BREAKING
- While most of the public API did not change,
we've re-written a large chunk of the runtime code to support a wider range of use cases.
This is likely to provide more stability, but if `0.4.x` works in your setup but `0.5.x` doesn't,
please file us an issue - we would love to address it!
- The `disableRefreshCheck` option have been removed.
([#285](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/285))
It has long been effect-less and deprecated since `0.3.x`.
- The `overlay.useLegacyWDSSockets` have been removed.
([#498](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/498))
It is aimed to support WDS below `3.6.0` (published in June 2019),
but looking at current usage and download stats,
we've decided it is best to drop support for the old socket format moving forward.
- Handling of port `0` have been removed.
([#337](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/337))
- `html-entities` have been bumped to `2.x`.
([#321](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/321))
- `react-refresh` have been bumped to `0.10.0`.
([#353](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/353))
### Features
- Added WDS v4 support with new socket defaults through Webpack config
([#241](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/241),
[#286](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/286),
[#392](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/392),
[#413](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/413),
[#479](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/479))
- Added the `overlay.sockProtocol` option
([#242](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/242))
- Added monorepo compatibility via the the `library` option
([#273](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/273))
- Rewritten URL handling using WHATWG `URL` APIs with automatic pony-filling
([#278](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/278),
[#332](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/332),
[#378](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/378))
- Rewritten Webpack 5 compatibility using new APIs and hooks
([#319](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/319),
[#372](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/372),
[#434](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/434),
[#483](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/483))
- Rewritten refresh runtime to be fully module system aware
([#337](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/337),
[#461](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/461),
[#482](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/482),
[#492](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/492))
- Rewritten Webpack 4 and 5 checks using feature detection on compiler
([#415](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/415))
- Added support for `experiments.topLevelAwait`
([#435](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/435),
[#447](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/447),
[#493](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/493))
- Added retry logic when socket initialisation fails
([#446](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/446))
### Fixes
- Relaxed peer dependency requirement on `type-fest`
([#257](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/257),
[c02018a](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/c02018a853f58341d44ea9f1b56a9568ffe7b657),
[#484](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/484))
- Relaxed requirement on the `overlay` option to accept relative paths
([#284](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/284))
- Patched unstable initialisation of global scope across module boundaries
([#290](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/290),
[#369](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/369),
[#464](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/464),
[#505](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/505))
- Patched quote escaping in injected runtime code
([#306](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/306))
- Invalidate updates outside of Refresh boundary for consistency
([#307](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/307))
- Properly throw when an ambiguous entrypoint is received while using Webpack 4
([#320](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/320))
- Fixed overlay script source detection for WDS when no `src` is found
([#331](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/331))
- Fixed possible Stack Overflow through self-referencing
([#370](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/370),
[#380](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/380))
- Relaxed errors on HMR not found to not crash JS parsing
([#371](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/371))
- Ensure overlay code won't run if disabled
([#374](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/374))
- Relaxed peer dependency requirement on `@types/webpack`
([#414](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/414))
- Fixed compiler error overlay crashes when messages are empty
([#462](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/462))
- Swapped `ansi-html` to `ansi-html-community` to fix ReDoS vulnerability
([#501](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/501))
### Internal
- More stable testing infrastructure
([#234](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/234))
- Run tests by default on Webpack 5
([#440](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/440))
- Rewrite documentation and fix outstanding issues
([#283](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/283),
[#291](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/291),
[#311](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/311),
[#376](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/376),
[#480](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/480),
[#497](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/497),
[#499](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/499))
- Added documentation on community plugins: `react-refresh-typescript` and `swc`
([#248](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/248),
[fbe1f27](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/fbe1f270de46c4a308d996a4487653a95aebfc25),
[#450](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/450))
## 0.4.3 (2 November 2020)
### Fixes
- Fixed Yarn 2 PnP compatibility with absolute `react-refresh/runtime` imports
([#230](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/230))
- Fixed Webpack 5 compatibility by requiring `__webpack_require__`
([#233](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/233))
- Fixed IE 11 compatibility in socket code
([4033e6af](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/4033e6af279f5f50396ff24fb1ec89c41fc2df32))
- Relaxed peer dependency requirement for `react-refresh` to allow `0.9.x`
([747c19ba](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/747c19ba43d81f19a042d44922d0518c6403528c))
## 0.4.2 (3 September 2020)
### Fixes
- Patched loader to use with Node.js global `fetch` polyfills
([#193](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/193))
- Patched default `include` and `exclude` options to be case-insensitive
([#194](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/194))
## 0.4.1 (28 July 2020)
### Fixes
- Fixed accidental use of testing alias `webpack.next` in published plugin code
([#167](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/167))
## 0.4.0 (28 July 2020)
### BREAKING
- Minimum required Node.js version have been bumped to 10 as Node.js 8 is EOL now.
- Minimum required Webpack version is now `v4.43.0` or later as we adopted the new `module.hot.invalidate` API.
([#89](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/89))
The new API enabled us to bail out of the HMR loop less frequently and provide a better experience.
If you really cannot upgrade, you can stay on `0.3.3` for the time being.
- While most of our public API did not change, this release is closer to a rewrite than a refactor.
A lot of files have moved to provide easier access for advanced users and frameworks.
([#122](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/122))
You can check the difference in the PR to see what have moved and where they are now.
- The `useLegacyWDSSockets` option is now scoped under the `overlay` option.
([#153](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/153))
### Features
- Adopted the `module.hot.invalidate()` API, which means we will now bail out less often
([#89](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/89))
- Attach runtime on Webpack's global scope instead of `window`, making the plugin platform-agnostic
([#102](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/102))
- Added stable support for **Webpack 5** and beta support for **Module Federation**
([#123](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/123),
[#132](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/132),
[#164](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/164))
- Socket integration URL detection via `document.currentScript`
([#133](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/133))
- Relaxed requirements for "required" `overlay` options to receive `false` as value
([#154](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/154))
- Prefixed all errors thrown by the plugin
([#161](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/161))
- Eliminated use of soon-to-be-deprecated `lodash.debounce` package
([#163](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/163))
### Fixes
- Fixed circular references for `__react_refresh_error_overlay__` and `__react_refresh_utils`
([#116](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/116))
- Fixed IE11 compatibility
([#106](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/106),
[#121](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/121))
- Rearranged directories to provide more ergonomic imports
([#122](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/122))
- Fixed issues with Babel/ESLint/Flow regarding loader ordering and runtime cleanup
([#129](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/129),
[#140](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/140))
- Correctly detecting the HMR plugin
([#130](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/130),
[#160](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/160))
- Fixed unwanted injection of error overlay in non-browser environment
([#146](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/146))
- Scoped the `useLegacyWDSSockets` options under `overlay` to reflect its true use
([#153](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/153))
- Fixed non-preserved relative ordering of Webpack entries
([#165](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/165))
### Internal
- Full HMR test suite - we are confident the plugin works!
([#93](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/93),
[#96](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/96))
- Unit tests for all plugin-related Node.js code
([#127](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/127))
## 0.3.3 (29 May 2020)
### Fixes
- Removed unrecoverable React errors check and its corresponding bail out logic on hot dispose
([#104](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/104))
## 0.3.2 (22 May 2020)
### Fixes
- Fixed error in overlay when stack trace is unavailable
([#91](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/91))
- Fixed IE11 compatibility
([#98](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/98))
## 0.3.1 (11 May 2020)
### Fixes
- Relaxed peer dependency requirements for `webpack-plugin-serve`
([64bb6b8](https://github.com/pmmmwh/react-refresh-webpack-plugin/commit/64bb6b8c26bbf9e51484ef7a109c0921be7889ff))
## 0.3.0 (10 May 2020)
### BREAKING
- Deprecated the `disableRefreshCheck` flag
([#60](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/60))
### Features
- Added custom error overlay support
([#44](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/44))
- Added example project to use TypeScript without usual Babel settings
([#46](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/46))
- Added custom socket parameters for WDS
([#52](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/52))
- Added TypeScript definition files
([#65](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/65))
- Added stricter options validation rules
([#62](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/62))
- Added option to configure socket runtime to support more hot integrations
([#64](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/64))
- Added support for `webpack-plugin-serve`
([#74](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/74))
### Fixes
- Fixed non-dismissible overlay for build warnings
([#57](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/57))
- Fixed electron compatibility
([#58](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/58))
- Fixed optional peer dependencies to be truly optional
([#59](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/59))
- Fixed compatibility issues caused by `node-url`
([#61](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/61))
- Removed check for `react` import for compatibility
([#69](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/69))
## 0.2.0 (2 March 2020)
### Features
- Added `webpack-hot-middleware` support
([#23](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/23))
### Fixes
- Fixed dependency on a global `this` variable to better support web workers
([#29](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/29))
## 0.1.3 (19 December 2019)
### Fixes
- Fixed runtime template injection when the `runtimeChunks` optimization is used in Webpack
([#26](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/26))
## 0.1.2 (18 December 2019)
### Fixes
- Fixed caching of Webpack loader to significantly improve performance
([#22](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/22))
## 0.1.1 (13 December 2019)
### Fixes
- Fixed usage of WDS SockJS fallback
([#17](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/17))
## 0.1.0 (7 December 2019)
- Initial public release
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Michael Mok
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# React Refresh Webpack Plugin
[actions]: https://github.com/pmmmwh/react-refresh-webpack-plugin/actions/workflows/ci.yml
[actions:badge]: https://img.shields.io/github/actions/workflow/status/pmmmwh/react-refresh-webpack-plugin/ci.yml?branch=main
[license:badge]: https://img.shields.io/github/license/pmmmwh/react-refresh-webpack-plugin
[npm:latest]: https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin/v/latest
[npm:latest:badge]: https://img.shields.io/npm/v/@pmmmwh/react-refresh-webpack-plugin/latest
[npm:next]: https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin/v/next
[npm:next:badge]: https://img.shields.io/npm/v/@pmmmwh/react-refresh-webpack-plugin/next
[![GitHub Actions][actions:badge]][actions]
[![License][license:badge]](./LICENSE)
[![Latest Version][npm:latest:badge]][npm:latest]
[![Next Version][npm:next:badge]][npm:next]
A Webpack plugin to enable "Fast Refresh" (also known as _Hot Reloading_) for React components.
> We're hoping to land a v1 release soon - please help us by reporting any issues you've encountered!
## Getting Started
### Prerequisites
Ensure that you are using at least the minimum supported versions of this plugin's peer dependencies -
older versions unfortunately do not contain code to orchestrate "Fast Refresh",
and thus cannot be made compatible.
We recommend using the following versions:
| Dependency | Version |
| --------------- | ------------------------------------ |
| `Node.js` | `18.12.0`+, `20.x`, `22.x` |
| `react` | `16.13.0`+, `17.x`, `18.x` or `19.x` |
| `react-dom` | `16.13.0`+, `17.x`, `18.x` or `19.x` |
| `react-refresh` | `0.10.0`+ |
| `webpack` | `5.2.0`+ |
Minimum requirements
| Dependency | Version |
| --------------- | --------- |
| `Node.js` | `18.12.0` |
| `react` | `16.9.0` |
| `react-dom` | `16.9.0` |
| `react-refresh` | `0.10.0` |
| `webpack` | `5.2.0` |
Using custom renderers (e.g. react-three-fiber, react-pdf, ink)
To ensure full support of "Fast Refresh" with components rendered by custom renderers,
you should ensure the renderer you're using depends on a recent version of `react-reconciler`.
We recommend version `0.25.0` or above, but any versions above `0.22.0` should work.
If the renderer is not compatible, please file them an issue instead.
### Installation
With all prerequisites met, you can install this plugin using your package manager of choice:
```sh
# if you prefer npm
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
# if you prefer yarn
yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
# if you prefer pnpm
pnpm add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
```
The `react-refresh` package (from the React team) is a required peer dependency of this plugin.
We recommend using version `0.10.0` or above.
Support for TypeScript
TypeScript support is available out-of-the-box for those who use `webpack.config.ts`.
Our exported types however depends on `type-fest`, so you'll have to add it as a `devDependency`:
```sh
# if you prefer npm
npm install -D type-fest
# if you prefer yarn
yarn add -D type-fest
# if you prefer pnpm
pnpm add -D type-fest
```
> **:memo: Note**:
>
> `type-fest@4.x` only supports Node.js v16 or above,
> `type-fest@3.x` only supports Node.js v14.16 or above,
> and `type-fest@2.x` only supports Node.js v12.20 or above.
> If you're using an older version of Node.js, please install `type-fest@1.x`.
### Usage
For most setups, we recommend integrating with `babel-loader`.
It covers the most use cases and is officially supported by the React team.
The example below will assume you're using `webpack-dev-server`.
If you haven't done so, set up your development Webpack configuration for Hot Module Replacement (HMR).
```js
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
hot: true,
},
};
```
Using webpack-hot-middleware
```js
const webpack = require('webpack');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
plugins: [isDevelopment && new webpack.HotModuleReplacementPlugin()].filter(Boolean),
};
```
Using webpack-plugin-serve
```js
const { WebpackPluginServe } = require('webpack-plugin-serve');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
plugins: [isDevelopment && new WebpackPluginServe()].filter(Boolean),
};
```
Then, add the `react-refresh/babel` plugin to your Babel configuration and this plugin to your Webpack configuration.
```js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
},
},
],
},
],
},
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};
```
> **:memo: Note**:
>
> Ensure both the Babel transform (`react-refresh/babel`) and this plugin are enabled only in `development` mode!
Using ts-loader
> **:warning: Warning**:
> This is an un-official integration maintained by the community.
Install [`react-refresh-typescript`](https://github.com/Jack-Works/react-refresh-transformer/tree/main/typescript).
Ensure your TypeScript version is at least 4.0.
```sh
# if you prefer npm
npm install -D react-refresh-typescript
# if you prefer yarn
yarn add -D react-refresh-typescript
# if you prefer pnpm
pnpm add -D react-refresh-typescript
```
Then, instead of wiring up `react-refresh/babel` via `babel-loader`,
you can wire-up `react-refresh-typescript` with `ts-loader`:
```js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('ts-loader'),
options: {
getCustomTransformers: () => ({
before: [isDevelopment && ReactRefreshTypeScript()].filter(Boolean),
}),
transpileOnly: isDevelopment,
},
},
],
},
],
},
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};
```
> It is recommended to run `ts-loader` with `transpileOnly` is set to `true`.
> You can use `ForkTsCheckerWebpackPlugin` as an alternative if you need typechecking during development.
Using swc-loader
> **:warning: Warning**:
> This is an un-official integration maintained by the community.
Ensure your `@swc/core` version is at least `1.2.86`.
It is also recommended to use `swc-loader` version `0.1.13` or above.
Then, instead of wiring up `react-refresh/babel` via `babel-loader`,
you can wire-up `swc-loader` and use the `refresh` transform:
```js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('swc-loader'),
options: {
jsc: {
transform: {
react: {
development: isDevelopment,
refresh: isDevelopment,
},
},
},
},
},
],
},
],
},
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};
```
> Starting from version `0.1.13`, `swc-loader` will set the `development` option based on Webpack's `mode` option.
> `swc` won't enable fast refresh when `development` is `false`.
For more information on how to set up "Fast Refresh" with different integrations,
please check out [our examples](examples).
### Overlay Integration
This plugin integrates with the most common Webpack HMR solutions to surface errors during development -
in the form of an error overlay.
By default, `webpack-dev-server` is used,
but you can set the [`overlay.sockIntegration`](docs/API.md#sockintegration) option to match what you're using.
The supported versions are as follows:
| Dependency | Version |
| ------------------------ | ----------------- |
| `webpack-dev-server` | `4.8.0`+ or `5.x` |
| `webpack-hot-middleware` | `2.x` |
| `webpack-plugin-serve` | `1.x` |
## API
Please refer to [the API docs](docs/API.md) for all available options.
## FAQs and Troubleshooting
Please refer to [the Troubleshooting guide](docs/TROUBLESHOOTING.md) for FAQs and resolutions to common issues.
## License
This project is licensed under the terms of the [MIT License](/LICENSE).
## Special Thanks
================================================
FILE: client/ErrorOverlayEntry.js
================================================
/* global __react_refresh_error_overlay__, __react_refresh_socket__ */
if (process.env.NODE_ENV !== 'production') {
const events = require('./utils/errorEventHandlers.js');
const formatWebpackErrors = require('./utils/formatWebpackErrors.js');
const runWithRetry = require('./utils/retry.js');
// Setup error states
let isHotReload = false;
let hasRuntimeErrors = false;
/**
* Try dismissing the compile error overlay.
* This will also reset runtime error records (if any),
* because we have new source to evaluate.
* @returns {void}
*/
const tryDismissErrorOverlay = function () {
__react_refresh_error_overlay__.clearCompileError();
__react_refresh_error_overlay__.clearRuntimeErrors(!hasRuntimeErrors);
hasRuntimeErrors = false;
};
/**
* A function called after a compile success signal is received from Webpack.
* @returns {void}
*/
const handleCompileSuccess = function () {
isHotReload = true;
if (isHotReload) {
tryDismissErrorOverlay();
}
};
/**
* A function called after a compile errored signal is received from Webpack.
* @param {string[]} errors
* @returns {void}
*/
const handleCompileErrors = function (errors) {
isHotReload = true;
const formattedErrors = formatWebpackErrors(errors);
// Only show the first error
__react_refresh_error_overlay__.showCompileError(formattedErrors[0]);
};
/**
* Handles compilation messages from Webpack.
* Integrates with a compile error overlay.
* @param {*} message A Webpack HMR message sent via WebSockets.
* @returns {void}
*/
const compileMessageHandler = function (message) {
switch (message.type) {
case 'ok':
case 'still-ok':
case 'warnings': {
// TODO: Implement handling for warnings
handleCompileSuccess();
break;
}
case 'errors': {
handleCompileErrors(message.data);
break;
}
default: {
// Do nothing.
}
}
};
// Only register if no other overlay have been registered
if (
typeof window !== 'undefined' &&
!window.__reactRefreshOverlayInjected &&
__react_refresh_socket__
) {
// Registers handlers for compile errors with retry -
// This is to prevent mismatching injection order causing errors to be thrown
runWithRetry(
function initSocket() {
__react_refresh_socket__.init(compileMessageHandler);
},
3,
'Failed to set up the socket connection.'
);
// Registers handlers for runtime errors
events.handleError(function handleError(error) {
hasRuntimeErrors = true;
__react_refresh_error_overlay__.handleRuntimeError(error);
});
events.handleUnhandledRejection(function handleUnhandledPromiseRejection(error) {
hasRuntimeErrors = true;
__react_refresh_error_overlay__.handleRuntimeError(error);
});
// Mark overlay as injected to prevent double-injection
window.__reactRefreshOverlayInjected = true;
}
}
================================================
FILE: client/ReactRefreshEntry.js
================================================
/* global __react_refresh_library__ */
if (process.env.NODE_ENV !== 'production') {
const safeThis = require('core-js-pure/features/global-this');
const RefreshRuntime = require('react-refresh/runtime');
if (typeof safeThis !== 'undefined') {
var $RefreshInjected$ = '__reactRefreshInjected';
// Namespace the injected flag (if necessary) for monorepo compatibility
if (typeof __react_refresh_library__ !== 'undefined' && __react_refresh_library__) {
$RefreshInjected$ += '_' + __react_refresh_library__;
}
// Only inject the runtime if it hasn't been injected
if (!safeThis[$RefreshInjected$]) {
// Inject refresh runtime into global scope
RefreshRuntime.injectIntoGlobalHook(safeThis);
// Mark the runtime as injected to prevent double-injection
safeThis[$RefreshInjected$] = true;
}
}
}
================================================
FILE: client/package.json
================================================
{
"type": "commonjs"
}
================================================
FILE: client/utils/errorEventHandlers.js
================================================
/**
* @callback EventCallback
* @param {string | Error | null} context
* @returns {void}
*/
/**
* @callback EventHandler
* @param {Event} event
* @returns {void}
*/
/**
* A function that creates an event handler for the `error` event.
* @param {EventCallback} callback A function called to handle the error context.
* @returns {EventHandler} A handler for the `error` event.
*/
function createErrorHandler(callback) {
return function errorHandler(event) {
if (!event || !event.error) {
return callback(null);
}
if (event.error instanceof Error) {
return callback(event.error);
}
// A non-error was thrown, we don't have a trace. :(
// Look in your browser's devtools for more information
return callback(new Error(event.error));
};
}
/**
* A function that creates an event handler for the `unhandledrejection` event.
* @param {EventCallback} callback A function called to handle the error context.
* @returns {EventHandler} A handler for the `unhandledrejection` event.
*/
function createRejectionHandler(callback) {
return function rejectionHandler(event) {
if (!event || !event.reason) {
return callback(new Error('Unknown'));
}
if (event.reason instanceof Error) {
return callback(event.reason);
}
// A non-error was rejected, we don't have a trace :(
// Look in your browser's devtools for more information
return callback(new Error(event.reason));
};
}
/**
* Creates a handler that registers an EventListener on window for a valid type
* and calls a callback when the event fires.
* @param {string} eventType A valid DOM event type.
* @param {function(EventCallback): EventHandler} createHandler A function that creates an event handler.
* @returns {register} A function that registers the EventListener given a callback.
*/
function createWindowEventHandler(eventType, createHandler) {
/**
* @type {EventHandler | null} A cached event handler function.
*/
let eventHandler = null;
/**
* Unregisters an EventListener if it has been registered.
* @returns {void}
*/
function unregister() {
if (eventHandler === null) {
return;
}
window.removeEventListener(eventType, eventHandler);
eventHandler = null;
}
/**
* Registers an EventListener if it hasn't been registered.
* @param {EventCallback} callback A function called after the event handler to handle its context.
* @returns {unregister | void} A function to unregister the registered EventListener if registration is performed.
*/
function register(callback) {
if (eventHandler !== null) {
return;
}
eventHandler = createHandler(callback);
window.addEventListener(eventType, eventHandler);
return unregister;
}
return register;
}
const handleError = createWindowEventHandler('error', createErrorHandler);
const handleUnhandledRejection = createWindowEventHandler(
'unhandledrejection',
createRejectionHandler
);
module.exports = {
handleError: handleError,
handleUnhandledRejection: handleUnhandledRejection,
};
================================================
FILE: client/utils/formatWebpackErrors.js
================================================
/**
* @typedef {Object} WebpackErrorObj
* @property {string} moduleIdentifier
* @property {string} moduleName
* @property {string} message
*/
const friendlySyntaxErrorLabel = 'Syntax error:';
/**
* Checks if the error message is for a syntax error.
* @param {string} message The raw Webpack error message.
* @returns {boolean} Whether the error message is for a syntax error.
*/
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
/**
* Cleans up Webpack error messages.
*
* This implementation is based on the one from [create-react-app](https://github.com/facebook/create-react-app/blob/edc671eeea6b7d26ac3f1eb2050e50f75cf9ad5d/packages/react-dev-utils/formatWebpackMessages.js).
* @param {string} message The raw Webpack error message.
* @returns {string} The formatted Webpack error message.
*/
function formatMessage(message) {
let lines = message.split('\n');
// Strip Webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter(function (line) {
return !/Module [A-z ]+\(from/.test(line);
});
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1);
}
// Remove duplicated newlines
lines = lines.filter(function (line, index, arr) {
return index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim();
});
// Clean up the file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
lines[1]
.replace('Error: ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:'),
];
}
message = lines.join('\n');
// Clean up syntax errors
message = message.replace('SyntaxError:', friendlySyntaxErrorLabel);
// Internal stacks are generally useless, so we strip them -
// except the stacks containing `webpack:`,
// because they're normally from user code generated by webpack.
message = message.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, ''); // at ... ...:x:y
message = message.replace(/^\s*at\s((?!webpack:).)*[\s)]*(\n|$)/gm, ''); // at ...
message = message.replace(/^\s*at\s(\n|$)/gm, ''); // at
return message.trim();
}
/**
* Formats Webpack error messages into a more readable format.
* @param {Array} errors An array of Webpack error messages.
* @returns {string[]} The formatted Webpack error messages.
*/
function formatWebpackErrors(errors) {
let formattedErrors = errors.map(function (errorObjOrMessage) {
// Webpack 5 compilation errors are in the form of descriptor objects,
// so we have to join pieces to get the format we want.
if (typeof errorObjOrMessage === 'object') {
return formatMessage([errorObjOrMessage.moduleName, errorObjOrMessage.message].join('\n'));
}
// Webpack 4 compilation errors are strings
return formatMessage(errorObjOrMessage);
});
if (formattedErrors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
}
return formattedErrors;
}
module.exports = formatWebpackErrors;
================================================
FILE: client/utils/retry.js
================================================
function runWithRetry(callback, maxRetries, message) {
function executeWithRetryAndTimeout(currentCount) {
try {
if (currentCount > maxRetries - 1) {
console.warn('[React Refresh]', message);
return;
}
callback();
} catch (err) {
setTimeout(
function () {
executeWithRetryAndTimeout(currentCount + 1);
},
Math.pow(10, currentCount)
);
}
}
executeWithRetryAndTimeout(0);
}
module.exports = runWithRetry;
================================================
FILE: docs/API.md
================================================
# API
## Directives
The `react-refresh/babel` plugin provide support to directive comments out of the box.
### `reset`
```js
/* @refresh reset */
```
This directive tells React Refresh to force reset state on every refresh (current file only).
This can be useful, for example, to reset error boundary components' state,
so it doesn't persist when new code is executed.
## Options
This plugin accepts a few options to tweak its behaviour.
In usual scenarios, you probably wouldn't have to reach for them -
they exist specifically to enable integration in more advanced/complicated setups.
### `ReactRefreshPluginOptions`
```ts
interface ReactRefreshPluginOptions {
forceEnable?: boolean;
exclude?: string | RegExp | Array;
include?: string | RegExp | Array;
library?: string;
esModule?: boolean | ESModuleOptions;
overlay?: boolean | ErrorOverlayOptions;
}
```
#### `forceEnable`
Type: `boolean`
Default: `undefined`
Enables the plugin forcefully.
It is useful if you want to:
- Use the plugin in production;
- Use the plugin with the `none` mode in Webpack without setting `NODE_ENV`;
- Use the plugin in environments we do not support, such as `electron-prerender`
(**WARNING: Proceed at your own risk!**).
#### `exclude`
Type: `string | RegExp | Array`
Default: `/node_modules/`
Exclude files from being processed by the plugin.
This is similar to the `module.rules` option in Webpack.
#### `include`
Type: `string | RegExp | Array`
Default: `/\.([jt]sx?|flow)$/i`
Include files to be processed by the plugin.
This is similar to the `module.rules` option in Webpack.
#### `library`
Type: `string`
Default: `''`, or `output.uniqueName` in Webpack 5, or `output.library` for both Webpack 4/5 if set
Sets a namespace for the React Refresh runtime.
This is similar to the `output.uniqueName` in Webpack 5 or the `output.library` option in Webpack 4/5.
It is most useful when multiple instances of React Refresh is running together simultaneously.
#### `esModule`
Type: `boolean | ESModuleOptions`
Default: `undefined` (auto-detection)
Enables strict ES Modules compatible runtime.
By default, the plugin will try to infer the module system same as Webpack 5,
either via the `type` property in `package.json` (`commonjs` and `module`),
or via the file extension (`.cjs` and `.mjs`).
It is most useful when you want to enforce output of native ESM code.
See the [`ESModuleOptions`](#esmoduleoptions) section below for more details on the object API.
#### `overlay`
Type: `boolean | ErrorOverlayOptions`
Default:
```json5
{
entry: '@pmmmwh/react-refresh-webpack-plugin/client/ErrorOverlayEntry',
module: '@pmmmwh/react-refresh-webpack-plugin/overlay',
sockIntegration: 'wds',
}
```
Modifies behaviour of the plugin's error overlay integration:
- If `overlay` is not provided or `true`, the **DEFAULT** behaviour will be used;
- If `overlay` is `false`, the error overlay integration will be **DISABLED**;
- If `overlay` is an object (`ErrorOverlayOptions`), it will act with respect to what is provided
(\*NOTE: This is targeted for ADVANCED use cases.).
See the [`ErrorOverlayOptions`](#erroroverlayoptions) section below for more details on the object API.
### `ESModuleOptions`
```ts
interface ESModuleOptions {
exclude?: string | RegExp | Array;
include?: string | RegExp | Array;
}
```
#### `exclude`
Type: `string | RegExp | Array`
Default: `/node_modules/`
Exclude files from being processed as ESM.
This is similar to the `module.rules` option in Webpack.
#### `include`
Type: `string | RegExp | Array`
Default: `/\.([jt]sx?|flow)$/i`
Include files to be processed as ESM.
This is similar to the `module.rules` option in Webpack.
### `ErrorOverlayOptions`
```ts
interface ErrorOverlayOptions {
entry?: string | false;
module?: string | false;
sockIntegration?: 'wds' | 'whm' | 'wps' | false | string;
}
```
#### `entry`
Type: `string | false`
Default: `'@pmmmwh/react-refresh-webpack-plugin/client/ErrorOverlayEntry'`
The **PATH** to a file/module that sets up the error overlay integration.
Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
When set to `false`, no code will be injected for this stage.
#### `module`
Type: `string | false`
Default: `'@pmmmwh/react-refresh-webpack-plugin/overlay'`
The **PATH** to a file/module to be used as an error overlay (e.g. `react-error-overlay`).
Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
The provided file should contain two **NAMED** exports:
```ts
function handleRuntimeError(error: Error) {}
function clearRuntimeErrors() {}
```
- `handleRuntimeError` is invoked when a **RUNTIME** error is **CAUGHT** (e.g. during module initialisation or execution);
- `clearRuntimeErrors` is invoked when a module is **RE-INITIALISED** via "Fast Refresh".
If the default `entry` is used, the file should contain two more **NAMED** exports:
```ts
function showCompileError(webpackErrorMessage: string) {}
function clearCompileError() {}
```
- `showCompileError` is invoked when an error occurred during a Webpack compilation
(NOTE: `webpackErrorMessage` might be ANSI encoded depending on the integration);
- `clearCompileError` is invoked when a new Webpack compilation is started (i.e. HMR rebuild).
> Note: if you want to use `react-error-overlay` as a value to this option,
> you should instead use `react-dev-utils/refreshOverlayInterop` or implement a similar interop.
> The APIs expected by this plugin is slightly different from what `react-error-overlay` provides out-of-the-box.
#### `sockIntegration`
Default: `wds`
Type: `wds`, `whm`, `wps`, `false` or `string`
The HMR integration that the error overlay will interact with -
it can either be a short form name of the integration (`wds`, `whm`, `wps`),
or a **PATH** to a file/module that sets up a connection to receive Webpack build messages.
Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
Common HMR integrations (for Webpack) are support by this plugin out-of-the-box:
- For `webpack-dev-server`, you can skip this option or set it to `wds`;
- For `webpack-hot-middleware`, set this option to `whm`;
- For `webpack-plugin-serve`, set this option to `wps`.
If you use any other HMR integrations (e.g. custom ones), or if you want to customise how the connection is being setup,
you will need to implement a message client in the provided file/module.
You can reference implementations inside the [`sockets`](https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/main/sockets) directory.
================================================
FILE: docs/TROUBLESHOOTING.md
================================================
# Troubleshooting
## Coming from `react-hot-loader`
If you are coming from `react-hot-loader`, before using the plugin,
you have to ensure that you've completely erased the integration of RHL from your app:
- Removed the `react-hot-loader/babel` Babel plugin (from Babel config variants or Webpack config)
- Removed the `react-hot-loader/patch` Webpack entry
- Removed the `react-hot-loader/webpack` Webpack loader
- Removed the alias of `react-dom` to `@hot-loader/react-dom` (from `package.json` or Webpack config)
- Removed imports and their usages from `react-hot-loader`
This has to be done because, internally,
`react-hot-loader` intercepts and reconciles the React tree before React can try to re-render it.
That in turn breaks mechanisms the plugin depends on to deliver the experience.
## Compatibility with `npm@7`
`npm@7` have brought back the behaviour of auto-installing peer dependencies with new semantics,
but their support for optional peer dependencies,
used by this plugin to provide support to multiple integrations without bundling them all,
are known to be a bit lacking.
If you encounter the `ERESOLVE` error code while running `npm install` -
you can fallback to use the legacy dependency resolution algorithm and it should resolve the issue:
```sh
npm install --legacy-peer-deps
```
## Usage with CSS Files/Imports
This plugin does not provide HMR for CSS.
To achieve that,
you should be using [`style-loader`](https://github.com/webpack-contrib/style-loader) or [`mini-css-extract-plugin`](https://github.com/webpack-contrib/mini-css-extract-plugin).
Both provides HMR capabilities out of the box for Webpack 5 -
if are still on Webpack 4 and uses `mini-css-extract-plugin`, you might have to [do some setup](https://github.com/webpack-contrib/mini-css-extract-plugin/#hot-module-reloading-hmr).
## Usage with Indirection (like Workers and JS Templates)
If you share the Babel config for files in an indirect code path (e.g. Web Workers, JS Templates with partial pre-render) and all your other source files,
you might experience this error:
```
Uncaught ReferenceError: $RefreshReg$ is not defined
```
The reason is that when using child compilers (e.g. `html-webpack-plugin`, `worker-plugin`), plugins are usually not applied (but loaders are).
This means that code processed by `react-refresh/babel` is not further transformed by this plugin and will lead to broken builds.
To solve this issue, you can choose one of the following workarounds:
**Sloppy**
In the entry of your indirect code path (e.g. some `index.js`), add the following two lines:
```js
self.$RefreshReg$ = () => {};
self.$RefreshSig$ = () => () => {};
```
This basically acts as a "polyfill" for helpers expected by `react-refresh/babel`, so the worker can run properly.
**Simple**
Ensure all exports within the indirect code path are not named in `PascalCase`.
This will tell the Babel plugin to do nothing when it hits those files.
In general, the `PascalCase` naming scheme should be reserved for React components only,
and it doesn't really make sense for them to exist within non-React-rendering contexts.
**Robust but complex**
In your Webpack configuration, alter the Babel setup as follows:
```js
{
rules: [
// JS-related rules only
{
oneOf: [
{
test: /\.[jt]s$/,
include: '',
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// Your Babel config here
},
},
},
{
test: /\.[jt]sx?$/,
include: '',
exclude: ['', /node_modules/],
use: {
loader: 'babel-loader',
options: {
// Your Babel config here
plugins: [isDevelopment && 'react-refresh/babel'].filter(Boolean),
},
},
},
],
},
// Any other rules, such as CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
];
}
```
This would ensure that your indirect code path will not be processed by `react-refresh/babel`,
thus eliminating the problem completely.
## Hot Module Replacement (HMR) is not enabled
This means that you have not enabled HMR for Webpack, or we are unable to detect a working HMR implementation from the compilation context.
**Using `webpack-dev-server`**
Set the `hot` or `hotOnly` options to `true`.
**Using `webpack-hot-middleware`**
Add `HotModuleReplacementPlugin` to your list of plugins to use in development mode.
**Using `webpack-plugin-serve`**
Set the `hmr` option to `true`.
## State not preserved for class components
This is intended behaviour.
If you're coming from [react-hot-loader](https://github.com/gaearon/react-hot-loader), this might be the biggest surprise for you.
The main rationale behind this is listed in [this wish list for hot reloading](https://overreacted.io/my-wishlist-for-hot-reloading/):
> It is better to lose local state than to behave incorrectly.
>
> It is better to lose local state than use an old version.
The truth is, historically, hot reloading for class components was never reliable and maintainable.
It was implemented based on workarounds like wrapping components with Proxies.
While these workarounds made hot reloading "work" on the surface, they led to inconsistencies in other departments:
- Lifecycle methods will fire randomly at random times;
- Type checks will fail randomly (`.type === Component` is `false`); and
- Mutation of React's internals, which is difficult to manage and will need to play catch up with React as we move into the future (a-la Concurrent Mode).
Thus, to keep fast refresh reliable and resilient to errors (with recovery for most cases), class components will always be re-mounted on a hot update.
## Edits always lead to full reload
In most cases, if the plugin is applied correctly, it would mean that we weren't able to set up boundaries to stop update propagation.
It can be narrowed down to a few unsupported patterns:
1. Un-named/non-pascal-case-named components
See [this tweet](https://twitter.com/dan_abramov/status/1255229440860262400) for drawbacks of not giving component proper names.
They are impossible to support because we have no ways to statically determine they are React-related.
This issue also exist for other React developer tools, like the [hooks ESLint plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks).
Internal components in HOCs also have to conform to this rule.
```jsx
// won't work
export default () => ;
export default function () {
return ;
}
export default function divContainer() {
return ;
}
```
2. Chain of files leading to root with none containing React-related content only
This pattern cannot be supported because we cannot ensure non-React-related content are free of side effects.
Usually with this error you will see something like this in the browser console:
```
Ignored an update to unaccepted module ... [a very long path]
```
3. `export * from 'namespace'` (TypeScript only)
This only affect users using TypeScript on Babel.
This pattern is only supported when you don't mix normal exports with type exports, or when all your exports conform to the `PascalCase` rule.
This is because we cannot statically analyse the exports from the namespace to determine whether we can set up a boundary and stop update propagation.
## Component not updating with bundle splitting techniques
Webpack allows various bundle splitting techniques to improve performance and cacheability.
However, these techniques often result in a shuffled execution order, which will break fast refresh.
To make fast refresh work properly, make sure your Webpack configuration comply to the following rules:
1. All React-related packages (including custom reconcilers) should be in the same chunk with `react-refresh/runtime`
Because fast refresh internally uses the React DevTools protocol and have to be registered before any React code runs,
all React-related stuff needs to be in the same chunk to ensure execution order and object equality in the form of `WeakMap` keys.
**Using DLL plugin**
Ensure the entries for the DLL include `react-refresh/runtime`,
and the `mode` option is set to `development`.
```js
module.exports = {
mode: 'development',
entry: ['react', 'react-dom', 'react-refresh/runtime'],
};
```
**Using multiple entries**
Ensure the `react` chunk includes `react-refresh/runtime`.
```js
module.exports = {
entry: {
main: 'index.js',
vendors: ['react', 'react-dom', 'react-refresh/runtime'],
},
};
```
2. Only one copy of both the HMR runtime and the plugin's runtime should be embedded for one Webpack app
This concern only applies when you have multiple entry points.
You can use Webpack's `optimization.runtimeChunk` option to enforce this.
```js
module.exports = {
optimization: {
runtimeChunk: 'single',
},
};
```
## Externalising React
Fast refresh relies on initialising code before **ANY** React code is ran.
If you externalise React, however, it is likely that this plugin cannot inject the necessary runtime code before it.
You can deal with this in a few ways (also see [#334](https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/334) for relevant discussion).
**Production-only externalisation**
The simplest solution to this issue is to simply not externalise React in development.
This would guarantee any code injected by this plugin run before any React code,
and would require the least manual tweaking.
**Use React DevTools**
If the execution environment is something you can control, and you wanted to externalise React in development,
you can use React DevTools which would inject hooks to the environment for React to attach to.
React Refresh should be able to hook into copies of React connected this way even it runs afterwards,
but do note that React DevTools does not inject hooks over a frame boundary (`iframe`).
**Externalise React Refresh**
If all solutions above are not applicable, you can also externalise `react-refresh/runtime` together with React.
We provide an entrypoint to easily achieve this - `@pmmmwh/react-refresh-webpack-plugin/umd/client.min.js`.
If you would like to use the provided script, ensure that it is loaded before React and/or React-DOM.
You can load this script via any CDN for `npm`, such as `jsDelivr` and `unpkg`:
```html
```
If you don't want to use the provided script,
you can check out [this sandbox](https://codesandbox.io/s/react-refresh-externals-14fpn) for an example on how this could be done manually.
## Running multiple instances of React Refresh simultaneously
If you are running on a micro-frontend architecture (e.g. Module Federation in Webpack 5),
you should set the `library` output to ensure proper namespacing in the runtime injection script.
**Using Webpack's `output.uniqueName` option (Webpack 5 only)**
```js
module.exports = {
output: {
uniqueName: 'YourLibrary',
},
};
```
**Using Webpack's `output.library` option**
```js
module.exports = {
output: {
library: 'YourLibrary',
},
};
```
**Using the plugin's `library` option**
```js
module.exports = {
plugins: [
new ReactRefreshPlugin({
library: 'YourLibrary',
}),
],
};
```
================================================
FILE: examples/flow-with-babel/.flowconfig
================================================
[ignore]
[include]
[libs]
[lints]
[options]
[strict]
================================================
FILE: examples/flow-with-babel/babel.config.js
================================================
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'@babel/preset-env',
'@babel/preset-flow',
// Enable development transform of React with new automatic runtime
['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
================================================
FILE: examples/flow-with-babel/package.json
================================================
{
"name": "using-flow-with-babel",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-flow": "^7.25.9",
"@babel/preset-react": "^7.26.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"flow-bin": "^0.266.1",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"start": "webpack serve --hot",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/flow-with-babel/public/index.html
================================================
Flow React App
================================================
FILE: examples/flow-with-babel/src/App.jsx
================================================
// @flow
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/flow-with-babel/src/ArrowFunction.jsx
================================================
// @flow
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/flow-with-babel/src/ClassDefault.jsx
================================================
// @flow
import { Component } from 'react';
class ClassDefault extends Component<{}> {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/flow-with-babel/src/ClassNamed.jsx
================================================
// @flow
import { Component } from 'react';
export class ClassNamed extends Component<{}> {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/flow-with-babel/src/FunctionDefault.jsx
================================================
// @flow
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/flow-with-babel/src/FunctionNamed.jsx
================================================
// @flow
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/flow-with-babel/src/LazyComponent.jsx
================================================
// @flow
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/flow-with-babel/src/index.js
================================================
// @flow
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render();
================================================
FILE: examples/flow-with-babel/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
================================================
FILE: examples/typescript-with-babel/babel.config.js
================================================
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
// Enable development transform of React with new automatic runtime
['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
================================================
FILE: examples/typescript-with-babel/package.json
================================================
{
"name": "using-typescript-with-babel",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.27.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"typescript": "~5.8.3",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"start": "webpack serve --hot",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/typescript-with-babel/public/index.html
================================================
TS React App
================================================
FILE: examples/typescript-with-babel/src/App.tsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/typescript-with-babel/src/ArrowFunction.tsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/typescript-with-babel/src/ClassDefault.tsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/typescript-with-babel/src/ClassNamed.tsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/typescript-with-babel/src/FunctionDefault.tsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/typescript-with-babel/src/FunctionNamed.tsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/typescript-with-babel/src/LazyComponent.tsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/typescript-with-babel/src/index.tsx
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container!);
root.render();
================================================
FILE: examples/typescript-with-babel/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "ESNext",
"moduleResolution": "node",
"outDir": "dist",
"target": "ESNext",
"sourceMap": true
},
"include": ["src"]
}
================================================
FILE: examples/typescript-with-babel/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.tsx',
},
module: {
rules: [
{
test: /\.tsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
};
================================================
FILE: examples/typescript-with-swc/package.json
================================================
{
"name": "using-typescript-with-swc",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@swc/core": "^1.11.16",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"core-js": "^3.41.0",
"cross-env": "^7.0.3",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"swc-loader": "^0.2.6",
"typescript": "~5.8.3",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"start": "webpack serve --hot",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/typescript-with-swc/public/index.html
================================================
swc React App
================================================
FILE: examples/typescript-with-swc/src/App.tsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/typescript-with-swc/src/ArrowFunction.tsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/typescript-with-swc/src/ClassDefault.tsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/typescript-with-swc/src/ClassNamed.tsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/typescript-with-swc/src/FunctionDefault.tsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/typescript-with-swc/src/FunctionNamed.tsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/typescript-with-swc/src/LazyComponent.tsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/typescript-with-swc/src/index.tsx
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container!);
root.render();
================================================
FILE: examples/typescript-with-swc/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "ESNext",
"moduleResolution": "node",
"outDir": "dist",
"target": "ESNext",
"sourceMap": true
},
"include": ["src"]
}
================================================
FILE: examples/typescript-with-swc/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.tsx',
},
module: {
rules: [
{
test: /\.tsx?$/,
include: path.join(__dirname, 'src'),
use: [
{
loader: 'swc-loader',
options: {
env: { mode: 'usage' },
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
dynamicImport: true,
},
transform: {
react: {
// swc-loader will check whether webpack mode is 'development'
// and set this automatically starting from 0.1.13. You could also set it yourself.
// swc won't enable fast refresh when development is false
runtime: 'automatic',
refresh: isDevelopment,
},
},
},
},
},
],
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
};
================================================
FILE: examples/typescript-with-tsc/package.json
================================================
{
"name": "using-typescript-with-tsc",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"cross-env": "^7.0.3",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"react-refresh-typescript": "^2.0.10",
"ts-loader": "^9.5.2",
"typescript": "~5.8.3",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"start": "webpack serve --hot",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/typescript-with-tsc/public/index.html
================================================
TSC React App
================================================
FILE: examples/typescript-with-tsc/src/App.tsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/typescript-with-tsc/src/ArrowFunction.tsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/typescript-with-tsc/src/ClassDefault.tsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/typescript-with-tsc/src/ClassNamed.tsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/typescript-with-tsc/src/FunctionDefault.tsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/typescript-with-tsc/src/FunctionNamed.tsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/typescript-with-tsc/src/LazyComponent.tsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/typescript-with-tsc/src/index.tsx
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container!);
root.render();
================================================
FILE: examples/typescript-with-tsc/tsconfig.dev.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
},
"include": ["src"]
}
================================================
FILE: examples/typescript-with-tsc/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "ESNext",
"moduleResolution": "node",
"outDir": "dist",
"target": "ESNext",
"sourceMap": true
},
"include": ["src"]
}
================================================
FILE: examples/typescript-with-tsc/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.tsx',
},
module: {
rules: [
{
test: /\.tsx?$/,
include: path.join(__dirname, 'src'),
use: [
{
loader: 'ts-loader',
options: {
configFile: isDevelopment ? 'tsconfig.dev.json' : 'tsconfig.json',
transpileOnly: isDevelopment,
...(isDevelopment && {
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
}),
},
},
],
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
};
================================================
FILE: examples/webpack-dev-server/babel.config.js
================================================
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'@babel/preset-env',
// Enable development transform of React with new automatic runtime
['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
================================================
FILE: examples/webpack-dev-server/package.json
================================================
{
"name": "using-webpack-dev-server",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"start": "webpack serve --hot",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/webpack-dev-server/public/index.html
================================================
WDS React App
================================================
FILE: examples/webpack-dev-server/src/App.jsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/webpack-dev-server/src/ArrowFunction.jsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/webpack-dev-server/src/ClassDefault.jsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/webpack-dev-server/src/ClassNamed.jsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/webpack-dev-server/src/FunctionDefault.jsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/webpack-dev-server/src/FunctionNamed.jsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/webpack-dev-server/src/LazyComponent.jsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/webpack-dev-server/src/index.js
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render();
================================================
FILE: examples/webpack-dev-server/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
================================================
FILE: examples/webpack-hot-middleware/babel.config.js
================================================
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'@babel/preset-env',
// Enable development transform of React with new automatic runtime
['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
================================================
FILE: examples/webpack-hot-middleware/package.json
================================================
{
"name": "using-webpack-hot-middleware",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"express": "^5.2.0",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"webpack": "^5.98.0",
"webpack-dev-middleware": "^7.4.2",
"webpack-hot-middleware": "^2.26.1"
},
"scripts": {
"start": "node ./server.js",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/webpack-hot-middleware/public/index.html
================================================
WHM React App
================================================
FILE: examples/webpack-hot-middleware/server.js
================================================
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const app = express();
const compiler = webpack(config);
app.use(
require('webpack-dev-middleware')(compiler, {
publicPath: config.output.publicPath,
})
);
app.use(
require(`webpack-hot-middleware`)(compiler, {
log: false,
path: `/__webpack_hmr`,
heartbeat: 10 * 1000,
})
);
app.get('*', (req, res, next) => {
const filename = path.join(compiler.outputPath, 'index.html');
compiler.outputFileSystem.readFile(filename, (err, result) => {
if (err) {
return next(err);
}
res.set('content-type', 'text/html');
res.send(result);
res.end();
});
});
app.listen(8080, () => console.log('App is listening on port 8080!'));
================================================
FILE: examples/webpack-hot-middleware/src/App.jsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/webpack-hot-middleware/src/ArrowFunction.jsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/webpack-hot-middleware/src/ClassDefault.jsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/webpack-hot-middleware/src/ClassNamed.jsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/webpack-hot-middleware/src/FunctionDefault.jsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/webpack-hot-middleware/src/FunctionNamed.jsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/webpack-hot-middleware/src/LazyComponent.jsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/webpack-hot-middleware/src/index.js
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render();
================================================
FILE: examples/webpack-hot-middleware/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: {
main: ['webpack-hot-middleware/client', './src/index.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new webpack.HotModuleReplacementPlugin(),
isDevelopment &&
new ReactRefreshPlugin({
overlay: {
sockIntegration: 'whm',
},
}),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
================================================
FILE: examples/webpack-plugin-serve/babel.config.js
================================================
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'@babel/preset-env',
// Enable development transform of React with new automatic runtime
['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
================================================
FILE: examples/webpack-plugin-serve/package.json
================================================
{
"name": "using-webpack-plugin-serve",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"html-webpack-plugin": "^5.6.3",
"react-refresh": "^0.17.0",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-plugin-serve": "^1.6.0"
},
"scripts": {
"start": "webpack --watch",
"build": "cross-env NODE_ENV=production webpack"
}
}
================================================
FILE: examples/webpack-plugin-serve/public/index.html
================================================
WPS React App
================================================
FILE: examples/webpack-plugin-serve/src/App.jsx
================================================
import { lazy, Suspense } from 'react';
import { ArrowFunction } from './ArrowFunction';
import ClassDefault from './ClassDefault';
import { ClassNamed } from './ClassNamed';
import FunctionDefault from './FunctionDefault';
import { FunctionNamed } from './FunctionNamed';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
);
}
export default App;
================================================
FILE: examples/webpack-plugin-serve/src/ArrowFunction.jsx
================================================
export const ArrowFunction = () => Arrow Function
;
================================================
FILE: examples/webpack-plugin-serve/src/ClassDefault.jsx
================================================
import { Component } from 'react';
class ClassDefault extends Component {
render() {
return Default Export Class
;
}
}
export default ClassDefault;
================================================
FILE: examples/webpack-plugin-serve/src/ClassNamed.jsx
================================================
import { Component } from 'react';
export class ClassNamed extends Component {
render() {
return Named Export Class
;
}
}
================================================
FILE: examples/webpack-plugin-serve/src/FunctionDefault.jsx
================================================
function FunctionDefault() {
return Default Export Function
;
}
export default FunctionDefault;
================================================
FILE: examples/webpack-plugin-serve/src/FunctionNamed.jsx
================================================
export function FunctionNamed() {
return Named Export Function
;
}
================================================
FILE: examples/webpack-plugin-serve/src/LazyComponent.jsx
================================================
function LazyComponent() {
return Lazy Component
;
}
export default LazyComponent;
================================================
FILE: examples/webpack-plugin-serve/src/index.js
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render();
================================================
FILE: examples/webpack-plugin-serve/webpack.config.js
================================================
const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { WebpackPluginServe: ServePlugin } = require('webpack-plugin-serve');
const isDevelopment = process.env.NODE_ENV !== 'production';
const outputPath = path.resolve(__dirname, 'dist');
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: {
main: ['webpack-plugin-serve/client', './src/index.js'],
},
output: {
filename: 'bundle.js',
path: outputPath,
publicPath: '/',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment &&
new ServePlugin({
port: 8080,
static: outputPath,
status: false,
}),
isDevelopment &&
new ReactRefreshPlugin({
overlay: {
sockIntegration: 'wps',
},
}),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
================================================
FILE: jest.config.js
================================================
module.exports = {
globalSetup: '/jest-global-setup.js',
globalTeardown: '/jest-global-teardown.js',
prettierPath: null,
resolver: '/jest-resolver.js',
rootDir: 'test',
setupFilesAfterEnv: ['/jest-test-setup.js'],
testEnvironment: '/jest-environment.js',
testMatch: ['/**/*.test.js'],
};
================================================
FILE: lib/globals.js
================================================
/**
* Gets current bundle's global scope identifier for React Refresh.
* @param {Record} runtimeGlobals The Webpack runtime globals.
* @returns {string} The React Refresh global scope within the Webpack bundle.
*/
module.exports.getRefreshGlobalScope = (runtimeGlobals) => {
return `${runtimeGlobals.require || '__webpack_require__'}.$Refresh$`;
};
================================================
FILE: lib/index.js
================================================
const { validate: validateOptions } = require('schema-utils');
const { getRefreshGlobalScope } = require('./globals');
const {
getAdditionalEntries,
getIntegrationEntry,
getSocketIntegration,
injectRefreshLoader,
makeRefreshRuntimeModule,
normalizeOptions,
} = require('./utils');
const schema = require('./options.json');
class ReactRefreshPlugin {
/**
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
*/
constructor(options = {}) {
validateOptions(schema, options, {
name: 'React Refresh Plugin',
baseDataPath: 'options',
});
/**
* @readonly
* @type {import('./types').NormalizedPluginOptions}
*/
this.options = normalizeOptions(options);
}
/**
* Applies the plugin.
* @param {import('webpack').Compiler} compiler A webpack compiler object.
* @returns {void}
*/
apply(compiler) {
// Skip processing in non-development mode, but allow manual force-enabling
if (
// Webpack do not set process.env.NODE_ENV, so we need to check for mode.
// Ref: https://github.com/webpack/webpack/issues/7074
(compiler.options.mode !== 'development' ||
// We also check for production process.env.NODE_ENV,
// in case it was set and mode is non-development (e.g. 'none')
(process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
!this.options.forceEnable
) {
return;
}
const logger = compiler.getInfrastructureLogger(this.constructor.name);
// Get Webpack imports from compiler instance (if available) -
// this allow mono-repos to use different versions of Webpack without conflicts.
const webpack = compiler.webpack || require('webpack');
const {
DefinePlugin,
EntryDependency,
EntryPlugin,
ModuleFilenameHelpers,
NormalModule,
ProvidePlugin,
RuntimeGlobals,
Template,
} = webpack;
// Inject react-refresh context to all Webpack entry points.
const { overlayEntries, prependEntries } = getAdditionalEntries(this.options);
// Prepended entries does not care about injection order,
// so we can utilise EntryPlugin for simpler logic.
for (const entry of prependEntries) {
new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
}
const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
const socketEntryData = [];
compiler.hooks.make.tap(
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
(compilation) => {
// Exhaustively search all entries for `integrationEntry`.
// If found, mark those entries and the index of `integrationEntry`.
for (const [name, entryData] of compilation.entries.entries()) {
const index = entryData.dependencies.findIndex(
(dep) => dep.request && dep.request.includes(integrationEntry)
);
if (index !== -1) {
socketEntryData.push({ name, index });
}
}
}
);
// Overlay entries need to be injected AFTER integration's entry,
// so we will loop through everything in `finishMake` instead of `make`.
// This ensures we can traverse all entry points and inject stuff with the correct order.
for (const [idx, entry] of overlayEntries.entries()) {
compiler.hooks.finishMake.tapPromise(
{
name: this.constructor.name,
stage: Number.MIN_SAFE_INTEGER + (overlayEntries.length - idx - 1),
},
(compilation) => {
// Only hook into the current compiler
if (compilation.compiler !== compiler) {
return Promise.resolve();
}
const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }];
return Promise.all(
injectData.map(({ name, index }) => {
return new Promise((resolve, reject) => {
const options = { name };
const dep = EntryPlugin.createDependency(entry, options);
compilation.addEntry(compiler.context, dep, options, (err) => {
if (err) return reject(err);
// If the entry is not a global one,
// and we have registered the index for integration entry,
// we will reorder all entry dependencies to our desired order.
// That is, to have additional entries DIRECTLY behind integration entry.
if (name && typeof index !== 'undefined') {
const entryData = compilation.entries.get(name);
entryData.dependencies.splice(
index + 1,
0,
entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
);
}
resolve();
});
});
})
);
}
);
}
// Inject necessary modules and variables to bundle's global scope
const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {});
/** @type {Record}*/
const definedModules = {
// Mapping of react-refresh globals to Webpack runtime globals
$RefreshReg$: `${refreshGlobal}.register`,
$RefreshSig$: `${refreshGlobal}.signature`,
'typeof $RefreshReg$': 'function',
'typeof $RefreshSig$': 'function',
// Library mode
__react_refresh_library__: JSON.stringify(
Template.toIdentifier(
this.options.library ||
compiler.options.output.uniqueName ||
compiler.options.output.library
)
),
};
/** @type {Record} */
const providedModules = {
__react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
};
if (this.options.overlay === false) {
// Stub errorOverlay module so their calls can be erased
definedModules.__react_refresh_error_overlay__ = false;
definedModules.__react_refresh_socket__ = false;
} else {
if (this.options.overlay.module) {
providedModules.__react_refresh_error_overlay__ = require.resolve(
this.options.overlay.module
);
}
if (this.options.overlay.sockIntegration) {
providedModules.__react_refresh_socket__ = getSocketIntegration(
this.options.overlay.sockIntegration
);
}
}
new DefinePlugin(definedModules).apply(compiler);
new ProvidePlugin(providedModules).apply(compiler);
const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
let loggedHotWarning = false;
compiler.hooks.compilation.tap(
this.constructor.name,
(compilation, { normalModuleFactory }) => {
// Only hook into the current compiler
if (compilation.compiler !== compiler) {
return;
}
// Set factory for EntryDependency which is used to initialise the module
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);
const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
compilation.hooks.additionalTreeRuntimeRequirements.tap(
this.constructor.name,
// Setup react-refresh globals with a Webpack runtime module
(chunk, runtimeRequirements) => {
runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
runtimeRequirements.add(RuntimeGlobals.moduleCache);
runtimeRequirements.add(refreshGlobal);
compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule());
}
);
normalModuleFactory.hooks.afterResolve.tap(
this.constructor.name,
// Add react-refresh loader to process files that matches specified criteria
(resolveData) => {
injectRefreshLoader(resolveData.createData, {
match,
options: {
const: compilation.runtimeTemplate.supportsConst(),
esModule: this.options.esModule,
},
});
}
);
NormalModule.getCompilationHooks(compilation).loader.tap(
// `Infinity` ensures this check will run only after all other taps
{ name: this.constructor.name, stage: Infinity },
// Check for existence of the HMR runtime -
// it is the foundation to this plugin working correctly
(context) => {
if (!context.hot && !loggedHotWarning) {
logger.warn(
[
'Hot Module Replacement (HMR) is not enabled!',
'React Refresh requires HMR to function properly.',
].join(' ')
);
loggedHotWarning = true;
}
}
);
}
);
}
}
module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
module.exports = ReactRefreshPlugin;
================================================
FILE: lib/options.json
================================================
{
"additionalProperties": false,
"type": "object",
"definitions": {
"Path": { "type": "string" },
"MatchCondition": {
"anyOf": [{ "instanceof": "RegExp" }, { "$ref": "#/definitions/Path" }]
},
"MatchConditions": {
"type": "array",
"items": { "$ref": "#/definitions/MatchCondition" },
"minItems": 1
},
"ESModuleOptions": {
"additionalProperties": false,
"type": "object",
"properties": {
"exclude": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
},
"include": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
}
}
},
"OverlayOptions": {
"additionalProperties": false,
"type": "object",
"properties": {
"entry": {
"anyOf": [{ "const": false }, { "$ref": "#/definitions/Path" }]
},
"module": {
"anyOf": [{ "const": false }, { "$ref": "#/definitions/Path" }]
},
"sockIntegration": {
"anyOf": [
{ "const": false },
{ "enum": ["wds", "whm", "wps"] },
{ "$ref": "#/definitions/Path" }
]
}
}
}
},
"properties": {
"esModule": {
"anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/ESModuleOptions" }]
},
"exclude": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
},
"forceEnable": { "type": "boolean" },
"include": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
},
"library": { "type": "string" },
"overlay": {
"anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/OverlayOptions" }]
}
}
}
================================================
FILE: lib/runtime/RefreshUtils.js
================================================
/* global __webpack_require__ */
var Refresh = require('react-refresh/runtime');
/**
* Extracts exports from a webpack module object.
* @param {string} moduleId A Webpack module ID.
* @returns {*} An exports object from the module.
*/
function getModuleExports(moduleId) {
if (typeof moduleId === 'undefined') {
// `moduleId` is unavailable, which indicates that this module is not in the cache,
// which means we won't be able to capture any exports,
// and thus they cannot be refreshed safely.
// These are likely runtime or dynamically generated modules.
return {};
}
var maybeModule = __webpack_require__.c[moduleId];
if (typeof maybeModule === 'undefined') {
// `moduleId` is available but the module in cache is unavailable,
// which indicates the module is somehow corrupted (e.g. broken Webpacak `module` globals).
// We will warn the user (as this is likely a mistake) and assume they cannot be refreshed.
console.warn('[React Refresh] Failed to get exports for module: ' + moduleId + '.');
return {};
}
var exportsOrPromise = maybeModule.exports;
if (typeof Promise !== 'undefined' && exportsOrPromise instanceof Promise) {
return exportsOrPromise.then(function (exports) {
return exports;
});
}
return exportsOrPromise;
}
/**
* Calculates the signature of a React refresh boundary.
* If this signature changes, it's unsafe to accept the boundary.
*
* This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L795-L816).
* @param {*} moduleExports A Webpack module exports object.
* @returns {string[]} A React refresh boundary signature array.
*/
function getReactRefreshBoundarySignature(moduleExports) {
var signature = [];
signature.push(Refresh.getFamilyByType(moduleExports));
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
return signature;
}
for (var key in moduleExports) {
if (key === '__esModule') {
continue;
}
signature.push(key);
signature.push(Refresh.getFamilyByType(moduleExports[key]));
}
return signature;
}
/**
* Creates a data object to be retained across refreshes.
* This object should not transtively reference previous exports,
* which can form infinite chain of objects across refreshes, which can pressure RAM.
*
* @param {*} moduleExports A Webpack module exports object.
* @returns {*} A React refresh boundary signature array.
*/
function getWebpackHotData(moduleExports) {
return {
signature: getReactRefreshBoundarySignature(moduleExports),
isReactRefreshBoundary: isReactRefreshBoundary(moduleExports),
};
}
/**
* Creates a helper that performs a delayed React refresh.
* @returns {function(function(): void): void} A debounced React refresh function.
*/
function createDebounceUpdate() {
/**
* A cached setTimeout handler.
* @type {number | undefined}
*/
var refreshTimeout;
/**
* Performs react refresh on a delay and clears the error overlay.
* @param {function(): void} callback
* @returns {void}
*/
function enqueueUpdate(callback) {
if (typeof refreshTimeout === 'undefined') {
refreshTimeout = setTimeout(function () {
refreshTimeout = undefined;
Refresh.performReactRefresh();
callback();
}, 30);
}
}
return enqueueUpdate;
}
/**
* Checks if all exports are likely a React component.
*
* This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L748-L774).
* @param {*} moduleExports A Webpack module exports object.
* @returns {boolean} Whether the exports are React component like.
*/
function isReactRefreshBoundary(moduleExports) {
if (Refresh.isLikelyComponentType(moduleExports)) {
return true;
}
if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
return false;
}
var hasExports = false;
var areAllExportsComponents = true;
for (var key in moduleExports) {
hasExports = true;
// This is the ES Module indicator flag
if (key === '__esModule') {
continue;
}
// We can (and have to) safely execute getters here,
// as Webpack manually assigns harmony exports to getters,
// without any side-effects attached.
// Ref: https://github.com/webpack/webpack/blob/b93048643fe74de2a6931755911da1212df55897/lib/MainTemplate.js#L281
var exportValue = moduleExports[key];
if (!Refresh.isLikelyComponentType(exportValue)) {
areAllExportsComponents = false;
}
}
return hasExports && areAllExportsComponents;
}
/**
* Checks if exports are likely a React component and registers them.
*
* This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L818-L835).
* @param {*} moduleExports A Webpack module exports object.
* @param {string} moduleId A Webpack module ID.
* @returns {void}
*/
function registerExportsForReactRefresh(moduleExports, moduleId) {
if (Refresh.isLikelyComponentType(moduleExports)) {
// Register module.exports if it is likely a component
Refresh.register(moduleExports, moduleId + ' %exports%');
}
if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over the exports.
return;
}
for (var key in moduleExports) {
// Skip registering the ES Module indicator
if (key === '__esModule') {
continue;
}
var exportValue = moduleExports[key];
if (Refresh.isLikelyComponentType(exportValue)) {
var typeID = moduleId + ' %exports% ' + key;
Refresh.register(exportValue, typeID);
}
}
}
/**
* Compares previous and next module objects to check for mutated boundaries.
*
* This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L776-L792).
* @param {*} prevSignature The signature of the current Webpack module exports object.
* @param {*} nextSignature The signature of the next Webpack module exports object.
* @returns {boolean} Whether the React refresh boundary should be invalidated.
*/
function shouldInvalidateReactRefreshBoundary(prevSignature, nextSignature) {
if (prevSignature.length !== nextSignature.length) {
return true;
}
for (var i = 0; i < nextSignature.length; i += 1) {
if (prevSignature[i] !== nextSignature[i]) {
return true;
}
}
return false;
}
var enqueueUpdate = createDebounceUpdate();
function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isTest) {
registerExportsForReactRefresh(moduleExports, moduleId);
if (webpackHot) {
var isHotUpdate = !!webpackHot.data;
var prevData;
if (isHotUpdate) {
prevData = webpackHot.data.prevData;
}
if (isReactRefreshBoundary(moduleExports)) {
webpackHot.dispose(
/**
* A callback to performs a full refresh if React has unrecoverable errors,
* and also caches the to-be-disposed module.
* @param {*} data A hot module data object from Webpack HMR.
* @returns {void}
*/
function hotDisposeCallback(data) {
// We have to mutate the data object to get data registered and cached
data.prevData = getWebpackHotData(moduleExports);
}
);
webpackHot.accept(
/**
* An error handler to allow self-recovering behaviours.
* @param {Error} error An error occurred during evaluation of a module.
* @returns {void}
*/
function hotErrorHandler(error) {
if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {
refreshOverlay.handleRuntimeError(error);
}
if (typeof isTest !== 'undefined' && isTest) {
if (window.onHotAcceptError) {
window.onHotAcceptError(error.message);
}
}
__webpack_require__.c[moduleId].hot.accept(hotErrorHandler);
}
);
if (isHotUpdate) {
if (
prevData &&
prevData.isReactRefreshBoundary &&
shouldInvalidateReactRefreshBoundary(
prevData.signature,
getReactRefreshBoundarySignature(moduleExports)
)
) {
webpackHot.invalidate();
} else {
enqueueUpdate(
/**
* A function to dismiss the error overlay after performing React refresh.
* @returns {void}
*/
function updateCallback() {
if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {
refreshOverlay.clearRuntimeErrors();
}
}
);
}
}
} else {
if (isHotUpdate && typeof prevData !== 'undefined') {
webpackHot.invalidate();
}
}
}
}
module.exports = Object.freeze({
enqueueUpdate: enqueueUpdate,
executeRuntime: executeRuntime,
getModuleExports: getModuleExports,
isReactRefreshBoundary: isReactRefreshBoundary,
registerExportsForReactRefresh: registerExportsForReactRefresh,
});
================================================
FILE: lib/types.js
================================================
/**
* @typedef {Object} ErrorOverlayOptions
* @property {string | false} [entry] Path to a JS file that sets up the error overlay integration.
* @property {string | false} [module] The error overlay module to use.
* @property {import('type-fest').LiteralUnion<'wds' | 'whm' | 'wps' | false, string>} [sockIntegration] Path to a JS file that sets up the Webpack socket integration.
*/
/**
* @typedef {import('type-fest').SetRequired} NormalizedErrorOverlayOptions
*/
/**
* @typedef {Object} ReactRefreshPluginOptions
* @property {boolean | import('../loader/types').ESModuleOptions} [esModule] Enables strict ES Modules compatible runtime.
* @property {string | RegExp | Array} [exclude] Files to explicitly exclude from processing.
* @property {boolean} [forceEnable] Enables the plugin forcefully.
* @property {string | RegExp | Array} [include] Files to explicitly include for processing.
* @property {string} [library] Name of the library bundle.
* @property {boolean | ErrorOverlayOptions} [overlay] Modifies how the error overlay integration works in the plugin.
*/
/**
* @typedef {Object} OverlayOverrides
* @property {false | NormalizedErrorOverlayOptions} overlay Modifies how the error overlay integration works in the plugin.
*/
/**
* @typedef {import('type-fest').SetRequired, 'exclude' | 'include'> & OverlayOverrides} NormalizedPluginOptions
*/
module.exports = {};
================================================
FILE: lib/utils/getAdditionalEntries.js
================================================
/**
* @typedef {Object} AdditionalEntries
* @property {string[]} prependEntries
* @property {string[]} overlayEntries
*/
/**
* Creates an object that contains two entry arrays: the prependEntries and overlayEntries
* @param {import('../types').NormalizedPluginOptions} options Configuration options for this plugin.
* @returns {AdditionalEntries} An object that contains the Webpack entries for prepending and the overlay feature
*/
function getAdditionalEntries(options) {
const prependEntries = [
// React-refresh runtime
require.resolve('../../client/ReactRefreshEntry'),
];
const overlayEntries = [
// Error overlay runtime
options.overlay && options.overlay.entry && require.resolve(options.overlay.entry),
].filter(Boolean);
return { prependEntries, overlayEntries };
}
module.exports = getAdditionalEntries;
================================================
FILE: lib/utils/getIntegrationEntry.js
================================================
/**
* Gets entry point of a supported socket integration.
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
* @returns {string | undefined} Path to the resolved integration entry point.
*/
function getIntegrationEntry(integrationType) {
let resolvedEntry;
switch (integrationType) {
case 'whm': {
resolvedEntry = 'webpack-hot-middleware/client';
break;
}
case 'wps': {
resolvedEntry = 'webpack-plugin-serve/client';
break;
}
}
return resolvedEntry;
}
module.exports = getIntegrationEntry;
================================================
FILE: lib/utils/getSocketIntegration.js
================================================
/**
* Gets the socket integration to use for Webpack messages.
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
* @returns {string} Path to the resolved socket integration module.
*/
function getSocketIntegration(integrationType) {
let resolvedSocketIntegration;
switch (integrationType) {
case 'wds': {
resolvedSocketIntegration = require.resolve('../../sockets/WDSSocket');
break;
}
case 'whm': {
resolvedSocketIntegration = require.resolve('../../sockets/WHMEventSource');
break;
}
case 'wps': {
resolvedSocketIntegration = require.resolve('../../sockets/WPSSocket');
break;
}
default: {
resolvedSocketIntegration = require.resolve(integrationType);
break;
}
}
return resolvedSocketIntegration;
}
module.exports = getSocketIntegration;
================================================
FILE: lib/utils/index.js
================================================
const getAdditionalEntries = require('./getAdditionalEntries');
const getIntegrationEntry = require('./getIntegrationEntry');
const getSocketIntegration = require('./getSocketIntegration');
const injectRefreshLoader = require('./injectRefreshLoader');
const makeRefreshRuntimeModule = require('./makeRefreshRuntimeModule');
const normalizeOptions = require('./normalizeOptions');
module.exports = {
getAdditionalEntries,
getIntegrationEntry,
getSocketIntegration,
injectRefreshLoader,
makeRefreshRuntimeModule,
normalizeOptions,
};
================================================
FILE: lib/utils/injectRefreshLoader.js
================================================
const path = require('node:path');
/**
* @callback MatchObject
* @param {string} [str]
* @returns {boolean}
*/
/**
* @typedef {Object} InjectLoaderOptions
* @property {MatchObject} match A function to include/exclude files to be processed.
* @property {import('../../loader/types').ReactRefreshLoaderOptions} [options] Options passed to the loader.
*/
const resolvedLoader = require.resolve('../../loader');
const reactRefreshPath = path.dirname(require.resolve('react-refresh'));
const refreshUtilsPath = path.join(__dirname, '../runtime/RefreshUtils');
/**
* Injects refresh loader to all JavaScript-like and user-specified files.
* @param {*} moduleData Module factory creation data.
* @param {InjectLoaderOptions} injectOptions Options to alter how the loader is injected.
* @returns {*} The injected module factory creation data.
*/
function injectRefreshLoader(moduleData, injectOptions) {
const { match, options } = injectOptions;
// Include and exclude user-specified files
if (!match(moduleData.matchResource || moduleData.resource)) return moduleData;
// Include and exclude dynamically generated modules from other loaders
if (moduleData.matchResource && !match(moduleData.request)) return moduleData;
// Exclude files referenced as assets
if (moduleData.type.includes('asset')) return moduleData;
// Check to prevent double injection
if (moduleData.loaders.find(({ loader }) => loader === resolvedLoader)) return moduleData;
// Skip react-refresh and the plugin's runtime utils to prevent self-referencing -
// this is useful when using the plugin as a direct dependency,
// or when node_modules are specified to be processed.
if (
moduleData.resource.includes(reactRefreshPath) ||
moduleData.resource.includes(refreshUtilsPath)
) {
return moduleData;
}
// As we inject runtime code for each module,
// it is important to run the injected loader after everything.
// This way we can ensure that all code-processing have been done,
// and we won't risk breaking tools like Flow or ESLint.
moduleData.loaders.unshift({
loader: resolvedLoader,
options,
});
return moduleData;
}
module.exports = injectRefreshLoader;
================================================
FILE: lib/utils/makeRefreshRuntimeModule.js
================================================
/**
* Makes a runtime module to intercept module execution for React Refresh.
* This module creates an isolated `__webpack_require__` function for each module,
* and injects a `$Refresh$` object into it for use by React Refresh.
* @param {import('webpack')} webpack The Webpack exports.
* @returns {typeof import('webpack').RuntimeModule} The runtime module class.
*/
function makeRefreshRuntimeModule(webpack) {
return class ReactRefreshRuntimeModule extends webpack.RuntimeModule {
constructor() {
// Second argument is the `stage` for this runtime module -
// we'll use the same stage as Webpack's HMR runtime module for safety.
super('react refresh', webpack.RuntimeModule.STAGE_BASIC);
}
/**
* @returns {string} runtime code
*/
generate() {
if (!this.compilation) throw new Error('Webpack compilation missing!');
const { runtimeTemplate } = this.compilation;
const constDeclaration = runtimeTemplate.supportsConst() ? 'const' : 'var';
return webpack.Template.asString([
`${constDeclaration} setup = ${runtimeTemplate.basicFunction('moduleId', [
`${constDeclaration} refresh = {`,
webpack.Template.indent([
`moduleId: moduleId,`,
`register: ${runtimeTemplate.basicFunction('type, id', [
`${constDeclaration} typeId = moduleId + ' ' + id;`,
'refresh.runtime.register(type, typeId);',
])},`,
`signature: ${runtimeTemplate.returningFunction(
'refresh.runtime.createSignatureFunctionForTransform()'
)},`,
`runtime: {`,
webpack.Template.indent([
`createSignatureFunctionForTransform: ${runtimeTemplate.returningFunction(
runtimeTemplate.returningFunction('type', 'type')
)},`,
`register: ${runtimeTemplate.emptyFunction()}`,
]),
`},`,
]),
`};`,
`return refresh;`,
])};`,
'',
`${webpack.RuntimeGlobals.interceptModuleExecution}.push(${runtimeTemplate.basicFunction(
'options',
[
`${constDeclaration} originalFactory = options.factory;`,
// Using a function declaration -
// ensures `this` would propagate for modules relying on it
`options.factory = function(moduleObject, moduleExports, webpackRequire) {`,
webpack.Template.indent([
// Our require function delegates to the original require function
`${constDeclaration} hotRequire = ${runtimeTemplate.returningFunction(
'webpackRequire(request)',
'request'
)};`,
// The propery descriptor factory below ensures all properties but `$Refresh$`
// are proxied through to the original require function
`${constDeclaration} createPropertyDescriptor = ${runtimeTemplate.basicFunction(
'name',
[
`return {`,
webpack.Template.indent([
`configurable: true,`,
`enumerable: true,`,
`get: ${runtimeTemplate.returningFunction('webpackRequire[name]')},`,
`set: ${runtimeTemplate.basicFunction('value', [
'webpackRequire[name] = value;',
])},`,
]),
`};`,
]
)};`,
`for (${constDeclaration} name in webpackRequire) {`,
webpack.Template.indent([
'if (name === "$Refresh$") continue;',
'if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {',
webpack.Template.indent([
`Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));`,
]),
`}`,
]),
`}`,
`hotRequire.$Refresh$ = setup(options.id);`,
`originalFactory.call(this, moduleObject, moduleExports, hotRequire);`,
]),
'};',
]
)});`,
]);
}
};
}
module.exports = makeRefreshRuntimeModule;
================================================
FILE: lib/utils/normalizeOptions.js
================================================
const { d, n } = require('../../options');
/**
* Normalizes the options for the plugin.
* @param {import('../types').ReactRefreshPluginOptions} options Non-normalized plugin options.
* @returns {import('../types').NormalizedPluginOptions} Normalized plugin options.
*/
const normalizeOptions = (options) => {
d(options, 'exclude', /node_modules/i);
d(options, 'include', /\.([cm]js|[jt]sx?|flow)$/i);
d(options, 'forceEnable');
d(options, 'library');
n(options, 'overlay', (overlay) => {
/** @type {import('../types').NormalizedErrorOverlayOptions} */
const defaults = {
entry: require.resolve('../../client/ErrorOverlayEntry'),
module: require.resolve('../../overlay'),
sockIntegration: 'wds',
};
if (overlay === false) {
return false;
}
if (typeof overlay === 'undefined' || overlay === true) {
return defaults;
}
d(overlay, 'entry', defaults.entry);
d(overlay, 'module', defaults.module);
d(overlay, 'sockIntegration', defaults.sockIntegration);
return overlay;
});
return options;
};
module.exports = normalizeOptions;
================================================
FILE: loader/index.js
================================================
// This is a patch for mozilla/source-map#349 -
// internally, it uses the existence of the `fetch` global to toggle browser behaviours.
// That check, however, will break when `fetch` polyfills are used for SSR setups.
// We "reset" the polyfill here to ensure it won't interfere with source-map generation.
const originalFetch = global.fetch;
delete global.fetch;
const { validate: validateOptions } = require('schema-utils');
const { SourceMapConsumer, SourceNode } = require('source-map');
const {
getIdentitySourceMap,
getModuleSystem,
getRefreshModuleRuntime,
normalizeOptions,
webpackGlobal,
} = require('./utils');
const schema = require('./options.json');
const RefreshRuntimePath = require
.resolve('react-refresh')
.replace(/\\/g, '/')
.replace(/'/g, "\\'");
/**
* A simple Webpack loader to inject react-refresh HMR code into modules.
*
* [Reference for Loader API](https://webpack.js.org/api/loaders/)
* @this {import('webpack').LoaderContext}
* @param {string} source The original module source code.
* @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
* @param {*} [meta] The loader metadata passed in.
* @returns {void}
*/
function ReactRefreshLoader(source, inputSourceMap, meta) {
let options = this.getOptions();
validateOptions(schema, options, {
baseDataPath: 'options',
name: 'React Refresh Loader',
});
options = normalizeOptions(options);
const callback = this.async();
const { ModuleFilenameHelpers, Template } = this._compiler.webpack || require('webpack');
const RefreshSetupRuntimes = {
cjs: Template.asString(
`${webpackGlobal}.$Refresh$.runtime = require('${RefreshRuntimePath}');`
),
esm: Template.asString([
`import * as __react_refresh_runtime__ from '${RefreshRuntimePath}';`,
`${webpackGlobal}.$Refresh$.runtime = __react_refresh_runtime__;`,
]),
};
/**
* @this {import('webpack').LoaderContext}
* @param {string} source
* @param {import('source-map').RawSourceMap} [inputSourceMap]
* @returns {Promise<[string, import('source-map').RawSourceMap]>}
*/
async function _loader(source, inputSourceMap) {
/** @type {'esm' | 'cjs'} */
const moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);
const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
const RefreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: options.const,
moduleSystem,
});
if (this.sourceMap) {
let originalSourceMap = inputSourceMap;
if (!originalSourceMap) {
originalSourceMap = getIdentitySourceMap(source, this.resourcePath);
}
return SourceMapConsumer.with(originalSourceMap, undefined, (consumer) => {
const node = SourceNode.fromStringWithSourceMap(source, consumer);
node.prepend([RefreshSetupRuntime, '\n\n']);
node.add(['\n\n', RefreshModuleRuntime]);
const { code, map } = node.toStringWithSourceMap();
return [code, map.toJSON()];
});
} else {
return [[RefreshSetupRuntime, source, RefreshModuleRuntime].join('\n\n'), inputSourceMap];
}
}
_loader.call(this, source, inputSourceMap).then(
([code, map]) => {
callback(null, code, map, meta);
},
(error) => {
callback(error);
}
);
}
module.exports = ReactRefreshLoader;
// Restore the original value of the `fetch` global, if it exists
if (originalFetch) {
global.fetch = originalFetch;
}
================================================
FILE: loader/options.json
================================================
{
"additionalProperties": false,
"type": "object",
"definitions": {
"MatchCondition": {
"anyOf": [{ "instanceof": "RegExp", "tsType": "RegExp" }, { "$ref": "#/definitions/Path" }]
},
"MatchConditions": {
"type": "array",
"items": { "$ref": "#/definitions/MatchCondition" },
"minItems": 1
},
"Path": { "type": "string" },
"ESModuleOptions": {
"additionalProperties": false,
"type": "object",
"properties": {
"exclude": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
},
"include": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
}
}
}
},
"properties": {
"const": { "type": "boolean" },
"esModule": { "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/ESModuleOptions" }] }
}
}
================================================
FILE: loader/types.js
================================================
/**
* @typedef {Object} ESModuleOptions
* @property {string | RegExp | Array} [exclude] Files to explicitly exclude from flagged as ES Modules.
* @property {string | RegExp | Array} [include] Files to explicitly include for flagged as ES Modules.
*/
/**
* @typedef {Object} ReactRefreshLoaderOptions
* @property {boolean} [const] Enables usage of ES6 `const` and `let` in generated runtime code.
* @property {boolean | ESModuleOptions} [esModule] Enables strict ES Modules compatible runtime.
*/
/**
* @typedef {import('type-fest').SetRequired} NormalizedLoaderOptions
*/
module.exports = {};
================================================
FILE: loader/utils/getIdentitySourceMap.js
================================================
const { SourceMapGenerator } = require('source-map');
/**
* Generates an identity source map from a source file.
* @param {string} source The content of the source file.
* @param {string} resourcePath The name of the source file.
* @returns {import('source-map').RawSourceMap} The identity source map.
*/
function getIdentitySourceMap(source, resourcePath) {
const sourceMap = new SourceMapGenerator();
sourceMap.setSourceContent(resourcePath, source);
source.split('\n').forEach((_, index) => {
sourceMap.addMapping({
source: resourcePath,
original: {
line: index + 1,
column: 0,
},
generated: {
line: index + 1,
column: 0,
},
});
});
return sourceMap.toJSON();
}
module.exports = getIdentitySourceMap;
================================================
FILE: loader/utils/getModuleSystem.js
================================================
const fs = require('node:fs/promises');
const path = require('node:path');
/** @type {Map} */
let packageJsonTypeMap = new Map();
/**
* Infers the current active module system from loader context and options.
* @this {import('webpack').loader.LoaderContext}
* @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
* @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
* @return {Promise<'esm' | 'cjs'>} The inferred module system.
*/
async function getModuleSystem(ModuleFilenameHelpers, options) {
// Check loader options -
// if `esModule` is set we don't have to do extra guess work.
switch (typeof options.esModule) {
case 'boolean': {
return options.esModule ? 'esm' : 'cjs';
}
case 'object': {
if (
options.esModule.include &&
ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.include)
) {
return 'esm';
}
if (
options.esModule.exclude &&
ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.exclude)
) {
return 'cjs';
}
break;
}
default: // Do nothing
}
// Check current resource's extension
if (/\.mjs$/.test(this.resourcePath)) return 'esm';
if (/\.cjs$/.test(this.resourcePath)) return 'cjs';
if (typeof this.addMissingDependency !== 'function') {
// This is Webpack 4 which does not support `import.meta`.
// We assume `.js` files are CommonJS because the output cannot be ESM anyway.
return 'cjs';
}
// We will assume CommonJS if we cannot determine otherwise
let packageJsonType = '';
// We begin our search for relevant `package.json` files,
// at the directory of the resource being loaded.
// These paths should already be resolved,
// but we resolve them again to ensure we are dealing with an aboslute path.
const resourceContext = path.dirname(this.resourcePath);
let searchPath = resourceContext;
let previousSearchPath = '';
// We start our search just above the root context of the webpack compilation
const stopPath = path.dirname(this.rootContext);
// If the module context is a resolved symlink outside the `rootContext` path,
// then we will never find the `stopPath` - so we also halt when we hit the root.
// Note that there is a potential that the wrong `package.json` is found in some pathalogical cases,
// such as a folder that is conceptually a package + does not have an ancestor `package.json`,
// but there exists a `package.json` higher up.
// This might happen if you have a folder of utility JS files that you symlink but did not organize as a package.
// We consider this an unsupported edge case for now.
while (searchPath !== stopPath && searchPath !== previousSearchPath) {
// If we have already determined the `package.json` type for this path we can stop searching.
// We do however still need to cache the found value,
// from the `resourcePath` folder up to the matching `searchPath`,
// to avoid retracing these steps when processing sibling resources.
if (packageJsonTypeMap.has(searchPath)) {
packageJsonType = packageJsonTypeMap.get(searchPath);
let currentPath = resourceContext;
while (currentPath !== searchPath) {
// We set the found type at least level from `resourcePath` folder up to the matching `searchPath`
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
break;
}
let packageJsonPath = path.join(searchPath, 'package.json');
try {
const packageSource = await fs.readFile(packageJsonPath, 'utf-8');
try {
const packageObject = JSON.parse(packageSource);
// Any package.json is sufficient as long as it can be parsed.
// If it does not explicitly have a `type: "module"` it will be assumed to be CommonJS.
packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
packageJsonTypeMap.set(searchPath, packageJsonType);
// We set the type in the cache for all paths from the `resourcePath` folder,
// up to the matching `searchPath` to avoid retracing these steps when processing sibling resources.
let currentPath = resourceContext;
while (currentPath !== searchPath) {
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
} catch (e) {
// `package.json` exists but could not be parsed.
// We track it as a dependency so we can reload if this file changes.
}
this.addDependency(packageJsonPath);
break;
} catch (e) {
// `package.json` does not exist.
// We track it as a missing dependency so we can reload if this file is added.
this.addMissingDependency(packageJsonPath);
}
// Try again at the next level up
previousSearchPath = searchPath;
searchPath = path.dirname(searchPath);
}
// Check `package.json` for the `type` field -
// fallback to use `cjs` for anything ambiguous.
return packageJsonType === 'module' ? 'esm' : 'cjs';
}
module.exports = getModuleSystem;
================================================
FILE: loader/utils/getRefreshModuleRuntime.js
================================================
const webpackGlobal = require('./webpackGlobal');
/**
* @typedef ModuleRuntimeOptions {Object}
* @property {boolean} const Use ES6 `const` and `let` in generated runtime code.
* @property {'cjs' | 'esm'} moduleSystem The module system to be used.
*/
/**
* Generates code appended to each JS-like module for react-refresh capabilities.
*
* `__react_refresh_utils__` will be replaced with actual utils during source parsing by `webpack.ProvidePlugin`.
*
* [Reference for Runtime Injection](https://github.com/webpack/webpack/blob/b07d3b67d2252f08e4bb65d354a11c9b69f8b434/lib/HotModuleReplacementPlugin.js#L419)
* [Reference for HMR Error Recovery](https://github.com/webpack/webpack/issues/418#issuecomment-490296365)
*
* @param {import('webpack').Template} Webpack's templating helpers.
* @param {ModuleRuntimeOptions} options The refresh module runtime options.
* @returns {string} The refresh module runtime template.
*/
function getRefreshModuleRuntime(Template, options) {
const constDeclaration = options.const ? 'const' : 'var';
const letDeclaration = options.const ? 'let' : 'var';
const webpackHot = options.moduleSystem === 'esm' ? 'import.meta.webpackHot' : 'module.hot';
return Template.asString([
`${constDeclaration} $ReactRefreshModuleId$ = ${webpackGlobal}.$Refresh$.moduleId;`,
`${constDeclaration} $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(`,
Template.indent('$ReactRefreshModuleId$'),
');',
'',
'function $ReactRefreshModuleRuntime$(exports) {',
Template.indent([
`if (${webpackHot}) {`,
Template.indent([
`${letDeclaration} errorOverlay;`,
"if (typeof __react_refresh_error_overlay__ !== 'undefined') {",
Template.indent('errorOverlay = __react_refresh_error_overlay__;'),
'}',
`${letDeclaration} testMode;`,
"if (typeof __react_refresh_test__ !== 'undefined') {",
Template.indent('testMode = __react_refresh_test__;'),
'}',
'return __react_refresh_utils__.executeRuntime(',
Template.indent([
'exports,',
'$ReactRefreshModuleId$,',
`${webpackHot},`,
'errorOverlay,',
'testMode',
]),
');',
]),
'}',
]),
'}',
'',
"if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {",
Template.indent('$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);'),
'} else {',
Template.indent('$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);'),
'}',
]);
}
module.exports = getRefreshModuleRuntime;
================================================
FILE: loader/utils/index.js
================================================
const getIdentitySourceMap = require('./getIdentitySourceMap');
const getModuleSystem = require('./getModuleSystem');
const getRefreshModuleRuntime = require('./getRefreshModuleRuntime');
const normalizeOptions = require('./normalizeOptions');
const webpackGlobal = require('./webpackGlobal');
module.exports = {
getIdentitySourceMap,
getModuleSystem,
getRefreshModuleRuntime,
normalizeOptions,
webpackGlobal,
};
================================================
FILE: loader/utils/normalizeOptions.js
================================================
const { d, n } = require('../../options');
/**
* Normalizes the options for the loader.
* @param {import('../types').ReactRefreshLoaderOptions} options Non-normalized loader options.
* @returns {import('../types').NormalizedLoaderOptions} Normalized loader options.
*/
const normalizeOptions = (options) => {
d(options, 'const', false);
n(options, 'esModule', (esModule) => {
if (typeof esModule === 'boolean' || typeof esModule === 'undefined') {
return esModule;
}
d(esModule, 'include');
d(esModule, 'exclude');
return esModule;
});
return options;
};
module.exports = normalizeOptions;
================================================
FILE: loader/utils/webpackGlobal.js
================================================
// When available, use `__webpack_global__` which is a stable runtime function to use `__webpack_require__` in this compilation
// See: https://github.com/webpack/webpack/issues/20139
const webpackGlobal = `(typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__)`;
module.exports = webpackGlobal;
================================================
FILE: options/index.js
================================================
/**
* Sets a constant default value for the property when it is undefined.
* @template T
* @template {keyof T} Property
* @param {T} object An object.
* @param {Property} property A property of the provided object.
* @param {T[Property]} [defaultValue] The default value to set for the property.
* @returns {T[Property]} The defaulted property value.
*/
const d = (object, property, defaultValue) => {
if (typeof object[property] === 'undefined' && typeof defaultValue !== 'undefined') {
object[property] = defaultValue;
}
return object[property];
};
/**
* Resolves the value for a nested object option.
* @template T
* @template {keyof T} Property
* @template Result
* @param {T} object An object.
* @param {Property} property A property of the provided object.
* @param {function(T | undefined): Result} fn The handler to resolve the property's value.
* @returns {Result} The resolved option value.
*/
const n = (object, property, fn) => {
object[property] = fn(object[property]);
return object[property];
};
module.exports = { d, n };
================================================
FILE: overlay/components/CompileErrorTrace.js
================================================
const Anser = require('anser');
const entities = require('html-entities');
const utils = require('../utils.js');
/**
* @typedef {Object} CompileErrorTraceProps
* @property {string} errorMessage
*/
/**
* A formatter that turns Webpack compile error messages into highlighted HTML source traces.
* @param {Document} document
* @param {HTMLElement} root
* @param {CompileErrorTraceProps} props
* @returns {void}
*/
function CompileErrorTrace(document, root, props) {
const errorParts = props.errorMessage.split('\n');
if (errorParts.length) {
if (errorParts[0]) {
errorParts[0] = utils.formatFilename(errorParts[0]);
}
const errorMessage = errorParts.splice(1, 1)[0];
if (errorMessage) {
// Strip filename from the error message
errorParts.unshift(errorMessage.replace(/^(.*:)\s.*:(\s.*)$/, '$1$2'));
}
}
const stackContainer = document.createElement('pre');
stackContainer.style.fontFamily = [
'"SFMono-Regular"',
'Consolas',
'Liberation Mono',
'Menlo',
'Courier',
'monospace',
].join(', ');
stackContainer.style.margin = '0';
stackContainer.style.whiteSpace = 'pre-wrap';
const entries = Anser.ansiToJson(
entities.encode(errorParts.join('\n'), { level: 'html5', mode: 'nonAscii' }),
{
json: true,
remove_empty: true,
use_classes: true,
}
);
for (let i = 0; i < entries.length; i += 1) {
const entry = entries[i];
const elem = document.createElement('span');
elem.innerHTML = entry.content;
elem.style.color = entry.fg ? `var(--color-${entry.fg})` : undefined;
elem.style.wordBreak = 'break-word';
switch (entry.decoration) {
case 'bold':
elem.style.fontWeight = 800;
break;
case 'italic':
elem.style.fontStyle = 'italic';
break;
}
stackContainer.appendChild(elem);
}
root.appendChild(stackContainer);
}
module.exports = CompileErrorTrace;
================================================
FILE: overlay/components/PageHeader.js
================================================
const Spacer = require('./Spacer.js');
const theme = require('../theme.js');
/**
* @typedef {Object} PageHeaderProps
* @property {string} [message]
* @property {string} title
* @property {string} [topOffset]
*/
/**
* The header of the overlay.
* @param {Document} document
* @param {HTMLElement} root
* @param {PageHeaderProps} props
* @returns {void}
*/
function PageHeader(document, root, props) {
const pageHeaderContainer = document.createElement('div');
pageHeaderContainer.style.background = theme['dark-background'];
pageHeaderContainer.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.3)';
pageHeaderContainer.style.color = theme.white;
pageHeaderContainer.style.left = '0';
pageHeaderContainer.style.right = '0';
pageHeaderContainer.style.padding = '1rem 1.5rem';
pageHeaderContainer.style.paddingLeft = 'max(1.5rem, env(safe-area-inset-left))';
pageHeaderContainer.style.paddingRight = 'max(1.5rem, env(safe-area-inset-right))';
pageHeaderContainer.style.position = 'fixed';
pageHeaderContainer.style.top = props.topOffset || '0';
const title = document.createElement('h3');
title.innerText = props.title;
title.style.color = theme.red;
title.style.fontSize = '1.125rem';
title.style.lineHeight = '1.3';
title.style.margin = '0';
pageHeaderContainer.appendChild(title);
if (props.message) {
title.style.margin = '0 0 0.5rem';
const message = document.createElement('span');
message.innerText = props.message;
message.style.color = theme.white;
message.style.wordBreak = 'break-word';
pageHeaderContainer.appendChild(message);
}
root.appendChild(pageHeaderContainer);
// This has to run after appending elements to root
// because we need to actual mounted height.
Spacer(document, root, {
space: pageHeaderContainer.offsetHeight.toString(10),
});
}
module.exports = PageHeader;
================================================
FILE: overlay/components/RuntimeErrorFooter.js
================================================
const Spacer = require('./Spacer.js');
const theme = require('../theme.js');
/**
* @typedef {Object} RuntimeErrorFooterProps
* @property {string} [initialFocus]
* @property {boolean} multiple
* @property {function(MouseEvent): void} onClickCloseButton
* @property {function(MouseEvent): void} onClickNextButton
* @property {function(MouseEvent): void} onClickPrevButton
*/
/**
* A fixed footer that handles pagination of runtime errors.
* @param {Document} document
* @param {HTMLElement} root
* @param {RuntimeErrorFooterProps} props
* @returns {void}
*/
function RuntimeErrorFooter(document, root, props) {
const footer = document.createElement('div');
footer.style.backgroundColor = theme['dark-background'];
footer.style.bottom = '0';
footer.style.boxShadow = '0 -1px 4px rgba(0, 0, 0, 0.3)';
footer.style.height = '2.5rem';
footer.style.left = '0';
footer.style.right = '0';
footer.style.lineHeight = '2.5rem';
footer.style.paddingBottom = '0';
footer.style.paddingBottom = 'env(safe-area-inset-bottom)';
footer.style.position = 'fixed';
footer.style.textAlign = 'center';
footer.style.zIndex = '2';
const BUTTON_CONFIGS = {
prev: {
id: 'prev',
label: '◀ Prev',
onClick: props.onClickPrevButton,
},
close: {
id: 'close',
label: '× Close',
onClick: props.onClickCloseButton,
},
next: {
id: 'next',
label: 'Next ▶',
onClick: props.onClickNextButton,
},
};
let buttons = [BUTTON_CONFIGS.close];
if (props.multiple) {
buttons = [BUTTON_CONFIGS.prev, BUTTON_CONFIGS.close, BUTTON_CONFIGS.next];
}
/** @type {HTMLButtonElement | undefined} */
let initialFocusButton;
for (let i = 0; i < buttons.length; i += 1) {
const buttonConfig = buttons[i];
const button = document.createElement('button');
button.id = buttonConfig.id;
button.innerHTML = buttonConfig.label;
button.tabIndex = 1;
button.style.backgroundColor = theme['dark-background'];
button.style.border = 'none';
button.style.color = theme.white;
button.style.cursor = 'pointer';
button.style.fontSize = 'inherit';
button.style.height = '100%';
button.style.padding = '0.5rem 0.75rem';
button.style.width = `${(100 / buttons.length).toString(10)}%`;
button.addEventListener('click', buttonConfig.onClick);
if (buttonConfig.id === props.initialFocus) {
initialFocusButton = button;
}
footer.appendChild(button);
}
root.appendChild(footer);
Spacer(document, root, { space: '2.5rem' });
if (initialFocusButton) {
initialFocusButton.focus();
}
}
module.exports = RuntimeErrorFooter;
================================================
FILE: overlay/components/RuntimeErrorHeader.js
================================================
const Spacer = require('./Spacer.js');
const theme = require('../theme.js');
/**
* @typedef {Object} RuntimeErrorHeaderProps
* @property {number} currentErrorIndex
* @property {number} totalErrors
*/
/**
* A fixed header that shows the total runtime error count.
* @param {Document} document
* @param {HTMLElement} root
* @param {RuntimeErrorHeaderProps} props
* @returns {void}
*/
function RuntimeErrorHeader(document, root, props) {
const header = document.createElement('div');
header.innerText = `Error ${props.currentErrorIndex + 1} of ${props.totalErrors}`;
header.style.backgroundColor = theme.red;
header.style.color = theme.white;
header.style.fontWeight = '500';
header.style.height = '2.5rem';
header.style.left = '0';
header.style.lineHeight = '2.5rem';
header.style.position = 'fixed';
header.style.textAlign = 'center';
header.style.top = '0';
header.style.width = '100vw';
header.style.zIndex = '2';
root.appendChild(header);
Spacer(document, root, { space: '2.5rem' });
}
module.exports = RuntimeErrorHeader;
================================================
FILE: overlay/components/RuntimeErrorStack.js
================================================
const ErrorStackParser = require('error-stack-parser');
const theme = require('../theme.js');
const utils = require('../utils.js');
/**
* @typedef {Object} RuntimeErrorStackProps
* @property {Error} error
*/
/**
* A formatter that turns runtime error stacks into highlighted HTML stacks.
* @param {Document} document
* @param {HTMLElement} root
* @param {RuntimeErrorStackProps} props
* @returns {void}
*/
function RuntimeErrorStack(document, root, props) {
const stackTitle = document.createElement('h4');
stackTitle.innerText = 'Call Stack';
stackTitle.style.color = theme.white;
stackTitle.style.fontSize = '1.0625rem';
stackTitle.style.fontWeight = '500';
stackTitle.style.lineHeight = '1.3';
stackTitle.style.margin = '0 0 0.5rem';
const stackContainer = document.createElement('div');
stackContainer.style.fontSize = '0.8125rem';
stackContainer.style.lineHeight = '1.3';
stackContainer.style.whiteSpace = 'pre-wrap';
let errorStacks;
try {
errorStacks = ErrorStackParser.parse(props.error);
} catch (e) {
errorStacks = [];
stackContainer.innerHTML = 'No stack trace is available for this error!';
}
for (let i = 0; i < Math.min(errorStacks.length, 10); i += 1) {
const currentStack = errorStacks[i];
const functionName = document.createElement('code');
functionName.innerHTML = ` ${currentStack.functionName || '(anonymous function)'}`;
functionName.style.color = theme.yellow;
functionName.style.fontFamily = [
'"SFMono-Regular"',
'Consolas',
'Liberation Mono',
'Menlo',
'Courier',
'monospace',
].join(', ');
const fileName = document.createElement('div');
fileName.innerHTML =
' ' +
utils.formatFilename(currentStack.fileName) +
':' +
currentStack.lineNumber +
':' +
currentStack.columnNumber;
fileName.style.color = theme.white;
fileName.style.fontSize = '0.6875rem';
fileName.style.marginBottom = '0.25rem';
stackContainer.appendChild(functionName);
stackContainer.appendChild(fileName);
}
root.appendChild(stackTitle);
root.appendChild(stackContainer);
}
module.exports = RuntimeErrorStack;
================================================
FILE: overlay/components/Spacer.js
================================================
/**
* @typedef {Object} SpacerProps
* @property {string} space
*/
/**
* An empty element to add spacing manually.
* @param {Document} document
* @param {HTMLElement} root
* @param {SpacerProps} props
* @returns {void}
*/
function Spacer(document, root, props) {
const spacer = document.createElement('div');
spacer.style.paddingBottom = props.space;
root.appendChild(spacer);
}
module.exports = Spacer;
================================================
FILE: overlay/containers/CompileErrorContainer.js
================================================
const CompileErrorTrace = require('../components/CompileErrorTrace.js');
const PageHeader = require('../components/PageHeader.js');
const Spacer = require('../components/Spacer.js');
/**
* @typedef {Object} CompileErrorContainerProps
* @property {string} errorMessage
*/
/**
* A container to render Webpack compilation error messages with source trace.
* @param {Document} document
* @param {HTMLElement} root
* @param {CompileErrorContainerProps} props
* @returns {void}
*/
function CompileErrorContainer(document, root, props) {
PageHeader(document, root, {
title: 'Failed to compile.',
});
CompileErrorTrace(document, root, { errorMessage: props.errorMessage });
Spacer(document, root, { space: '1rem' });
}
module.exports = CompileErrorContainer;
================================================
FILE: overlay/containers/RuntimeErrorContainer.js
================================================
const PageHeader = require('../components/PageHeader.js');
const RuntimeErrorStack = require('../components/RuntimeErrorStack.js');
const Spacer = require('../components/Spacer.js');
/**
* @typedef {Object} RuntimeErrorContainerProps
* @property {Error} currentError
*/
/**
* A container to render runtime error messages with stack trace.
* @param {Document} document
* @param {HTMLElement} root
* @param {RuntimeErrorContainerProps} props
* @returns {void}
*/
function RuntimeErrorContainer(document, root, props) {
PageHeader(document, root, {
message: props.currentError.message,
title: props.currentError.name,
topOffset: '2.5rem',
});
RuntimeErrorStack(document, root, {
error: props.currentError,
});
Spacer(document, root, { space: '1rem' });
}
module.exports = RuntimeErrorContainer;
================================================
FILE: overlay/index.js
================================================
const RuntimeErrorFooter = require('./components/RuntimeErrorFooter.js');
const RuntimeErrorHeader = require('./components/RuntimeErrorHeader.js');
const CompileErrorContainer = require('./containers/CompileErrorContainer.js');
const RuntimeErrorContainer = require('./containers/RuntimeErrorContainer.js');
const theme = require('./theme.js');
const utils = require('./utils.js');
/**
* @callback RenderFn
* @returns {void}
*/
/* ===== Cached elements for DOM manipulations ===== */
/**
* The iframe that contains the overlay.
* @type {HTMLIFrameElement}
*/
let iframeRoot = null;
/**
* The document object from the iframe root, used to create and render elements.
* @type {Document}
*/
let rootDocument = null;
/**
* The root div elements will attach to.
* @type {HTMLDivElement}
*/
let root = null;
/**
* A Cached function to allow deferred render.
* @type {RenderFn | null}
*/
let scheduledRenderFn = null;
/* ===== Overlay State ===== */
/**
* The latest error message from Webpack compilation.
* @type {string}
*/
let currentCompileErrorMessage = '';
/**
* Index of the error currently shown by the overlay.
* @type {number}
*/
let currentRuntimeErrorIndex = 0;
/**
* The latest runtime error objects.
* @type {Error[]}
*/
let currentRuntimeErrors = [];
/**
* The render mode the overlay is currently in.
* @type {'compileError' | 'runtimeError' | null}
*/
let currentMode = null;
/**
* @typedef {Object} IframeProps
* @property {function(): void} onIframeLoad
*/
/**
* Creates the main `iframe` the overlay will attach to.
* Accepts a callback to be ran after iframe is initialized.
* @param {Document} document
* @param {HTMLElement} root
* @param {IframeProps} props
* @returns {HTMLIFrameElement}
*/
function IframeRoot(document, root, props) {
const iframe = document.createElement('iframe');
iframe.id = 'react-refresh-overlay';
iframe.src = 'about:blank';
iframe.style.border = 'none';
iframe.style.height = '100%';
iframe.style.left = '0';
iframe.style.minHeight = '100vh';
iframe.style.minHeight = '-webkit-fill-available';
iframe.style.position = 'fixed';
iframe.style.top = '0';
iframe.style.width = '100vw';
iframe.style.zIndex = '2147483647';
iframe.addEventListener('load', function onLoad() {
// Reset margin of iframe body
iframe.contentDocument.body.style.margin = '0';
props.onIframeLoad();
});
// We skip mounting and returns as we need to ensure
// the load event is fired after we setup the global variable
return iframe;
}
/**
* Creates the main `div` element for the overlay to render.
* @param {Document} document
* @param {HTMLElement} root
* @returns {HTMLDivElement}
*/
function OverlayRoot(document, root) {
const div = document.createElement('div');
div.id = 'react-refresh-overlay-error';
// Apply ANSI theme
div.style.setProperty('--color-ansi-selection', theme.selection);
div.style.setProperty('--color-ansi-bg', theme.background);
div.style.setProperty('--color-ansi-fg', theme.white);
div.style.setProperty('--color-ansi-white', theme.white);
div.style.setProperty('--color-ansi-black', theme.black);
div.style.setProperty('--color-ansi-blue', theme.blue);
div.style.setProperty('--color-ansi-cyan', theme.cyan);
div.style.setProperty('--color-ansi-green', theme.green);
div.style.setProperty('--color-ansi-magenta', theme.magenta);
div.style.setProperty('--color-ansi-red', theme.red);
div.style.setProperty('--color-ansi-yellow', theme.yellow);
div.style.setProperty('--color-ansi-bright-white', theme['bright-white']);
div.style.setProperty('--color-ansi-bright-black', theme['bright-black']);
div.style.setProperty('--color-ansi-bright-blue', theme['bright-blue']);
div.style.setProperty('--color-ansi-bright-cyan', theme['bright-cyan']);
div.style.setProperty('--color-ansi-bright-green', theme['bright-green']);
div.style.setProperty('--color-ansi-bright-magenta', theme['bright-magenta']);
div.style.setProperty('--color-ansi-bright-red', theme['bright-red']);
div.style.setProperty('--color-ansi-bright-yellow', theme['bright-yellow']);
// Style the contents container
div.style.backgroundColor = theme.background;
div.style.boxSizing = 'border-box';
div.style.color = theme.white;
div.style.fontFamily = ['-apple-system', '"Source Sans Pro"', 'sans-serif'].join(', ');
div.style.fontSize = '0.875rem';
div.style.height = '100%';
div.style.lineHeight = '1.3';
div.style.overflow = 'auto';
div.style.padding = '1rem 1.5rem 0';
div.style.paddingTop = 'max(1rem, env(safe-area-inset-top))';
div.style.paddingRight = 'max(1.5rem, env(safe-area-inset-right))';
div.style.paddingBottom = 'env(safe-area-inset-bottom)';
div.style.paddingLeft = 'max(1.5rem, env(safe-area-inset-left))';
div.style.width = '100vw';
root.appendChild(div);
return div;
}
/**
* Ensures the iframe root and the overlay root are both initialized before render.
* If check fails, render will be deferred until both roots are initialized.
* @param {RenderFn} renderFn A function that triggers a DOM render.
* @returns {void}
*/
function ensureRootExists(renderFn) {
if (root) {
// Overlay root is ready, we can render right away.
renderFn();
return;
}
// Creating an iframe may be asynchronous so we'll defer render.
// In case of multiple calls, function from the last call will be used.
scheduledRenderFn = renderFn;
if (iframeRoot) {
// Iframe is already ready, it will fire the load event.
return;
}
// Create the iframe root, and, the overlay root inside it when it is ready.
iframeRoot = IframeRoot(document, document.body, {
onIframeLoad: function onIframeLoad() {
rootDocument = iframeRoot.contentDocument;
root = OverlayRoot(rootDocument, rootDocument.body);
scheduledRenderFn();
},
});
// We have to mount here to ensure `iframeRoot` is set when `onIframeLoad` fires.
// This is because onIframeLoad() will be called synchronously
// or asynchronously depending on the browser.
document.body.appendChild(iframeRoot);
}
/**
* Creates the main `div` element for the overlay to render.
* @returns {void}
*/
function render() {
ensureRootExists(function () {
const currentFocus = rootDocument.activeElement;
let currentFocusId;
if (currentFocus.localName === 'button' && currentFocus.id) {
currentFocusId = currentFocus.id;
}
utils.removeAllChildren(root);
if (currentCompileErrorMessage) {
currentMode = 'compileError';
CompileErrorContainer(rootDocument, root, {
errorMessage: currentCompileErrorMessage,
});
} else if (currentRuntimeErrors.length) {
currentMode = 'runtimeError';
RuntimeErrorHeader(rootDocument, root, {
currentErrorIndex: currentRuntimeErrorIndex,
totalErrors: currentRuntimeErrors.length,
});
RuntimeErrorContainer(rootDocument, root, {
currentError: currentRuntimeErrors[currentRuntimeErrorIndex],
});
RuntimeErrorFooter(rootDocument, root, {
initialFocus: currentFocusId,
multiple: currentRuntimeErrors.length > 1,
onClickCloseButton: function onClose() {
clearRuntimeErrors();
},
onClickNextButton: function onNext() {
if (currentRuntimeErrorIndex === currentRuntimeErrors.length - 1) {
return;
}
currentRuntimeErrorIndex += 1;
ensureRootExists(render);
},
onClickPrevButton: function onPrev() {
if (currentRuntimeErrorIndex === 0) {
return;
}
currentRuntimeErrorIndex -= 1;
ensureRootExists(render);
},
});
}
});
}
/**
* Destroys the state of the overlay.
* @returns {void}
*/
function cleanup() {
// Clean up and reset all internal state.
document.body.removeChild(iframeRoot);
scheduledRenderFn = null;
root = null;
iframeRoot = null;
}
/**
* Clears Webpack compilation errors and dismisses the compile error overlay.
* @returns {void}
*/
function clearCompileError() {
if (!root || currentMode !== 'compileError') {
return;
}
currentCompileErrorMessage = '';
currentMode = null;
cleanup();
}
/**
* Clears runtime error records and dismisses the runtime error overlay.
* @param {boolean} [dismissOverlay] Whether to dismiss the overlay or not.
* @returns {void}
*/
function clearRuntimeErrors(dismissOverlay) {
if (!root || currentMode !== 'runtimeError') {
return;
}
currentRuntimeErrorIndex = 0;
currentRuntimeErrors = [];
if (typeof dismissOverlay === 'undefined' || dismissOverlay) {
currentMode = null;
cleanup();
}
}
/**
* Shows the compile error overlay with the specific Webpack error message.
* @param {string} message
* @returns {void}
*/
function showCompileError(message) {
if (!message) {
return;
}
currentCompileErrorMessage = message;
render();
}
/**
* Shows the runtime error overlay with the specific error records.
* @param {Error[]} errors
* @returns {void}
*/
function showRuntimeErrors(errors) {
if (!errors || !errors.length) {
return;
}
currentRuntimeErrors = errors;
render();
}
/**
* The debounced version of `showRuntimeErrors` to prevent frequent renders
* due to rapid firing listeners.
* @param {Error[]} errors
* @returns {void}
*/
const debouncedShowRuntimeErrors = utils.debounce(showRuntimeErrors, 30);
/**
* Detects if an error is a Webpack compilation error.
* @param {Error} error The error of interest.
* @returns {boolean} If the error is a Webpack compilation error.
*/
function isWebpackCompileError(error) {
return /Module [A-z ]+\(from/.test(error.message) || /Cannot find module/.test(error.message);
}
/**
* Handles runtime error contexts captured with EventListeners.
* Integrates with a runtime error overlay.
* @param {Error} error A valid error object.
* @returns {void}
*/
function handleRuntimeError(error) {
if (error && !isWebpackCompileError(error) && currentRuntimeErrors.indexOf(error) === -1) {
currentRuntimeErrors = currentRuntimeErrors.concat(error);
}
debouncedShowRuntimeErrors(currentRuntimeErrors);
}
module.exports = Object.freeze({
clearCompileError: clearCompileError,
clearRuntimeErrors: clearRuntimeErrors,
handleRuntimeError: handleRuntimeError,
showCompileError: showCompileError,
showRuntimeErrors: showRuntimeErrors,
});
================================================
FILE: overlay/package.json
================================================
{
"type": "commonjs"
}
================================================
FILE: overlay/theme.js
================================================
/**
* @typedef {Object} Theme
* @property {string} black
* @property {string} bright-black
* @property {string} red
* @property {string} bright-red
* @property {string} green
* @property {string} bright-green
* @property {string} yellow
* @property {string} bright-yellow
* @property {string} blue
* @property {string} bright-blue
* @property {string} magenta
* @property {string} bright-magenta
* @property {string} cyan
* @property {string} bright-cyan
* @property {string} white
* @property {string} bright-white
* @property {string} lightgrey
* @property {string} darkgrey
* @property {string} grey
* @property {string} dimgrey
*/
/**
* @type {Theme} theme
* A collection of colors to be used by the overlay.
* Partially adopted from Tomorrow Night Bright.
*/
const theme = {
black: '#000000',
'bright-black': '#474747',
red: '#D34F56',
'bright-red': '#dd787d',
green: '#B9C954',
'bright-green': '#c9d57b',
yellow: '#E6C452',
'bright-yellow': '#ecd37f',
blue: '#7CA7D8',
'bright-blue': '#a3c1e4',
magenta: '#C299D6',
'bright-magenta': '#d8bde5',
cyan: '#73BFB1',
'bright-cyan': '#96cfc5',
white: '#FFFFFF',
'bright-white': '#FFFFFF',
background: '#474747',
'dark-background': '#343434',
selection: 'rgba(234, 234, 234, 0.5)',
};
module.exports = theme;
================================================
FILE: overlay/utils.js
================================================
/**
* Debounce a function to delay invoking until wait (ms) have elapsed since the last invocation.
* @param {function(...*): *} fn The function to be debounced.
* @param {number} wait Milliseconds to wait before invoking again.
* @return {function(...*): void} The debounced function.
*/
function debounce(fn, wait) {
/**
* A cached setTimeout handler.
* @type {number | undefined}
*/
let timer;
/**
* @returns {void}
*/
function debounced() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
return fn.apply(context, args);
}, wait);
}
return debounced;
}
/**
* Prettify a filename from error stacks into the desired format.
* @param {string} filename The filename to be formatted.
* @returns {string} The formatted filename.
*/
function formatFilename(filename) {
// Strip away protocol and domain for compiled files
const htmlMatch = /^https?:\/\/(.*)\/(.*)/.exec(filename);
if (htmlMatch && htmlMatch[1] && htmlMatch[2]) {
return htmlMatch[2];
}
// Strip everything before the first directory for source files
const sourceMatch = /\/.*?([^./]+[/|\\].*)$/.exec(filename);
if (sourceMatch && sourceMatch[1]) {
return sourceMatch[1].replace(/\?$/, '');
}
// Unknown filename type, use it as is
return filename;
}
/**
* Remove all children of an element.
* @param {HTMLElement} element A valid HTML element.
* @param {number} [skip] Number of elements to skip removing.
* @returns {void}
*/
function removeAllChildren(element, skip) {
/** @type {Node[]} */
const childList = Array.prototype.slice.call(
element.childNodes,
typeof skip !== 'undefined' ? skip : 0
);
for (let i = 0; i < childList.length; i += 1) {
element.removeChild(childList[i]);
}
}
module.exports = {
debounce: debounce,
formatFilename: formatFilename,
removeAllChildren: removeAllChildren,
};
================================================
FILE: package.json
================================================
{
"name": "@pmmmwh/react-refresh-webpack-plugin",
"version": "0.6.2",
"description": "An **EXPERIMENTAL** Webpack plugin to enable \"Fast Refresh\" (also previously known as _Hot Reloading_) for React components.",
"keywords": [
"react",
"javascript",
"webpack",
"refresh",
"hmr",
"hotreload",
"livereload",
"live",
"edit",
"hot",
"reload"
],
"homepage": "https://github.com/pmmmwh/react-refresh-webpack-plugin#readme",
"bugs": {
"url": "https://github.com/pmmmwh/react-refresh-webpack-plugin/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pmmmwh/react-refresh-webpack-plugin.git"
},
"license": "MIT",
"author": "Michael Mok",
"main": "lib/index.js",
"types": "types/lib/index.d.ts",
"files": [
"client",
"lib",
"loader",
"options",
"overlay",
"sockets",
"types",
"umd"
],
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
"scripts": {
"test": "run-s -c test:pre \"test:exec {@}\" test:post --",
"test:wds-4": "cross-env WDS_VERSION=4 yarn test",
"test:exec": "node --experimental-vm-modules scripts/test.js",
"test:pre": "run-s -c test:pre:*",
"test:pre:0": "yarn link",
"test:pre:1": "yarn link @pmmmwh/react-refresh-webpack-plugin",
"test:post": "run-s -c test:post:*",
"test:post:0": "yarn unlink @pmmmwh/react-refresh-webpack-plugin",
"test:post:1": "yarn unlink",
"lint": "eslint --report-unused-disable-directives --ext .js,.jsx .",
"lint:fix": "yarn lint --fix",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\"",
"types:clean": "del types",
"types:compile": "tsc",
"types:prune-private": "del \"types/*/*\" \"!types/{lib,loader,options}/{index,types}.d.ts\"",
"generate:client-external": "webpack",
"generate:types": "run-s types:clean types:compile types:prune-private \"format --log-level=silent\"",
"prepublishOnly": "run-p generate:*"
},
"dependencies": {
"anser": "^2.1.1",
"core-js-pure": "^3.23.3",
"error-stack-parser": "^2.0.6",
"html-entities": "^2.1.0",
"schema-utils": "^4.2.0",
"source-map": "^0.7.3"
},
"devDependencies": {
"@babel/core": "^7.24.6",
"@babel/plugin-transform-modules-commonjs": "^7.24.6",
"@types/cross-spawn": "^6.0.6",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/json-schema": "^7.0.15",
"@types/module-alias": "^2.0.4",
"@types/node": "^24.10.1",
"@types/webpack": "^5.28.5",
"babel-loader": "^10.0.0",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.5",
"del-cli": "^6.0.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.1.3",
"fs-extra": "^11.2.0",
"get-port": "^7.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-location-mock": "^2.0.0",
"memfs": "^4.9.2",
"module-alias": "^2.2.3",
"nanoid": "^3.3.8",
"npm-run-all2": "^7.0.2",
"prettier": "^3.3.0",
"puppeteer": "^24.4.0",
"react-refresh": "^0.18.0",
"sourcemap-validator": "^2.1.0",
"terser-webpack-plugin": "^5.3.10",
"type-fest": "^4.41.0",
"typescript": "~5.9.3",
"webpack": "^5.94.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.0.4",
"webpack-dev-server-v4": "npm:webpack-dev-server@^4.8.0",
"webpack-hot-middleware": "^2.26.1",
"webpack-plugin-serve": "^1.6.0",
"yn": "^4.0.0"
},
"peerDependencies": {
"@types/webpack": "5.x",
"react-refresh": ">=0.10.0 <1.0.0",
"sockjs-client": "^1.4.0",
"type-fest": ">=0.17.0 <6.0.0",
"webpack": "^5.0.0",
"webpack-dev-server": "^4.8.0 || 5.x",
"webpack-hot-middleware": "2.x",
"webpack-plugin-serve": "1.x"
},
"peerDependenciesMeta": {
"@types/webpack": {
"optional": true
},
"sockjs-client": {
"optional": true
},
"type-fest": {
"optional": true
},
"webpack-dev-server": {
"optional": true
},
"webpack-hot-middleware": {
"optional": true
},
"webpack-plugin-serve": {
"optional": true
}
},
"resolutions": {
"memfs": "^4.0.0",
"rimraf": "^5.0.0",
"type-fest": "^4.41.0"
},
"engines": {
"node": ">=18.12"
}
}
================================================
FILE: scripts/test.js
================================================
// Setup environment before any code -
// this makes sure everything coming after will run in the correct env.
process.env.NODE_ENV = 'test';
// Crash on unhandled rejections instead of failing silently.
process.on('unhandledRejection', (reason) => {
throw reason;
});
const jest = require('jest');
const yn = require('yn');
let argv = process.argv.slice(2);
if (yn(process.env.CI)) {
// Use CI mode
argv.push('--ci');
// Parallelized puppeteer tests have high memory overhead in CI environments.
// Fall back to run in series so tests could run faster.
argv.push('--runInBand');
// Add JUnit reporter
argv.push('--reporters="default"');
argv.push('--reporters="jest-junit"');
}
if (yn(process.env.DEBUG)) {
argv.push('--verbose');
}
void jest.run(argv);
================================================
FILE: sockets/WDSSocket.js
================================================
/**
* Initializes a socket server for HMR for webpack-dev-server.
* @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
* @returns {void}
*/
function initWDSSocket(messageHandler) {
const { default: SockJSClient } = require('webpack-dev-server/client/clients/SockJSClient');
const { default: WebSocketClient } = require('webpack-dev-server/client/clients/WebSocketClient');
const { client } = require('webpack-dev-server/client/socket');
/** @type {WebSocket} */
let connection;
if (client instanceof SockJSClient) {
connection = client.sock;
} else if (client instanceof WebSocketClient) {
connection = client.client;
} else {
throw new Error('Failed to determine WDS client type');
}
connection.addEventListener('message', function onSocketMessage(message) {
messageHandler(JSON.parse(message.data));
});
}
module.exports = { init: initWDSSocket };
================================================
FILE: sockets/WHMEventSource.js
================================================
/**
* The hard-coded singleton key for webpack-hot-middleware's client instance.
*
* [Ref](https://github.com/webpack-contrib/webpack-hot-middleware/blob/cb29abb9dde435a1ac8e9b19f82d7d36b1093198/client.js#L152)
*/
const singletonKey = '__webpack_hot_middleware_reporter__';
/**
* Initializes a socket server for HMR for webpack-hot-middleware.
* @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
* @returns {void}
*/
function initWHMEventSource(messageHandler) {
const client = window[singletonKey];
client.useCustomOverlay({
showProblems: function showProblems(type, data) {
const error = {
data: data,
type: type,
};
messageHandler(error);
},
clear: function clear() {
messageHandler({ type: 'ok' });
},
});
}
module.exports = { init: initWHMEventSource };
================================================
FILE: sockets/WPSSocket.js
================================================
/* global ʎɐɹɔosǝʌɹǝs */
const { ClientSocket } = require('webpack-plugin-serve/lib/client/ClientSocket');
/**
* Initializes a socket server for HMR for webpack-plugin-serve.
* @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
* @returns {void}
*/
function initWPSSocket(messageHandler) {
/**
* The hard-coded options injection key from webpack-plugin-serve.
*
* [Ref](https://github.com/shellscape/webpack-plugin-serve/blob/aeb49f14e900802c98df4a4607a76bc67b1cffdf/lib/index.js#L258)
* @type {Object | undefined}
*/
let options;
try {
options = ʎɐɹɔosǝʌɹǝs;
} catch (e) {
// Bail out because this indicates the plugin is not included
return;
}
const { address, client = {}, secure } = options;
const protocol = secure ? 'wss' : 'ws';
const socket = new ClientSocket(client, protocol + '://' + (client.address || address) + '/wps');
socket.addEventListener('message', function listener(message) {
const { action, data } = JSON.parse(message.data);
switch (action) {
case 'done': {
messageHandler({ type: 'ok' });
break;
}
case 'problems': {
if (data.errors.length) {
messageHandler({ type: 'errors', data: data.errors });
} else if (data.warnings.length) {
messageHandler({ type: 'warnings', data: data.warnings });
}
break;
}
default: {
// Do nothing
}
}
});
}
module.exports = { init: initWPSSocket };
================================================
FILE: sockets/package.json
================================================
{
"type": "commonjs"
}
================================================
FILE: test/conformance/ReactRefresh.test.js
================================================
/**
* @jest-environment /conformance/environment
*/
const getSandbox = require('../helpers/sandbox');
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1028-L1087
it('re-runs accepted modules', async () => {
const [session] = await getSandbox();
// Bootstrap test and reload session to not rely on auto-refresh semantics
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write('foo.js', `window.log('init FooV1'); require('./bar');`);
await session.write(
'bar.js',
`window.log('init BarV1'); module.exports = function Bar() { return null; };`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV1']);
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV2'); module.exports = function Bar() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV2']);
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV3'); module.exports = function Bar() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV3']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1089-L1176
it('propagates a hot update to closest accepted module', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
// Exporting a component marks it as auto-accepting.
`window.log('init FooV1'); require('./bar'); module.exports = function Foo() {};`
);
await session.write('bar.js', `window.log('init BarV1');`);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV1']);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2');`);
expect(session.logs).toStrictEqual([
// FIXME: Metro order:
// 'init BarV2',
// 'init FooV1',
'init FooV1',
'init BarV2',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
]);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV3');`);
expect(session.logs).toStrictEqual([
// // FIXME: Metro order:
// 'init BarV3',
// 'init FooV1',
'init FooV1',
'init BarV3',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
]);
// We edited Bar so that it accepts itself.
// We still re-run Foo because the exports of Bar changed.
session.resetState();
await session.patch(
'bar.js',
// Exporting a component marks it as auto-accepting.
`window.log('init BarV4'); module.exports = function Bar() {};`
);
expect(session.logs).toStrictEqual([
// // FIXME: Metro order:
// 'init BarV4',
// 'init FooV1',
'init FooV1',
'init BarV4',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
]);
// Further edits to Bar don't re-run Foo.
session.resetState();
await session.patch('bar.js', `window.log('init BarV5'); module.exports = function Bar() {};`);
expect(session.logs).toStrictEqual(['init BarV5']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1178-L1346
it('propagates hot update to all inverse dependencies', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
// This is the module graph:
// MiddleA*
// / \
// Root* - MiddleB* - Leaf
// \
// MiddleC
//
// * - accepts update
//
// We expect that editing Leaf will propagate to
// MiddleA and MiddleB both of which can handle updates.
await session.write(
'root.js',
`
window.log('init RootV1');
require('./middleA');
require('./middleB');
require('./middleC');
module.exports = function Root() {};
`
);
await session.write(
'middleA.js',
`window.log('init MiddleAV1'); require('./leaf'); module.exports = function MiddleA() {};`
);
await session.write(
'middleB.js',
`window.log('init MiddleBV1'); require('./leaf'); module.exports = function MiddleB() {};
`
);
// This one doesn't import leaf and also doesn't export a component,
// so, it doesn't accept its own updates.
await session.write('middleC.js', `window.log('init MiddleCV1'); module.exports = {};`);
// Doesn't accept its own updates; they will propagate.
await session.write('leaf.js', `window.log('init LeafV1'); module.exports = {};`);
session.resetState();
await session.patch(
'index.js',
`require('./root'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual([
'init RootV1',
'init MiddleAV1',
'init LeafV1',
'init MiddleBV1',
'init MiddleCV1',
]);
// We edited Leaf, but it doesn't accept.
// So we expect it to re-run together with MiddleA and MiddleB which do.
session.resetState();
await session.patch('leaf.js', `window.log('init LeafV2'); module.exports = {};`);
expect(session.logs).toStrictEqual(['init MiddleAV1', 'init LeafV2', 'init MiddleBV1']);
// Let's try the same one more time.
session.resetState();
await session.patch('leaf.js', `window.log('init LeafV3'); module.exports = {};`);
expect(session.logs).toStrictEqual(['init MiddleAV1', 'init LeafV3', 'init MiddleBV1']);
// Now edit MiddleB. It should accept and re-run alone.
session.resetState();
await session.patch(
'middleB.js',
`window.log('init MiddleBV2'); require('./leaf'); module.exports = function MiddleB() {};
`
);
expect(session.logs).toStrictEqual(['init MiddleBV2']);
// Finally, edit MiddleC. It didn't accept so it should bubble to Root.
session.resetState();
await session.patch('middleC.js', `window.log('init MiddleCV2'); module.exports = {};`);
expect(session.logs).toStrictEqual(['init RootV1', 'init MiddleCV2']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1348-L1445
it('runs dependencies before dependents', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
// This is the module graph:
// MiddleA* ----
// / | \
// Root MiddleB ----- Leaf
//
// * - refresh boundary (exports a component)
//
// We expect that editing Leaf will propagate to
// MiddleA which is a Refresh Boundary.
//
// However, it's essential that code for MiddleB executes *before* MiddleA on updates.
await session.write(
'root.js',
`window.log('init RootV1'); require('./middleA'); module.exports = function Root() {};`
);
await session.write(
'middleA.js',
`window.log('init MiddleAV1');
const Leaf = require('./leaf');
const MiddleB = require('./middleB');
module.exports = function MiddleA() {
return Leaf * MiddleB;
};`
);
await session.write(
'middleB.js',
`window.log('init MiddleBV1'); const Leaf = require('./leaf'); module.exports = Leaf;`
);
await session.write(
'leaf.js',
// Doesn't accept its own updates; they will propagate.
`window.log('init LeafV1'); module.exports = 2;`
);
session.resetState();
await session.patch(
'index.js',
`require('./root'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual([
'init RootV1',
'init MiddleAV1',
'init LeafV1',
'init MiddleBV1',
]);
session.resetState();
await session.patch('leaf.js', `window.log('init LeafV2'); module.exports = 3;`);
expect(session.logs).toStrictEqual(['init MiddleAV1', 'init LeafV2', 'init MiddleBV1']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1447-L1537
it('provides fresh value for module.exports in parents', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
// This module accepts itself
`const BarValue = require('./bar');
window.log('init FooV1 with BarValue = ' + BarValue);
module.exports = function Foo() {};`
);
await session.write(
'bar.js',
// This module will propagate to the parent
`window.log('init BarV1'); module.exports = 1;`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1 with BarValue = 1']);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2'); module.exports = 2;`);
expect(session.logs).toStrictEqual(['init BarV2', 'init FooV1 with BarValue = 2']);
// Let's try this again.
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); module.exports = 3;`);
expect(session.logs).toStrictEqual(['init BarV3', 'init FooV1 with BarValue = 3']);
// Now let's edit the parent which accepts itself
session.resetState();
await session.patch(
'foo.js',
`const BarValue = require('./bar');
window.log('init FooV2 with BarValue = ' + BarValue);
module.exports = function Foo() {};`
);
expect(session.logs).toStrictEqual(['init FooV2 with BarValue = 3']);
// Verify editing the child didn't break after parent update.
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); module.exports = 4;`);
expect(session.logs).toStrictEqual(['init BarV4', 'init FooV2 with BarValue = 4']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1539-L1629
it('provides fresh value for exports.* in parents', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
// This module accepts itself
`
const BarValue = require('./bar').value;
window.log('init FooV1 with BarValue = ' + BarValue);
exports.Foo = function Foo() {};`
);
await session.write(
'bar.js',
// This module will propagate to the parent
`window.log('init BarV1'); exports.value = 1;`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1 with BarValue = 1']);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2'); exports.value = 2;`);
expect(session.logs).toStrictEqual(['init BarV2', 'init FooV1 with BarValue = 2']);
// Let's try this again
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); exports.value = 3;`);
expect(session.logs).toStrictEqual(['init BarV3', 'init FooV1 with BarValue = 3']);
// Now let's edit the parent which accepts itself
session.resetState();
await session.patch(
'foo.js',
`
const BarValue = require('./bar').value;
window.log('init FooV2 with BarValue = ' + BarValue);
exports.Foo = function Foo() {};`
);
expect(session.logs).toStrictEqual(['init FooV2 with BarValue = 3']);
// Verify editing the child didn't break after parent update
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); exports.value = 4;`);
expect(session.logs).toStrictEqual(['init BarV4', 'init FooV2 with BarValue = 4']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1631-L1727
it('provides fresh value for ES6 named import in parents', async () => {
const [session] = await getSandbox({ esModule: true });
await session.write('root.js', `export default function Noop() { return null; };`);
await session.write('index.js', `import Root from './root.js'; Root();`);
await session.reload();
await session.write(
'foo.js',
// This module accepts itself
`
import { value as BarValue } from './bar.js';
window.log('init FooV1 with BarValue = ' + BarValue);
export function Foo() {};`
);
await session.write(
'bar.js',
// This module will propagate to the parent
`window.log('init BarV1'); export const value = 1;`
);
session.resetState();
await session.patch(
'root.js',
`import './foo.js'; export default function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1 with BarValue = 1']);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2'); export const value = 2;`);
expect(session.logs).toStrictEqual(['init BarV2', 'init FooV1 with BarValue = 2']);
// Let's try this again
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); export const value = 3;`);
expect(session.logs).toStrictEqual(['init BarV3', 'init FooV1 with BarValue = 3']);
// Now let's edit the parent which accepts itself
session.resetState();
await session.patch(
'foo.js',
`
import { value as BarValue } from './bar.js';
window.log('init FooV2 with BarValue = ' + BarValue);
export function Foo() {};`
);
expect(session.logs).toStrictEqual(['init FooV2 with BarValue = 3']);
// Verify editing the child didn't break after parent update
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); export const value = 4;`);
expect(session.logs).toStrictEqual(['init BarV4', 'init FooV2 with BarValue = 4']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1729-L1825
it('provides fresh value for ES6 default import in parents', async () => {
const [session] = await getSandbox({ esModule: true });
await session.write('root.js', `export default function Noop() { return null; };`);
await session.write('index.js', `import Root from './root.js'; Root();`);
await session.reload();
await session.write(
'foo.js',
// This module accepts itself
`
import BarValue from './bar.js';
window.log('init FooV1 with BarValue = ' + BarValue);
export default function Foo() {};`
);
await session.write(
'bar.js',
// This module will propagate to the parent
`window.log('init BarV1'); export default 1;`
);
session.resetState();
await session.patch(
'root.js',
`import './foo.js'; export default function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1 with BarValue = 1']);
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2'); export default 2;`);
expect(session.logs).toStrictEqual(['init BarV2', 'init FooV1 with BarValue = 2']);
// Let's try this again
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); export default 3;`);
expect(session.logs).toStrictEqual(['init BarV3', 'init FooV1 with BarValue = 3']);
// Now let's edit the parent which accepts itself
session.resetState();
await session.patch(
'foo.js',
`
import BarValue from './bar.js';
window.log('init FooV2 with BarValue = ' + BarValue);
export default function Foo() {};`
);
expect(session.logs).toStrictEqual(['init FooV2 with BarValue = 3']);
// Verify editing the child didn't break after parent update
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); export default 4;`);
expect(session.logs).toStrictEqual(['init BarV4', 'init FooV2 with BarValue = 4']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// Currently, webpack does not stop propagation after errors,
// but rather stops execution in parent after the errored module.
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1827-L1938
it('stops execution after module-level errors', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
`const Bar = require('./bar');
window.log('init FooV1');
module.exports = function Foo() {};`
);
await session.write(
'bar.js',
// This module normally propagates to the parent.
`module.exports = 'V1'; window.log('init BarV1');`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1']);
expect(session.errors).toHaveLength(0);
// We only edited Bar.
// Normally it would propagate to the parent.
// But the error should stop the execution early.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV2'); module.exports = 'V2'; throw new Error('init error during BarV2');`
);
expect(session.logs).toStrictEqual(['init BarV2']);
expect(session.errors).toHaveLength(1);
expect(session.errors[0]).toStrictEqual('init error during BarV2');
// Let's make another error.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV3'); throw new Error('init error during BarV3');`
);
expect(session.logs).toStrictEqual(['init BarV3']);
expect(session.errors).toHaveLength(1);
expect(session.errors[0]).toStrictEqual('init error during BarV3');
// Finally, let's fix the code.
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); module.exports = 'V4';`);
expect(session.logs).toStrictEqual(['init BarV4', 'init FooV1']);
expect(session.errors).toHaveLength(0);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1940-L2049
it('can continue hot updates after module-level errors with module.exports', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write('foo.js', `require('./bar'); window.log('init FooV1');`);
await session.write(
'bar.js',
// This module accepts itself
`window.log('init BarV1'); module.exports = function Bar() {};`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1']);
expect(session.errors).toHaveLength(0);
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV2'); module.exports = function Bar() {}; throw new Error('init error during BarV2');`
);
expect(session.logs).toStrictEqual(['init BarV2']);
expect(session.errors).toHaveLength(1);
expect(session.errors[0]).toBe('init error during BarV2');
// Let's fix the code.
session.resetState();
await session.patch('bar.js', `window.log('init BarV4'); module.exports = function Bar() {};`);
expect(session.logs).toStrictEqual(['init BarV4']);
expect(session.errors).toHaveLength(0);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2051-L2162
it('can continue hot updates after module-level errors with ES6 exports', async () => {
const [session] = await getSandbox({ esModule: true });
await session.write('root.js', `export default function Noop() { return null; };`);
await session.write('index.js', `import Root from './root.js'; Root();`);
await session.reload();
await session.write('foo.js', `import Bar from './bar.js'; Bar(); window.log('init FooV1');`);
await session.write(
'bar.js',
// This module accepts itself
`window.log('init BarV1'); export default function Bar() {};`
);
session.resetState();
await session.patch(
'root.js',
`import './foo.js'; export default function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1']);
expect(session.errors).toHaveLength(0);
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV2'); export default function Bar() {}; throw new Error('init error during BarV2');`
);
expect(session.logs).toStrictEqual(['init BarV2']);
expect(session.errors).toHaveLength(1);
expect(session.errors[0]).toBe('init error during BarV2');
// Let's fix the code.
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); export default function Bar() {};`);
expect(session.logs).toStrictEqual(['init BarV3']);
expect(session.errors).toHaveLength(0);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2164-L2272
it('does not accumulate stale exports over time', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
// This module accepts itself
`const BarExports = require('./bar');
window.log('init FooV1 with BarExports = ' + JSON.stringify(BarExports));
module.exports = function Foo() {};`
);
await session.write(
'bar.js',
// This module will propagate to the parent
`window.log('init BarV1'); exports.a = 1; exports.b = 2;`
);
session.resetState();
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV1', 'init FooV1 with BarExports = {"a":1,"b":2}']);
session.resetState();
await session.patch(
'bar.js',
// These are completely different exports
`window.log('init BarV2'); exports.c = 3; exports.d = 4;`
);
// Make sure we don't see {a, b} anymore.
expect(session.logs).toStrictEqual(['init BarV2', 'init FooV1 with BarExports = {"c":3,"d":4}']);
// Also edit the parent and verify the same again
session.resetState();
await session.patch(
'foo.js',
`const BarExports = require('./bar');
window.log('init FooV2 with BarExports = ' + JSON.stringify(BarExports));
module.exports = function Foo() {};`
);
expect(session.logs).toStrictEqual(['init FooV2 with BarExports = {"c":3,"d":4}']);
// Temporarily crash the child.
session.resetState();
await session.patch('bar.js', `throw new Error('oh no');`);
expect(session.logs).toStrictEqual([]);
// Try one last time to edit the child.
session.resetState();
await session.patch(
'bar.js',
// These are completely different exports
`window.log('init BarV3'); exports.e = 5; exports.f = 6;`
);
expect(session.logs).toStrictEqual(['init BarV3', 'init FooV2 with BarExports = {"e":5,"f":6}']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2274-L2318
it('bails out if update bubbles to the root via the only path', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = () => null;`);
await session.reload();
await session.write('foo.js', `window.log('init FooV1'); require('./bar');`);
await session.write('bar.js', `window.log('init BarV1');`);
session.resetState();
await session.patch('index.js', `require('./foo'); module.exports = () => null;`);
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV1']);
// Because root will not except,
// we need to reload the session to make sure the app is in an updated state.
await session.reload();
// Neither Bar nor Foo accepted, so update reached the root.
session.resetState();
await session.patch(
'bar.js',
`if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('init', 'init BarV2');
}`
);
await expect(session.evaluate(() => window.localStorage.getItem('init'))).resolves.toEqual(
'init BarV2'
);
// Expect full refresh.
// TODO:
// expect(Refresh.performReactRefresh).not.toHaveBeenCalled()
// expect(Refresh.performFullRefresh).toHaveBeenCalled()
expect(session.didFullRefresh).toBe(true);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2320-L2410
it('bails out if the update bubbles to the root via one of the paths', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = () => null;`);
await session.reload();
await session.write('foo.js', `window.log('init FooV1'); require('./bar'); require('./baz');`);
await session.write(
'bar.js',
// This module accepts itself
`window.log('init BarV1'); require('./qux'); module.exports = function Bar() {};`
);
await session.write(
'baz.js',
// This one doesn't accept itself,
// causing updates to Qux to bubble through the root.
`window.log('init BazV1'); require('./qux');`
);
await session.write(
'qux.js',
// Doesn't accept itself, and only one its parent path accepts.
`window.log('init QuxV1');`
);
session.resetState();
await session.patch('index.js', `require('./foo'); module.exports = () => null;`);
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV1', 'init QuxV1', 'init BazV1']);
// Because root will not except,
// we need to reload the session to make sure the app is in an updated state.
await session.reload();
// Edit Bar. It should self-accept.
session.resetState();
await session.patch(
'bar.js',
`window.log('init BarV2'); require('./qux'); module.exports = function Bar() {};`
);
expect(session.logs).toStrictEqual(['init BarV2']);
// TODO:
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
expect(session.didFullRefresh).toBe(false);
// Edit Qux. It should bubble. Baz accepts the update, Bar won't.
// So this update should bubble through the root.
session.resetState();
await session.patch(
'qux.js',
`if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('init', 'init QuxV2');
}`
);
await expect(session.evaluate(() => window.localStorage.getItem('init'))).resolves.toEqual(
'init QuxV2'
);
// Expect full refresh.
// TODO:
// expect(Refresh.performReactRefresh).not.toHaveBeenCalled()
// expect(Refresh.performFullRefresh).toHaveBeenCalled()
expect(session.didFullRefresh).toBe(true);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2412-L2511
it('propagates a module that stops accepting in next version', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = () => null;`);
await session.reload();
// Accept in parent
await session.write(
'foo.js',
`window.log('init FooV1'); require('./bar'); module.exports = function Foo() {};`
);
// Accept in child
await session.write('bar.js', `window.log('init BarV1'); module.exports = function Bar() {};`);
await session.patch('index.js', `require('./foo'); module.exports = () => null;`);
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV1']);
// Because root will not except,
// we need to reload the session to make sure the app is in an updated state.
await session.reload();
// Verify the child can accept itself
session.resetState();
await session.patch('bar.js', `window.log('init BarV1.1'); module.exports = function Bar() {};`);
expect(session.logs).toStrictEqual(['init BarV1.1']);
// Now let's change the child to *not* accept itself.
// We'll expect that now the parent will handle the evaluation.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2');`);
// We re-run Bar and expect to stop there.
// However, it didn't export a component, so we go higher.
// We stop at Foo which currently _does_ export a component.
expect(session.logs).toStrictEqual(
// Bar is evaluated twice:
// 1. To invalidate itself once it realizes it's no longer acceptable.
// 2. As a child of Foo re-evaluating.
['init BarV2', 'init FooV1', 'init BarV2']
);
// Change it back so that the child accepts itself.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2'); module.exports = function Bar() {};`);
// Since the export list changed, we have to re-run both the parent and the child.
expect(session.logs).toStrictEqual(['init FooV1', 'init BarV2']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
expect(session.didFullRefresh).toBe(false);
// Editing the child alone now doesn't reevaluate the parent.
session.resetState();
await session.patch('bar.js', `window.log('init BarV3'); module.exports = function Bar() {};`);
expect(session.logs).toStrictEqual(['init BarV3']);
// Finally, edit the parent in a way that changes the export.
// It would still be accepted on its own -
// but it's incompatible with the past version which didn't have two exports.
await session.evaluate(() => window.localStorage.setItem('init', ''));
await session.patch(
'foo.js',
`
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('init', 'init FooV2');
}
exports.Foo = function Foo() {};
exports.FooFoo = function FooFoo() {};`
);
// Check that we attempted to evaluate, but had to fall back to full refresh.
await expect(session.evaluate(() => window.localStorage.getItem('init'))).resolves.toEqual(
'init FooV2'
);
// TODO:
// expect(Refresh.performFullRefresh).toHaveBeenCalled();
// expect(Refresh.performReactRefresh).not.toHaveBeenCalled();
expect(session.didFullRefresh).toBe(true);
});
// https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2513-L2562
it('can replace a module before it is loaded', async () => {
const [session] = await getSandbox();
await session.write('index.js', `module.exports = function Noop() { return null; };`);
await session.reload();
await session.write(
'foo.js',
`window.log('init FooV1'); exports.loadBar = function () { require('./bar'); };`
);
await session.write('bar.js', `window.log('init BarV1');`);
await session.patch(
'index.js',
`require('./foo'); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init FooV1']);
// Replace Bar before it is loaded.
session.resetState();
await session.patch('bar.js', `window.log('init BarV2');`);
expect(session.logs).toStrictEqual([]);
// Now force Bar to load. It should use the latest version.
await session.patch(
'index.js',
`const { loadBar } = require('./foo'); loadBar(); module.exports = function Noop() { return null; };`
);
expect(session.logs).toStrictEqual(['init BarV2']);
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
expect(session.didFullRefresh).toBe(false);
});
================================================
FILE: test/conformance/environment.js
================================================
const puppeteer = require('puppeteer');
const TestEnvironment = require('../jest-environment');
class SandboxEnvironment extends TestEnvironment {
async setup() {
await super.setup();
const wsEndpoint = process.env.PUPPETEER_WS_ENDPOINT;
if (!wsEndpoint) {
throw new Error('Puppeteer wsEndpoint not found!');
}
this.global.browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
});
}
async teardown() {
await super.teardown();
if (this.global.browser) {
await this.global.browser.disconnect();
}
}
}
module.exports = SandboxEnvironment;
================================================
FILE: test/helpers/compilation/fixtures/source-map-loader.js
================================================
module.exports = function sourceMapLoader(source) {
const callback = this.async();
callback(null, source, this.query.sourceMap);
};
================================================
FILE: test/helpers/compilation/index.js
================================================
const path = require('path');
const { createFsFromVolume, Volume } = require('memfs');
const webpack = require('webpack');
const normalizeErrors = require('./normalizeErrors');
const BUNDLE_FILENAME = 'main';
const CONTEXT_PATH = path.join(__dirname, '../..', 'loader/fixtures');
const OUTPUT_PATH = path.join(__dirname, 'dist');
/**
* @typedef {Object} CompilationModule
* @property {string} execution
* @property {string} parsed
* @property {string} [sourceMap]
*/
/**
* @typedef {Object} CompilationSession
* @property {*[]} errors
* @property {*[]} warnings
* @property {CompilationModule} module
*/
/**
* Gets a Webpack compiler instance to test loader operations.
* @param {string} subContext
* @param {Object} [options]
* @param {boolean | string} [options.devtool]
* @param {import('../../../loader/types').ReactRefreshLoaderOptions} [options.loaderOptions]
* @param {*} [options.prevSourceMap]
* @returns {Promise}
*/
async function getCompilation(subContext, options = {}) {
const compiler = webpack({
mode: 'development',
cache: false,
context: path.join(CONTEXT_PATH, subContext),
devtool: options.devtool || false,
entry: {
[BUNDLE_FILENAME]: './index.js',
},
output: {
filename: '[name].js',
hashFunction: 'xxhash64',
path: OUTPUT_PATH,
},
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
use: [
{
loader: require.resolve('@pmmmwh/react-refresh-webpack-plugin/loader'),
options: options.loaderOptions,
},
!!options.devtool &&
Object.prototype.hasOwnProperty.call(options, 'prevSourceMap') && {
loader: path.join(__dirname, 'fixtures/source-map-loader.js'),
options: {
sourceMap: options.prevSourceMap,
},
},
].filter(Boolean),
},
],
},
plugins: [new webpack.HotModuleReplacementPlugin()],
// Options below forces Webpack to:
// 1. Move Webpack runtime into the runtime chunk;
// 2. Move node_modules into the vendor chunk with a stable name.
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
name: (module, chunks, cacheGroupKey) => cacheGroupKey,
},
},
});
// Use an in-memory file system to prevent emitting files
compiler.outputFileSystem = createFsFromVolume(new Volume());
/** @type {import('memfs').IFs} */
const compilerOutputFs = compiler.outputFileSystem;
/** @type {import('webpack').Stats | undefined} */
let compilationStats;
await new Promise((resolve, reject) => {
compiler.run((error, stats) => {
if (error) {
reject(error);
return;
}
compilationStats = stats;
// The compiler have to be explicitly closed
compiler.close(() => {
resolve();
});
});
});
return {
/** @type {*[]} */
get errors() {
return normalizeErrors(compilationStats.compilation.errors);
},
/** @type {*[]} */
get warnings() {
return normalizeErrors(compilationStats.compilation.errors);
},
/** @type {CompilationModule} */
get module() {
const compilationModules = compilationStats.toJson({ source: true }).modules;
if (!compilationModules) {
throw new Error('Module compilation stats not found!');
}
const parsed = compilationModules.find(({ name }) => name === './index.js');
if (!parsed) {
throw new Error('Fixture module is not found in compilation stats!');
}
let execution;
try {
execution = compilerOutputFs
.readFileSync(path.join(OUTPUT_PATH, `${BUNDLE_FILENAME}.js`))
.toString();
} catch (error) {
execution = error.toString();
}
/** @type {string | undefined} */
let sourceMap;
const [, sourceMapUrl] = execution.match(/\/\/# sourceMappingURL=(.*)$/) || [];
const isInlineSourceMap = !!sourceMapUrl && /^data:application\/json;/.test(sourceMapUrl);
if (!isInlineSourceMap) {
try {
sourceMap = JSON.stringify(
JSON.parse(
compilerOutputFs.readFileSync(path.join(OUTPUT_PATH, sourceMapUrl)).toString()
),
null,
2
);
} catch (error) {
sourceMap = error.toString();
}
}
return {
parsed: parsed.source,
execution,
sourceMap,
};
},
};
}
module.exports = getCompilation;
================================================
FILE: test/helpers/compilation/normalizeErrors.js
================================================
/**
* @param {string} str
* @return {string}
*/
function removeCwd(str) {
let cwd = process.cwd();
let result = str;
const isWin = process.platform === 'win32';
if (isWin) {
cwd = cwd.replace(/\\/g, '/');
result = result.replace(/\\/g, '/');
}
return result.replace(new RegExp(cwd, 'g'), '');
}
/**
* @param {Error[]} errors
* @return {string[]}
*/
function normalizeErrors(errors) {
return errors.map((error) => {
// Output nested error messages in full -
// this is useful for checking loader validation errors, for example.
if ('error' in error) {
return removeCwd(error.error.message);
}
return removeCwd(error.message.split('\n').slice(0, 2).join('\n'));
});
}
module.exports = normalizeErrors;
================================================
FILE: test/helpers/sandbox/aliasWDSv4.js
================================================
const moduleAlias = require('module-alias');
moduleAlias.addAliases({ 'webpack-dev-server': 'webpack-dev-server-v4' });
================================================
FILE: test/helpers/sandbox/configs.js
================================================
const path = require('path');
const BUNDLE_FILENAME = 'main';
/**
* @param {number} port
* @returns {string}
*/
function getIndexHTML(port) {
return `
Sandbox React App
`;
}
/**
* @param {boolean} esModule
* @returns {string}
*/
function getPackageJson(esModule = false) {
return `
{
"type": "${esModule ? 'module' : 'commonjs'}"
}
`;
}
/**
* @param {string} srcDir
* @returns {string}
*/
function getWDSConfig(srcDir) {
return `
const { DefinePlugin, ProgressPlugin } = require('webpack');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
context: '${srcDir}',
devtool: false,
entry: {
'${BUNDLE_FILENAME}': [
'${path.join(__dirname, 'fixtures/hmr-notifier.js')}',
'./index.js',
],
},
module: {
rules: [
{
test: /\\.jsx?$/,
include: '${srcDir}',
use: [
{
loader: '${require.resolve('babel-loader')}',
options: {
babelrc: false,
plugins: ['${require.resolve('react-refresh/babel')}'],
}
}
],
},
],
},
output: {
hashFunction: 'xxhash64',
},
plugins: [
new DefinePlugin({ __react_refresh_test__: true }),
new ProgressPlugin((percentage) => {
if (percentage === 1) {
console.log("Webpack compilation complete.");
}
}),
new ReactRefreshPlugin(),
],
resolve: {
alias: ${JSON.stringify(
{
...(WDS_VERSION === 4 && { 'webpack-dev-server': 'webpack-dev-server-v4' }),
},
null,
2
)},
extensions: ['.js', '.jsx'],
},
};
`;
}
module.exports = { getIndexHTML, getPackageJson, getWDSConfig };
================================================
FILE: test/helpers/sandbox/fixtures/hmr-notifier.js
================================================
if (module.hot) {
module.hot.addStatusHandler(function (status) {
if (status === 'idle') {
if (window.onHotSuccess) {
window.onHotSuccess();
}
}
});
}
================================================
FILE: test/helpers/sandbox/index.js
================================================
const path = require('path');
const fse = require('fs-extra');
const { nanoid } = require('nanoid');
const { getIndexHTML, getPackageJson, getWDSConfig } = require('./configs');
const { killTestProcess, spawnWebpackServe } = require('./spawn');
// Extends the timeout for tests using the sandbox
jest.setTimeout(1000 * 60);
// Setup a global "queue" of cleanup handlers to allow auto-teardown of tests,
// even when they did not run the cleanup function.
/** @type {Map>} */
const cleanupHandlers = new Map();
afterEach(async () => {
for (const [, callback] of cleanupHandlers) {
await callback();
}
});
/**
* Logs output to the console (only in debug mode).
* @param {...*} args
* @returns {void}
*/
const log = (...args) => {
if (__DEBUG__) {
console.log(...args);
}
};
/**
* Pause current asynchronous execution for provided milliseconds.
* @param {number} ms
* @returns {Promise}
*/
const sleep = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
/**
* @typedef {Object} SandboxSession
* @property {boolean} didFullRefresh
* @property {*[]} errors
* @property {*[]} logs
* @property {function(): void} resetState
* @property {function(string, string): Promise} write
* @property {function(string, string): Promise} patch
* @property {function(string): Promise} remove
* @property {function(*, ...*=): Promise<*>} evaluate
* @property {function(): Promise} reload
*/
const rootSandboxDir = path.join(__dirname, '../..', '__tmp__');
/**
* Creates a Webpack and Puppeteer backed sandbox to execute HMR operations on.
* @param {Object} [options]
* @param {boolean} [options.esModule]
* @param {string} [options.id]
* @param {Map} [options.initialFiles]
* @returns {Promise<[SandboxSession, function(): Promise]>}
*/
async function getSandbox({ esModule = false, id = nanoid(), initialFiles = new Map() } = {}) {
const { default: getPort } = await import('get-port');
const port = await getPort();
// Get sandbox directory paths
const sandboxDir = path.join(rootSandboxDir, id);
const srcDir = path.join(sandboxDir, 'src');
const publicDir = path.join(sandboxDir, 'public');
// In case of an ID clash, remove the existing sandbox directory
await fse.remove(sandboxDir);
// Create the sandbox source directory
await fse.mkdirp(srcDir);
// Create the sandbox public directory
await fse.mkdirp(publicDir);
// Write necessary files to sandbox
await fse.writeFile(path.join(sandboxDir, 'webpack.config.js'), getWDSConfig(srcDir));
await fse.writeFile(path.join(publicDir, 'index.html'), getIndexHTML(port));
await fse.writeFile(path.join(srcDir, 'package.json'), getPackageJson(esModule));
await fse.writeFile(
path.join(srcDir, 'index.js'),
esModule
? `export default function Sandbox() { return 'new sandbox'; }`
: "module.exports = function Sandbox() { return 'new sandbox'; };"
);
// Write initial files to sandbox
for (const [filePath, fileContent] of initialFiles.entries()) {
await fse.writeFile(path.join(srcDir, filePath), fileContent);
}
// TODO: Add handling for webpack-hot-middleware and webpack-plugin-serve
const app = await spawnWebpackServe(port, { public: publicDir, root: sandboxDir, src: srcDir });
/** @type {import('puppeteer').Page} */
const page = await browser.newPage();
await page.goto(`http://localhost:${port}/`);
let didFullRefresh = false;
/** @type {string[]} */
let errors = [];
/** @type {string[]} */
let logs = [];
// Expose logging and hot callbacks to the page
await Promise.all([
page.exposeFunction('log', (...args) => {
logs.push(args.join(' '));
}),
page.exposeFunction('onHotAcceptError', (errorMessage) => {
errors.push(errorMessage);
}),
page.exposeFunction('onHotSuccess', () => {
page.emit('hotSuccess');
}),
]);
// Reset testing logs and errors on any navigation.
// This is done for the main frame only,
// because child frames (e.g. iframes) might attach to the document,
// which will cause this event to fire.
page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) {
resetState();
}
});
/** @returns {void} */
function resetState() {
errors = [];
logs = [];
}
async function cleanupSandbox() {
try {
await page.close();
await killTestProcess(app);
if (!__DEBUG__) {
await fse.remove(sandboxDir);
}
// Remove current cleanup handler from the global queue since it has been called
cleanupHandlers.delete(id);
} catch (e) {
// Do nothing
}
}
// Cache the cleanup handler for global cleanup
// This is done in case tests fail and async handlers are kept alive
cleanupHandlers.set(id, cleanupSandbox);
return [
{
/** @type {boolean} */
get didFullRefresh() {
return didFullRefresh;
},
/** @type {*[]} */
get errors() {
return errors;
},
/** @type {*[]} */
get logs() {
return logs;
},
/** @returns {void} */
resetState,
/**
* @param {string} fileName
* @param {string} content
* @returns {Promise}
*/
async write(fileName, content) {
// Update the file on filesystem
const fullFileName = path.join(srcDir, fileName);
const directory = path.dirname(fullFileName);
await fse.mkdirp(directory);
await fse.writeFile(fullFileName, content);
},
/**
* @param {string} fileName
* @param {string} content
* @returns {Promise}
*/
async patch(fileName, content) {
// Register an event for HMR completion
let hmrStatus = 'pending';
// Parallelize file writing and event listening to prevent race conditions
await Promise.all([
this.write(fileName, content),
new Promise((resolve) => {
const hmrTimeout = setTimeout(() => {
hmrStatus = 'timeout';
resolve();
}, 30 * 1000);
// Frame Navigate and Hot Success events have to be exclusive,
// so we remove the other listener when one of them is triggered.
/**
* @param {import('puppeteer').Frame} frame
* @returns {void}
*/
const onFrameNavigate = (frame) => {
if (frame === page.mainFrame()) {
page.off('hotSuccess', onHotSuccess);
clearTimeout(hmrTimeout);
hmrStatus = 'reloaded';
resolve();
}
};
/**
* @returns {void}
*/
const onHotSuccess = () => {
page.off('framenavigated', onFrameNavigate);
clearTimeout(hmrTimeout);
hmrStatus = 'success';
resolve();
};
// Make sure that the event listener is bound to trigger only once
page.once('framenavigated', onFrameNavigate);
page.once('hotSuccess', onHotSuccess);
}),
]);
if (hmrStatus === 'reloaded') {
log('Application reloaded.');
didFullRefresh = didFullRefresh || true;
} else if (hmrStatus === 'success') {
log('Hot update complete.');
} else {
throw new Error(`Application is in an inconsistent state: ${hmrStatus}.`);
}
// Slow down tests to wait for re-rendering
await sleep(1000);
},
/**
* @param {string} fileName
* @returns {Promise}
*/
async remove(fileName) {
const fullFileName = path.join(srcDir, fileName);
await fse.remove(fullFileName);
},
/**
* @param {*} fn
* @param {...*} restArgs
* @returns {Promise<*>}
*/
async evaluate(fn, ...restArgs) {
if (typeof fn === 'function') {
return await page.evaluate(fn, ...restArgs);
} else {
throw new Error('You must pass a function to be evaluated in the browser!');
}
},
/** @returns {Promise} */
async reload() {
await page.reload({ waitUntil: 'networkidle2' });
didFullRefresh = false;
},
},
cleanupSandbox,
];
}
module.exports = getSandbox;
================================================
FILE: test/helpers/sandbox/spawn.js
================================================
const path = require('path');
const spawn = require('cross-spawn');
/**
* @param {string} packageName
* @returns {string}
*/
function getPackageExecutable(packageName, binName) {
let { bin: binPath } = require(`${packageName}/package.json`);
// "bin": { "package": "bin.js" }
if (typeof binPath === 'object') {
binPath = binPath[binName || packageName];
}
if (!binPath) {
throw new Error(`Package ${packageName} does not have an executable!`);
}
return require.resolve(path.join(packageName, binPath));
}
/**
* @param {import('child_process').ChildProcess | void} instance
* @returns {void}
*/
function killTestProcess(instance) {
if (!instance) {
return;
}
try {
process.kill(instance.pid);
} catch (error) {
if (
process.platform === 'win32' &&
typeof error.message === 'string' &&
(error.message.includes(`no running instance of the task`) ||
error.message.includes(`not found`))
) {
// Windows throws an error if the process is already dead
return;
}
throw error;
}
}
/**
* @typedef {Object} SpawnOptions
* @property {string} [cwd]
* @property {*} [env]
* @property {string | RegExp} [successMessage]
*/
/**
* @param {string} processPath
* @param {*[]} argv
* @param {SpawnOptions} [options]
* @returns {Promise}
*/
function spawnTestProcess(processPath, argv, options = {}) {
const cwd = options.cwd || path.resolve(__dirname, '../../..');
const env = {
...process.env,
NODE_ENV: 'development',
...options.env,
};
const successRegex = new RegExp(options.successMessage || 'webpack compilation complete.', 'i');
return new Promise((resolve, reject) => {
const instance = spawn(processPath, argv, { cwd, env });
let didResolve = false;
/**
* @param {Buffer} data
* @returns {void}
*/
function handleStdout(data) {
const message = data.toString();
if (successRegex.test(message)) {
if (!didResolve) {
didResolve = true;
resolve(instance);
}
}
if (__DEBUG__) {
process.stdout.write(message);
}
}
/**
* @param {Buffer} data
* @returns {void}
*/
function handleStderr(data) {
const message = data.toString();
if (__DEBUG__) {
process.stderr.write(message);
}
}
instance.stdout.on('data', handleStdout);
instance.stderr.on('data', handleStderr);
instance.on('close', () => {
instance.stdout.removeListener('data', handleStdout);
instance.stderr.removeListener('data', handleStderr);
if (!didResolve) {
didResolve = true;
resolve();
}
});
instance.on('error', (error) => {
reject(error);
});
});
}
/**
* @param {number} port
* @param {Object} dirs
* @param {string} dirs.public
* @param {string} dirs.root
* @param {string} dirs.src
* @param {SpawnOptions} [options]
* @returns {Promise}
*/
function spawnWebpackServe(port, dirs, options = {}) {
const webpackBin = getPackageExecutable('webpack-cli', 'webpack-cli');
const NODE_OPTIONS = [
// This requires a script to alias `webpack-dev-server` -
// both v4 and v5 are installed,
// so we have to ensure that they resolve to the correct variant.
WDS_VERSION === 4 && `--require "${require.resolve('./aliasWDSv4')}"`,
]
.filter(Boolean)
.join(' ');
return spawnTestProcess(
webpackBin,
[
'serve',
'--no-color',
'--no-client-overlay',
'--config',
path.join(dirs.root, 'webpack.config.js'),
'--static-directory',
dirs.public,
'--hot',
'--port',
port,
],
{
...options,
env: { ...options.env, ...(NODE_OPTIONS && { NODE_OPTIONS }) },
}
);
}
module.exports = {
killTestProcess,
spawnWebpackServe,
};
================================================
FILE: test/jest-environment.js
================================================
const { TestEnvironment: NodeEnvironment } = require('jest-environment-node');
const yn = require('yn');
class TestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.__DEBUG__ = yn(process.env.DEBUG);
this.global.WDS_VERSION = parseInt(process.env.WDS_VERSION || 5);
}
}
module.exports = TestEnvironment;
================================================
FILE: test/jest-global-setup.js
================================================
const puppeteer = require('puppeteer');
const yn = require('yn');
async function setup() {
if (yn(process.env.BROWSER, { default: true })) {
const browser = await puppeteer.launch({
devtools: yn(process.env.DEBUG, { default: false }),
headless: yn(process.env.HEADLESS, {
// Force headless mode in CI environments
default: yn(process.env.CI, { default: false }),
}),
});
global.__BROWSER_INSTANCE__ = browser;
process.env.PUPPETEER_WS_ENDPOINT = browser.wsEndpoint();
}
}
module.exports = setup;
================================================
FILE: test/jest-global-teardown.js
================================================
async function teardown() {
if (global.__BROWSER_INSTANCE__) {
await global.__BROWSER_INSTANCE__.close();
}
}
module.exports = teardown;
================================================
FILE: test/jest-resolver.js
================================================
/**
* @param {string} request
* @param {*} options
* @return {string}
*/
function resolver(request, options) {
// This acts as a mock for `require.resolve('react-refresh')`,
// since the current mocking behaviour of Jest is not symmetrical,
// i.e. only `require` is mocked but not `require.resolve`.
if (request === 'react-refresh') {
return 'react-refresh';
}
return options.defaultResolver(request, options);
}
module.exports = resolver;
================================================
FILE: test/jest-test-setup.js
================================================
require('jest-location-mock');
/**
* Skips a test block conditionally.
* @param {boolean} condition The condition to skip the test block.
* @param {string} blockName The name of the test block.
* @param {import('@jest/types').Global.BlockFn} blockFn The test block function.
* @returns {void}
*/
describe.skipIf = (condition, blockName, blockFn) => {
if (condition) {
return describe.skip(blockName, blockFn);
}
return describe(blockName, blockFn);
};
/**
* Skips a test conditionally.
* @param {boolean} condition The condition to skip the test.
* @param {string} testName The name of the test.
* @param {import('@jest/types').Global.TestFn} fn The test function.
* @param {number} [timeout] The time to wait before aborting.
* @returns {void}
*/
test.skipIf = (condition, testName, fn, timeout) => {
if (condition) {
return test.skip(testName, fn);
}
return test(testName, fn, timeout);
};
it.skipIf = test.skipIf;
================================================
FILE: test/loader/fixtures/auto/package.json
================================================
{
"name": "auto"
}
================================================
FILE: test/loader/fixtures/cjs/esm/index.js
================================================
export default 'esm';
================================================
FILE: test/loader/fixtures/cjs/esm/package.json
================================================
{
"type": "module"
}
================================================
FILE: test/loader/fixtures/cjs/index.js
================================================
module.exports = 'Test';
================================================
FILE: test/loader/fixtures/cjs/package.json
================================================
{
"name": "cjs",
"type": "commonjs"
}
================================================
FILE: test/loader/fixtures/esm/cjs/index.js
================================================
module.exports = 'cjs';
================================================
FILE: test/loader/fixtures/esm/cjs/package.json
================================================
{
"type": "commonjs"
}
================================================
FILE: test/loader/fixtures/esm/index.js
================================================
export default 'Test';
================================================
FILE: test/loader/fixtures/esm/package.json
================================================
{
"name": "esm",
"type": "module"
}
================================================
FILE: test/loader/loader.test.js
================================================
const validate = require('sourcemap-validator');
const mockFetch = require('../mocks/fetch');
describe('loader', () => {
let getCompilation;
beforeEach(() => {
jest.isolateModules(() => {
getCompilation = require('../helpers/compilation');
});
});
describe('on Webpack 5', () => {
it('should work for CommonJS', async () => {
const compilation = await getCompilation('cjs');
const { execution, parsed } = compilation.module;
expect(parsed).toMatchInlineSnapshot(`
"(typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.runtime = require('react-refresh');
module.exports = 'Test';
var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (module.hot) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
module.hot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
expect(execution).toMatchInlineSnapshot(`
"(self["webpackChunkcjs"] = self["webpackChunkcjs"] || []).push([["main"],{
/***/ "./index.js":
/*!******************!*\\
!*** ./index.js ***!
\\******************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
(typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.runtime = __webpack_require__(/*! react-refresh */ "../../../../node_modules/react-refresh/runtime.js");
module.exports = 'Test';
var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (true) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
module.hot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}
/***/ })
},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ __webpack_require__.O(0, ["defaultVendors"], () => (__webpack_exec__("./index.js")));
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ }
]);"
`);
expect(compilation.errors).toStrictEqual([]);
expect(compilation.warnings).toStrictEqual([]);
});
it('should work for ES Modules', async () => {
const compilation = await getCompilation('esm');
const { execution, parsed } = compilation.module;
expect(parsed).toMatchInlineSnapshot(`
"import * as __react_refresh_runtime__ from 'react-refresh';
(typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.runtime = __react_refresh_runtime__;
export default 'Test';
var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (import.meta.webpackHot) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
import.meta.webpackHot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
expect(execution).toMatchInlineSnapshot(`
""use strict";
(self["webpackChunkesm"] = self["webpackChunkesm"] || []).push([["main"],{
/***/ "./index.js":
/*!******************!*\\
!*** ./index.js ***!
\\******************/
/***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
var react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache;
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var react_refresh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react-refresh */ "../../../../node_modules/react-refresh/runtime.js");
(typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.runtime = /*#__PURE__*/ (react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache || (react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache = __webpack_require__.t(react_refresh__WEBPACK_IMPORTED_MODULE_0__, 2)));
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ('Test');
var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (true) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
__webpack_module__.hot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}
/***/ })
},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ __webpack_require__.O(0, ["defaultVendors"], () => (__webpack_exec__("./index.js")));
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ }
]);"
`);
expect(compilation.errors).toStrictEqual([]);
expect(compilation.warnings).toStrictEqual([]);
});
it('should generate valid source map when the "devtool" option is specified', async () => {
const compilation = await getCompilation('cjs', { devtool: 'source-map' });
const { execution, sourceMap } = compilation.module;
expect(sourceMap).toMatchInlineSnapshot(`
"{
"version": 3,
"file": "main.js",
"mappings": ";;;;;;;;;;AAAA",
"sources": [
"webpack://cjs/./index.js"
],
"sourcesContent": [
"module.exports = 'Test';\\n"
],
"names": [],
"ignoreList": [],
"sourceRoot": ""
}"
`);
expect(() => {
validate(execution, sourceMap);
}).not.toThrow();
});
});
it('should generate valid source map when undefined source map is provided', async () => {
const compilation = await getCompilation('cjs', {
devtool: 'source-map',
prevSourceMap: undefined,
});
const { execution, sourceMap } = compilation.module;
expect(() => {
validate(execution, sourceMap);
}).not.toThrow();
});
it('should generate valid source map when null source map is provided', async () => {
const compilation = await getCompilation('cjs', {
devtool: 'source-map',
prevSourceMap: null,
});
const { execution, sourceMap } = compilation.module;
expect(() => {
validate(execution, sourceMap);
}).not.toThrow();
});
it('should generate valid source map when source map string is provided', async () => {
const compilation = await getCompilation('cjs', {
devtool: 'source-map',
prevSourceMap: JSON.stringify({
version: 3,
sources: ['cjs'],
names: [],
mappings: 'AAAA;AACA',
sourcesContent: ["module.exports = 'Test';\n"],
}),
});
const { execution, sourceMap } = compilation.module;
expect(() => {
validate(execution, sourceMap);
}).not.toThrow();
});
it('should generate valid source map when source map object is provided', async () => {
const compilation = await getCompilation('cjs', {
devtool: 'source-map',
prevSourceMap: {
version: 3,
sources: ['cjs'],
names: [],
mappings: 'AAAA;AACA',
sourcesContent: ["module.exports = 'Test';\n"],
},
});
const { execution, sourceMap } = compilation.module;
expect(() => {
validate(execution, sourceMap);
}).not.toThrow();
});
it('should work with global fetch polyfill', async () => {
const [fetch] = mockFetch();
await expect(getCompilation('cjs')).resolves.not.toThrow();
expect(global.fetch).toStrictEqual(fetch);
});
});
================================================
FILE: test/loader/unit/getIdentitySourceMap.test.js
================================================
const { SourceMapConsumer } = require('source-map');
const validate = require('sourcemap-validator');
const getIdentitySourceMap = require('../../../loader/utils/getIdentitySourceMap');
describe('getIdentitySourceMap', () => {
it('should generate valid source map with source equality', async () => {
const source = "module.exports = 'Test'";
const path = 'index.js';
const identityMap = getIdentitySourceMap(source, path);
expect(() => {
validate(source, JSON.stringify(identityMap));
}).not.toThrow();
const sourceMapConsumer = await new SourceMapConsumer(identityMap);
expect(sourceMapConsumer.sourceContentFor(path)).toBe(source);
});
});
================================================
FILE: test/loader/unit/getModuleSystem.test.js
================================================
const path = require('path');
const { ModuleFilenameHelpers } = require('webpack');
describe('getModuleSystem', () => {
let getModuleSystem;
beforeEach(() => {
jest.isolateModules(() => {
getModuleSystem = require('../../../loader/utils/getModuleSystem');
});
});
it('should return `esm` when `options.esModule` is true', async () => {
await expect(getModuleSystem.call({}, ModuleFilenameHelpers, { esModule: true })).resolves.toBe(
'esm'
);
});
it('should return `cjs` when `options.esModule` is false', async () => {
await expect(
getModuleSystem.call({}, ModuleFilenameHelpers, { esModule: false })
).resolves.toBe('cjs');
});
it('should return `esm` when `resourcePath` matches `options.esModule.include`', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: 'include',
},
ModuleFilenameHelpers,
{ esModule: { include: /include/ } }
)
).resolves.toBe('esm');
});
it('should return `cjs` when `resourcePath` matches `options.esModule.exclude`', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: 'exclude',
},
ModuleFilenameHelpers,
{ esModule: { exclude: /exclude/ } }
)
).resolves.toBe('cjs');
});
it('should return `esm` when `resourcePath` ends with `.mjs` extension', async () => {
await expect(
getModuleSystem.call({ resourcePath: 'index.mjs' }, ModuleFilenameHelpers, {})
).resolves.toBe('esm');
});
it('should return `cjs` when `resourcePath` ends with `.cjs` extension', async () => {
await expect(
getModuleSystem.call({ resourcePath: 'index.cjs' }, ModuleFilenameHelpers, {})
).resolves.toBe('cjs');
});
it('should return `esm` when `package.json` uses the `module` type', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/esm', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('esm');
});
it('should return `esm` when `package.json` uses the `module` type nested inside a cjs package', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs/esm', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('esm');
});
it('should return `cjs` when `package.json` uses the `commonjs` type', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('cjs');
});
it('should return `cjs` when `package.json` uses the `commonjs` type nexted insdie an esm package', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/esm/cjs', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('cjs');
});
it('should return `cjs` when nothing matches', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/auto', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/auto'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{ esModule: {} }
)
).resolves.toBe('cjs');
});
});
================================================
FILE: test/loader/unit/getRefreshModuleRuntime.test.js
================================================
const { Template } = require('webpack');
const getRefreshModuleRuntime = require('../../../loader/utils/getRefreshModuleRuntime');
describe('getRefreshModuleRuntime', () => {
it('should return working refresh module runtime without const using CommonJS', () => {
const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: false,
moduleSystem: 'cjs',
});
expect(refreshModuleRuntime.indexOf('var')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('const')).toBe(-1);
expect(refreshModuleRuntime.indexOf('let')).toBe(-1);
expect(refreshModuleRuntime.indexOf('module.hot')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).toBe(-1);
expect(refreshModuleRuntime).toMatchInlineSnapshot(`
"var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (module.hot) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
module.hot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
});
it('should return working refresh module runtime with const using CommonJS', () => {
const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: true,
moduleSystem: 'cjs',
});
expect(refreshModuleRuntime.indexOf('var')).toBe(-1);
expect(refreshModuleRuntime.indexOf('const')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('let')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('module.hot')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).toBe(-1);
expect(refreshModuleRuntime).toMatchInlineSnapshot(`
"const $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
const $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (module.hot) {
let errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
let testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
module.hot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
});
it('should return working refresh module runtime without const using ES Modules', () => {
const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: false,
moduleSystem: 'esm',
});
expect(refreshModuleRuntime.indexOf('var')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('const')).toBe(-1);
expect(refreshModuleRuntime.indexOf('let')).toBe(-1);
expect(refreshModuleRuntime.indexOf('module.hot')).toBe(-1);
expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).not.toBe(-1);
expect(refreshModuleRuntime).toMatchInlineSnapshot(`
"var $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (import.meta.webpackHot) {
var errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
var testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
import.meta.webpackHot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
});
it('should return working refresh module runtime with const using ES Modules', () => {
const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: true,
moduleSystem: 'esm',
});
expect(refreshModuleRuntime.indexOf('var')).toBe(-1);
expect(refreshModuleRuntime.indexOf('const')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('let')).not.toBe(-1);
expect(refreshModuleRuntime.indexOf('module.hot')).toBe(-1);
expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).not.toBe(-1);
expect(refreshModuleRuntime).toMatchInlineSnapshot(`
"const $ReactRefreshModuleId$ = (typeof __webpack_global__ !== 'undefined' ? __webpack_global__ : __webpack_require__).$Refresh$.moduleId;
const $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
$ReactRefreshModuleId$
);
function $ReactRefreshModuleRuntime$(exports) {
if (import.meta.webpackHot) {
let errorOverlay;
if (typeof __react_refresh_error_overlay__ !== 'undefined') {
errorOverlay = __react_refresh_error_overlay__;
}
let testMode;
if (typeof __react_refresh_test__ !== 'undefined') {
testMode = __react_refresh_test__;
}
return __react_refresh_utils__.executeRuntime(
exports,
$ReactRefreshModuleId$,
import.meta.webpackHot,
errorOverlay,
testMode
);
}
}
if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
} else {
$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
}"
`);
});
});
================================================
FILE: test/loader/unit/normalizeOptions.test.js
================================================
const normalizeOptions = require('../../../loader/utils/normalizeOptions');
/** @type {Partial} */
const DEFAULT_OPTIONS = {
const: false,
esModule: undefined,
};
describe('normalizeOptions', () => {
it('should return default options when an empty object is received', () => {
expect(normalizeOptions({})).toStrictEqual(DEFAULT_OPTIONS);
});
it('should return user options', () => {
expect(
normalizeOptions({
const: true,
esModule: {
exclude: 'exclude',
include: 'include',
},
})
).toStrictEqual({
const: true,
esModule: {
exclude: 'exclude',
include: 'include',
},
});
});
it('should return true for overlay options when it is true', () => {
expect(normalizeOptions({ esModule: true })).toStrictEqual({
...DEFAULT_OPTIONS,
esModule: true,
});
});
it('should return false for esModule when it is false', () => {
expect(normalizeOptions({ esModule: false })).toStrictEqual({
...DEFAULT_OPTIONS,
esModule: false,
});
});
it('should return undefined for esModule when it is undefined', () => {
expect(normalizeOptions({ esModule: undefined })).toStrictEqual({
...DEFAULT_OPTIONS,
esModule: undefined,
});
});
it('should return undefined for esModule when it is undefined', () => {
expect(normalizeOptions({ esModule: undefined })).toStrictEqual({
...DEFAULT_OPTIONS,
esModule: undefined,
});
});
});
================================================
FILE: test/loader/validateOptions.test.js
================================================
describe('validateOptions', () => {
let getCompilation;
beforeEach(() => {
jest.isolateModules(() => {
getCompilation = require('../helpers/compilation');
});
});
it('should accept "const" when it is true', async () => {
const compilation = await getCompilation('cjs', { loaderOptions: { const: true } });
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "const" when it is false', async () => {
const compilation = await getCompilation('cjs', { loaderOptions: { const: false } });
expect(compilation.errors).toStrictEqual([]);
});
it('should reject "const" when it is not a boolean', async () => {
const compilation = await getCompilation('cjs', { loaderOptions: { const: 1 } });
expect(compilation.errors).toHaveLength(1);
expect(compilation.errors[0]).toMatchInlineSnapshot(`
"Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
- options.const should be a boolean."
`);
});
it('should accept "esModule" when it is true', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: true },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule" when it is false', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: false },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule" when it is undefined', async () => {
const compilation = await getCompilation('cjs', { loaderOptions: {} });
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule" when it is an empty object', async () => {
const compilation = await getCompilation('cjs', { loaderOptions: { esModule: {} } });
expect(compilation.errors).toStrictEqual([]);
});
it('should reject "esModule" when it is not a boolean nor an object', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: 'esModule' },
});
expect(compilation.errors).toHaveLength(1);
expect(compilation.errors[0]).toMatchInlineSnapshot(`
"Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
- options.esModule should be one of these:
boolean | object { exclude?, include? }
Details:
* options.esModule should be a boolean.
* options.esModule should be an object:
object { exclude?, include? }"
`);
});
it('should accept "esModule.exclude" when it is a RegExp', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: { exclude: /index\.js/ } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.exclude" when it is an absolute path string', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: { exclude: '/index.js' } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.exclude" when it is a string', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: { exclude: 'index.js' } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.exclude" when it is an array of RegExp or strings', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: { exclude: [/index\.js/, 'index.js'] } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should reject "esModule.exclude" when it is an object', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { esModule: { exclude: {} } },
});
expect(compilation.errors).toHaveLength(1);
expect(compilation.errors[0]).toMatchInlineSnapshot(`
"Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
- options.esModule should be one of these:
boolean | object { exclude?, include? }
Details:
* options.esModule.exclude should be one of these:
RegExp | string
Details:
* options.esModule.exclude should be an instance of RegExp.
* options.esModule.exclude should be a string.
* options.esModule.exclude should be an array:
[RegExp | string, ...] (should not have fewer than 1 item)
* options.esModule.exclude should be one of these:
RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)"
`);
});
it('should accept "esModule.include" when it is a RegExp', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: { include: /index\.js/ } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.include" when it is an absolute path string', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: { include: '/index.js' } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.include" when it is a string', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: { include: 'index.js' } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should accept "esModule.include" when it is an array of RegExp or strings', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: { include: [/index\.js/, 'index.js'] } },
});
expect(compilation.errors).toStrictEqual([]);
});
it('should reject "esModule.include" when it is an object', async () => {
const compilation = await getCompilation('esm', {
loaderOptions: { esModule: { include: {} } },
});
expect(compilation.errors).toHaveLength(1);
expect(compilation.errors[0]).toMatchInlineSnapshot(`
"Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
- options.esModule should be one of these:
boolean | object { exclude?, include? }
Details:
* options.esModule.include should be one of these:
RegExp | string
Details:
* options.esModule.include should be an instance of RegExp.
* options.esModule.include should be a string.
* options.esModule.include should be an array:
[RegExp | string, ...] (should not have fewer than 1 item)
* options.esModule.include should be one of these:
RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)"
`);
});
it('should reject any unknown options', async () => {
const compilation = await getCompilation('cjs', {
loaderOptions: { unknown: 'unknown' },
});
expect(compilation.errors).toHaveLength(1);
expect(compilation.errors[0]).toMatchInlineSnapshot(`
"Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
- options has an unknown property 'unknown'. These properties are valid:
object { const?, esModule? }"
`);
});
});
================================================
FILE: test/mocks/fetch.js
================================================
/** @type {Set} */
const cleanupHandlers = new Set();
afterEach(() => {
[...cleanupHandlers].map((callback) => callback());
});
const mockFetch = () => {
const originalFetch = global.fetch;
const fetchMock = new Function();
global.fetch = fetchMock;
function mockRestore() {
global.fetch = originalFetch;
}
cleanupHandlers.add(mockRestore);
return [fetchMock, mockRestore];
};
module.exports = mockFetch;
================================================
FILE: test/unit/fixtures/socketIntegration.js
================================================
module.exports = {};
================================================
FILE: test/unit/getAdditionalEntries.test.js
================================================
const getAdditionalEntries = require('../../lib/utils/getAdditionalEntries');
const ErrorOverlayEntry = require.resolve('../../client/ErrorOverlayEntry');
const ReactRefreshEntry = require.resolve('../../client/ReactRefreshEntry');
describe('getAdditionalEntries', () => {
it('should work with default settings', () => {
expect(getAdditionalEntries({ overlay: { entry: ErrorOverlayEntry } })).toStrictEqual({
overlayEntries: [ErrorOverlayEntry],
prependEntries: [ReactRefreshEntry],
});
});
it('should skip overlay entries when overlay is false in options', () => {
expect(getAdditionalEntries({ overlay: false })).toStrictEqual({
overlayEntries: [],
prependEntries: [ReactRefreshEntry],
});
});
});
================================================
FILE: test/unit/getIntegrationEntry.test.js
================================================
const getIntegrationEntry = require('../../lib/utils/getIntegrationEntry');
describe('getIntegrationEntry', () => {
it('should work with webpack-hot-middleware', () => {
expect(getIntegrationEntry('whm')).toStrictEqual('webpack-hot-middleware/client');
});
it('should work with webpack-plugin-serve', () => {
expect(getIntegrationEntry('wps')).toStrictEqual('webpack-plugin-serve/client');
});
it('should return undefined for webpack-dev-server', () => {
expect(getIntegrationEntry('wds')).toStrictEqual(undefined);
});
it('should return undefined for unknown integrations', () => {
expect(getIntegrationEntry('unknown')).toStrictEqual(undefined);
});
});
================================================
FILE: test/unit/getSocketIntegration.test.js
================================================
const getSocketIntegration = require('../../lib/utils/getSocketIntegration');
describe('getSocketIntegration', () => {
it('should work with webpack-dev-server', () => {
const WDSSocket = require.resolve('../../sockets/WDSSocket');
expect(getSocketIntegration('wds')).toStrictEqual(WDSSocket);
});
it('should work with webpack-hot-middleware', () => {
const WHMEventSource = require.resolve('../../sockets/WHMEventSource');
expect(getSocketIntegration('whm')).toStrictEqual(WHMEventSource);
});
it('should work with webpack-plugin-serve', () => {
const WPSSocket = require.resolve('../../sockets/WPSSocket');
expect(getSocketIntegration('wps')).toStrictEqual(WPSSocket);
});
it('should resolve when module path is provided', () => {
const FixtureSocket = require.resolve('./fixtures/socketIntegration');
expect(getSocketIntegration(FixtureSocket)).toStrictEqual(FixtureSocket);
});
it('should throw when non-path string is provided', () => {
expect(() => getSocketIntegration('unknown')).toThrowErrorMatchingInlineSnapshot(
`"Cannot find module 'unknown' from '../lib/utils/getSocketIntegration.js'"`
);
});
});
================================================
FILE: test/unit/globals.test.js
================================================
const { getRefreshGlobalScope } = require('../../lib/globals');
describe('getRefreshGlobalScope', () => {
it('should work for Webpack 5', () => {
const { RuntimeGlobals } = require('webpack');
expect(getRefreshGlobalScope(RuntimeGlobals)).toStrictEqual('__webpack_require__.$Refresh$');
});
});
================================================
FILE: test/unit/makeRefreshRuntimeModule.test.js
================================================
const makeRefreshRuntimeModule = require('../../lib/utils/makeRefreshRuntimeModule');
describe('makeRefreshRuntimeModule', () => {
beforeEach(() => {
global.__webpack_require__ = { i: [] };
});
afterAll(() => {
delete global.__webpack_require__;
});
it('should make runtime module', () => {
const webpack = require('webpack');
let RefreshRuntimeModule;
expect(() => {
RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
}).not.toThrow();
expect(() => {
new RefreshRuntimeModule();
}).not.toThrow();
});
it('should generate with ES5 settings', () => {
const webpack = require('webpack');
const RuntimeTemplate = require('webpack/lib/RuntimeTemplate');
const RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
const instance = new RefreshRuntimeModule();
instance.compilation = {
runtimeTemplate: new RuntimeTemplate(
{},
{ environment: { arrowFunction: false, const: false } },
(i) => i
),
};
const runtime = instance.generate();
expect(runtime).toMatchInlineSnapshot(`
"var setup = function(moduleId) {
var refresh = {
moduleId: moduleId,
register: function(type, id) {
var typeId = moduleId + ' ' + id;
refresh.runtime.register(type, typeId);
},
signature: function() { return refresh.runtime.createSignatureFunctionForTransform(); },
runtime: {
createSignatureFunctionForTransform: function() { return function(type) { return type; }; },
register: function() {}
},
};
return refresh;
};
__webpack_require__.i.push(function(options) {
var originalFactory = options.factory;
options.factory = function(moduleObject, moduleExports, webpackRequire) {
var hotRequire = function(request) { return webpackRequire(request); };
var createPropertyDescriptor = function(name) {
return {
configurable: true,
enumerable: true,
get: function() { return webpackRequire[name]; },
set: function(value) {
webpackRequire[name] = value;
},
};
};
for (var name in webpackRequire) {
if (name === "$Refresh$") continue;
if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {
Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
}
}
hotRequire.$Refresh$ = setup(options.id);
originalFactory.call(this, moduleObject, moduleExports, hotRequire);
};
});"
`);
expect(() => {
eval(runtime);
}).not.toThrow();
});
it('should make working runtime module with ES6 settings', () => {
const webpack = require('webpack');
const RuntimeTemplate = require('webpack/lib/RuntimeTemplate');
const RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
const instance = new RefreshRuntimeModule();
instance.compilation = {
runtimeTemplate: new RuntimeTemplate(
{},
{ environment: { arrowFunction: true, const: true } },
(i) => i
),
};
const runtime = instance.generate();
expect(runtime).toMatchInlineSnapshot(`
"const setup = (moduleId) => {
const refresh = {
moduleId: moduleId,
register: (type, id) => {
const typeId = moduleId + ' ' + id;
refresh.runtime.register(type, typeId);
},
signature: () => (refresh.runtime.createSignatureFunctionForTransform()),
runtime: {
createSignatureFunctionForTransform: () => ((type) => (type)),
register: x => {}
},
};
return refresh;
};
__webpack_require__.i.push((options) => {
const originalFactory = options.factory;
options.factory = function(moduleObject, moduleExports, webpackRequire) {
const hotRequire = (request) => (webpackRequire(request));
const createPropertyDescriptor = (name) => {
return {
configurable: true,
enumerable: true,
get: () => (webpackRequire[name]),
set: (value) => {
webpackRequire[name] = value;
},
};
};
for (const name in webpackRequire) {
if (name === "$Refresh$") continue;
if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {
Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
}
}
hotRequire.$Refresh$ = setup(options.id);
originalFactory.call(this, moduleObject, moduleExports, hotRequire);
};
});"
`);
expect(() => {
eval(runtime);
}).not.toThrow();
});
});
================================================
FILE: test/unit/normalizeOptions.test.js
================================================
const normalizeOptions = require('../../lib/utils/normalizeOptions');
/** @type {Partial} */
const DEFAULT_OPTIONS = {
exclude: /node_modules/i,
include: /\.([cm]js|[jt]sx?|flow)$/i,
overlay: {
entry: require.resolve('../../client/ErrorOverlayEntry'),
module: require.resolve('../../overlay'),
sockIntegration: 'wds',
},
};
describe('normalizeOptions', () => {
it('should return default options when an empty object is received', () => {
expect(normalizeOptions({})).toStrictEqual(DEFAULT_OPTIONS);
});
it('should return user options', () => {
expect(
normalizeOptions({
exclude: 'exclude',
forceEnable: true,
include: 'include',
library: 'library',
overlay: {
entry: 'entry',
module: 'overlay',
sockIntegration: 'whm',
},
})
).toStrictEqual({
exclude: 'exclude',
forceEnable: true,
include: 'include',
library: 'library',
overlay: {
entry: 'entry',
module: 'overlay',
sockIntegration: 'whm',
},
});
});
it('should return default for overlay options when it is true', () => {
expect(normalizeOptions({ overlay: true })).toStrictEqual(DEFAULT_OPTIONS);
});
it('should return false for overlay options when it is false', () => {
expect(normalizeOptions({ overlay: false })).toStrictEqual({
...DEFAULT_OPTIONS,
overlay: false,
});
});
it('should keep "overlay.entry" when it is false', () => {
const options = { ...DEFAULT_OPTIONS };
options.overlay.entry = false;
expect(normalizeOptions(options)).toStrictEqual(options);
});
it('should keep "overlay.module" when it is false', () => {
const options = { ...DEFAULT_OPTIONS };
options.overlay.module = false;
expect(normalizeOptions(options)).toStrictEqual(options);
});
it('should keep "overlay.sockIntegration" when it is false', () => {
const options = { ...DEFAULT_OPTIONS };
options.overlay.sockIntegration = false;
expect(normalizeOptions(options)).toStrictEqual(options);
});
});
================================================
FILE: test/unit/validateOptions.test.js
================================================
const ReactRefreshPlugin = require('../../lib');
describe('validateOptions', () => {
it('should accept "exclude" when it is a RegExp', () => {
expect(() => {
new ReactRefreshPlugin({ exclude: /test/ });
}).not.toThrow();
});
it('should accept "exclude" when it is an absolute path string', () => {
expect(() => {
new ReactRefreshPlugin({ exclude: '/test' });
}).not.toThrow();
});
it('should accept "exclude" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({ exclude: 'test' });
}).not.toThrow();
});
it('should accept "exclude" when it is an array of RegExp or strings', () => {
expect(() => {
new ReactRefreshPlugin({ exclude: [/test/, 'test'] });
}).not.toThrow();
});
it('should reject "exclude" when it is an object', () => {
expect(() => {
new ReactRefreshPlugin({ exclude: {} });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.exclude should be one of these:
RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)
Details:
* options.exclude should be one of these:
RegExp | string
Details:
* options.exclude should be an instance of RegExp.
* options.exclude should be a string.
* options.exclude should be an array:
[RegExp | string, ...] (should not have fewer than 1 item)"
`);
});
it('should accept "forceEnable" when it is true', () => {
expect(() => {
new ReactRefreshPlugin({ forceEnable: true });
}).not.toThrow();
});
it('should accept "forceEnable" when it is false', () => {
expect(() => {
new ReactRefreshPlugin({ forceEnable: false });
}).not.toThrow();
});
it('should reject "forceEnable" when it is not a boolean', () => {
expect(() => {
new ReactRefreshPlugin({ forceEnable: 1 });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.forceEnable should be a boolean."
`);
});
it('should accept "include" when it is a RegExp', () => {
expect(() => {
new ReactRefreshPlugin({ include: /test/ });
}).not.toThrow();
});
it('should accept "include" when it is an absolute path string', () => {
expect(() => {
new ReactRefreshPlugin({ include: '/test' });
}).not.toThrow();
});
it('should accept "include" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({ include: 'test' });
}).not.toThrow();
});
it('should accept "include" when it is an array of RegExp or strings', () => {
expect(() => {
new ReactRefreshPlugin({ include: [/test/, 'test'] });
}).not.toThrow();
});
it('should reject "include" when it is an object', () => {
expect(() => {
new ReactRefreshPlugin({ include: {} });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.include should be one of these:
RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)
Details:
* options.include should be one of these:
RegExp | string
Details:
* options.include should be an instance of RegExp.
* options.include should be a string.
* options.include should be an array:
[RegExp | string, ...] (should not have fewer than 1 item)"
`);
});
it('should accept "library" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({ library: 'library' });
}).not.toThrow();
});
it('should reject "library" when it is not a string', () => {
expect(() => {
new ReactRefreshPlugin({ library: [] });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.library should be a string."
`);
});
it('should accept "overlay" when it is true', () => {
expect(() => {
new ReactRefreshPlugin({ overlay: true });
}).not.toThrow();
});
it('should accept "overlay" when it is false', () => {
expect(() => {
new ReactRefreshPlugin({ overlay: false });
}).not.toThrow();
});
it('should accept "overlay" when it is an empty object', () => {
expect(() => {
new ReactRefreshPlugin({ overlay: {} });
}).not.toThrow();
});
it('should reject "overlay" when it is not a boolean nor an object', () => {
expect(() => {
new ReactRefreshPlugin({ overlay: 'overlay' });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.overlay should be one of these:
boolean | object { entry?, module?, sockIntegration? }
Details:
* options.overlay should be a boolean.
* options.overlay should be an object:
object { entry?, module?, sockIntegration? }"
`);
});
it('should accept "overlay.entry" when it is an absolute path string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { entry: '/test' },
});
}).not.toThrow();
});
it('should accept "overlay.entry" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { entry: 'test' },
});
}).not.toThrow();
});
it('should accept "overlay.entry" when it is false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { entry: false },
});
}).not.toThrow();
});
it('should reject "overlay.entry" when it is not a string nor false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { entry: true },
});
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.overlay should be one of these:
boolean | object { entry?, module?, sockIntegration? }
Details:
* options.overlay.entry should be one of these:
false | string
Details:
* options.overlay.entry should be equal to constant false.
* options.overlay.entry should be a string."
`);
});
it('should accept "overlay.module" when it is an absolute path string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { module: '/test' },
});
}).not.toThrow();
});
it('should accept "overlay.module" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { module: 'test' },
});
}).not.toThrow();
});
it('should accept "overlay.module" when it is false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { module: false },
});
}).not.toThrow();
});
it('should reject "overlay.module" when it is not a string nor false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { module: true },
});
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.overlay should be one of these:
boolean | object { entry?, module?, sockIntegration? }
Details:
* options.overlay.module should be one of these:
false | string
Details:
* options.overlay.module should be equal to constant false.
* options.overlay.module should be a string."
`);
});
it('should accept "overlay.sockIntegration" when it is "wds"', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: 'wds' },
});
}).not.toThrow();
});
it('should accept "overlay.sockIntegration" when it is "whm"', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: 'whm' },
});
}).not.toThrow();
});
it('should accept "overlay.sockIntegration" when it is "wps"', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: 'wps' },
});
}).not.toThrow();
});
it('should accept "overlay.sockIntegration" when it is an absolute path string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: '/test' },
});
}).not.toThrow();
});
it('should accept "overlay.sockIntegration" when it is a string', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: 'test' },
});
}).not.toThrow();
});
it('should accept "overlay.sockIntegration" when it is false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: false },
});
}).not.toThrow();
});
it('should reject "overlay.sockIntegration" when it is not a string nor false', () => {
expect(() => {
new ReactRefreshPlugin({
overlay: { sockIntegration: true },
});
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options.overlay should be one of these:
boolean | object { entry?, module?, sockIntegration? }
Details:
* options.overlay.sockIntegration should be one of these:
false | "wds" | "whm" | "wps" | string
Details:
* options.overlay.sockIntegration should be equal to constant false.
* options.overlay.sockIntegration should be one of these:
"wds" | "whm" | "wps"
* options.overlay.sockIntegration should be a string."
`);
});
it('should reject any unknown options', () => {
expect(() => {
new ReactRefreshPlugin({ unknown: true });
}).toThrowErrorMatchingInlineSnapshot(`
"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema.
- options has an unknown property 'unknown'. These properties are valid:
object { esModule?, exclude?, forceEnable?, include?, library?, overlay? }"
`);
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"module": "commonjs",
"noUnusedLocals": true,
"outDir": "types",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "es2018"
},
"include": ["./lib/*", "./loader/*", "./options/*"],
"exclude": ["node_modules"]
}
================================================
FILE: types/lib/index.d.ts
================================================
export = ReactRefreshPlugin;
declare class ReactRefreshPlugin {
/**
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
*/
constructor(options?: import('./types').ReactRefreshPluginOptions | undefined);
/**
* @readonly
* @type {import('./types').NormalizedPluginOptions}
*/
readonly options: import('./types').NormalizedPluginOptions;
/**
* Applies the plugin.
* @param {import('webpack').Compiler} compiler A webpack compiler object.
* @returns {void}
*/
apply(compiler: import('webpack').Compiler): void;
}
declare namespace ReactRefreshPlugin {
export { ReactRefreshPlugin };
}
================================================
FILE: types/lib/types.d.ts
================================================
export type ErrorOverlayOptions = {
/**
* Path to a JS file that sets up the error overlay integration.
*/
entry?: string | false | undefined;
/**
* The error overlay module to use.
*/
module?: string | false | undefined;
/**
* Path to a JS file that sets up the Webpack socket integration.
*/
sockIntegration?:
| import('type-fest').LiteralUnion
| undefined;
};
export type NormalizedErrorOverlayOptions = import('type-fest').SetRequired<
ErrorOverlayOptions,
'entry' | 'module' | 'sockIntegration'
>;
export type ReactRefreshPluginOptions = {
/**
* Enables strict ES Modules compatible runtime.
*/
esModule?: boolean | import('../loader/types').ESModuleOptions | undefined;
/**
* Files to explicitly exclude from processing.
*/
exclude?: string | RegExp | (string | RegExp)[] | undefined;
/**
* Enables the plugin forcefully.
*/
forceEnable?: boolean | undefined;
/**
* Files to explicitly include for processing.
*/
include?: string | RegExp | (string | RegExp)[] | undefined;
/**
* Name of the library bundle.
*/
library?: string | undefined;
/**
* Modifies how the error overlay integration works in the plugin.
*/
overlay?: boolean | ErrorOverlayOptions | undefined;
};
export type OverlayOverrides = {
/**
* Modifies how the error overlay integration works in the plugin.
*/
overlay: false | NormalizedErrorOverlayOptions;
};
export type NormalizedPluginOptions = import('type-fest').SetRequired<
import('type-fest').Except,
'exclude' | 'include'
> &
OverlayOverrides;
================================================
FILE: types/loader/index.d.ts
================================================
export = ReactRefreshLoader;
/**
* A simple Webpack loader to inject react-refresh HMR code into modules.
*
* [Reference for Loader API](https://webpack.js.org/api/loaders/)
* @this {import('webpack').LoaderContext}
* @param {string} source The original module source code.
* @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
* @param {*} [meta] The loader metadata passed in.
* @returns {void}
*/
declare function ReactRefreshLoader(
this: import('webpack').LoaderContext,
source: string,
inputSourceMap?: import('source-map').RawSourceMap | undefined,
meta?: any
): void;
================================================
FILE: types/loader/types.d.ts
================================================
export type ESModuleOptions = {
/**
* Files to explicitly exclude from flagged as ES Modules.
*/
exclude?: string | RegExp | (string | RegExp)[] | undefined;
/**
* Files to explicitly include for flagged as ES Modules.
*/
include?: string | RegExp | (string | RegExp)[] | undefined;
};
export type ReactRefreshLoaderOptions = {
/**
* Enables usage of ES6 `const` and `let` in generated runtime code.
*/
const?: boolean | undefined;
/**
* Enables strict ES Modules compatible runtime.
*/
esModule?: boolean | ESModuleOptions | undefined;
};
export type NormalizedLoaderOptions = import('type-fest').SetRequired<
ReactRefreshLoaderOptions,
'const'
>;
================================================
FILE: types/options/index.d.ts
================================================
/**
* Sets a constant default value for the property when it is undefined.
* @template T
* @template {keyof T} Property
* @param {T} object An object.
* @param {Property} property A property of the provided object.
* @param {T[Property]} [defaultValue] The default value to set for the property.
* @returns {T[Property]} The defaulted property value.
*/
export function d(
object: T,
property: Property,
defaultValue?: T[Property] | undefined
): T[Property];
/**
* Resolves the value for a nested object option.
* @template T
* @template {keyof T} Property
* @template Result
* @param {T} object An object.
* @param {Property} property A property of the provided object.
* @param {function(T | undefined): Result} fn The handler to resolve the property's value.
* @returns {Result} The resolved option value.
*/
export function n(
object: T,
property: Property,
fn: (arg0: T | undefined) => Result
): Result;
================================================
FILE: webpack.config.js
================================================
const path = require('node:path');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
client: './client/ReactRefreshEntry.js',
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
format: { comments: false },
},
}),
],
nodeEnv: 'development',
},
output: {
filename: '[name].min.js',
path: path.resolve(__dirname, 'umd'),
},
};