├── .gitattributes
├── client
├── package.json
├── utils
│ ├── retry.js
│ ├── errorEventHandlers.js
│ └── formatWebpackErrors.js
├── ReactRefreshEntry.js
└── ErrorOverlayEntry.js
├── overlay
├── package.json
├── components
│ ├── Spacer.js
│ ├── RuntimeErrorHeader.js
│ ├── PageHeader.js
│ ├── CompileErrorTrace.js
│ ├── RuntimeErrorStack.js
│ └── RuntimeErrorFooter.js
├── containers
│ ├── CompileErrorContainer.js
│ └── RuntimeErrorContainer.js
├── theme.js
└── utils.js
├── sockets
├── package.json
├── WHMEventSource.js
├── WDSSocket.js
└── WPSSocket.js
├── test
├── loader
│ ├── fixtures
│ │ ├── cjs
│ │ │ ├── esm
│ │ │ │ ├── index.js
│ │ │ │ └── package.json
│ │ │ ├── index.js
│ │ │ └── package.json
│ │ ├── esm
│ │ │ ├── index.js
│ │ │ ├── cjs
│ │ │ │ ├── index.js
│ │ │ │ └── package.json
│ │ │ └── package.json
│ │ └── auto
│ │ │ └── package.json
│ ├── unit
│ │ ├── getIdentitySourceMap.test.js
│ │ ├── normalizeOptions.test.js
│ │ ├── getModuleSystem.test.js
│ │ └── getRefreshModuleRuntime.test.js
│ └── validateOptions.test.js
├── unit
│ ├── fixtures
│ │ └── socketIntegration.js
│ ├── globals.test.js
│ ├── getIntegrationEntry.test.js
│ ├── getAdditionalEntries.test.js
│ ├── getSocketIntegration.test.js
│ ├── normalizeOptions.test.js
│ └── makeRefreshRuntimeModule.test.js
├── helpers
│ ├── sandbox
│ │ ├── aliasWDSv4.js
│ │ ├── fixtures
│ │ │ └── hmr-notifier.js
│ │ ├── configs.js
│ │ ├── spawn.js
│ │ └── index.js
│ └── compilation
│ │ ├── fixtures
│ │ └── source-map-loader.js
│ │ ├── normalizeErrors.js
│ │ └── index.js
├── jest-global-teardown.js
├── jest-environment.js
├── jest-resolver.js
├── mocks
│ └── fetch.js
├── jest-global-setup.js
├── conformance
│ └── environment.js
└── jest-test-setup.js
├── .prettierignore
├── .prettierrc.json
├── examples
├── webpack-dev-server
│ ├── src
│ │ ├── ArrowFunction.jsx
│ │ ├── FunctionNamed.jsx
│ │ ├── LazyComponent.jsx
│ │ ├── FunctionDefault.jsx
│ │ ├── ClassNamed.jsx
│ │ ├── index.js
│ │ ├── ClassDefault.jsx
│ │ └── App.jsx
│ ├── public
│ │ └── index.html
│ ├── babel.config.js
│ ├── package.json
│ └── webpack.config.js
├── typescript-with-babel
│ ├── src
│ │ ├── ArrowFunction.tsx
│ │ ├── FunctionNamed.tsx
│ │ ├── LazyComponent.tsx
│ │ ├── FunctionDefault.tsx
│ │ ├── ClassNamed.tsx
│ │ ├── index.tsx
│ │ ├── ClassDefault.tsx
│ │ └── App.tsx
│ ├── public
│ │ └── index.html
│ ├── tsconfig.json
│ ├── babel.config.js
│ ├── package.json
│ └── webpack.config.js
├── typescript-with-swc
│ ├── src
│ │ ├── ArrowFunction.tsx
│ │ ├── FunctionNamed.tsx
│ │ ├── LazyComponent.tsx
│ │ ├── FunctionDefault.tsx
│ │ ├── ClassNamed.tsx
│ │ ├── index.tsx
│ │ ├── ClassDefault.tsx
│ │ └── App.tsx
│ ├── public
│ │ └── index.html
│ ├── tsconfig.json
│ ├── package.json
│ └── webpack.config.js
├── typescript-with-tsc
│ ├── src
│ │ ├── ArrowFunction.tsx
│ │ ├── FunctionNamed.tsx
│ │ ├── LazyComponent.tsx
│ │ ├── FunctionDefault.tsx
│ │ ├── ClassNamed.tsx
│ │ ├── index.tsx
│ │ ├── ClassDefault.tsx
│ │ └── App.tsx
│ ├── tsconfig.dev.json
│ ├── public
│ │ └── index.html
│ ├── tsconfig.json
│ ├── package.json
│ └── webpack.config.js
├── webpack-hot-middleware
│ ├── src
│ │ ├── ArrowFunction.jsx
│ │ ├── FunctionNamed.jsx
│ │ ├── LazyComponent.jsx
│ │ ├── FunctionDefault.jsx
│ │ ├── ClassNamed.jsx
│ │ ├── index.js
│ │ ├── ClassDefault.jsx
│ │ └── App.jsx
│ ├── public
│ │ └── index.html
│ ├── babel.config.js
│ ├── package.json
│ ├── server.js
│ └── webpack.config.js
├── webpack-plugin-serve
│ ├── src
│ │ ├── ArrowFunction.jsx
│ │ ├── FunctionNamed.jsx
│ │ ├── LazyComponent.jsx
│ │ ├── FunctionDefault.jsx
│ │ ├── ClassNamed.jsx
│ │ ├── index.js
│ │ ├── ClassDefault.jsx
│ │ └── App.jsx
│ ├── public
│ │ └── index.html
│ ├── babel.config.js
│ ├── package.json
│ └── webpack.config.js
└── flow-with-babel
│ ├── src
│ ├── ArrowFunction.jsx
│ ├── FunctionNamed.jsx
│ ├── LazyComponent.jsx
│ ├── FunctionDefault.jsx
│ ├── ClassNamed.jsx
│ ├── index.js
│ ├── ClassDefault.jsx
│ └── App.jsx
│ ├── .flowconfig
│ ├── public
│ └── index.html
│ ├── babel.config.js
│ ├── package.json
│ └── webpack.config.js
├── .babelrc.json
├── .eslintignore
├── .gitignore
├── lib
├── globals.js
├── utils
│ ├── index.js
│ ├── getIntegrationEntry.js
│ ├── getAdditionalEntries.js
│ ├── getSocketIntegration.js
│ ├── normalizeOptions.js
│ ├── injectRefreshLoader.js
│ └── makeRefreshRuntimeModule.js
├── types.js
├── options.json
└── index.js
├── jest.config.js
├── loader
├── utils
│ ├── index.js
│ ├── normalizeOptions.js
│ ├── getIdentitySourceMap.js
│ ├── getRefreshModuleRuntime.js
│ └── getModuleSystem.js
├── types.js
├── options.json
└── index.js
├── tsconfig.json
├── webpack.config.js
├── types
├── lib
│ ├── index.d.ts
│ └── types.d.ts
├── loader
│ ├── index.d.ts
│ └── types.d.ts
└── options
│ └── index.d.ts
├── scripts
└── test.js
├── LICENSE
├── options
└── index.js
├── .eslintrc.json
├── .github
└── workflows
│ └── ci.yml
├── package.json
└── docs
└── API.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/overlay/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/sockets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/test/loader/fixtures/cjs/esm/index.js:
--------------------------------------------------------------------------------
1 | export default 'esm';
2 |
--------------------------------------------------------------------------------
/test/loader/fixtures/cjs/index.js:
--------------------------------------------------------------------------------
1 | module.exports = 'Test';
2 |
--------------------------------------------------------------------------------
/test/loader/fixtures/esm/index.js:
--------------------------------------------------------------------------------
1 | export default 'Test';
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *dist/
2 | *node_modules/
3 | *umd/
4 | *__tmp__
5 |
--------------------------------------------------------------------------------
/test/loader/fixtures/esm/cjs/index.js:
--------------------------------------------------------------------------------
1 | module.exports = 'cjs';
2 |
--------------------------------------------------------------------------------
/test/unit/fixtures/socketIntegration.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/test/loader/fixtures/auto/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auto"
3 | }
4 |
--------------------------------------------------------------------------------
/test/loader/fixtures/cjs/esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/test/loader/fixtures/esm/cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/test/loader/fixtures/esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esm",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/test/loader/fixtures/cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cjs",
3 | "type": "commonjs"
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/ArrowFunction.jsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () =>
Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/ArrowFunction.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () => Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/ArrowFunction.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () => Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/ArrowFunction.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () => Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/ArrowFunction.jsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () => Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/ArrowFunction.jsx:
--------------------------------------------------------------------------------
1 | export const ArrowFunction = () => Arrow Function
;
2 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/ArrowFunction.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export const ArrowFunction = () => Arrow Function
;
4 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/FunctionNamed.jsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": ["@babel/plugin-transform-modules-commonjs"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/FunctionNamed.tsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/FunctionNamed.tsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/FunctionNamed.tsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/FunctionNamed.jsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/FunctionNamed.jsx:
--------------------------------------------------------------------------------
1 | export function FunctionNamed() {
2 | return Named Export Function
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 |
11 | [strict]
12 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/FunctionNamed.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export function FunctionNamed() {
4 | return Named Export Function
;
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *dist/
2 | *node_modules/
3 | *umd/
4 | *__tmp__
5 |
6 | # Ignore examples because they might have custom ESLint configurations
7 | examples/*
8 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/LazyComponent.jsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/LazyComponent.tsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/LazyComponent.tsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/LazyComponent.tsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/LazyComponent.jsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/LazyComponent.jsx:
--------------------------------------------------------------------------------
1 | function LazyComponent() {
2 | return Lazy Component
;
3 | }
4 |
5 | export default LazyComponent;
6 |
--------------------------------------------------------------------------------
/test/helpers/sandbox/aliasWDSv4.js:
--------------------------------------------------------------------------------
1 | const moduleAlias = require('module-alias');
2 |
3 | moduleAlias.addAliases({ 'webpack-dev-server': 'webpack-dev-server-v4' });
4 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/LazyComponent.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | function LazyComponent() {
4 | return Lazy Component
;
5 | }
6 |
7 | export default LazyComponent;
8 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/FunctionDefault.jsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/FunctionDefault.tsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/FunctionDefault.tsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/FunctionDefault.tsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/FunctionDefault.jsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/FunctionDefault.jsx:
--------------------------------------------------------------------------------
1 | function FunctionDefault() {
2 | return Default Export Function
;
3 | }
4 |
5 | export default FunctionDefault;
6 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsxdev"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/FunctionDefault.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | function FunctionDefault() {
4 | return Default Export Function
;
5 | }
6 |
7 | export default FunctionDefault;
8 |
--------------------------------------------------------------------------------
/test/jest-global-teardown.js:
--------------------------------------------------------------------------------
1 | async function teardown() {
2 | if (global.__BROWSER_INSTANCE__) {
3 | await global.__BROWSER_INSTANCE__.close();
4 | }
5 | }
6 |
7 | module.exports = teardown;
8 |
--------------------------------------------------------------------------------
/test/helpers/compilation/fixtures/source-map-loader.js:
--------------------------------------------------------------------------------
1 | module.exports = function sourceMapLoader(source) {
2 | const callback = this.async();
3 | callback(null, source, this.query.sourceMap);
4 | };
5 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/ClassNamed.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/ClassNamed.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/ClassNamed.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/ClassNamed.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/ClassNamed.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/ClassNamed.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export class ClassNamed extends Component {
4 | render() {
5 | return Named Export Class
;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/ClassNamed.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { Component } from 'react';
4 |
5 | export class ClassNamed extends Component<{}> {
6 | render() {
7 | return Named Export Class
;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container!);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container!);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/index.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/index.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container!);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/index.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './App';
3 |
4 | const container = document.getElementById('app');
5 | const root = createRoot(container);
6 | root.render();
7 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createRoot } from 'react-dom/client';
4 | import App from './App';
5 |
6 | const container = document.getElementById('app');
7 | const root = createRoot(container);
8 | root.render();
9 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/ClassDefault.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/ClassDefault.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/ClassDefault.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/ClassDefault.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/ClassDefault.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/ClassDefault.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class ClassDefault extends Component {
4 | render() {
5 | return Default Export Class
;
6 | }
7 | }
8 |
9 | export default ClassDefault;
10 |
--------------------------------------------------------------------------------
/test/helpers/sandbox/fixtures/hmr-notifier.js:
--------------------------------------------------------------------------------
1 | if (module.hot) {
2 | module.hot.addStatusHandler(function (status) {
3 | if (status === 'idle') {
4 | if (window.onHotSuccess) {
5 | window.onHotSuccess();
6 | }
7 | }
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/ClassDefault.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { Component } from 'react';
4 |
5 | class ClassDefault extends Component<{}> {
6 | render() {
7 | return Default Export Class
;
8 | }
9 | }
10 |
11 | export default ClassDefault;
12 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Flow React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | TS React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | swc React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | TSC React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WDS React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WHM React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WPS React App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "outDir": "dist",
8 | "target": "ESNext",
9 | "sourceMap": true
10 | },
11 | "include": ["src"]
12 | }
13 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "outDir": "dist",
8 | "target": "ESNext",
9 | "sourceMap": true
10 | },
11 | "include": ["src"]
12 | }
13 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "outDir": "dist",
8 | "target": "ESNext",
9 | "sourceMap": true
10 | },
11 | "include": ["src"]
12 | }
13 |
--------------------------------------------------------------------------------
/test/unit/globals.test.js:
--------------------------------------------------------------------------------
1 | const { getRefreshGlobalScope } = require('../../lib/globals');
2 |
3 | describe('getRefreshGlobalScope', () => {
4 | it('should work for Webpack 5', () => {
5 | const { RuntimeGlobals } = require('webpack');
6 | expect(getRefreshGlobalScope(RuntimeGlobals)).toStrictEqual('__webpack_require__.$Refresh$');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | *node_modules
5 |
6 | # distribution
7 | *dist
8 | *umd
9 |
10 | # misc
11 | .DS_Store
12 |
13 | # logs
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # editor config
19 | .idea
20 | .vscode
21 |
22 | # test artifacts
23 | *__tmp__
24 |
--------------------------------------------------------------------------------
/lib/globals.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets current bundle's global scope identifier for React Refresh.
3 | * @param {Record} runtimeGlobals The Webpack runtime globals.
4 | * @returns {string} The React Refresh global scope within the Webpack bundle.
5 | */
6 | module.exports.getRefreshGlobalScope = (runtimeGlobals) => {
7 | return `${runtimeGlobals.require || '__webpack_require__'}.$Refresh$`;
8 | };
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globalSetup: '/jest-global-setup.js',
3 | globalTeardown: '/jest-global-teardown.js',
4 | prettierPath: null,
5 | resolver: '/jest-resolver.js',
6 | rootDir: 'test',
7 | setupFilesAfterEnv: ['/jest-test-setup.js'],
8 | testEnvironment: '/jest-environment.js',
9 | testMatch: ['/**/*.test.js'],
10 | };
11 |
--------------------------------------------------------------------------------
/loader/utils/index.js:
--------------------------------------------------------------------------------
1 | const getIdentitySourceMap = require('./getIdentitySourceMap');
2 | const getModuleSystem = require('./getModuleSystem');
3 | const getRefreshModuleRuntime = require('./getRefreshModuleRuntime');
4 | const normalizeOptions = require('./normalizeOptions');
5 |
6 | module.exports = {
7 | getIdentitySourceMap,
8 | getModuleSystem,
9 | getRefreshModuleRuntime,
10 | normalizeOptions,
11 | };
12 |
--------------------------------------------------------------------------------
/test/jest-environment.js:
--------------------------------------------------------------------------------
1 | const { TestEnvironment: NodeEnvironment } = require('jest-environment-node');
2 | const yn = require('yn');
3 |
4 | class TestEnvironment extends NodeEnvironment {
5 | async setup() {
6 | await super.setup();
7 |
8 | this.global.__DEBUG__ = yn(process.env.DEBUG);
9 | this.global.WDS_VERSION = parseInt(process.env.WDS_VERSION || 5);
10 | }
11 | }
12 |
13 | module.exports = TestEnvironment;
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "esModuleInterop": true,
7 | "module": "commonjs",
8 | "noUnusedLocals": true,
9 | "outDir": "types",
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "target": "es2018"
14 | },
15 | "include": ["./lib/*", "./loader/*", "./options/*"],
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------
/overlay/components/Spacer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} SpacerProps
3 | * @property {string} space
4 | */
5 |
6 | /**
7 | * An empty element to add spacing manually.
8 | * @param {Document} document
9 | * @param {HTMLElement} root
10 | * @param {SpacerProps} props
11 | * @returns {void}
12 | */
13 | function Spacer(document, root, props) {
14 | const spacer = document.createElement('div');
15 | spacer.style.paddingBottom = props.space;
16 | root.appendChild(spacer);
17 | }
18 |
19 | module.exports = Spacer;
20 |
--------------------------------------------------------------------------------
/test/jest-resolver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} request
3 | * @param {*} options
4 | * @return {string}
5 | */
6 | function resolver(request, options) {
7 | // This acts as a mock for `require.resolve('react-refresh')`,
8 | // since the current mocking behaviour of Jest is not symmetrical,
9 | // i.e. only `require` is mocked but not `require.resolve`.
10 | if (request === 'react-refresh') {
11 | return 'react-refresh';
12 | }
13 |
14 | return options.defaultResolver(request, options);
15 | }
16 |
17 | module.exports = resolver;
18 |
--------------------------------------------------------------------------------
/test/mocks/fetch.js:
--------------------------------------------------------------------------------
1 | /** @type {Set} */
2 | const cleanupHandlers = new Set();
3 | afterEach(() => {
4 | [...cleanupHandlers].map((callback) => callback());
5 | });
6 |
7 | const mockFetch = () => {
8 | const originalFetch = global.fetch;
9 |
10 | const fetchMock = new Function();
11 | global.fetch = fetchMock;
12 |
13 | function mockRestore() {
14 | global.fetch = originalFetch;
15 | }
16 |
17 | cleanupHandlers.add(mockRestore);
18 |
19 | return [fetchMock, mockRestore];
20 | };
21 |
22 | module.exports = mockFetch;
23 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | // This caches the Babel config
3 | api.cache.using(() => process.env.NODE_ENV);
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | // Enable development transform of React with new automatic runtime
8 | ['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
9 | ],
10 | // Applies the react-refresh Babel plugin on non-production modes only
11 | ...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | // This caches the Babel config
3 | api.cache.using(() => process.env.NODE_ENV);
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | // Enable development transform of React with new automatic runtime
8 | ['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
9 | ],
10 | // Applies the react-refresh Babel plugin on non-production modes only
11 | ...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | // This caches the Babel config
3 | api.cache.using(() => process.env.NODE_ENV);
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | // Enable development transform of React with new automatic runtime
8 | ['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
9 | ],
10 | // Applies the react-refresh Babel plugin on non-production modes only
11 | ...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | // This caches the Babel config
3 | api.cache.using(() => process.env.NODE_ENV);
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | '@babel/preset-flow',
8 | // Enable development transform of React with new automatic runtime
9 | ['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
10 | ],
11 | // Applies the react-refresh Babel plugin on non-production modes only
12 | ...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/client/utils/retry.js:
--------------------------------------------------------------------------------
1 | function runWithRetry(callback, maxRetries, message) {
2 | function executeWithRetryAndTimeout(currentCount) {
3 | try {
4 | if (currentCount > maxRetries - 1) {
5 | console.warn('[React Refresh]', message);
6 | return;
7 | }
8 |
9 | callback();
10 | } catch (err) {
11 | setTimeout(
12 | function () {
13 | executeWithRetryAndTimeout(currentCount + 1);
14 | },
15 | Math.pow(10, currentCount)
16 | );
17 | }
18 | }
19 |
20 | executeWithRetryAndTimeout(0);
21 | }
22 |
23 | module.exports = runWithRetry;
24 |
--------------------------------------------------------------------------------
/lib/utils/index.js:
--------------------------------------------------------------------------------
1 | const getAdditionalEntries = require('./getAdditionalEntries');
2 | const getIntegrationEntry = require('./getIntegrationEntry');
3 | const getSocketIntegration = require('./getSocketIntegration');
4 | const injectRefreshLoader = require('./injectRefreshLoader');
5 | const makeRefreshRuntimeModule = require('./makeRefreshRuntimeModule');
6 | const normalizeOptions = require('./normalizeOptions');
7 |
8 | module.exports = {
9 | getAdditionalEntries,
10 | getIntegrationEntry,
11 | getSocketIntegration,
12 | injectRefreshLoader,
13 | makeRefreshRuntimeModule,
14 | normalizeOptions,
15 | };
16 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | // This caches the Babel config
3 | api.cache.using(() => process.env.NODE_ENV);
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | '@babel/preset-typescript',
8 | // Enable development transform of React with new automatic runtime
9 | ['@babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
10 | ],
11 | // Applies the react-refresh Babel plugin on non-production modes only
12 | ...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: {
7 | client: './client/ReactRefreshEntry.js',
8 | },
9 | optimization: {
10 | minimize: true,
11 | minimizer: [
12 | new TerserPlugin({
13 | extractComments: false,
14 | terserOptions: {
15 | format: { comments: false },
16 | },
17 | }),
18 | ],
19 | nodeEnv: 'development',
20 | },
21 | output: {
22 | filename: '[name].min.js',
23 | path: path.resolve(__dirname, 'umd'),
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/test/jest-global-setup.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const yn = require('yn');
3 |
4 | async function setup() {
5 | if (yn(process.env.BROWSER, { default: true })) {
6 | const browser = await puppeteer.launch({
7 | devtools: yn(process.env.DEBUG, { default: false }),
8 | headless: yn(process.env.HEADLESS, {
9 | // Force headless mode in CI environments
10 | default: yn(process.env.CI, { default: false }),
11 | }),
12 | });
13 |
14 | global.__BROWSER_INSTANCE__ = browser;
15 |
16 | process.env.PUPPETEER_WS_ENDPOINT = browser.wsEndpoint();
17 | }
18 | }
19 |
20 | module.exports = setup;
21 |
--------------------------------------------------------------------------------
/lib/utils/getIntegrationEntry.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets entry point of a supported socket integration.
3 | * @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
4 | * @returns {string | undefined} Path to the resolved integration entry point.
5 | */
6 | function getIntegrationEntry(integrationType) {
7 | let resolvedEntry;
8 | switch (integrationType) {
9 | case 'whm': {
10 | resolvedEntry = 'webpack-hot-middleware/client';
11 | break;
12 | }
13 | case 'wps': {
14 | resolvedEntry = 'webpack-plugin-serve/client';
15 | break;
16 | }
17 | }
18 |
19 | return resolvedEntry;
20 | }
21 |
22 | module.exports = getIntegrationEntry;
23 |
--------------------------------------------------------------------------------
/loader/types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ESModuleOptions
3 | * @property {string | RegExp | Array} [exclude] Files to explicitly exclude from flagged as ES Modules.
4 | * @property {string | RegExp | Array} [include] Files to explicitly include for flagged as ES Modules.
5 | */
6 |
7 | /**
8 | * @typedef {Object} ReactRefreshLoaderOptions
9 | * @property {boolean} [const] Enables usage of ES6 `const` and `let` in generated runtime code.
10 | * @property {boolean | ESModuleOptions} [esModule] Enables strict ES Modules compatible runtime.
11 | */
12 |
13 | /**
14 | * @typedef {import('type-fest').SetRequired} NormalizedLoaderOptions
15 | */
16 |
17 | module.exports = {};
18 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/test/conformance/environment.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const TestEnvironment = require('../jest-environment');
3 |
4 | class SandboxEnvironment extends TestEnvironment {
5 | async setup() {
6 | await super.setup();
7 |
8 | const wsEndpoint = process.env.PUPPETEER_WS_ENDPOINT;
9 | if (!wsEndpoint) {
10 | throw new Error('Puppeteer wsEndpoint not found!');
11 | }
12 |
13 | this.global.browser = await puppeteer.connect({
14 | browserWSEndpoint: wsEndpoint,
15 | });
16 | }
17 |
18 | async teardown() {
19 | await super.teardown();
20 |
21 | if (this.global.browser) {
22 | await this.global.browser.disconnect();
23 | }
24 | }
25 | }
26 |
27 | module.exports = SandboxEnvironment;
28 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import { ArrowFunction } from './ArrowFunction';
3 | import ClassDefault from './ClassDefault';
4 | import { ClassNamed } from './ClassNamed';
5 | import FunctionDefault from './FunctionDefault';
6 | import { FunctionNamed } from './FunctionNamed';
7 |
8 | const LazyComponent = lazy(() => import('./LazyComponent'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Loading}>
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/loader/utils/normalizeOptions.js:
--------------------------------------------------------------------------------
1 | const { d, n } = require('../../options');
2 |
3 | /**
4 | * Normalizes the options for the loader.
5 | * @param {import('../types').ReactRefreshLoaderOptions} options Non-normalized loader options.
6 | * @returns {import('../types').NormalizedLoaderOptions} Normalized loader options.
7 | */
8 | const normalizeOptions = (options) => {
9 | d(options, 'const', false);
10 |
11 | n(options, 'esModule', (esModule) => {
12 | if (typeof esModule === 'boolean' || typeof esModule === 'undefined') {
13 | return esModule;
14 | }
15 |
16 | d(esModule, 'include');
17 | d(esModule, 'exclude');
18 |
19 | return esModule;
20 | });
21 |
22 | return options;
23 | };
24 |
25 | module.exports = normalizeOptions;
26 |
--------------------------------------------------------------------------------
/types/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | export = ReactRefreshPlugin;
2 | declare class ReactRefreshPlugin {
3 | /**
4 | * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
5 | */
6 | constructor(options?: import('./types').ReactRefreshPluginOptions | undefined);
7 | /**
8 | * @readonly
9 | * @type {import('./types').NormalizedPluginOptions}
10 | */
11 | readonly options: import('./types').NormalizedPluginOptions;
12 | /**
13 | * Applies the plugin.
14 | * @param {import('webpack').Compiler} compiler A webpack compiler object.
15 | * @returns {void}
16 | */
17 | apply(compiler: import('webpack').Compiler): void;
18 | }
19 | declare namespace ReactRefreshPlugin {
20 | export { ReactRefreshPlugin };
21 | }
22 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/src/App.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { lazy, Suspense } from 'react';
4 | import { ArrowFunction } from './ArrowFunction';
5 | import ClassDefault from './ClassDefault';
6 | import { ClassNamed } from './ClassNamed';
7 | import FunctionDefault from './FunctionDefault';
8 | import { FunctionNamed } from './FunctionNamed';
9 |
10 | const LazyComponent = lazy(() => import('./LazyComponent'));
11 |
12 | function App() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Loading}>
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/test/loader/unit/getIdentitySourceMap.test.js:
--------------------------------------------------------------------------------
1 | const { SourceMapConsumer } = require('source-map');
2 | const validate = require('sourcemap-validator');
3 | const getIdentitySourceMap = require('../../../loader/utils/getIdentitySourceMap');
4 |
5 | describe('getIdentitySourceMap', () => {
6 | it('should generate valid source map with source equality', async () => {
7 | const source = "module.exports = 'Test'";
8 | const path = 'index.js';
9 |
10 | const identityMap = getIdentitySourceMap(source, path);
11 | expect(() => {
12 | validate(source, JSON.stringify(identityMap));
13 | }).not.toThrow();
14 |
15 | const sourceMapConsumer = await new SourceMapConsumer(identityMap);
16 | expect(sourceMapConsumer.sourceContentFor(path)).toBe(source);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/test/unit/getIntegrationEntry.test.js:
--------------------------------------------------------------------------------
1 | const getIntegrationEntry = require('../../lib/utils/getIntegrationEntry');
2 |
3 | describe('getIntegrationEntry', () => {
4 | it('should work with webpack-hot-middleware', () => {
5 | expect(getIntegrationEntry('whm')).toStrictEqual('webpack-hot-middleware/client');
6 | });
7 |
8 | it('should work with webpack-plugin-serve', () => {
9 | expect(getIntegrationEntry('wps')).toStrictEqual('webpack-plugin-serve/client');
10 | });
11 |
12 | it('should return undefined for webpack-dev-server', () => {
13 | expect(getIntegrationEntry('wds')).toStrictEqual(undefined);
14 | });
15 |
16 | it('should return undefined for unknown integrations', () => {
17 | expect(getIntegrationEntry('unknown')).toStrictEqual(undefined);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/types/loader/index.d.ts:
--------------------------------------------------------------------------------
1 | export = ReactRefreshLoader;
2 | /**
3 | * A simple Webpack loader to inject react-refresh HMR code into modules.
4 | *
5 | * [Reference for Loader API](https://webpack.js.org/api/loaders/)
6 | * @this {import('webpack').LoaderContext}
7 | * @param {string} source The original module source code.
8 | * @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
9 | * @param {*} [meta] The loader metadata passed in.
10 | * @returns {void}
11 | */
12 | declare function ReactRefreshLoader(
13 | this: import('webpack').LoaderContext,
14 | source: string,
15 | inputSourceMap?: import('source-map').RawSourceMap | undefined,
16 | meta?: any
17 | ): void;
18 |
--------------------------------------------------------------------------------
/types/loader/types.d.ts:
--------------------------------------------------------------------------------
1 | export type ESModuleOptions = {
2 | /**
3 | * Files to explicitly exclude from flagged as ES Modules.
4 | */
5 | exclude?: string | RegExp | (string | RegExp)[] | undefined;
6 | /**
7 | * Files to explicitly include for flagged as ES Modules.
8 | */
9 | include?: string | RegExp | (string | RegExp)[] | undefined;
10 | };
11 | export type ReactRefreshLoaderOptions = {
12 | /**
13 | * Enables usage of ES6 `const` and `let` in generated runtime code.
14 | */
15 | const?: boolean | undefined;
16 | /**
17 | * Enables strict ES Modules compatible runtime.
18 | */
19 | esModule?: boolean | ESModuleOptions | undefined;
20 | };
21 | export type NormalizedLoaderOptions = import('type-fest').SetRequired<
22 | ReactRefreshLoaderOptions,
23 | 'const'
24 | >;
25 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-webpack-dev-server",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.26.10",
11 | "@babel/preset-env": "^7.26.9",
12 | "@babel/preset-react": "^7.26.3",
13 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
14 | "babel-loader": "^10.0.0",
15 | "cross-env": "^7.0.3",
16 | "html-webpack-plugin": "^5.6.3",
17 | "react-refresh": "^0.17.0",
18 | "webpack": "^5.98.0",
19 | "webpack-cli": "^6.0.1",
20 | "webpack-dev-server": "^5.2.1"
21 | },
22 | "scripts": {
23 | "start": "webpack serve --hot",
24 | "build": "cross-env NODE_ENV=production webpack"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-webpack-plugin-serve",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.26.10",
11 | "@babel/preset-env": "^7.26.9",
12 | "@babel/preset-react": "^7.26.3",
13 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
14 | "babel-loader": "^10.0.0",
15 | "cross-env": "^7.0.3",
16 | "html-webpack-plugin": "^5.6.3",
17 | "react-refresh": "^0.17.0",
18 | "webpack": "^5.98.0",
19 | "webpack-cli": "^6.0.1",
20 | "webpack-plugin-serve": "^1.6.0"
21 | },
22 | "scripts": {
23 | "start": "webpack --watch",
24 | "build": "cross-env NODE_ENV=production webpack"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-webpack-hot-middleware",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.26.10",
11 | "@babel/preset-env": "^7.26.9",
12 | "@babel/preset-react": "^7.26.3",
13 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
14 | "babel-loader": "^10.0.0",
15 | "cross-env": "^7.0.3",
16 | "express": "^5.2.0",
17 | "html-webpack-plugin": "^5.6.3",
18 | "react-refresh": "^0.17.0",
19 | "webpack": "^5.98.0",
20 | "webpack-dev-middleware": "^7.4.2",
21 | "webpack-hot-middleware": "^2.26.1"
22 | },
23 | "scripts": {
24 | "start": "node ./server.js",
25 | "build": "cross-env NODE_ENV=production webpack"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/unit/getAdditionalEntries.test.js:
--------------------------------------------------------------------------------
1 | const getAdditionalEntries = require('../../lib/utils/getAdditionalEntries');
2 |
3 | const ErrorOverlayEntry = require.resolve('../../client/ErrorOverlayEntry');
4 | const ReactRefreshEntry = require.resolve('../../client/ReactRefreshEntry');
5 |
6 | describe('getAdditionalEntries', () => {
7 | it('should work with default settings', () => {
8 | expect(getAdditionalEntries({ overlay: { entry: ErrorOverlayEntry } })).toStrictEqual({
9 | overlayEntries: [ErrorOverlayEntry],
10 | prependEntries: [ReactRefreshEntry],
11 | });
12 | });
13 |
14 | it('should skip overlay entries when overlay is false in options', () => {
15 | expect(getAdditionalEntries({ overlay: false })).toStrictEqual({
16 | overlayEntries: [],
17 | prependEntries: [ReactRefreshEntry],
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-flow-with-babel",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.26.10",
11 | "@babel/preset-env": "^7.26.9",
12 | "@babel/preset-flow": "^7.25.9",
13 | "@babel/preset-react": "^7.26.3",
14 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
15 | "babel-loader": "^10.0.0",
16 | "cross-env": "^7.0.3",
17 | "flow-bin": "^0.266.1",
18 | "html-webpack-plugin": "^5.6.3",
19 | "react-refresh": "^0.17.0",
20 | "webpack": "^5.98.0",
21 | "webpack-cli": "^6.0.1",
22 | "webpack-dev-server": "^5.2.1"
23 | },
24 | "scripts": {
25 | "start": "webpack serve --hot",
26 | "build": "cross-env NODE_ENV=production webpack"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-typescript-with-tsc",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
11 | "@types/react": "^19.0.0",
12 | "@types/react-dom": "^19.0.0",
13 | "cross-env": "^7.0.3",
14 | "fork-ts-checker-webpack-plugin": "^9.1.0",
15 | "html-webpack-plugin": "^5.6.3",
16 | "react-refresh": "^0.17.0",
17 | "react-refresh-typescript": "^2.0.10",
18 | "ts-loader": "^9.5.2",
19 | "typescript": "~5.8.3",
20 | "webpack": "^5.98.0",
21 | "webpack-cli": "^6.0.1",
22 | "webpack-dev-server": "^5.2.1"
23 | },
24 | "scripts": {
25 | "start": "webpack serve --hot",
26 | "build": "cross-env NODE_ENV=production webpack"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-typescript-with-swc",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
11 | "@swc/core": "^1.11.16",
12 | "@types/react": "^19.0.0",
13 | "@types/react-dom": "^19.0.0",
14 | "core-js": "^3.41.0",
15 | "cross-env": "^7.0.3",
16 | "fork-ts-checker-webpack-plugin": "^9.1.0",
17 | "html-webpack-plugin": "^5.6.3",
18 | "react-refresh": "^0.17.0",
19 | "swc-loader": "^0.2.6",
20 | "typescript": "~5.8.3",
21 | "webpack": "^5.98.0",
22 | "webpack-cli": "^6.0.1",
23 | "webpack-dev-server": "^5.2.1"
24 | },
25 | "scripts": {
26 | "start": "webpack serve --hot",
27 | "build": "cross-env NODE_ENV=production webpack"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/overlay/containers/CompileErrorContainer.js:
--------------------------------------------------------------------------------
1 | const CompileErrorTrace = require('../components/CompileErrorTrace.js');
2 | const PageHeader = require('../components/PageHeader.js');
3 | const Spacer = require('../components/Spacer.js');
4 |
5 | /**
6 | * @typedef {Object} CompileErrorContainerProps
7 | * @property {string} errorMessage
8 | */
9 |
10 | /**
11 | * A container to render Webpack compilation error messages with source trace.
12 | * @param {Document} document
13 | * @param {HTMLElement} root
14 | * @param {CompileErrorContainerProps} props
15 | * @returns {void}
16 | */
17 | function CompileErrorContainer(document, root, props) {
18 | PageHeader(document, root, {
19 | title: 'Failed to compile.',
20 | });
21 | CompileErrorTrace(document, root, { errorMessage: props.errorMessage });
22 | Spacer(document, root, { space: '1rem' });
23 | }
24 |
25 | module.exports = CompileErrorContainer;
26 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | // Setup environment before any code -
2 | // this makes sure everything coming after will run in the correct env.
3 | process.env.NODE_ENV = 'test';
4 |
5 | // Crash on unhandled rejections instead of failing silently.
6 | process.on('unhandledRejection', (reason) => {
7 | throw reason;
8 | });
9 |
10 | const jest = require('jest');
11 | const yn = require('yn');
12 |
13 | let argv = process.argv.slice(2);
14 |
15 | if (yn(process.env.CI)) {
16 | // Use CI mode
17 | argv.push('--ci');
18 | // Parallelized puppeteer tests have high memory overhead in CI environments.
19 | // Fall back to run in series so tests could run faster.
20 | argv.push('--runInBand');
21 | // Add JUnit reporter
22 | argv.push('--reporters="default"');
23 | argv.push('--reporters="jest-junit"');
24 | }
25 |
26 | if (yn(process.env.DEBUG)) {
27 | argv.push('--verbose');
28 | }
29 |
30 | void jest.run(argv);
31 |
--------------------------------------------------------------------------------
/test/helpers/compilation/normalizeErrors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} str
3 | * @return {string}
4 | */
5 | function removeCwd(str) {
6 | let cwd = process.cwd();
7 | let result = str;
8 |
9 | const isWin = process.platform === 'win32';
10 | if (isWin) {
11 | cwd = cwd.replace(/\\/g, '/');
12 | result = result.replace(/\\/g, '/');
13 | }
14 |
15 | return result.replace(new RegExp(cwd, 'g'), '');
16 | }
17 |
18 | /**
19 | * @param {Error[]} errors
20 | * @return {string[]}
21 | */
22 | function normalizeErrors(errors) {
23 | return errors.map((error) => {
24 | // Output nested error messages in full -
25 | // this is useful for checking loader validation errors, for example.
26 | if ('error' in error) {
27 | return removeCwd(error.error.message);
28 | }
29 | return removeCwd(error.message.split('\n').slice(0, 2).join('\n'));
30 | });
31 | }
32 |
33 | module.exports = normalizeErrors;
34 |
--------------------------------------------------------------------------------
/loader/utils/getIdentitySourceMap.js:
--------------------------------------------------------------------------------
1 | const { SourceMapGenerator } = require('source-map');
2 |
3 | /**
4 | * Generates an identity source map from a source file.
5 | * @param {string} source The content of the source file.
6 | * @param {string} resourcePath The name of the source file.
7 | * @returns {import('source-map').RawSourceMap} The identity source map.
8 | */
9 | function getIdentitySourceMap(source, resourcePath) {
10 | const sourceMap = new SourceMapGenerator();
11 | sourceMap.setSourceContent(resourcePath, source);
12 |
13 | source.split('\n').forEach((_, index) => {
14 | sourceMap.addMapping({
15 | source: resourcePath,
16 | original: {
17 | line: index + 1,
18 | column: 0,
19 | },
20 | generated: {
21 | line: index + 1,
22 | column: 0,
23 | },
24 | });
25 | });
26 |
27 | return sourceMap.toJSON();
28 | }
29 |
30 | module.exports = getIdentitySourceMap;
31 |
--------------------------------------------------------------------------------
/client/ReactRefreshEntry.js:
--------------------------------------------------------------------------------
1 | /* global __react_refresh_library__ */
2 |
3 | if (process.env.NODE_ENV !== 'production') {
4 | const safeThis = require('core-js-pure/features/global-this');
5 | const RefreshRuntime = require('react-refresh/runtime');
6 |
7 | if (typeof safeThis !== 'undefined') {
8 | var $RefreshInjected$ = '__reactRefreshInjected';
9 | // Namespace the injected flag (if necessary) for monorepo compatibility
10 | if (typeof __react_refresh_library__ !== 'undefined' && __react_refresh_library__) {
11 | $RefreshInjected$ += '_' + __react_refresh_library__;
12 | }
13 |
14 | // Only inject the runtime if it hasn't been injected
15 | if (!safeThis[$RefreshInjected$]) {
16 | // Inject refresh runtime into global scope
17 | RefreshRuntime.injectIntoGlobalHook(safeThis);
18 |
19 | // Mark the runtime as injected to prevent double-injection
20 | safeThis[$RefreshInjected$] = true;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/flow-with-babel/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | const isDevelopment = process.env.NODE_ENV !== 'production';
6 |
7 | module.exports = {
8 | mode: isDevelopment ? 'development' : 'production',
9 | devServer: {
10 | client: { overlay: false },
11 | },
12 | entry: {
13 | main: './src/index.js',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.jsx?$/,
19 | include: path.join(__dirname, 'src'),
20 | use: 'babel-loader',
21 | },
22 | ],
23 | },
24 | plugins: [
25 | isDevelopment && new ReactRefreshPlugin(),
26 | new HtmlWebpackPlugin({
27 | filename: './index.html',
28 | template: './public/index.html',
29 | }),
30 | ].filter(Boolean),
31 | resolve: {
32 | extensions: ['.js', '.jsx'],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/examples/webpack-dev-server/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | const isDevelopment = process.env.NODE_ENV !== 'production';
6 |
7 | module.exports = {
8 | mode: isDevelopment ? 'development' : 'production',
9 | devServer: {
10 | client: { overlay: false },
11 | },
12 | entry: {
13 | main: './src/index.js',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.jsx?$/,
19 | include: path.join(__dirname, 'src'),
20 | use: 'babel-loader',
21 | },
22 | ],
23 | },
24 | plugins: [
25 | isDevelopment && new ReactRefreshPlugin(),
26 | new HtmlWebpackPlugin({
27 | filename: './index.html',
28 | template: './public/index.html',
29 | }),
30 | ].filter(Boolean),
31 | resolve: {
32 | extensions: ['.js', '.jsx'],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/lib/utils/getAdditionalEntries.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} AdditionalEntries
3 | * @property {string[]} prependEntries
4 | * @property {string[]} overlayEntries
5 | */
6 |
7 | /**
8 | * Creates an object that contains two entry arrays: the prependEntries and overlayEntries
9 | * @param {import('../types').NormalizedPluginOptions} options Configuration options for this plugin.
10 | * @returns {AdditionalEntries} An object that contains the Webpack entries for prepending and the overlay feature
11 | */
12 | function getAdditionalEntries(options) {
13 | const prependEntries = [
14 | // React-refresh runtime
15 | require.resolve('../../client/ReactRefreshEntry'),
16 | ];
17 |
18 | const overlayEntries = [
19 | // Error overlay runtime
20 | options.overlay && options.overlay.entry && require.resolve(options.overlay.entry),
21 | ].filter(Boolean);
22 |
23 | return { prependEntries, overlayEntries };
24 | }
25 |
26 | module.exports = getAdditionalEntries;
27 |
--------------------------------------------------------------------------------
/overlay/containers/RuntimeErrorContainer.js:
--------------------------------------------------------------------------------
1 | const PageHeader = require('../components/PageHeader.js');
2 | const RuntimeErrorStack = require('../components/RuntimeErrorStack.js');
3 | const Spacer = require('../components/Spacer.js');
4 |
5 | /**
6 | * @typedef {Object} RuntimeErrorContainerProps
7 | * @property {Error} currentError
8 | */
9 |
10 | /**
11 | * A container to render runtime error messages with stack trace.
12 | * @param {Document} document
13 | * @param {HTMLElement} root
14 | * @param {RuntimeErrorContainerProps} props
15 | * @returns {void}
16 | */
17 | function RuntimeErrorContainer(document, root, props) {
18 | PageHeader(document, root, {
19 | message: props.currentError.message,
20 | title: props.currentError.name,
21 | topOffset: '2.5rem',
22 | });
23 | RuntimeErrorStack(document, root, {
24 | error: props.currentError,
25 | });
26 | Spacer(document, root, { space: '1rem' });
27 | }
28 |
29 | module.exports = RuntimeErrorContainer;
30 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const webpack = require('webpack');
4 | const config = require('./webpack.config.js');
5 |
6 | const app = express();
7 | const compiler = webpack(config);
8 |
9 | app.use(
10 | require('webpack-dev-middleware')(compiler, {
11 | publicPath: config.output.publicPath,
12 | })
13 | );
14 |
15 | app.use(
16 | require(`webpack-hot-middleware`)(compiler, {
17 | log: false,
18 | path: `/__webpack_hmr`,
19 | heartbeat: 10 * 1000,
20 | })
21 | );
22 |
23 | app.get('*', (req, res, next) => {
24 | const filename = path.join(compiler.outputPath, 'index.html');
25 | compiler.outputFileSystem.readFile(filename, (err, result) => {
26 | if (err) {
27 | return next(err);
28 | }
29 | res.set('content-type', 'text/html');
30 | res.send(result);
31 | res.end();
32 | });
33 | });
34 |
35 | app.listen(8080, () => console.log('App is listening on port 8080!'));
36 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "using-typescript-with-babel",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^19.0.0",
7 | "react-dom": "^19.0.0"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.26.10",
11 | "@babel/preset-env": "^7.26.9",
12 | "@babel/preset-react": "^7.26.3",
13 | "@babel/preset-typescript": "^7.27.0",
14 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
15 | "@types/react": "^19.0.0",
16 | "@types/react-dom": "^19.0.0",
17 | "babel-loader": "^10.0.0",
18 | "cross-env": "^7.0.3",
19 | "fork-ts-checker-webpack-plugin": "^9.1.0",
20 | "html-webpack-plugin": "^5.6.3",
21 | "react-refresh": "^0.17.0",
22 | "typescript": "~5.8.3",
23 | "webpack": "^5.98.0",
24 | "webpack-cli": "^6.0.1",
25 | "webpack-dev-server": "^5.2.1"
26 | },
27 | "scripts": {
28 | "start": "webpack serve --hot",
29 | "build": "cross-env NODE_ENV=production webpack"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sockets/WHMEventSource.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The hard-coded singleton key for webpack-hot-middleware's client instance.
3 | *
4 | * [Ref](https://github.com/webpack-contrib/webpack-hot-middleware/blob/cb29abb9dde435a1ac8e9b19f82d7d36b1093198/client.js#L152)
5 | */
6 | const singletonKey = '__webpack_hot_middleware_reporter__';
7 |
8 | /**
9 | * Initializes a socket server for HMR for webpack-hot-middleware.
10 | * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
11 | * @returns {void}
12 | */
13 | function initWHMEventSource(messageHandler) {
14 | const client = window[singletonKey];
15 |
16 | client.useCustomOverlay({
17 | showProblems: function showProblems(type, data) {
18 | const error = {
19 | data: data,
20 | type: type,
21 | };
22 |
23 | messageHandler(error);
24 | },
25 | clear: function clear() {
26 | messageHandler({ type: 'ok' });
27 | },
28 | });
29 | }
30 |
31 | module.exports = { init: initWHMEventSource };
32 |
--------------------------------------------------------------------------------
/lib/utils/getSocketIntegration.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the socket integration to use for Webpack messages.
3 | * @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
4 | * @returns {string} Path to the resolved socket integration module.
5 | */
6 | function getSocketIntegration(integrationType) {
7 | let resolvedSocketIntegration;
8 | switch (integrationType) {
9 | case 'wds': {
10 | resolvedSocketIntegration = require.resolve('../../sockets/WDSSocket');
11 | break;
12 | }
13 | case 'whm': {
14 | resolvedSocketIntegration = require.resolve('../../sockets/WHMEventSource');
15 | break;
16 | }
17 | case 'wps': {
18 | resolvedSocketIntegration = require.resolve('../../sockets/WPSSocket');
19 | break;
20 | }
21 | default: {
22 | resolvedSocketIntegration = require.resolve(integrationType);
23 | break;
24 | }
25 | }
26 |
27 | return resolvedSocketIntegration;
28 | }
29 |
30 | module.exports = getSocketIntegration;
31 |
--------------------------------------------------------------------------------
/sockets/WDSSocket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Initializes a socket server for HMR for webpack-dev-server.
3 | * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
4 | * @returns {void}
5 | */
6 | function initWDSSocket(messageHandler) {
7 | const { default: SockJSClient } = require('webpack-dev-server/client/clients/SockJSClient');
8 | const { default: WebSocketClient } = require('webpack-dev-server/client/clients/WebSocketClient');
9 | const { client } = require('webpack-dev-server/client/socket');
10 |
11 | /** @type {WebSocket} */
12 | let connection;
13 | if (client instanceof SockJSClient) {
14 | connection = client.sock;
15 | } else if (client instanceof WebSocketClient) {
16 | connection = client.client;
17 | } else {
18 | throw new Error('Failed to determine WDS client type');
19 | }
20 |
21 | connection.addEventListener('message', function onSocketMessage(message) {
22 | messageHandler(JSON.parse(message.data));
23 | });
24 | }
25 |
26 | module.exports = { init: initWDSSocket };
27 |
--------------------------------------------------------------------------------
/test/jest-test-setup.js:
--------------------------------------------------------------------------------
1 | require('jest-location-mock');
2 |
3 | /**
4 | * Skips a test block conditionally.
5 | * @param {boolean} condition The condition to skip the test block.
6 | * @param {string} blockName The name of the test block.
7 | * @param {import('@jest/types').Global.BlockFn} blockFn The test block function.
8 | * @returns {void}
9 | */
10 | describe.skipIf = (condition, blockName, blockFn) => {
11 | if (condition) {
12 | return describe.skip(blockName, blockFn);
13 | }
14 | return describe(blockName, blockFn);
15 | };
16 |
17 | /**
18 | * Skips a test conditionally.
19 | * @param {boolean} condition The condition to skip the test.
20 | * @param {string} testName The name of the test.
21 | * @param {import('@jest/types').Global.TestFn} fn The test function.
22 | * @param {number} [timeout] The time to wait before aborting.
23 | * @returns {void}
24 | */
25 | test.skipIf = (condition, testName, fn, timeout) => {
26 | if (condition) {
27 | return test.skip(testName, fn);
28 | }
29 | return test(testName, fn, timeout);
30 | };
31 |
32 | it.skipIf = test.skipIf;
33 |
--------------------------------------------------------------------------------
/examples/typescript-with-babel/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | mode: isDevelopment ? 'development' : 'production',
10 | devServer: {
11 | client: { overlay: false },
12 | },
13 | entry: {
14 | main: './src/index.tsx',
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.tsx?$/,
20 | include: path.join(__dirname, 'src'),
21 | use: 'babel-loader',
22 | },
23 | ],
24 | },
25 | plugins: [
26 | isDevelopment && new ReactRefreshPlugin(),
27 | new ForkTsCheckerWebpackPlugin(),
28 | new HtmlWebpackPlugin({
29 | filename: './index.html',
30 | template: './public/index.html',
31 | }),
32 | ].filter(Boolean),
33 | resolve: {
34 | extensions: ['.js', '.ts', '.tsx'],
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/types/options/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets a constant default value for the property when it is undefined.
3 | * @template T
4 | * @template {keyof T} Property
5 | * @param {T} object An object.
6 | * @param {Property} property A property of the provided object.
7 | * @param {T[Property]} [defaultValue] The default value to set for the property.
8 | * @returns {T[Property]} The defaulted property value.
9 | */
10 | export function d(
11 | object: T,
12 | property: Property,
13 | defaultValue?: T[Property] | undefined
14 | ): T[Property];
15 | /**
16 | * Resolves the value for a nested object option.
17 | * @template T
18 | * @template {keyof T} Property
19 | * @template Result
20 | * @param {T} object An object.
21 | * @param {Property} property A property of the provided object.
22 | * @param {function(T | undefined): Result} fn The handler to resolve the property's value.
23 | * @returns {Result} The resolved option value.
24 | */
25 | export function n(
26 | object: T,
27 | property: Property,
28 | fn: (arg0: T | undefined) => Result
29 | ): Result;
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Michael Mok
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/loader/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "additionalProperties": false,
3 | "type": "object",
4 | "definitions": {
5 | "MatchCondition": {
6 | "anyOf": [{ "instanceof": "RegExp", "tsType": "RegExp" }, { "$ref": "#/definitions/Path" }]
7 | },
8 | "MatchConditions": {
9 | "type": "array",
10 | "items": { "$ref": "#/definitions/MatchCondition" },
11 | "minItems": 1
12 | },
13 | "Path": { "type": "string" },
14 | "ESModuleOptions": {
15 | "additionalProperties": false,
16 | "type": "object",
17 | "properties": {
18 | "exclude": {
19 | "anyOf": [
20 | { "$ref": "#/definitions/MatchCondition" },
21 | { "$ref": "#/definitions/MatchConditions" }
22 | ]
23 | },
24 | "include": {
25 | "anyOf": [
26 | { "$ref": "#/definitions/MatchCondition" },
27 | { "$ref": "#/definitions/MatchConditions" }
28 | ]
29 | }
30 | }
31 | }
32 | },
33 | "properties": {
34 | "const": { "type": "boolean" },
35 | "esModule": { "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/ESModuleOptions" }] }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/options/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets a constant default value for the property when it is undefined.
3 | * @template T
4 | * @template {keyof T} Property
5 | * @param {T} object An object.
6 | * @param {Property} property A property of the provided object.
7 | * @param {T[Property]} [defaultValue] The default value to set for the property.
8 | * @returns {T[Property]} The defaulted property value.
9 | */
10 | const d = (object, property, defaultValue) => {
11 | if (typeof object[property] === 'undefined' && typeof defaultValue !== 'undefined') {
12 | object[property] = defaultValue;
13 | }
14 | return object[property];
15 | };
16 |
17 | /**
18 | * Resolves the value for a nested object option.
19 | * @template T
20 | * @template {keyof T} Property
21 | * @template Result
22 | * @param {T} object An object.
23 | * @param {Property} property A property of the provided object.
24 | * @param {function(T | undefined): Result} fn The handler to resolve the property's value.
25 | * @returns {Result} The resolved option value.
26 | */
27 | const n = (object, property, fn) => {
28 | object[property] = fn(object[property]);
29 | return object[property];
30 | };
31 |
32 | module.exports = { d, n };
33 |
--------------------------------------------------------------------------------
/overlay/components/RuntimeErrorHeader.js:
--------------------------------------------------------------------------------
1 | const Spacer = require('./Spacer.js');
2 | const theme = require('../theme.js');
3 |
4 | /**
5 | * @typedef {Object} RuntimeErrorHeaderProps
6 | * @property {number} currentErrorIndex
7 | * @property {number} totalErrors
8 | */
9 |
10 | /**
11 | * A fixed header that shows the total runtime error count.
12 | * @param {Document} document
13 | * @param {HTMLElement} root
14 | * @param {RuntimeErrorHeaderProps} props
15 | * @returns {void}
16 | */
17 | function RuntimeErrorHeader(document, root, props) {
18 | const header = document.createElement('div');
19 | header.innerText = `Error ${props.currentErrorIndex + 1} of ${props.totalErrors}`;
20 | header.style.backgroundColor = theme.red;
21 | header.style.color = theme.white;
22 | header.style.fontWeight = '500';
23 | header.style.height = '2.5rem';
24 | header.style.left = '0';
25 | header.style.lineHeight = '2.5rem';
26 | header.style.position = 'fixed';
27 | header.style.textAlign = 'center';
28 | header.style.top = '0';
29 | header.style.width = '100vw';
30 | header.style.zIndex = '2';
31 |
32 | root.appendChild(header);
33 |
34 | Spacer(document, root, { space: '2.5rem' });
35 | }
36 |
37 | module.exports = RuntimeErrorHeader;
38 |
--------------------------------------------------------------------------------
/examples/webpack-hot-middleware/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const webpack = require('webpack');
5 |
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | mode: isDevelopment ? 'development' : 'production',
10 | entry: {
11 | main: ['webpack-hot-middleware/client', './src/index.js'],
12 | },
13 | output: {
14 | filename: 'bundle.js',
15 | path: path.resolve(__dirname, 'dist'),
16 | publicPath: '/',
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: path.join(__dirname, 'src'),
23 | use: 'babel-loader',
24 | },
25 | ],
26 | },
27 | plugins: [
28 | isDevelopment && new webpack.HotModuleReplacementPlugin(),
29 | isDevelopment &&
30 | new ReactRefreshPlugin({
31 | overlay: {
32 | sockIntegration: 'whm',
33 | },
34 | }),
35 | new HtmlWebpackPlugin({
36 | filename: './index.html',
37 | template: './public/index.html',
38 | }),
39 | ].filter(Boolean),
40 | resolve: {
41 | extensions: ['.js', '.jsx'],
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/lib/utils/normalizeOptions.js:
--------------------------------------------------------------------------------
1 | const { d, n } = require('../../options');
2 |
3 | /**
4 | * Normalizes the options for the plugin.
5 | * @param {import('../types').ReactRefreshPluginOptions} options Non-normalized plugin options.
6 | * @returns {import('../types').NormalizedPluginOptions} Normalized plugin options.
7 | */
8 | const normalizeOptions = (options) => {
9 | d(options, 'exclude', /node_modules/i);
10 | d(options, 'include', /\.([cm]js|[jt]sx?|flow)$/i);
11 | d(options, 'forceEnable');
12 | d(options, 'library');
13 |
14 | n(options, 'overlay', (overlay) => {
15 | /** @type {import('../types').NormalizedErrorOverlayOptions} */
16 | const defaults = {
17 | entry: require.resolve('../../client/ErrorOverlayEntry'),
18 | module: require.resolve('../../overlay'),
19 | sockIntegration: 'wds',
20 | };
21 |
22 | if (overlay === false) {
23 | return false;
24 | }
25 | if (typeof overlay === 'undefined' || overlay === true) {
26 | return defaults;
27 | }
28 |
29 | d(overlay, 'entry', defaults.entry);
30 | d(overlay, 'module', defaults.module);
31 | d(overlay, 'sockIntegration', defaults.sockIntegration);
32 |
33 | return overlay;
34 | });
35 |
36 | return options;
37 | };
38 |
39 | module.exports = normalizeOptions;
40 |
--------------------------------------------------------------------------------
/test/unit/getSocketIntegration.test.js:
--------------------------------------------------------------------------------
1 | const getSocketIntegration = require('../../lib/utils/getSocketIntegration');
2 |
3 | describe('getSocketIntegration', () => {
4 | it('should work with webpack-dev-server', () => {
5 | const WDSSocket = require.resolve('../../sockets/WDSSocket');
6 | expect(getSocketIntegration('wds')).toStrictEqual(WDSSocket);
7 | });
8 |
9 | it('should work with webpack-hot-middleware', () => {
10 | const WHMEventSource = require.resolve('../../sockets/WHMEventSource');
11 | expect(getSocketIntegration('whm')).toStrictEqual(WHMEventSource);
12 | });
13 |
14 | it('should work with webpack-plugin-serve', () => {
15 | const WPSSocket = require.resolve('../../sockets/WPSSocket');
16 | expect(getSocketIntegration('wps')).toStrictEqual(WPSSocket);
17 | });
18 |
19 | it('should resolve when module path is provided', () => {
20 | const FixtureSocket = require.resolve('./fixtures/socketIntegration');
21 | expect(getSocketIntegration(FixtureSocket)).toStrictEqual(FixtureSocket);
22 | });
23 |
24 | it('should throw when non-path string is provided', () => {
25 | expect(() => getSocketIntegration('unknown')).toThrowErrorMatchingInlineSnapshot(
26 | `"Cannot find module 'unknown' from '../lib/utils/getSocketIntegration.js'"`
27 | );
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/examples/webpack-plugin-serve/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const { WebpackPluginServe: ServePlugin } = require('webpack-plugin-serve');
5 |
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 | const outputPath = path.resolve(__dirname, 'dist');
8 |
9 | module.exports = {
10 | mode: isDevelopment ? 'development' : 'production',
11 | entry: {
12 | main: ['webpack-plugin-serve/client', './src/index.js'],
13 | },
14 | output: {
15 | filename: 'bundle.js',
16 | path: outputPath,
17 | publicPath: '/',
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.jsx?$/,
23 | include: path.join(__dirname, 'src'),
24 | use: 'babel-loader',
25 | },
26 | ],
27 | },
28 | plugins: [
29 | isDevelopment &&
30 | new ServePlugin({
31 | port: 8080,
32 | static: outputPath,
33 | status: false,
34 | }),
35 | isDevelopment &&
36 | new ReactRefreshPlugin({
37 | overlay: {
38 | sockIntegration: 'wps',
39 | },
40 | }),
41 | new HtmlWebpackPlugin({
42 | filename: './index.html',
43 | template: './public/index.html',
44 | }),
45 | ].filter(Boolean),
46 | resolve: {
47 | extensions: ['.js', '.jsx'],
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/overlay/theme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} Theme
3 | * @property {string} black
4 | * @property {string} bright-black
5 | * @property {string} red
6 | * @property {string} bright-red
7 | * @property {string} green
8 | * @property {string} bright-green
9 | * @property {string} yellow
10 | * @property {string} bright-yellow
11 | * @property {string} blue
12 | * @property {string} bright-blue
13 | * @property {string} magenta
14 | * @property {string} bright-magenta
15 | * @property {string} cyan
16 | * @property {string} bright-cyan
17 | * @property {string} white
18 | * @property {string} bright-white
19 | * @property {string} lightgrey
20 | * @property {string} darkgrey
21 | * @property {string} grey
22 | * @property {string} dimgrey
23 | */
24 |
25 | /**
26 | * @type {Theme} theme
27 | * A collection of colors to be used by the overlay.
28 | * Partially adopted from Tomorrow Night Bright.
29 | */
30 | const theme = {
31 | black: '#000000',
32 | 'bright-black': '#474747',
33 | red: '#D34F56',
34 | 'bright-red': '#dd787d',
35 | green: '#B9C954',
36 | 'bright-green': '#c9d57b',
37 | yellow: '#E6C452',
38 | 'bright-yellow': '#ecd37f',
39 | blue: '#7CA7D8',
40 | 'bright-blue': '#a3c1e4',
41 | magenta: '#C299D6',
42 | 'bright-magenta': '#d8bde5',
43 | cyan: '#73BFB1',
44 | 'bright-cyan': '#96cfc5',
45 | white: '#FFFFFF',
46 | 'bright-white': '#FFFFFF',
47 | background: '#474747',
48 | 'dark-background': '#343434',
49 | selection: 'rgba(234, 234, 234, 0.5)',
50 | };
51 |
52 | module.exports = theme;
53 |
--------------------------------------------------------------------------------
/examples/typescript-with-tsc/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const ReactRefreshTypeScript = require('react-refresh-typescript');
6 |
7 | const isDevelopment = process.env.NODE_ENV !== 'production';
8 |
9 | module.exports = {
10 | mode: isDevelopment ? 'development' : 'production',
11 | devServer: {
12 | client: { overlay: false },
13 | },
14 | entry: {
15 | main: './src/index.tsx',
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.tsx?$/,
21 | include: path.join(__dirname, 'src'),
22 | use: [
23 | {
24 | loader: 'ts-loader',
25 | options: {
26 | configFile: isDevelopment ? 'tsconfig.dev.json' : 'tsconfig.json',
27 | transpileOnly: isDevelopment,
28 | ...(isDevelopment && {
29 | getCustomTransformers: () => ({
30 | before: [ReactRefreshTypeScript()],
31 | }),
32 | }),
33 | },
34 | },
35 | ],
36 | },
37 | ],
38 | },
39 | plugins: [
40 | isDevelopment && new ReactRefreshPlugin(),
41 | new ForkTsCheckerWebpackPlugin(),
42 | new HtmlWebpackPlugin({
43 | filename: './index.html',
44 | template: './public/index.html',
45 | }),
46 | ].filter(Boolean),
47 | resolve: {
48 | extensions: ['.js', '.ts', '.tsx'],
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/lib/types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ErrorOverlayOptions
3 | * @property {string | false} [entry] Path to a JS file that sets up the error overlay integration.
4 | * @property {string | false} [module] The error overlay module to use.
5 | * @property {import('type-fest').LiteralUnion<'wds' | 'whm' | 'wps' | false, string>} [sockIntegration] Path to a JS file that sets up the Webpack socket integration.
6 | */
7 |
8 | /**
9 | * @typedef {import('type-fest').SetRequired} NormalizedErrorOverlayOptions
10 | */
11 |
12 | /**
13 | * @typedef {Object} ReactRefreshPluginOptions
14 | * @property {boolean | import('../loader/types').ESModuleOptions} [esModule] Enables strict ES Modules compatible runtime.
15 | * @property {string | RegExp | Array} [exclude] Files to explicitly exclude from processing.
16 | * @property {boolean} [forceEnable] Enables the plugin forcefully.
17 | * @property {string | RegExp | Array} [include] Files to explicitly include for processing.
18 | * @property {string} [library] Name of the library bundle.
19 | * @property {boolean | ErrorOverlayOptions} [overlay] Modifies how the error overlay integration works in the plugin.
20 | */
21 |
22 | /**
23 | * @typedef {Object} OverlayOverrides
24 | * @property {false | NormalizedErrorOverlayOptions} overlay Modifies how the error overlay integration works in the plugin.
25 | */
26 |
27 | /**
28 | * @typedef {import('type-fest').SetRequired, 'exclude' | 'include'> & OverlayOverrides} NormalizedPluginOptions
29 | */
30 |
31 | module.exports = {};
32 |
--------------------------------------------------------------------------------
/sockets/WPSSocket.js:
--------------------------------------------------------------------------------
1 | /* global ʎɐɹɔosǝʌɹǝs */
2 | const { ClientSocket } = require('webpack-plugin-serve/lib/client/ClientSocket');
3 |
4 | /**
5 | * Initializes a socket server for HMR for webpack-plugin-serve.
6 | * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
7 | * @returns {void}
8 | */
9 | function initWPSSocket(messageHandler) {
10 | /**
11 | * The hard-coded options injection key from webpack-plugin-serve.
12 | *
13 | * [Ref](https://github.com/shellscape/webpack-plugin-serve/blob/aeb49f14e900802c98df4a4607a76bc67b1cffdf/lib/index.js#L258)
14 | * @type {Object | undefined}
15 | */
16 | let options;
17 | try {
18 | options = ʎɐɹɔosǝʌɹǝs;
19 | } catch (e) {
20 | // Bail out because this indicates the plugin is not included
21 | return;
22 | }
23 |
24 | const { address, client = {}, secure } = options;
25 | const protocol = secure ? 'wss' : 'ws';
26 | const socket = new ClientSocket(client, protocol + '://' + (client.address || address) + '/wps');
27 |
28 | socket.addEventListener('message', function listener(message) {
29 | const { action, data } = JSON.parse(message.data);
30 |
31 | switch (action) {
32 | case 'done': {
33 | messageHandler({ type: 'ok' });
34 | break;
35 | }
36 | case 'problems': {
37 | if (data.errors.length) {
38 | messageHandler({ type: 'errors', data: data.errors });
39 | } else if (data.warnings.length) {
40 | messageHandler({ type: 'warnings', data: data.warnings });
41 | }
42 | break;
43 | }
44 | default: {
45 | // Do nothing
46 | }
47 | }
48 | });
49 | }
50 |
51 | module.exports = { init: initWPSSocket };
52 |
--------------------------------------------------------------------------------
/test/loader/unit/normalizeOptions.test.js:
--------------------------------------------------------------------------------
1 | const normalizeOptions = require('../../../loader/utils/normalizeOptions');
2 |
3 | /** @type {Partial} */
4 | const DEFAULT_OPTIONS = {
5 | const: false,
6 | esModule: undefined,
7 | };
8 |
9 | describe('normalizeOptions', () => {
10 | it('should return default options when an empty object is received', () => {
11 | expect(normalizeOptions({})).toStrictEqual(DEFAULT_OPTIONS);
12 | });
13 |
14 | it('should return user options', () => {
15 | expect(
16 | normalizeOptions({
17 | const: true,
18 | esModule: {
19 | exclude: 'exclude',
20 | include: 'include',
21 | },
22 | })
23 | ).toStrictEqual({
24 | const: true,
25 | esModule: {
26 | exclude: 'exclude',
27 | include: 'include',
28 | },
29 | });
30 | });
31 |
32 | it('should return true for overlay options when it is true', () => {
33 | expect(normalizeOptions({ esModule: true })).toStrictEqual({
34 | ...DEFAULT_OPTIONS,
35 | esModule: true,
36 | });
37 | });
38 |
39 | it('should return false for esModule when it is false', () => {
40 | expect(normalizeOptions({ esModule: false })).toStrictEqual({
41 | ...DEFAULT_OPTIONS,
42 | esModule: false,
43 | });
44 | });
45 |
46 | it('should return undefined for esModule when it is undefined', () => {
47 | expect(normalizeOptions({ esModule: undefined })).toStrictEqual({
48 | ...DEFAULT_OPTIONS,
49 | esModule: undefined,
50 | });
51 | });
52 |
53 | it('should return undefined for esModule when it is undefined', () => {
54 | expect(normalizeOptions({ esModule: undefined })).toStrictEqual({
55 | ...DEFAULT_OPTIONS,
56 | esModule: undefined,
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/types/lib/types.d.ts:
--------------------------------------------------------------------------------
1 | export type ErrorOverlayOptions = {
2 | /**
3 | * Path to a JS file that sets up the error overlay integration.
4 | */
5 | entry?: string | false | undefined;
6 | /**
7 | * The error overlay module to use.
8 | */
9 | module?: string | false | undefined;
10 | /**
11 | * Path to a JS file that sets up the Webpack socket integration.
12 | */
13 | sockIntegration?:
14 | | import('type-fest').LiteralUnion
15 | | undefined;
16 | };
17 | export type NormalizedErrorOverlayOptions = import('type-fest').SetRequired<
18 | ErrorOverlayOptions,
19 | 'entry' | 'module' | 'sockIntegration'
20 | >;
21 | export type ReactRefreshPluginOptions = {
22 | /**
23 | * Enables strict ES Modules compatible runtime.
24 | */
25 | esModule?: boolean | import('../loader/types').ESModuleOptions | undefined;
26 | /**
27 | * Files to explicitly exclude from processing.
28 | */
29 | exclude?: string | RegExp | (string | RegExp)[] | undefined;
30 | /**
31 | * Enables the plugin forcefully.
32 | */
33 | forceEnable?: boolean | undefined;
34 | /**
35 | * Files to explicitly include for processing.
36 | */
37 | include?: string | RegExp | (string | RegExp)[] | undefined;
38 | /**
39 | * Name of the library bundle.
40 | */
41 | library?: string | undefined;
42 | /**
43 | * Modifies how the error overlay integration works in the plugin.
44 | */
45 | overlay?: boolean | ErrorOverlayOptions | undefined;
46 | };
47 | export type OverlayOverrides = {
48 | /**
49 | * Modifies how the error overlay integration works in the plugin.
50 | */
51 | overlay: false | NormalizedErrorOverlayOptions;
52 | };
53 | export type NormalizedPluginOptions = import('type-fest').SetRequired<
54 | import('type-fest').Except,
55 | 'exclude' | 'include'
56 | > &
57 | OverlayOverrides;
58 |
--------------------------------------------------------------------------------
/examples/typescript-with-swc/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | mode: isDevelopment ? 'development' : 'production',
10 | devServer: {
11 | client: { overlay: false },
12 | },
13 | entry: {
14 | main: './src/index.tsx',
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.tsx?$/,
20 | include: path.join(__dirname, 'src'),
21 | use: [
22 | {
23 | loader: 'swc-loader',
24 | options: {
25 | env: { mode: 'usage' },
26 | jsc: {
27 | parser: {
28 | syntax: 'typescript',
29 | tsx: true,
30 | dynamicImport: true,
31 | },
32 | transform: {
33 | react: {
34 | // swc-loader will check whether webpack mode is 'development'
35 | // and set this automatically starting from 0.1.13. You could also set it yourself.
36 | // swc won't enable fast refresh when development is false
37 | runtime: 'automatic',
38 | refresh: isDevelopment,
39 | },
40 | },
41 | },
42 | },
43 | },
44 | ],
45 | },
46 | ],
47 | },
48 | plugins: [
49 | isDevelopment && new ReactRefreshPlugin(),
50 | new ForkTsCheckerWebpackPlugin(),
51 | new HtmlWebpackPlugin({
52 | filename: './index.html',
53 | template: './public/index.html',
54 | }),
55 | ].filter(Boolean),
56 | resolve: {
57 | extensions: ['.js', '.ts', '.tsx'],
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"],
3 | "parserOptions": {
4 | "ecmaVersion": 2022
5 | },
6 | "env": {
7 | "commonjs": true,
8 | "es2022": true,
9 | "node": true
10 | },
11 | "overrides": [
12 | {
13 | "files": ["client/**/*.js", "overlay/**/*.js", "lib/runtime/**/*.js", "sockets/**/*.js"],
14 | "parserOptions": {
15 | "ecmaVersion": 2015
16 | },
17 | "env": {
18 | "browser": true,
19 | "es6": true
20 | }
21 | },
22 | {
23 | "files": [
24 | "scripts/test.mjs",
25 | "test/jest-test-setup.js",
26 | "test/helpers/{,!(fixtures)*/}*.js",
27 | "test/mocks/**/*.js",
28 | "test/**/*.test.js"
29 | ],
30 | "env": {
31 | "jest": true
32 | },
33 | "globals": {
34 | "__DEBUG__": true,
35 | "WDS_VERSION": true,
36 | "browser": true,
37 | "window": true
38 | },
39 | "parserOptions": {
40 | "sourceType": "module"
41 | }
42 | },
43 | {
44 | "files": ["test/helpers/**/fixtures/*.js", "test/conformance/**/*.test.js"],
45 | "env": {
46 | "browser": true
47 | }
48 | },
49 | {
50 | "files": ["test/**/fixtures/esm/*.js"],
51 | "parserOptions": {
52 | "ecmaVersion": 2015,
53 | "sourceType": "module"
54 | },
55 | "env": {
56 | "commonjs": false,
57 | "es6": true
58 | }
59 | },
60 | {
61 | "files": ["test/**/fixtures/cjs/esm/*.js"],
62 | "parserOptions": {
63 | "ecmaVersion": 2015,
64 | "sourceType": "module"
65 | },
66 | "env": {
67 | "commonjs": false,
68 | "es6": true
69 | }
70 | },
71 | {
72 | "files": ["test/**/fixtures/esm/cjs/*.js"],
73 | "env": {
74 | "commonjs": true,
75 | "es6": true
76 | }
77 | }
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/overlay/components/PageHeader.js:
--------------------------------------------------------------------------------
1 | const Spacer = require('./Spacer.js');
2 | const theme = require('../theme.js');
3 |
4 | /**
5 | * @typedef {Object} PageHeaderProps
6 | * @property {string} [message]
7 | * @property {string} title
8 | * @property {string} [topOffset]
9 | */
10 |
11 | /**
12 | * The header of the overlay.
13 | * @param {Document} document
14 | * @param {HTMLElement} root
15 | * @param {PageHeaderProps} props
16 | * @returns {void}
17 | */
18 | function PageHeader(document, root, props) {
19 | const pageHeaderContainer = document.createElement('div');
20 | pageHeaderContainer.style.background = theme['dark-background'];
21 | pageHeaderContainer.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.3)';
22 | pageHeaderContainer.style.color = theme.white;
23 | pageHeaderContainer.style.left = '0';
24 | pageHeaderContainer.style.right = '0';
25 | pageHeaderContainer.style.padding = '1rem 1.5rem';
26 | pageHeaderContainer.style.paddingLeft = 'max(1.5rem, env(safe-area-inset-left))';
27 | pageHeaderContainer.style.paddingRight = 'max(1.5rem, env(safe-area-inset-right))';
28 | pageHeaderContainer.style.position = 'fixed';
29 | pageHeaderContainer.style.top = props.topOffset || '0';
30 |
31 | const title = document.createElement('h3');
32 | title.innerText = props.title;
33 | title.style.color = theme.red;
34 | title.style.fontSize = '1.125rem';
35 | title.style.lineHeight = '1.3';
36 | title.style.margin = '0';
37 | pageHeaderContainer.appendChild(title);
38 |
39 | if (props.message) {
40 | title.style.margin = '0 0 0.5rem';
41 |
42 | const message = document.createElement('span');
43 | message.innerText = props.message;
44 | message.style.color = theme.white;
45 | message.style.wordBreak = 'break-word';
46 | pageHeaderContainer.appendChild(message);
47 | }
48 |
49 | root.appendChild(pageHeaderContainer);
50 |
51 | // This has to run after appending elements to root
52 | // because we need to actual mounted height.
53 | Spacer(document, root, {
54 | space: pageHeaderContainer.offsetHeight.toString(10),
55 | });
56 | }
57 |
58 | module.exports = PageHeader;
59 |
--------------------------------------------------------------------------------
/overlay/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Debounce a function to delay invoking until wait (ms) have elapsed since the last invocation.
3 | * @param {function(...*): *} fn The function to be debounced.
4 | * @param {number} wait Milliseconds to wait before invoking again.
5 | * @return {function(...*): void} The debounced function.
6 | */
7 | function debounce(fn, wait) {
8 | /**
9 | * A cached setTimeout handler.
10 | * @type {number | undefined}
11 | */
12 | let timer;
13 |
14 | /**
15 | * @returns {void}
16 | */
17 | function debounced() {
18 | const context = this;
19 | const args = arguments;
20 |
21 | clearTimeout(timer);
22 | timer = setTimeout(function () {
23 | return fn.apply(context, args);
24 | }, wait);
25 | }
26 |
27 | return debounced;
28 | }
29 |
30 | /**
31 | * Prettify a filename from error stacks into the desired format.
32 | * @param {string} filename The filename to be formatted.
33 | * @returns {string} The formatted filename.
34 | */
35 | function formatFilename(filename) {
36 | // Strip away protocol and domain for compiled files
37 | const htmlMatch = /^https?:\/\/(.*)\/(.*)/.exec(filename);
38 | if (htmlMatch && htmlMatch[1] && htmlMatch[2]) {
39 | return htmlMatch[2];
40 | }
41 |
42 | // Strip everything before the first directory for source files
43 | const sourceMatch = /\/.*?([^./]+[/|\\].*)$/.exec(filename);
44 | if (sourceMatch && sourceMatch[1]) {
45 | return sourceMatch[1].replace(/\?$/, '');
46 | }
47 |
48 | // Unknown filename type, use it as is
49 | return filename;
50 | }
51 |
52 | /**
53 | * Remove all children of an element.
54 | * @param {HTMLElement} element A valid HTML element.
55 | * @param {number} [skip] Number of elements to skip removing.
56 | * @returns {void}
57 | */
58 | function removeAllChildren(element, skip) {
59 | /** @type {Node[]} */
60 | const childList = Array.prototype.slice.call(
61 | element.childNodes,
62 | typeof skip !== 'undefined' ? skip : 0
63 | );
64 |
65 | for (let i = 0; i < childList.length; i += 1) {
66 | element.removeChild(childList[i]);
67 | }
68 | }
69 |
70 | module.exports = {
71 | debounce: debounce,
72 | formatFilename: formatFilename,
73 | removeAllChildren: removeAllChildren,
74 | };
75 |
--------------------------------------------------------------------------------
/overlay/components/CompileErrorTrace.js:
--------------------------------------------------------------------------------
1 | const Anser = require('anser');
2 | const entities = require('html-entities');
3 | const utils = require('../utils.js');
4 |
5 | /**
6 | * @typedef {Object} CompileErrorTraceProps
7 | * @property {string} errorMessage
8 | */
9 |
10 | /**
11 | * A formatter that turns Webpack compile error messages into highlighted HTML source traces.
12 | * @param {Document} document
13 | * @param {HTMLElement} root
14 | * @param {CompileErrorTraceProps} props
15 | * @returns {void}
16 | */
17 | function CompileErrorTrace(document, root, props) {
18 | const errorParts = props.errorMessage.split('\n');
19 | if (errorParts.length) {
20 | if (errorParts[0]) {
21 | errorParts[0] = utils.formatFilename(errorParts[0]);
22 | }
23 |
24 | const errorMessage = errorParts.splice(1, 1)[0];
25 | if (errorMessage) {
26 | // Strip filename from the error message
27 | errorParts.unshift(errorMessage.replace(/^(.*:)\s.*:(\s.*)$/, '$1$2'));
28 | }
29 | }
30 |
31 | const stackContainer = document.createElement('pre');
32 | stackContainer.style.fontFamily = [
33 | '"SFMono-Regular"',
34 | 'Consolas',
35 | 'Liberation Mono',
36 | 'Menlo',
37 | 'Courier',
38 | 'monospace',
39 | ].join(', ');
40 | stackContainer.style.margin = '0';
41 | stackContainer.style.whiteSpace = 'pre-wrap';
42 |
43 | const entries = Anser.ansiToJson(
44 | entities.encode(errorParts.join('\n'), { level: 'html5', mode: 'nonAscii' }),
45 | {
46 | json: true,
47 | remove_empty: true,
48 | use_classes: true,
49 | }
50 | );
51 | for (let i = 0; i < entries.length; i += 1) {
52 | const entry = entries[i];
53 | const elem = document.createElement('span');
54 | elem.innerHTML = entry.content;
55 | elem.style.color = entry.fg ? `var(--color-${entry.fg})` : undefined;
56 | elem.style.wordBreak = 'break-word';
57 | switch (entry.decoration) {
58 | case 'bold':
59 | elem.style.fontWeight = 800;
60 | break;
61 | case 'italic':
62 | elem.style.fontStyle = 'italic';
63 | break;
64 | }
65 |
66 | stackContainer.appendChild(elem);
67 | }
68 |
69 | root.appendChild(stackContainer);
70 | }
71 |
72 | module.exports = CompileErrorTrace;
73 |
--------------------------------------------------------------------------------
/lib/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "additionalProperties": false,
3 | "type": "object",
4 | "definitions": {
5 | "Path": { "type": "string" },
6 | "MatchCondition": {
7 | "anyOf": [{ "instanceof": "RegExp" }, { "$ref": "#/definitions/Path" }]
8 | },
9 | "MatchConditions": {
10 | "type": "array",
11 | "items": { "$ref": "#/definitions/MatchCondition" },
12 | "minItems": 1
13 | },
14 | "ESModuleOptions": {
15 | "additionalProperties": false,
16 | "type": "object",
17 | "properties": {
18 | "exclude": {
19 | "anyOf": [
20 | { "$ref": "#/definitions/MatchCondition" },
21 | { "$ref": "#/definitions/MatchConditions" }
22 | ]
23 | },
24 | "include": {
25 | "anyOf": [
26 | { "$ref": "#/definitions/MatchCondition" },
27 | { "$ref": "#/definitions/MatchConditions" }
28 | ]
29 | }
30 | }
31 | },
32 | "OverlayOptions": {
33 | "additionalProperties": false,
34 | "type": "object",
35 | "properties": {
36 | "entry": {
37 | "anyOf": [{ "const": false }, { "$ref": "#/definitions/Path" }]
38 | },
39 | "module": {
40 | "anyOf": [{ "const": false }, { "$ref": "#/definitions/Path" }]
41 | },
42 | "sockIntegration": {
43 | "anyOf": [
44 | { "const": false },
45 | { "enum": ["wds", "whm", "wps"] },
46 | { "$ref": "#/definitions/Path" }
47 | ]
48 | }
49 | }
50 | }
51 | },
52 | "properties": {
53 | "esModule": {
54 | "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/ESModuleOptions" }]
55 | },
56 | "exclude": {
57 | "anyOf": [
58 | { "$ref": "#/definitions/MatchCondition" },
59 | { "$ref": "#/definitions/MatchConditions" }
60 | ]
61 | },
62 | "forceEnable": { "type": "boolean" },
63 | "include": {
64 | "anyOf": [
65 | { "$ref": "#/definitions/MatchCondition" },
66 | { "$ref": "#/definitions/MatchConditions" }
67 | ]
68 | },
69 | "library": { "type": "string" },
70 | "overlay": {
71 | "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/OverlayOptions" }]
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/utils/injectRefreshLoader.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | /**
4 | * @callback MatchObject
5 | * @param {string} [str]
6 | * @returns {boolean}
7 | */
8 |
9 | /**
10 | * @typedef {Object} InjectLoaderOptions
11 | * @property {MatchObject} match A function to include/exclude files to be processed.
12 | * @property {import('../../loader/types').ReactRefreshLoaderOptions} [options] Options passed to the loader.
13 | */
14 |
15 | const resolvedLoader = require.resolve('../../loader');
16 | const reactRefreshPath = path.dirname(require.resolve('react-refresh'));
17 | const refreshUtilsPath = path.join(__dirname, '../runtime/RefreshUtils');
18 |
19 | /**
20 | * Injects refresh loader to all JavaScript-like and user-specified files.
21 | * @param {*} moduleData Module factory creation data.
22 | * @param {InjectLoaderOptions} injectOptions Options to alter how the loader is injected.
23 | * @returns {*} The injected module factory creation data.
24 | */
25 | function injectRefreshLoader(moduleData, injectOptions) {
26 | const { match, options } = injectOptions;
27 |
28 | // Include and exclude user-specified files
29 | if (!match(moduleData.matchResource || moduleData.resource)) return moduleData;
30 | // Include and exclude dynamically generated modules from other loaders
31 | if (moduleData.matchResource && !match(moduleData.request)) return moduleData;
32 | // Exclude files referenced as assets
33 | if (moduleData.type.includes('asset')) return moduleData;
34 | // Check to prevent double injection
35 | if (moduleData.loaders.find(({ loader }) => loader === resolvedLoader)) return moduleData;
36 | // Skip react-refresh and the plugin's runtime utils to prevent self-referencing -
37 | // this is useful when using the plugin as a direct dependency,
38 | // or when node_modules are specified to be processed.
39 | if (
40 | moduleData.resource.includes(reactRefreshPath) ||
41 | moduleData.resource.includes(refreshUtilsPath)
42 | ) {
43 | return moduleData;
44 | }
45 |
46 | // As we inject runtime code for each module,
47 | // it is important to run the injected loader after everything.
48 | // This way we can ensure that all code-processing have been done,
49 | // and we won't risk breaking tools like Flow or ESLint.
50 | moduleData.loaders.unshift({
51 | loader: resolvedLoader,
52 | options,
53 | });
54 |
55 | return moduleData;
56 | }
57 |
58 | module.exports = injectRefreshLoader;
59 |
--------------------------------------------------------------------------------
/test/helpers/sandbox/configs.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const BUNDLE_FILENAME = 'main';
4 |
5 | /**
6 | * @param {number} port
7 | * @returns {string}
8 | */
9 | function getIndexHTML(port) {
10 | return `
11 |
12 |
13 |
14 |
15 | Sandbox React App
16 |
17 |
18 |
19 |
20 |
21 |
22 | `;
23 | }
24 |
25 | /**
26 | * @param {boolean} esModule
27 | * @returns {string}
28 | */
29 | function getPackageJson(esModule = false) {
30 | return `
31 | {
32 | "type": "${esModule ? 'module' : 'commonjs'}"
33 | }
34 | `;
35 | }
36 |
37 | /**
38 | * @param {string} srcDir
39 | * @returns {string}
40 | */
41 | function getWDSConfig(srcDir) {
42 | return `
43 | const { DefinePlugin, ProgressPlugin } = require('webpack');
44 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
45 |
46 | module.exports = {
47 | mode: 'development',
48 | context: '${srcDir}',
49 | devtool: false,
50 | entry: {
51 | '${BUNDLE_FILENAME}': [
52 | '${path.join(__dirname, 'fixtures/hmr-notifier.js')}',
53 | './index.js',
54 | ],
55 | },
56 | module: {
57 | rules: [
58 | {
59 | test: /\\.jsx?$/,
60 | include: '${srcDir}',
61 | use: [
62 | {
63 | loader: '${require.resolve('babel-loader')}',
64 | options: {
65 | babelrc: false,
66 | plugins: ['${require.resolve('react-refresh/babel')}'],
67 | }
68 | }
69 | ],
70 | },
71 | ],
72 | },
73 | output: {
74 | hashFunction: 'xxhash64',
75 | },
76 | plugins: [
77 | new DefinePlugin({ __react_refresh_test__: true }),
78 | new ProgressPlugin((percentage) => {
79 | if (percentage === 1) {
80 | console.log("Webpack compilation complete.");
81 | }
82 | }),
83 | new ReactRefreshPlugin(),
84 | ],
85 | resolve: {
86 | alias: ${JSON.stringify(
87 | {
88 | ...(WDS_VERSION === 4 && { 'webpack-dev-server': 'webpack-dev-server-v4' }),
89 | },
90 | null,
91 | 2
92 | )},
93 | extensions: ['.js', '.jsx'],
94 | },
95 | };
96 | `;
97 | }
98 |
99 | module.exports = { getIndexHTML, getPackageJson, getWDSConfig };
100 |
--------------------------------------------------------------------------------
/test/unit/normalizeOptions.test.js:
--------------------------------------------------------------------------------
1 | const normalizeOptions = require('../../lib/utils/normalizeOptions');
2 |
3 | /** @type {Partial} */
4 | const DEFAULT_OPTIONS = {
5 | exclude: /node_modules/i,
6 | include: /\.([cm]js|[jt]sx?|flow)$/i,
7 | overlay: {
8 | entry: require.resolve('../../client/ErrorOverlayEntry'),
9 | module: require.resolve('../../overlay'),
10 | sockIntegration: 'wds',
11 | },
12 | };
13 |
14 | describe('normalizeOptions', () => {
15 | it('should return default options when an empty object is received', () => {
16 | expect(normalizeOptions({})).toStrictEqual(DEFAULT_OPTIONS);
17 | });
18 |
19 | it('should return user options', () => {
20 | expect(
21 | normalizeOptions({
22 | exclude: 'exclude',
23 | forceEnable: true,
24 | include: 'include',
25 | library: 'library',
26 | overlay: {
27 | entry: 'entry',
28 | module: 'overlay',
29 | sockIntegration: 'whm',
30 | },
31 | })
32 | ).toStrictEqual({
33 | exclude: 'exclude',
34 | forceEnable: true,
35 | include: 'include',
36 | library: 'library',
37 | overlay: {
38 | entry: 'entry',
39 | module: 'overlay',
40 | sockIntegration: 'whm',
41 | },
42 | });
43 | });
44 |
45 | it('should return default for overlay options when it is true', () => {
46 | expect(normalizeOptions({ overlay: true })).toStrictEqual(DEFAULT_OPTIONS);
47 | });
48 |
49 | it('should return false for overlay options when it is false', () => {
50 | expect(normalizeOptions({ overlay: false })).toStrictEqual({
51 | ...DEFAULT_OPTIONS,
52 | overlay: false,
53 | });
54 | });
55 |
56 | it('should keep "overlay.entry" when it is false', () => {
57 | const options = { ...DEFAULT_OPTIONS };
58 | options.overlay.entry = false;
59 |
60 | expect(normalizeOptions(options)).toStrictEqual(options);
61 | });
62 |
63 | it('should keep "overlay.module" when it is false', () => {
64 | const options = { ...DEFAULT_OPTIONS };
65 | options.overlay.module = false;
66 |
67 | expect(normalizeOptions(options)).toStrictEqual(options);
68 | });
69 |
70 | it('should keep "overlay.sockIntegration" when it is false', () => {
71 | const options = { ...DEFAULT_OPTIONS };
72 | options.overlay.sockIntegration = false;
73 |
74 | expect(normalizeOptions(options)).toStrictEqual(options);
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/overlay/components/RuntimeErrorStack.js:
--------------------------------------------------------------------------------
1 | const ErrorStackParser = require('error-stack-parser');
2 | const theme = require('../theme.js');
3 | const utils = require('../utils.js');
4 |
5 | /**
6 | * @typedef {Object} RuntimeErrorStackProps
7 | * @property {Error} error
8 | */
9 |
10 | /**
11 | * A formatter that turns runtime error stacks into highlighted HTML stacks.
12 | * @param {Document} document
13 | * @param {HTMLElement} root
14 | * @param {RuntimeErrorStackProps} props
15 | * @returns {void}
16 | */
17 | function RuntimeErrorStack(document, root, props) {
18 | const stackTitle = document.createElement('h4');
19 | stackTitle.innerText = 'Call Stack';
20 | stackTitle.style.color = theme.white;
21 | stackTitle.style.fontSize = '1.0625rem';
22 | stackTitle.style.fontWeight = '500';
23 | stackTitle.style.lineHeight = '1.3';
24 | stackTitle.style.margin = '0 0 0.5rem';
25 |
26 | const stackContainer = document.createElement('div');
27 | stackContainer.style.fontSize = '0.8125rem';
28 | stackContainer.style.lineHeight = '1.3';
29 | stackContainer.style.whiteSpace = 'pre-wrap';
30 |
31 | let errorStacks;
32 | try {
33 | errorStacks = ErrorStackParser.parse(props.error);
34 | } catch (e) {
35 | errorStacks = [];
36 | stackContainer.innerHTML = 'No stack trace is available for this error!';
37 | }
38 |
39 | for (let i = 0; i < Math.min(errorStacks.length, 10); i += 1) {
40 | const currentStack = errorStacks[i];
41 |
42 | const functionName = document.createElement('code');
43 | functionName.innerHTML = ` ${currentStack.functionName || '(anonymous function)'}`;
44 | functionName.style.color = theme.yellow;
45 | functionName.style.fontFamily = [
46 | '"SFMono-Regular"',
47 | 'Consolas',
48 | 'Liberation Mono',
49 | 'Menlo',
50 | 'Courier',
51 | 'monospace',
52 | ].join(', ');
53 |
54 | const fileName = document.createElement('div');
55 | fileName.innerHTML =
56 | ' ' +
57 | utils.formatFilename(currentStack.fileName) +
58 | ':' +
59 | currentStack.lineNumber +
60 | ':' +
61 | currentStack.columnNumber;
62 | fileName.style.color = theme.white;
63 | fileName.style.fontSize = '0.6875rem';
64 | fileName.style.marginBottom = '0.25rem';
65 |
66 | stackContainer.appendChild(functionName);
67 | stackContainer.appendChild(fileName);
68 | }
69 |
70 | root.appendChild(stackTitle);
71 | root.appendChild(stackContainer);
72 | }
73 |
74 | module.exports = RuntimeErrorStack;
75 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | lint-and-format:
15 | name: Lint and Format
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout Repository
19 | uses: actions/checkout@v4
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | cache: yarn
24 | node-version: 22
25 | - name: Install Dependencies
26 | run: yarn install --frozen-lockfile
27 | - name: Check Linting
28 | run: yarn lint
29 | - name: Check Formatting
30 | run: yarn format:check
31 |
32 | test:
33 | name: Tests (Node ${{ matrix.node-version }} - WDS ${{ matrix.wds-version }})
34 | runs-on: ubuntu-latest
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | node-version:
39 | - '18'
40 | - '20'
41 | - '22'
42 | wds-version:
43 | - '4'
44 | - '5'
45 | steps:
46 | - name: Checkout Repository
47 | uses: actions/checkout@v4
48 | - name: Setup Node.js
49 | uses: actions/setup-node@v4
50 | with:
51 | cache: yarn
52 | node-version: ${{ matrix.node-version }}
53 | - name: Install Dependencies
54 | run: yarn install --frozen-lockfile
55 | - name: Run Tests
56 | run: yarn test --testPathIgnorePatterns conformance
57 | env:
58 | BROWSER: false
59 | WDS_VERSION: ${{ matrix.wds-version }}
60 |
61 | conformance:
62 | name: Conformance (Node ${{ matrix.node-version }} - WDS ${{ matrix.wds-version }})
63 | runs-on: ubuntu-latest
64 | strategy:
65 | fail-fast: false
66 | matrix:
67 | node-version:
68 | - '18'
69 | - '20'
70 | - '22'
71 | wds-version:
72 | - '4'
73 | - '5'
74 | steps:
75 | - name: Checkout Repository
76 | uses: actions/checkout@v4
77 | - name: Setup Node.js
78 | uses: actions/setup-node@v4
79 | with:
80 | cache: yarn
81 | node-version: ${{ matrix.node-version }}
82 | - name: Install Dependencies
83 | run: yarn install --frozen-lockfile
84 | - name: Disable AppArmor
85 | run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
86 | - name: Run Conformance Tests
87 | run: yarn test conformance
88 | env:
89 | WDS_VERSION: ${{ matrix.wds-version }}
90 |
--------------------------------------------------------------------------------
/loader/utils/getRefreshModuleRuntime.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef ModuleRuntimeOptions {Object}
3 | * @property {boolean} const Use ES6 `const` and `let` in generated runtime code.
4 | * @property {'cjs' | 'esm'} moduleSystem The module system to be used.
5 | */
6 |
7 | /**
8 | * Generates code appended to each JS-like module for react-refresh capabilities.
9 | *
10 | * `__react_refresh_utils__` will be replaced with actual utils during source parsing by `webpack.ProvidePlugin`.
11 | *
12 | * [Reference for Runtime Injection](https://github.com/webpack/webpack/blob/b07d3b67d2252f08e4bb65d354a11c9b69f8b434/lib/HotModuleReplacementPlugin.js#L419)
13 | * [Reference for HMR Error Recovery](https://github.com/webpack/webpack/issues/418#issuecomment-490296365)
14 | *
15 | * @param {import('webpack').Template} Webpack's templating helpers.
16 | * @param {ModuleRuntimeOptions} options The refresh module runtime options.
17 | * @returns {string} The refresh module runtime template.
18 | */
19 | function getRefreshModuleRuntime(Template, options) {
20 | const constDeclaration = options.const ? 'const' : 'var';
21 | const letDeclaration = options.const ? 'let' : 'var';
22 | const webpackHot = options.moduleSystem === 'esm' ? 'import.meta.webpackHot' : 'module.hot';
23 | return Template.asString([
24 | `${constDeclaration} $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;`,
25 | `${constDeclaration} $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(`,
26 | Template.indent('$ReactRefreshModuleId$'),
27 | ');',
28 | '',
29 | 'function $ReactRefreshModuleRuntime$(exports) {',
30 | Template.indent([
31 | `if (${webpackHot}) {`,
32 | Template.indent([
33 | `${letDeclaration} errorOverlay;`,
34 | "if (typeof __react_refresh_error_overlay__ !== 'undefined') {",
35 | Template.indent('errorOverlay = __react_refresh_error_overlay__;'),
36 | '}',
37 | `${letDeclaration} testMode;`,
38 | "if (typeof __react_refresh_test__ !== 'undefined') {",
39 | Template.indent('testMode = __react_refresh_test__;'),
40 | '}',
41 | 'return __react_refresh_utils__.executeRuntime(',
42 | Template.indent([
43 | 'exports,',
44 | '$ReactRefreshModuleId$,',
45 | `${webpackHot},`,
46 | 'errorOverlay,',
47 | 'testMode',
48 | ]),
49 | ');',
50 | ]),
51 | '}',
52 | ]),
53 | '}',
54 | '',
55 | "if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {",
56 | Template.indent('$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);'),
57 | '} else {',
58 | Template.indent('$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);'),
59 | '}',
60 | ]);
61 | }
62 |
63 | module.exports = getRefreshModuleRuntime;
64 |
--------------------------------------------------------------------------------
/overlay/components/RuntimeErrorFooter.js:
--------------------------------------------------------------------------------
1 | const Spacer = require('./Spacer.js');
2 | const theme = require('../theme.js');
3 |
4 | /**
5 | * @typedef {Object} RuntimeErrorFooterProps
6 | * @property {string} [initialFocus]
7 | * @property {boolean} multiple
8 | * @property {function(MouseEvent): void} onClickCloseButton
9 | * @property {function(MouseEvent): void} onClickNextButton
10 | * @property {function(MouseEvent): void} onClickPrevButton
11 | */
12 |
13 | /**
14 | * A fixed footer that handles pagination of runtime errors.
15 | * @param {Document} document
16 | * @param {HTMLElement} root
17 | * @param {RuntimeErrorFooterProps} props
18 | * @returns {void}
19 | */
20 | function RuntimeErrorFooter(document, root, props) {
21 | const footer = document.createElement('div');
22 | footer.style.backgroundColor = theme['dark-background'];
23 | footer.style.bottom = '0';
24 | footer.style.boxShadow = '0 -1px 4px rgba(0, 0, 0, 0.3)';
25 | footer.style.height = '2.5rem';
26 | footer.style.left = '0';
27 | footer.style.right = '0';
28 | footer.style.lineHeight = '2.5rem';
29 | footer.style.paddingBottom = '0';
30 | footer.style.paddingBottom = 'env(safe-area-inset-bottom)';
31 | footer.style.position = 'fixed';
32 | footer.style.textAlign = 'center';
33 | footer.style.zIndex = '2';
34 |
35 | const BUTTON_CONFIGS = {
36 | prev: {
37 | id: 'prev',
38 | label: '◀ Prev',
39 | onClick: props.onClickPrevButton,
40 | },
41 | close: {
42 | id: 'close',
43 | label: '× Close',
44 | onClick: props.onClickCloseButton,
45 | },
46 | next: {
47 | id: 'next',
48 | label: 'Next ▶',
49 | onClick: props.onClickNextButton,
50 | },
51 | };
52 |
53 | let buttons = [BUTTON_CONFIGS.close];
54 | if (props.multiple) {
55 | buttons = [BUTTON_CONFIGS.prev, BUTTON_CONFIGS.close, BUTTON_CONFIGS.next];
56 | }
57 |
58 | /** @type {HTMLButtonElement | undefined} */
59 | let initialFocusButton;
60 | for (let i = 0; i < buttons.length; i += 1) {
61 | const buttonConfig = buttons[i];
62 |
63 | const button = document.createElement('button');
64 | button.id = buttonConfig.id;
65 | button.innerHTML = buttonConfig.label;
66 | button.tabIndex = 1;
67 | button.style.backgroundColor = theme['dark-background'];
68 | button.style.border = 'none';
69 | button.style.color = theme.white;
70 | button.style.cursor = 'pointer';
71 | button.style.fontSize = 'inherit';
72 | button.style.height = '100%';
73 | button.style.padding = '0.5rem 0.75rem';
74 | button.style.width = `${(100 / buttons.length).toString(10)}%`;
75 | button.addEventListener('click', buttonConfig.onClick);
76 |
77 | if (buttonConfig.id === props.initialFocus) {
78 | initialFocusButton = button;
79 | }
80 |
81 | footer.appendChild(button);
82 | }
83 |
84 | root.appendChild(footer);
85 |
86 | Spacer(document, root, { space: '2.5rem' });
87 |
88 | if (initialFocusButton) {
89 | initialFocusButton.focus();
90 | }
91 | }
92 |
93 | module.exports = RuntimeErrorFooter;
94 |
--------------------------------------------------------------------------------
/client/ErrorOverlayEntry.js:
--------------------------------------------------------------------------------
1 | /* global __react_refresh_error_overlay__, __react_refresh_socket__ */
2 |
3 | if (process.env.NODE_ENV !== 'production') {
4 | const events = require('./utils/errorEventHandlers.js');
5 | const formatWebpackErrors = require('./utils/formatWebpackErrors.js');
6 | const runWithRetry = require('./utils/retry.js');
7 |
8 | // Setup error states
9 | let isHotReload = false;
10 | let hasRuntimeErrors = false;
11 |
12 | /**
13 | * Try dismissing the compile error overlay.
14 | * This will also reset runtime error records (if any),
15 | * because we have new source to evaluate.
16 | * @returns {void}
17 | */
18 | const tryDismissErrorOverlay = function () {
19 | __react_refresh_error_overlay__.clearCompileError();
20 | __react_refresh_error_overlay__.clearRuntimeErrors(!hasRuntimeErrors);
21 | hasRuntimeErrors = false;
22 | };
23 |
24 | /**
25 | * A function called after a compile success signal is received from Webpack.
26 | * @returns {void}
27 | */
28 | const handleCompileSuccess = function () {
29 | isHotReload = true;
30 |
31 | if (isHotReload) {
32 | tryDismissErrorOverlay();
33 | }
34 | };
35 |
36 | /**
37 | * A function called after a compile errored signal is received from Webpack.
38 | * @param {string[]} errors
39 | * @returns {void}
40 | */
41 | const handleCompileErrors = function (errors) {
42 | isHotReload = true;
43 |
44 | const formattedErrors = formatWebpackErrors(errors);
45 |
46 | // Only show the first error
47 | __react_refresh_error_overlay__.showCompileError(formattedErrors[0]);
48 | };
49 |
50 | /**
51 | * Handles compilation messages from Webpack.
52 | * Integrates with a compile error overlay.
53 | * @param {*} message A Webpack HMR message sent via WebSockets.
54 | * @returns {void}
55 | */
56 | const compileMessageHandler = function (message) {
57 | switch (message.type) {
58 | case 'ok':
59 | case 'still-ok':
60 | case 'warnings': {
61 | // TODO: Implement handling for warnings
62 | handleCompileSuccess();
63 | break;
64 | }
65 | case 'errors': {
66 | handleCompileErrors(message.data);
67 | break;
68 | }
69 | default: {
70 | // Do nothing.
71 | }
72 | }
73 | };
74 |
75 | // Only register if no other overlay have been registered
76 | if (
77 | typeof window !== 'undefined' &&
78 | !window.__reactRefreshOverlayInjected &&
79 | __react_refresh_socket__
80 | ) {
81 | // Registers handlers for compile errors with retry -
82 | // This is to prevent mismatching injection order causing errors to be thrown
83 | runWithRetry(
84 | function initSocket() {
85 | __react_refresh_socket__.init(compileMessageHandler);
86 | },
87 | 3,
88 | 'Failed to set up the socket connection.'
89 | );
90 | // Registers handlers for runtime errors
91 | events.handleError(function handleError(error) {
92 | hasRuntimeErrors = true;
93 | __react_refresh_error_overlay__.handleRuntimeError(error);
94 | });
95 | events.handleUnhandledRejection(function handleUnhandledPromiseRejection(error) {
96 | hasRuntimeErrors = true;
97 | __react_refresh_error_overlay__.handleRuntimeError(error);
98 | });
99 |
100 | // Mark overlay as injected to prevent double-injection
101 | window.__reactRefreshOverlayInjected = true;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/client/utils/errorEventHandlers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @callback EventCallback
3 | * @param {string | Error | null} context
4 | * @returns {void}
5 | */
6 | /**
7 | * @callback EventHandler
8 | * @param {Event} event
9 | * @returns {void}
10 | */
11 |
12 | /**
13 | * A function that creates an event handler for the `error` event.
14 | * @param {EventCallback} callback A function called to handle the error context.
15 | * @returns {EventHandler} A handler for the `error` event.
16 | */
17 | function createErrorHandler(callback) {
18 | return function errorHandler(event) {
19 | if (!event || !event.error) {
20 | return callback(null);
21 | }
22 | if (event.error instanceof Error) {
23 | return callback(event.error);
24 | }
25 | // A non-error was thrown, we don't have a trace. :(
26 | // Look in your browser's devtools for more information
27 | return callback(new Error(event.error));
28 | };
29 | }
30 |
31 | /**
32 | * A function that creates an event handler for the `unhandledrejection` event.
33 | * @param {EventCallback} callback A function called to handle the error context.
34 | * @returns {EventHandler} A handler for the `unhandledrejection` event.
35 | */
36 | function createRejectionHandler(callback) {
37 | return function rejectionHandler(event) {
38 | if (!event || !event.reason) {
39 | return callback(new Error('Unknown'));
40 | }
41 | if (event.reason instanceof Error) {
42 | return callback(event.reason);
43 | }
44 | // A non-error was rejected, we don't have a trace :(
45 | // Look in your browser's devtools for more information
46 | return callback(new Error(event.reason));
47 | };
48 | }
49 |
50 | /**
51 | * Creates a handler that registers an EventListener on window for a valid type
52 | * and calls a callback when the event fires.
53 | * @param {string} eventType A valid DOM event type.
54 | * @param {function(EventCallback): EventHandler} createHandler A function that creates an event handler.
55 | * @returns {register} A function that registers the EventListener given a callback.
56 | */
57 | function createWindowEventHandler(eventType, createHandler) {
58 | /**
59 | * @type {EventHandler | null} A cached event handler function.
60 | */
61 | let eventHandler = null;
62 |
63 | /**
64 | * Unregisters an EventListener if it has been registered.
65 | * @returns {void}
66 | */
67 | function unregister() {
68 | if (eventHandler === null) {
69 | return;
70 | }
71 | window.removeEventListener(eventType, eventHandler);
72 | eventHandler = null;
73 | }
74 |
75 | /**
76 | * Registers an EventListener if it hasn't been registered.
77 | * @param {EventCallback} callback A function called after the event handler to handle its context.
78 | * @returns {unregister | void} A function to unregister the registered EventListener if registration is performed.
79 | */
80 | function register(callback) {
81 | if (eventHandler !== null) {
82 | return;
83 | }
84 | eventHandler = createHandler(callback);
85 | window.addEventListener(eventType, eventHandler);
86 |
87 | return unregister;
88 | }
89 |
90 | return register;
91 | }
92 |
93 | const handleError = createWindowEventHandler('error', createErrorHandler);
94 | const handleUnhandledRejection = createWindowEventHandler(
95 | 'unhandledrejection',
96 | createRejectionHandler
97 | );
98 |
99 | module.exports = {
100 | handleError: handleError,
101 | handleUnhandledRejection: handleUnhandledRejection,
102 | };
103 |
--------------------------------------------------------------------------------
/client/utils/formatWebpackErrors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} WebpackErrorObj
3 | * @property {string} moduleIdentifier
4 | * @property {string} moduleName
5 | * @property {string} message
6 | */
7 |
8 | const friendlySyntaxErrorLabel = 'Syntax error:';
9 |
10 | /**
11 | * Checks if the error message is for a syntax error.
12 | * @param {string} message The raw Webpack error message.
13 | * @returns {boolean} Whether the error message is for a syntax error.
14 | */
15 | function isLikelyASyntaxError(message) {
16 | return message.indexOf(friendlySyntaxErrorLabel) !== -1;
17 | }
18 |
19 | /**
20 | * Cleans up Webpack error messages.
21 | *
22 | * 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).
23 | * @param {string} message The raw Webpack error message.
24 | * @returns {string} The formatted Webpack error message.
25 | */
26 | function formatMessage(message) {
27 | let lines = message.split('\n');
28 |
29 | // Strip Webpack-added headers off errors/warnings
30 | // https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
31 | lines = lines.filter(function (line) {
32 | return !/Module [A-z ]+\(from/.test(line);
33 | });
34 |
35 | // Remove leading newline
36 | if (lines.length > 2 && lines[1].trim() === '') {
37 | lines.splice(1, 1);
38 | }
39 |
40 | // Remove duplicated newlines
41 | lines = lines.filter(function (line, index, arr) {
42 | return index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim();
43 | });
44 |
45 | // Clean up the file name
46 | lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
47 |
48 | // Cleans up verbose "module not found" messages for files and packages.
49 | if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
50 | lines = [
51 | lines[0],
52 | lines[1]
53 | .replace('Error: ', '')
54 | .replace('Module not found: Cannot find file:', 'Cannot find file:'),
55 | ];
56 | }
57 |
58 | message = lines.join('\n');
59 |
60 | // Clean up syntax errors
61 | message = message.replace('SyntaxError:', friendlySyntaxErrorLabel);
62 |
63 | // Internal stacks are generally useless, so we strip them -
64 | // except the stacks containing `webpack:`,
65 | // because they're normally from user code generated by webpack.
66 | message = message.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, ''); // at ... ...:x:y
67 | message = message.replace(/^\s*at\s((?!webpack:).)*[\s)]*(\n|$)/gm, ''); // at ...
68 | message = message.replace(/^\s*at\s(\n|$)/gm, ''); // at
69 |
70 | return message.trim();
71 | }
72 |
73 | /**
74 | * Formats Webpack error messages into a more readable format.
75 | * @param {Array} errors An array of Webpack error messages.
76 | * @returns {string[]} The formatted Webpack error messages.
77 | */
78 | function formatWebpackErrors(errors) {
79 | let formattedErrors = errors.map(function (errorObjOrMessage) {
80 | // Webpack 5 compilation errors are in the form of descriptor objects,
81 | // so we have to join pieces to get the format we want.
82 | if (typeof errorObjOrMessage === 'object') {
83 | return formatMessage([errorObjOrMessage.moduleName, errorObjOrMessage.message].join('\n'));
84 | }
85 | // Webpack 4 compilation errors are strings
86 | return formatMessage(errorObjOrMessage);
87 | });
88 |
89 | if (formattedErrors.some(isLikelyASyntaxError)) {
90 | // If there are any syntax errors, show just them.
91 | formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
92 | }
93 | return formattedErrors;
94 | }
95 |
96 | module.exports = formatWebpackErrors;
97 |
--------------------------------------------------------------------------------
/loader/index.js:
--------------------------------------------------------------------------------
1 | // This is a patch for mozilla/source-map#349 -
2 | // internally, it uses the existence of the `fetch` global to toggle browser behaviours.
3 | // That check, however, will break when `fetch` polyfills are used for SSR setups.
4 | // We "reset" the polyfill here to ensure it won't interfere with source-map generation.
5 | const originalFetch = global.fetch;
6 | delete global.fetch;
7 |
8 | const { validate: validateOptions } = require('schema-utils');
9 | const { SourceMapConsumer, SourceNode } = require('source-map');
10 | const {
11 | getIdentitySourceMap,
12 | getModuleSystem,
13 | getRefreshModuleRuntime,
14 | normalizeOptions,
15 | } = require('./utils');
16 | const schema = require('./options.json');
17 |
18 | const RefreshRuntimePath = require
19 | .resolve('react-refresh')
20 | .replace(/\\/g, '/')
21 | .replace(/'/g, "\\'");
22 |
23 | /**
24 | * A simple Webpack loader to inject react-refresh HMR code into modules.
25 | *
26 | * [Reference for Loader API](https://webpack.js.org/api/loaders/)
27 | * @this {import('webpack').LoaderContext}
28 | * @param {string} source The original module source code.
29 | * @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
30 | * @param {*} [meta] The loader metadata passed in.
31 | * @returns {void}
32 | */
33 | function ReactRefreshLoader(source, inputSourceMap, meta) {
34 | let options = this.getOptions();
35 | validateOptions(schema, options, {
36 | baseDataPath: 'options',
37 | name: 'React Refresh Loader',
38 | });
39 |
40 | options = normalizeOptions(options);
41 |
42 | const callback = this.async();
43 |
44 | const { ModuleFilenameHelpers, Template } = this._compiler.webpack || require('webpack');
45 |
46 | const RefreshSetupRuntimes = {
47 | cjs: Template.asString(
48 | `__webpack_require__.$Refresh$.runtime = require('${RefreshRuntimePath}');`
49 | ),
50 | esm: Template.asString([
51 | `import * as __react_refresh_runtime__ from '${RefreshRuntimePath}';`,
52 | `__webpack_require__.$Refresh$.runtime = __react_refresh_runtime__;`,
53 | ]),
54 | };
55 |
56 | /**
57 | * @this {import('webpack').LoaderContext}
58 | * @param {string} source
59 | * @param {import('source-map').RawSourceMap} [inputSourceMap]
60 | * @returns {Promise<[string, import('source-map').RawSourceMap]>}
61 | */
62 | async function _loader(source, inputSourceMap) {
63 | /** @type {'esm' | 'cjs'} */
64 | const moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);
65 |
66 | const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
67 | const RefreshModuleRuntime = getRefreshModuleRuntime(Template, {
68 | const: options.const,
69 | moduleSystem,
70 | });
71 |
72 | if (this.sourceMap) {
73 | let originalSourceMap = inputSourceMap;
74 | if (!originalSourceMap) {
75 | originalSourceMap = getIdentitySourceMap(source, this.resourcePath);
76 | }
77 |
78 | return SourceMapConsumer.with(originalSourceMap, undefined, (consumer) => {
79 | const node = SourceNode.fromStringWithSourceMap(source, consumer);
80 |
81 | node.prepend([RefreshSetupRuntime, '\n\n']);
82 | node.add(['\n\n', RefreshModuleRuntime]);
83 |
84 | const { code, map } = node.toStringWithSourceMap();
85 | return [code, map.toJSON()];
86 | });
87 | } else {
88 | return [[RefreshSetupRuntime, source, RefreshModuleRuntime].join('\n\n'), inputSourceMap];
89 | }
90 | }
91 |
92 | _loader.call(this, source, inputSourceMap).then(
93 | ([code, map]) => {
94 | callback(null, code, map, meta);
95 | },
96 | (error) => {
97 | callback(error);
98 | }
99 | );
100 | }
101 |
102 | module.exports = ReactRefreshLoader;
103 |
104 | // Restore the original value of the `fetch` global, if it exists
105 | if (originalFetch) {
106 | global.fetch = originalFetch;
107 | }
108 |
--------------------------------------------------------------------------------
/lib/utils/makeRefreshRuntimeModule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Makes a runtime module to intercept module execution for React Refresh.
3 | * This module creates an isolated `__webpack_require__` function for each module,
4 | * and injects a `$Refresh$` object into it for use by React Refresh.
5 | * @param {import('webpack')} webpack The Webpack exports.
6 | * @returns {typeof import('webpack').RuntimeModule} The runtime module class.
7 | */
8 | function makeRefreshRuntimeModule(webpack) {
9 | return class ReactRefreshRuntimeModule extends webpack.RuntimeModule {
10 | constructor() {
11 | // Second argument is the `stage` for this runtime module -
12 | // we'll use the same stage as Webpack's HMR runtime module for safety.
13 | super('react refresh', webpack.RuntimeModule.STAGE_BASIC);
14 | }
15 |
16 | /**
17 | * @returns {string} runtime code
18 | */
19 | generate() {
20 | if (!this.compilation) throw new Error('Webpack compilation missing!');
21 |
22 | const { runtimeTemplate } = this.compilation;
23 | const constDeclaration = runtimeTemplate.supportsConst() ? 'const' : 'var';
24 | return webpack.Template.asString([
25 | `${constDeclaration} setup = ${runtimeTemplate.basicFunction('moduleId', [
26 | `${constDeclaration} refresh = {`,
27 | webpack.Template.indent([
28 | `moduleId: moduleId,`,
29 | `register: ${runtimeTemplate.basicFunction('type, id', [
30 | `${constDeclaration} typeId = moduleId + ' ' + id;`,
31 | 'refresh.runtime.register(type, typeId);',
32 | ])},`,
33 | `signature: ${runtimeTemplate.returningFunction(
34 | 'refresh.runtime.createSignatureFunctionForTransform()'
35 | )},`,
36 | `runtime: {`,
37 | webpack.Template.indent([
38 | `createSignatureFunctionForTransform: ${runtimeTemplate.returningFunction(
39 | runtimeTemplate.returningFunction('type', 'type')
40 | )},`,
41 | `register: ${runtimeTemplate.emptyFunction()}`,
42 | ]),
43 | `},`,
44 | ]),
45 | `};`,
46 | `return refresh;`,
47 | ])};`,
48 | '',
49 | `${webpack.RuntimeGlobals.interceptModuleExecution}.push(${runtimeTemplate.basicFunction(
50 | 'options',
51 | [
52 | `${constDeclaration} originalFactory = options.factory;`,
53 | // Using a function declaration -
54 | // ensures `this` would propagate for modules relying on it
55 | `options.factory = function(moduleObject, moduleExports, webpackRequire) {`,
56 | webpack.Template.indent([
57 | // Our require function delegates to the original require function
58 | `${constDeclaration} hotRequire = ${runtimeTemplate.returningFunction(
59 | 'webpackRequire(request)',
60 | 'request'
61 | )};`,
62 | // The propery descriptor factory below ensures all properties but `$Refresh$`
63 | // are proxied through to the original require function
64 | `${constDeclaration} createPropertyDescriptor = ${runtimeTemplate.basicFunction(
65 | 'name',
66 | [
67 | `return {`,
68 | webpack.Template.indent([
69 | `configurable: true,`,
70 | `enumerable: true,`,
71 | `get: ${runtimeTemplate.returningFunction('webpackRequire[name]')},`,
72 | `set: ${runtimeTemplate.basicFunction('value', [
73 | 'webpackRequire[name] = value;',
74 | ])},`,
75 | ]),
76 | `};`,
77 | ]
78 | )};`,
79 | `for (${constDeclaration} name in webpackRequire) {`,
80 | webpack.Template.indent([
81 | 'if (name === "$Refresh$") continue;',
82 | 'if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {',
83 | webpack.Template.indent([
84 | `Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));`,
85 | ]),
86 | `}`,
87 | ]),
88 | `}`,
89 | `hotRequire.$Refresh$ = setup(options.id);`,
90 | `originalFactory.call(this, moduleObject, moduleExports, hotRequire);`,
91 | ]),
92 | '};',
93 | ]
94 | )});`,
95 | ]);
96 | }
97 | };
98 | }
99 |
100 | module.exports = makeRefreshRuntimeModule;
101 |
--------------------------------------------------------------------------------
/test/helpers/sandbox/spawn.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const spawn = require('cross-spawn');
3 |
4 | /**
5 | * @param {string} packageName
6 | * @returns {string}
7 | */
8 | function getPackageExecutable(packageName, binName) {
9 | let { bin: binPath } = require(`${packageName}/package.json`);
10 | // "bin": { "package": "bin.js" }
11 | if (typeof binPath === 'object') {
12 | binPath = binPath[binName || packageName];
13 | }
14 | if (!binPath) {
15 | throw new Error(`Package ${packageName} does not have an executable!`);
16 | }
17 |
18 | return require.resolve(path.join(packageName, binPath));
19 | }
20 |
21 | /**
22 | * @param {import('child_process').ChildProcess | void} instance
23 | * @returns {void}
24 | */
25 | function killTestProcess(instance) {
26 | if (!instance) {
27 | return;
28 | }
29 |
30 | try {
31 | process.kill(instance.pid);
32 | } catch (error) {
33 | if (
34 | process.platform === 'win32' &&
35 | typeof error.message === 'string' &&
36 | (error.message.includes(`no running instance of the task`) ||
37 | error.message.includes(`not found`))
38 | ) {
39 | // Windows throws an error if the process is already dead
40 | return;
41 | }
42 |
43 | throw error;
44 | }
45 | }
46 |
47 | /**
48 | * @typedef {Object} SpawnOptions
49 | * @property {string} [cwd]
50 | * @property {*} [env]
51 | * @property {string | RegExp} [successMessage]
52 | */
53 |
54 | /**
55 | * @param {string} processPath
56 | * @param {*[]} argv
57 | * @param {SpawnOptions} [options]
58 | * @returns {Promise}
59 | */
60 | function spawnTestProcess(processPath, argv, options = {}) {
61 | const cwd = options.cwd || path.resolve(__dirname, '../../..');
62 | const env = {
63 | ...process.env,
64 | NODE_ENV: 'development',
65 | ...options.env,
66 | };
67 | const successRegex = new RegExp(options.successMessage || 'webpack compilation complete.', 'i');
68 |
69 | return new Promise((resolve, reject) => {
70 | const instance = spawn(processPath, argv, { cwd, env });
71 | let didResolve = false;
72 |
73 | /**
74 | * @param {Buffer} data
75 | * @returns {void}
76 | */
77 | function handleStdout(data) {
78 | const message = data.toString();
79 | if (successRegex.test(message)) {
80 | if (!didResolve) {
81 | didResolve = true;
82 | resolve(instance);
83 | }
84 | }
85 |
86 | if (__DEBUG__) {
87 | process.stdout.write(message);
88 | }
89 | }
90 |
91 | /**
92 | * @param {Buffer} data
93 | * @returns {void}
94 | */
95 | function handleStderr(data) {
96 | const message = data.toString();
97 |
98 | if (__DEBUG__) {
99 | process.stderr.write(message);
100 | }
101 | }
102 |
103 | instance.stdout.on('data', handleStdout);
104 | instance.stderr.on('data', handleStderr);
105 |
106 | instance.on('close', () => {
107 | instance.stdout.removeListener('data', handleStdout);
108 | instance.stderr.removeListener('data', handleStderr);
109 |
110 | if (!didResolve) {
111 | didResolve = true;
112 | resolve();
113 | }
114 | });
115 |
116 | instance.on('error', (error) => {
117 | reject(error);
118 | });
119 | });
120 | }
121 |
122 | /**
123 | * @param {number} port
124 | * @param {Object} dirs
125 | * @param {string} dirs.public
126 | * @param {string} dirs.root
127 | * @param {string} dirs.src
128 | * @param {SpawnOptions} [options]
129 | * @returns {Promise}
130 | */
131 | function spawnWebpackServe(port, dirs, options = {}) {
132 | const webpackBin = getPackageExecutable('webpack-cli', 'webpack-cli');
133 |
134 | const NODE_OPTIONS = [
135 | // This requires a script to alias `webpack-dev-server` -
136 | // both v4 and v5 are installed,
137 | // so we have to ensure that they resolve to the correct variant.
138 | WDS_VERSION === 4 && `--require "${require.resolve('./aliasWDSv4')}"`,
139 | ]
140 | .filter(Boolean)
141 | .join(' ');
142 |
143 | return spawnTestProcess(
144 | webpackBin,
145 | [
146 | 'serve',
147 | '--no-color',
148 | '--no-client-overlay',
149 | '--config',
150 | path.join(dirs.root, 'webpack.config.js'),
151 | '--static-directory',
152 | dirs.public,
153 | '--hot',
154 | '--port',
155 | port,
156 | ],
157 | {
158 | ...options,
159 | env: { ...options.env, ...(NODE_OPTIONS && { NODE_OPTIONS }) },
160 | }
161 | );
162 | }
163 |
164 | module.exports = {
165 | killTestProcess,
166 | spawnWebpackServe,
167 | };
168 |
--------------------------------------------------------------------------------
/test/loader/unit/getModuleSystem.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { ModuleFilenameHelpers } = require('webpack');
3 |
4 | describe('getModuleSystem', () => {
5 | let getModuleSystem;
6 |
7 | beforeEach(() => {
8 | jest.isolateModules(() => {
9 | getModuleSystem = require('../../../loader/utils/getModuleSystem');
10 | });
11 | });
12 |
13 | it('should return `esm` when `options.esModule` is true', async () => {
14 | await expect(getModuleSystem.call({}, ModuleFilenameHelpers, { esModule: true })).resolves.toBe(
15 | 'esm'
16 | );
17 | });
18 |
19 | it('should return `cjs` when `options.esModule` is false', async () => {
20 | await expect(
21 | getModuleSystem.call({}, ModuleFilenameHelpers, { esModule: false })
22 | ).resolves.toBe('cjs');
23 | });
24 |
25 | it('should return `esm` when `resourcePath` matches `options.esModule.include`', async () => {
26 | await expect(
27 | getModuleSystem.call(
28 | {
29 | resourcePath: 'include',
30 | },
31 | ModuleFilenameHelpers,
32 | { esModule: { include: /include/ } }
33 | )
34 | ).resolves.toBe('esm');
35 | });
36 |
37 | it('should return `cjs` when `resourcePath` matches `options.esModule.exclude`', async () => {
38 | await expect(
39 | getModuleSystem.call(
40 | {
41 | resourcePath: 'exclude',
42 | },
43 | ModuleFilenameHelpers,
44 | { esModule: { exclude: /exclude/ } }
45 | )
46 | ).resolves.toBe('cjs');
47 | });
48 |
49 | it('should return `esm` when `resourcePath` ends with `.mjs` extension', async () => {
50 | await expect(
51 | getModuleSystem.call({ resourcePath: 'index.mjs' }, ModuleFilenameHelpers, {})
52 | ).resolves.toBe('esm');
53 | });
54 |
55 | it('should return `cjs` when `resourcePath` ends with `.cjs` extension', async () => {
56 | await expect(
57 | getModuleSystem.call({ resourcePath: 'index.cjs' }, ModuleFilenameHelpers, {})
58 | ).resolves.toBe('cjs');
59 | });
60 |
61 | it('should return `esm` when `package.json` uses the `module` type', async () => {
62 | await expect(
63 | getModuleSystem.call(
64 | {
65 | resourcePath: path.resolve(__dirname, '..', 'fixtures/esm', 'index.js'),
66 | rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
67 | addDependency: () => {},
68 | addMissingDependency: () => {},
69 | },
70 | ModuleFilenameHelpers,
71 | {}
72 | )
73 | ).resolves.toBe('esm');
74 | });
75 |
76 | it('should return `esm` when `package.json` uses the `module` type nested inside a cjs package', async () => {
77 | await expect(
78 | getModuleSystem.call(
79 | {
80 | resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs/esm', 'index.js'),
81 | rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
82 | addDependency: () => {},
83 | addMissingDependency: () => {},
84 | },
85 | ModuleFilenameHelpers,
86 | {}
87 | )
88 | ).resolves.toBe('esm');
89 | });
90 |
91 | it('should return `cjs` when `package.json` uses the `commonjs` type', async () => {
92 | await expect(
93 | getModuleSystem.call(
94 | {
95 | resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs', 'index.js'),
96 | rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
97 | addDependency: () => {},
98 | addMissingDependency: () => {},
99 | },
100 | ModuleFilenameHelpers,
101 | {}
102 | )
103 | ).resolves.toBe('cjs');
104 | });
105 |
106 | it('should return `cjs` when `package.json` uses the `commonjs` type nexted insdie an esm package', async () => {
107 | await expect(
108 | getModuleSystem.call(
109 | {
110 | resourcePath: path.resolve(__dirname, '..', 'fixtures/esm/cjs', 'index.js'),
111 | rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
112 | addDependency: () => {},
113 | addMissingDependency: () => {},
114 | },
115 | ModuleFilenameHelpers,
116 | {}
117 | )
118 | ).resolves.toBe('cjs');
119 | });
120 |
121 | it('should return `cjs` when nothing matches', async () => {
122 | await expect(
123 | getModuleSystem.call(
124 | {
125 | resourcePath: path.resolve(__dirname, '..', 'fixtures/auto', 'index.js'),
126 | rootContext: path.resolve(__dirname, '..', 'fixtures/auto'),
127 | addDependency: () => {},
128 | addMissingDependency: () => {},
129 | },
130 | ModuleFilenameHelpers,
131 | { esModule: {} }
132 | )
133 | ).resolves.toBe('cjs');
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/test/unit/makeRefreshRuntimeModule.test.js:
--------------------------------------------------------------------------------
1 | const makeRefreshRuntimeModule = require('../../lib/utils/makeRefreshRuntimeModule');
2 |
3 | describe('makeRefreshRuntimeModule', () => {
4 | beforeEach(() => {
5 | global.__webpack_require__ = { i: [] };
6 | });
7 |
8 | afterAll(() => {
9 | delete global.__webpack_require__;
10 | });
11 |
12 | it('should make runtime module', () => {
13 | const webpack = require('webpack');
14 |
15 | let RefreshRuntimeModule;
16 | expect(() => {
17 | RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
18 | }).not.toThrow();
19 |
20 | expect(() => {
21 | new RefreshRuntimeModule();
22 | }).not.toThrow();
23 | });
24 |
25 | it('should generate with ES5 settings', () => {
26 | const webpack = require('webpack');
27 | const RuntimeTemplate = require('webpack/lib/RuntimeTemplate');
28 |
29 | const RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
30 | const instance = new RefreshRuntimeModule();
31 | instance.compilation = {
32 | runtimeTemplate: new RuntimeTemplate(
33 | {},
34 | { environment: { arrowFunction: false, const: false } },
35 | (i) => i
36 | ),
37 | };
38 |
39 | const runtime = instance.generate();
40 | expect(runtime).toMatchInlineSnapshot(`
41 | "var setup = function(moduleId) {
42 | var refresh = {
43 | moduleId: moduleId,
44 | register: function(type, id) {
45 | var typeId = moduleId + ' ' + id;
46 | refresh.runtime.register(type, typeId);
47 | },
48 | signature: function() { return refresh.runtime.createSignatureFunctionForTransform(); },
49 | runtime: {
50 | createSignatureFunctionForTransform: function() { return function(type) { return type; }; },
51 | register: function() {}
52 | },
53 | };
54 | return refresh;
55 | };
56 |
57 | __webpack_require__.i.push(function(options) {
58 | var originalFactory = options.factory;
59 | options.factory = function(moduleObject, moduleExports, webpackRequire) {
60 | var hotRequire = function(request) { return webpackRequire(request); };
61 | var createPropertyDescriptor = function(name) {
62 | return {
63 | configurable: true,
64 | enumerable: true,
65 | get: function() { return webpackRequire[name]; },
66 | set: function(value) {
67 | webpackRequire[name] = value;
68 | },
69 | };
70 | };
71 | for (var name in webpackRequire) {
72 | if (name === "$Refresh$") continue;
73 | if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {
74 | Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
75 | }
76 | }
77 | hotRequire.$Refresh$ = setup(options.id);
78 | originalFactory.call(this, moduleObject, moduleExports, hotRequire);
79 | };
80 | });"
81 | `);
82 | expect(() => {
83 | eval(runtime);
84 | }).not.toThrow();
85 | });
86 |
87 | it('should make working runtime module with ES6 settings', () => {
88 | const webpack = require('webpack');
89 | const RuntimeTemplate = require('webpack/lib/RuntimeTemplate');
90 |
91 | const RefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
92 | const instance = new RefreshRuntimeModule();
93 | instance.compilation = {
94 | runtimeTemplate: new RuntimeTemplate(
95 | {},
96 | { environment: { arrowFunction: true, const: true } },
97 | (i) => i
98 | ),
99 | };
100 |
101 | const runtime = instance.generate();
102 | expect(runtime).toMatchInlineSnapshot(`
103 | "const setup = (moduleId) => {
104 | const refresh = {
105 | moduleId: moduleId,
106 | register: (type, id) => {
107 | const typeId = moduleId + ' ' + id;
108 | refresh.runtime.register(type, typeId);
109 | },
110 | signature: () => (refresh.runtime.createSignatureFunctionForTransform()),
111 | runtime: {
112 | createSignatureFunctionForTransform: () => ((type) => (type)),
113 | register: x => {}
114 | },
115 | };
116 | return refresh;
117 | };
118 |
119 | __webpack_require__.i.push((options) => {
120 | const originalFactory = options.factory;
121 | options.factory = function(moduleObject, moduleExports, webpackRequire) {
122 | const hotRequire = (request) => (webpackRequire(request));
123 | const createPropertyDescriptor = (name) => {
124 | return {
125 | configurable: true,
126 | enumerable: true,
127 | get: () => (webpackRequire[name]),
128 | set: (value) => {
129 | webpackRequire[name] = value;
130 | },
131 | };
132 | };
133 | for (const name in webpackRequire) {
134 | if (name === "$Refresh$") continue;
135 | if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {
136 | Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
137 | }
138 | }
139 | hotRequire.$Refresh$ = setup(options.id);
140 | originalFactory.call(this, moduleObject, moduleExports, hotRequire);
141 | };
142 | });"
143 | `);
144 | expect(() => {
145 | eval(runtime);
146 | }).not.toThrow();
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pmmmwh/react-refresh-webpack-plugin",
3 | "version": "0.6.2",
4 | "description": "An **EXPERIMENTAL** Webpack plugin to enable \"Fast Refresh\" (also previously known as _Hot Reloading_) for React components.",
5 | "keywords": [
6 | "react",
7 | "javascript",
8 | "webpack",
9 | "refresh",
10 | "hmr",
11 | "hotreload",
12 | "livereload",
13 | "live",
14 | "edit",
15 | "hot",
16 | "reload"
17 | ],
18 | "homepage": "https://github.com/pmmmwh/react-refresh-webpack-plugin#readme",
19 | "bugs": {
20 | "url": "https://github.com/pmmmwh/react-refresh-webpack-plugin/issues"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/pmmmwh/react-refresh-webpack-plugin.git"
25 | },
26 | "license": "MIT",
27 | "author": "Michael Mok",
28 | "main": "lib/index.js",
29 | "types": "types/lib/index.d.ts",
30 | "files": [
31 | "client",
32 | "lib",
33 | "loader",
34 | "options",
35 | "overlay",
36 | "sockets",
37 | "types",
38 | "umd"
39 | ],
40 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
41 | "scripts": {
42 | "test": "run-s -c test:pre \"test:exec {@}\" test:post --",
43 | "test:wds-4": "cross-env WDS_VERSION=4 yarn test",
44 | "test:exec": "node --experimental-vm-modules scripts/test.js",
45 | "test:pre": "run-s -c test:pre:*",
46 | "test:pre:0": "yarn link",
47 | "test:pre:1": "yarn link @pmmmwh/react-refresh-webpack-plugin",
48 | "test:post": "run-s -c test:post:*",
49 | "test:post:0": "yarn unlink @pmmmwh/react-refresh-webpack-plugin",
50 | "test:post:1": "yarn unlink",
51 | "lint": "eslint --report-unused-disable-directives --ext .js,.jsx .",
52 | "lint:fix": "yarn lint --fix",
53 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
54 | "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\"",
55 | "types:clean": "del types",
56 | "types:compile": "tsc",
57 | "types:prune-private": "del \"types/*/*\" \"!types/{lib,loader,options}/{index,types}.d.ts\"",
58 | "generate:client-external": "webpack",
59 | "generate:types": "run-s types:clean types:compile types:prune-private \"format --log-level=silent\"",
60 | "prepublishOnly": "run-p generate:*"
61 | },
62 | "dependencies": {
63 | "anser": "^2.1.1",
64 | "core-js-pure": "^3.23.3",
65 | "error-stack-parser": "^2.0.6",
66 | "html-entities": "^2.1.0",
67 | "schema-utils": "^4.2.0",
68 | "source-map": "^0.7.3"
69 | },
70 | "devDependencies": {
71 | "@babel/core": "^7.24.6",
72 | "@babel/plugin-transform-modules-commonjs": "^7.24.6",
73 | "@types/cross-spawn": "^6.0.6",
74 | "@types/fs-extra": "^11.0.4",
75 | "@types/jest": "^29.5.12",
76 | "@types/json-schema": "^7.0.15",
77 | "@types/module-alias": "^2.0.4",
78 | "@types/node": "^24.10.1",
79 | "@types/webpack": "^5.28.5",
80 | "babel-loader": "^10.0.0",
81 | "cross-env": "^7.0.3",
82 | "cross-spawn": "^7.0.5",
83 | "del-cli": "^6.0.0",
84 | "eslint": "^8.6.0",
85 | "eslint-config-prettier": "^10.1.1",
86 | "eslint-plugin-prettier": "^5.1.3",
87 | "fs-extra": "^11.2.0",
88 | "get-port": "^7.1.0",
89 | "jest": "^29.7.0",
90 | "jest-environment-jsdom": "^29.7.0",
91 | "jest-environment-node": "^29.7.0",
92 | "jest-junit": "^16.0.0",
93 | "jest-location-mock": "^2.0.0",
94 | "memfs": "^4.9.2",
95 | "module-alias": "^2.2.3",
96 | "nanoid": "^3.3.8",
97 | "npm-run-all2": "^7.0.2",
98 | "prettier": "^3.3.0",
99 | "puppeteer": "^24.4.0",
100 | "react-refresh": "^0.18.0",
101 | "sourcemap-validator": "^2.1.0",
102 | "terser-webpack-plugin": "^5.3.10",
103 | "type-fest": "^4.41.0",
104 | "typescript": "~5.9.3",
105 | "webpack": "^5.94.0",
106 | "webpack-cli": "^6.0.1",
107 | "webpack-dev-server": "^5.0.4",
108 | "webpack-dev-server-v4": "npm:webpack-dev-server@^4.8.0",
109 | "webpack-hot-middleware": "^2.26.1",
110 | "webpack-plugin-serve": "^1.6.0",
111 | "yn": "^4.0.0"
112 | },
113 | "peerDependencies": {
114 | "@types/webpack": "5.x",
115 | "react-refresh": ">=0.10.0 <1.0.0",
116 | "sockjs-client": "^1.4.0",
117 | "type-fest": ">=0.17.0 <6.0.0",
118 | "webpack": "^5.0.0",
119 | "webpack-dev-server": "^4.8.0 || 5.x",
120 | "webpack-hot-middleware": "2.x",
121 | "webpack-plugin-serve": "1.x"
122 | },
123 | "peerDependenciesMeta": {
124 | "@types/webpack": {
125 | "optional": true
126 | },
127 | "sockjs-client": {
128 | "optional": true
129 | },
130 | "type-fest": {
131 | "optional": true
132 | },
133 | "webpack-dev-server": {
134 | "optional": true
135 | },
136 | "webpack-hot-middleware": {
137 | "optional": true
138 | },
139 | "webpack-plugin-serve": {
140 | "optional": true
141 | }
142 | },
143 | "resolutions": {
144 | "memfs": "^4.0.0",
145 | "rimraf": "^5.0.0",
146 | "type-fest": "^4.41.0"
147 | },
148 | "engines": {
149 | "node": ">=18.12"
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/test/helpers/compilation/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createFsFromVolume, Volume } = require('memfs');
3 | const webpack = require('webpack');
4 | const normalizeErrors = require('./normalizeErrors');
5 |
6 | const BUNDLE_FILENAME = 'main';
7 | const CONTEXT_PATH = path.join(__dirname, '../..', 'loader/fixtures');
8 | const OUTPUT_PATH = path.join(__dirname, 'dist');
9 |
10 | /**
11 | * @typedef {Object} CompilationModule
12 | * @property {string} execution
13 | * @property {string} parsed
14 | * @property {string} [sourceMap]
15 | */
16 |
17 | /**
18 | * @typedef {Object} CompilationSession
19 | * @property {*[]} errors
20 | * @property {*[]} warnings
21 | * @property {CompilationModule} module
22 | */
23 |
24 | /**
25 | * Gets a Webpack compiler instance to test loader operations.
26 | * @param {string} subContext
27 | * @param {Object} [options]
28 | * @param {boolean | string} [options.devtool]
29 | * @param {import('../../../loader/types').ReactRefreshLoaderOptions} [options.loaderOptions]
30 | * @param {*} [options.prevSourceMap]
31 | * @returns {Promise}
32 | */
33 | async function getCompilation(subContext, options = {}) {
34 | const compiler = webpack({
35 | mode: 'development',
36 | cache: false,
37 | context: path.join(CONTEXT_PATH, subContext),
38 | devtool: options.devtool || false,
39 | entry: {
40 | [BUNDLE_FILENAME]: './index.js',
41 | },
42 | output: {
43 | filename: '[name].js',
44 | hashFunction: 'xxhash64',
45 | path: OUTPUT_PATH,
46 | },
47 | module: {
48 | rules: [
49 | {
50 | exclude: /node_modules/,
51 | test: /\.js$/,
52 | use: [
53 | {
54 | loader: require.resolve('@pmmmwh/react-refresh-webpack-plugin/loader'),
55 | options: options.loaderOptions,
56 | },
57 | !!options.devtool &&
58 | Object.prototype.hasOwnProperty.call(options, 'prevSourceMap') && {
59 | loader: path.join(__dirname, 'fixtures/source-map-loader.js'),
60 | options: {
61 | sourceMap: options.prevSourceMap,
62 | },
63 | },
64 | ].filter(Boolean),
65 | },
66 | ],
67 | },
68 | plugins: [new webpack.HotModuleReplacementPlugin()],
69 | // Options below forces Webpack to:
70 | // 1. Move Webpack runtime into the runtime chunk;
71 | // 2. Move node_modules into the vendor chunk with a stable name.
72 | optimization: {
73 | runtimeChunk: 'single',
74 | splitChunks: {
75 | chunks: 'all',
76 | name: (module, chunks, cacheGroupKey) => cacheGroupKey,
77 | },
78 | },
79 | });
80 |
81 | // Use an in-memory file system to prevent emitting files
82 | compiler.outputFileSystem = createFsFromVolume(new Volume());
83 |
84 | /** @type {import('memfs').IFs} */
85 | const compilerOutputFs = compiler.outputFileSystem;
86 | /** @type {import('webpack').Stats | undefined} */
87 | let compilationStats;
88 |
89 | await new Promise((resolve, reject) => {
90 | compiler.run((error, stats) => {
91 | if (error) {
92 | reject(error);
93 | return;
94 | }
95 |
96 | compilationStats = stats;
97 |
98 | // The compiler have to be explicitly closed
99 | compiler.close(() => {
100 | resolve();
101 | });
102 | });
103 | });
104 |
105 | return {
106 | /** @type {*[]} */
107 | get errors() {
108 | return normalizeErrors(compilationStats.compilation.errors);
109 | },
110 | /** @type {*[]} */
111 | get warnings() {
112 | return normalizeErrors(compilationStats.compilation.errors);
113 | },
114 | /** @type {CompilationModule} */
115 | get module() {
116 | const compilationModules = compilationStats.toJson({ source: true }).modules;
117 | if (!compilationModules) {
118 | throw new Error('Module compilation stats not found!');
119 | }
120 |
121 | const parsed = compilationModules.find(({ name }) => name === './index.js');
122 | if (!parsed) {
123 | throw new Error('Fixture module is not found in compilation stats!');
124 | }
125 |
126 | let execution;
127 | try {
128 | execution = compilerOutputFs
129 | .readFileSync(path.join(OUTPUT_PATH, `${BUNDLE_FILENAME}.js`))
130 | .toString();
131 | } catch (error) {
132 | execution = error.toString();
133 | }
134 |
135 | /** @type {string | undefined} */
136 | let sourceMap;
137 | const [, sourceMapUrl] = execution.match(/\/\/# sourceMappingURL=(.*)$/) || [];
138 | const isInlineSourceMap = !!sourceMapUrl && /^data:application\/json;/.test(sourceMapUrl);
139 | if (!isInlineSourceMap) {
140 | try {
141 | sourceMap = JSON.stringify(
142 | JSON.parse(
143 | compilerOutputFs.readFileSync(path.join(OUTPUT_PATH, sourceMapUrl)).toString()
144 | ),
145 | null,
146 | 2
147 | );
148 | } catch (error) {
149 | sourceMap = error.toString();
150 | }
151 | }
152 |
153 | return {
154 | parsed: parsed.source,
155 | execution,
156 | sourceMap,
157 | };
158 | },
159 | };
160 | }
161 |
162 | module.exports = getCompilation;
163 |
--------------------------------------------------------------------------------
/loader/utils/getModuleSystem.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 |
4 | /** @type {Map} */
5 | let packageJsonTypeMap = new Map();
6 |
7 | /**
8 | * Infers the current active module system from loader context and options.
9 | * @this {import('webpack').loader.LoaderContext}
10 | * @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
11 | * @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
12 | * @return {Promise<'esm' | 'cjs'>} The inferred module system.
13 | */
14 | async function getModuleSystem(ModuleFilenameHelpers, options) {
15 | // Check loader options -
16 | // if `esModule` is set we don't have to do extra guess work.
17 | switch (typeof options.esModule) {
18 | case 'boolean': {
19 | return options.esModule ? 'esm' : 'cjs';
20 | }
21 | case 'object': {
22 | if (
23 | options.esModule.include &&
24 | ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.include)
25 | ) {
26 | return 'esm';
27 | }
28 | if (
29 | options.esModule.exclude &&
30 | ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.exclude)
31 | ) {
32 | return 'cjs';
33 | }
34 |
35 | break;
36 | }
37 | default: // Do nothing
38 | }
39 |
40 | // Check current resource's extension
41 | if (/\.mjs$/.test(this.resourcePath)) return 'esm';
42 | if (/\.cjs$/.test(this.resourcePath)) return 'cjs';
43 |
44 | if (typeof this.addMissingDependency !== 'function') {
45 | // This is Webpack 4 which does not support `import.meta`.
46 | // We assume `.js` files are CommonJS because the output cannot be ESM anyway.
47 | return 'cjs';
48 | }
49 |
50 | // We will assume CommonJS if we cannot determine otherwise
51 | let packageJsonType = '';
52 |
53 | // We begin our search for relevant `package.json` files,
54 | // at the directory of the resource being loaded.
55 | // These paths should already be resolved,
56 | // but we resolve them again to ensure we are dealing with an aboslute path.
57 | const resourceContext = path.dirname(this.resourcePath);
58 | let searchPath = resourceContext;
59 | let previousSearchPath = '';
60 | // We start our search just above the root context of the webpack compilation
61 | const stopPath = path.dirname(this.rootContext);
62 |
63 | // If the module context is a resolved symlink outside the `rootContext` path,
64 | // then we will never find the `stopPath` - so we also halt when we hit the root.
65 | // Note that there is a potential that the wrong `package.json` is found in some pathalogical cases,
66 | // such as a folder that is conceptually a package + does not have an ancestor `package.json`,
67 | // but there exists a `package.json` higher up.
68 | // This might happen if you have a folder of utility JS files that you symlink but did not organize as a package.
69 | // We consider this an unsupported edge case for now.
70 | while (searchPath !== stopPath && searchPath !== previousSearchPath) {
71 | // If we have already determined the `package.json` type for this path we can stop searching.
72 | // We do however still need to cache the found value,
73 | // from the `resourcePath` folder up to the matching `searchPath`,
74 | // to avoid retracing these steps when processing sibling resources.
75 | if (packageJsonTypeMap.has(searchPath)) {
76 | packageJsonType = packageJsonTypeMap.get(searchPath);
77 |
78 | let currentPath = resourceContext;
79 | while (currentPath !== searchPath) {
80 | // We set the found type at least level from `resourcePath` folder up to the matching `searchPath`
81 | packageJsonTypeMap.set(currentPath, packageJsonType);
82 | currentPath = path.dirname(currentPath);
83 | }
84 | break;
85 | }
86 |
87 | let packageJsonPath = path.join(searchPath, 'package.json');
88 | try {
89 | const packageSource = await fs.readFile(packageJsonPath, 'utf-8');
90 | try {
91 | const packageObject = JSON.parse(packageSource);
92 |
93 | // Any package.json is sufficient as long as it can be parsed.
94 | // If it does not explicitly have a `type: "module"` it will be assumed to be CommonJS.
95 | packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
96 | packageJsonTypeMap.set(searchPath, packageJsonType);
97 |
98 | // We set the type in the cache for all paths from the `resourcePath` folder,
99 | // up to the matching `searchPath` to avoid retracing these steps when processing sibling resources.
100 | let currentPath = resourceContext;
101 | while (currentPath !== searchPath) {
102 | packageJsonTypeMap.set(currentPath, packageJsonType);
103 | currentPath = path.dirname(currentPath);
104 | }
105 | } catch (e) {
106 | // `package.json` exists but could not be parsed.
107 | // We track it as a dependency so we can reload if this file changes.
108 | }
109 |
110 | this.addDependency(packageJsonPath);
111 | break;
112 | } catch (e) {
113 | // `package.json` does not exist.
114 | // We track it as a missing dependency so we can reload if this file is added.
115 | this.addMissingDependency(packageJsonPath);
116 | }
117 |
118 | // Try again at the next level up
119 | previousSearchPath = searchPath;
120 | searchPath = path.dirname(searchPath);
121 | }
122 |
123 | // Check `package.json` for the `type` field -
124 | // fallback to use `cjs` for anything ambiguous.
125 | return packageJsonType === 'module' ? 'esm' : 'cjs';
126 | }
127 |
128 | module.exports = getModuleSystem;
129 |
--------------------------------------------------------------------------------
/test/loader/unit/getRefreshModuleRuntime.test.js:
--------------------------------------------------------------------------------
1 | const { Template } = require('webpack');
2 | const getRefreshModuleRuntime = require('../../../loader/utils/getRefreshModuleRuntime');
3 |
4 | describe('getRefreshModuleRuntime', () => {
5 | it('should return working refresh module runtime without const using CommonJS', () => {
6 | const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
7 | const: false,
8 | moduleSystem: 'cjs',
9 | });
10 |
11 | expect(refreshModuleRuntime.indexOf('var')).not.toBe(-1);
12 | expect(refreshModuleRuntime.indexOf('const')).toBe(-1);
13 | expect(refreshModuleRuntime.indexOf('let')).toBe(-1);
14 | expect(refreshModuleRuntime.indexOf('module.hot')).not.toBe(-1);
15 | expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).toBe(-1);
16 | expect(refreshModuleRuntime).toMatchInlineSnapshot(`
17 | "var $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;
18 | var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
19 | $ReactRefreshModuleId$
20 | );
21 |
22 | function $ReactRefreshModuleRuntime$(exports) {
23 | if (module.hot) {
24 | var errorOverlay;
25 | if (typeof __react_refresh_error_overlay__ !== 'undefined') {
26 | errorOverlay = __react_refresh_error_overlay__;
27 | }
28 | var testMode;
29 | if (typeof __react_refresh_test__ !== 'undefined') {
30 | testMode = __react_refresh_test__;
31 | }
32 | return __react_refresh_utils__.executeRuntime(
33 | exports,
34 | $ReactRefreshModuleId$,
35 | module.hot,
36 | errorOverlay,
37 | testMode
38 | );
39 | }
40 | }
41 |
42 | if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
43 | $ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
44 | } else {
45 | $ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
46 | }"
47 | `);
48 | });
49 |
50 | it('should return working refresh module runtime with const using CommonJS', () => {
51 | const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
52 | const: true,
53 | moduleSystem: 'cjs',
54 | });
55 |
56 | expect(refreshModuleRuntime.indexOf('var')).toBe(-1);
57 | expect(refreshModuleRuntime.indexOf('const')).not.toBe(-1);
58 | expect(refreshModuleRuntime.indexOf('let')).not.toBe(-1);
59 | expect(refreshModuleRuntime.indexOf('module.hot')).not.toBe(-1);
60 | expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).toBe(-1);
61 | expect(refreshModuleRuntime).toMatchInlineSnapshot(`
62 | "const $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;
63 | const $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
64 | $ReactRefreshModuleId$
65 | );
66 |
67 | function $ReactRefreshModuleRuntime$(exports) {
68 | if (module.hot) {
69 | let errorOverlay;
70 | if (typeof __react_refresh_error_overlay__ !== 'undefined') {
71 | errorOverlay = __react_refresh_error_overlay__;
72 | }
73 | let testMode;
74 | if (typeof __react_refresh_test__ !== 'undefined') {
75 | testMode = __react_refresh_test__;
76 | }
77 | return __react_refresh_utils__.executeRuntime(
78 | exports,
79 | $ReactRefreshModuleId$,
80 | module.hot,
81 | errorOverlay,
82 | testMode
83 | );
84 | }
85 | }
86 |
87 | if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
88 | $ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
89 | } else {
90 | $ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
91 | }"
92 | `);
93 | });
94 |
95 | it('should return working refresh module runtime without const using ES Modules', () => {
96 | const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
97 | const: false,
98 | moduleSystem: 'esm',
99 | });
100 |
101 | expect(refreshModuleRuntime.indexOf('var')).not.toBe(-1);
102 | expect(refreshModuleRuntime.indexOf('const')).toBe(-1);
103 | expect(refreshModuleRuntime.indexOf('let')).toBe(-1);
104 | expect(refreshModuleRuntime.indexOf('module.hot')).toBe(-1);
105 | expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).not.toBe(-1);
106 | expect(refreshModuleRuntime).toMatchInlineSnapshot(`
107 | "var $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;
108 | var $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
109 | $ReactRefreshModuleId$
110 | );
111 |
112 | function $ReactRefreshModuleRuntime$(exports) {
113 | if (import.meta.webpackHot) {
114 | var errorOverlay;
115 | if (typeof __react_refresh_error_overlay__ !== 'undefined') {
116 | errorOverlay = __react_refresh_error_overlay__;
117 | }
118 | var testMode;
119 | if (typeof __react_refresh_test__ !== 'undefined') {
120 | testMode = __react_refresh_test__;
121 | }
122 | return __react_refresh_utils__.executeRuntime(
123 | exports,
124 | $ReactRefreshModuleId$,
125 | import.meta.webpackHot,
126 | errorOverlay,
127 | testMode
128 | );
129 | }
130 | }
131 |
132 | if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
133 | $ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
134 | } else {
135 | $ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
136 | }"
137 | `);
138 | });
139 |
140 | it('should return working refresh module runtime with const using ES Modules', () => {
141 | const refreshModuleRuntime = getRefreshModuleRuntime(Template, {
142 | const: true,
143 | moduleSystem: 'esm',
144 | });
145 |
146 | expect(refreshModuleRuntime.indexOf('var')).toBe(-1);
147 | expect(refreshModuleRuntime.indexOf('const')).not.toBe(-1);
148 | expect(refreshModuleRuntime.indexOf('let')).not.toBe(-1);
149 | expect(refreshModuleRuntime.indexOf('module.hot')).toBe(-1);
150 | expect(refreshModuleRuntime.indexOf('import.meta.webpackHot')).not.toBe(-1);
151 | expect(refreshModuleRuntime).toMatchInlineSnapshot(`
152 | "const $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;
153 | const $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
154 | $ReactRefreshModuleId$
155 | );
156 |
157 | function $ReactRefreshModuleRuntime$(exports) {
158 | if (import.meta.webpackHot) {
159 | let errorOverlay;
160 | if (typeof __react_refresh_error_overlay__ !== 'undefined') {
161 | errorOverlay = __react_refresh_error_overlay__;
162 | }
163 | let testMode;
164 | if (typeof __react_refresh_test__ !== 'undefined') {
165 | testMode = __react_refresh_test__;
166 | }
167 | return __react_refresh_utils__.executeRuntime(
168 | exports,
169 | $ReactRefreshModuleId$,
170 | import.meta.webpackHot,
171 | errorOverlay,
172 | testMode
173 | );
174 | }
175 | }
176 |
177 | if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {
178 | $ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);
179 | } else {
180 | $ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);
181 | }"
182 | `);
183 | });
184 | });
185 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | ## Directives
4 |
5 | The `react-refresh/babel` plugin provide support to directive comments out of the box.
6 |
7 | ### `reset`
8 |
9 | ```js
10 | /* @refresh reset */
11 | ```
12 |
13 | This directive tells React Refresh to force reset state on every refresh (current file only).
14 | This can be useful, for example, to reset error boundary components' state,
15 | so it doesn't persist when new code is executed.
16 |
17 | ## Options
18 |
19 | This plugin accepts a few options to tweak its behaviour.
20 |
21 | In usual scenarios, you probably wouldn't have to reach for them -
22 | they exist specifically to enable integration in more advanced/complicated setups.
23 |
24 | ### `ReactRefreshPluginOptions`
25 |
26 | ```ts
27 | interface ReactRefreshPluginOptions {
28 | forceEnable?: boolean;
29 | exclude?: string | RegExp | Array;
30 | include?: string | RegExp | Array;
31 | library?: string;
32 | esModule?: boolean | ESModuleOptions;
33 | overlay?: boolean | ErrorOverlayOptions;
34 | }
35 | ```
36 |
37 | #### `forceEnable`
38 |
39 | Type: `boolean`
40 |
41 | Default: `undefined`
42 |
43 | Enables the plugin forcefully.
44 |
45 | It is useful if you want to:
46 |
47 | - Use the plugin in production;
48 | - Use the plugin with the `none` mode in Webpack without setting `NODE_ENV`;
49 | - Use the plugin in environments we do not support, such as `electron-prerender`
50 | (**WARNING: Proceed at your own risk!**).
51 |
52 | #### `exclude`
53 |
54 | Type: `string | RegExp | Array`
55 |
56 | Default: `/node_modules/`
57 |
58 | Exclude files from being processed by the plugin.
59 | This is similar to the `module.rules` option in Webpack.
60 |
61 | #### `include`
62 |
63 | Type: `string | RegExp | Array`
64 |
65 | Default: `/\.([jt]sx?|flow)$/i`
66 |
67 | Include files to be processed by the plugin.
68 | This is similar to the `module.rules` option in Webpack.
69 |
70 | #### `library`
71 |
72 | Type: `string`
73 |
74 | Default: `''`, or `output.uniqueName` in Webpack 5, or `output.library` for both Webpack 4/5 if set
75 |
76 | Sets a namespace for the React Refresh runtime.
77 | This is similar to the `output.uniqueName` in Webpack 5 or the `output.library` option in Webpack 4/5.
78 |
79 | It is most useful when multiple instances of React Refresh is running together simultaneously.
80 |
81 | #### `esModule`
82 |
83 | Type: `boolean | ESModuleOptions`
84 |
85 | Default: `undefined` (auto-detection)
86 |
87 | Enables strict ES Modules compatible runtime.
88 | By default, the plugin will try to infer the module system same as Webpack 5,
89 | either via the `type` property in `package.json` (`commonjs` and `module`),
90 | or via the file extension (`.cjs` and `.mjs`).
91 |
92 | It is most useful when you want to enforce output of native ESM code.
93 |
94 | See the [`ESModuleOptions`](#esmoduleoptions) section below for more details on the object API.
95 |
96 | #### `overlay`
97 |
98 | Type: `boolean | ErrorOverlayOptions`
99 |
100 | Default:
101 |
102 | ```json5
103 | {
104 | entry: '@pmmmwh/react-refresh-webpack-plugin/client/ErrorOverlayEntry',
105 | module: '@pmmmwh/react-refresh-webpack-plugin/overlay',
106 | sockIntegration: 'wds',
107 | }
108 | ```
109 |
110 | Modifies behaviour of the plugin's error overlay integration:
111 |
112 | - If `overlay` is not provided or `true`, the **DEFAULT** behaviour will be used;
113 | - If `overlay` is `false`, the error overlay integration will be **DISABLED**;
114 | - If `overlay` is an object (`ErrorOverlayOptions`), it will act with respect to what is provided
115 | (\*NOTE: This is targeted for ADVANCED use cases.).
116 |
117 | See the [`ErrorOverlayOptions`](#erroroverlayoptions) section below for more details on the object API.
118 |
119 | ### `ESModuleOptions`
120 |
121 | ```ts
122 | interface ESModuleOptions {
123 | exclude?: string | RegExp | Array;
124 | include?: string | RegExp | Array;
125 | }
126 | ```
127 |
128 | #### `exclude`
129 |
130 | Type: `string | RegExp | Array`
131 |
132 | Default: `/node_modules/`
133 |
134 | Exclude files from being processed as ESM.
135 | This is similar to the `module.rules` option in Webpack.
136 |
137 | #### `include`
138 |
139 | Type: `string | RegExp | Array`
140 |
141 | Default: `/\.([jt]sx?|flow)$/i`
142 |
143 | Include files to be processed as ESM.
144 | This is similar to the `module.rules` option in Webpack.
145 |
146 | ### `ErrorOverlayOptions`
147 |
148 | ```ts
149 | interface ErrorOverlayOptions {
150 | entry?: string | false;
151 | module?: string | false;
152 | sockIntegration?: 'wds' | 'whm' | 'wps' | false | string;
153 | }
154 | ```
155 |
156 | #### `entry`
157 |
158 | Type: `string | false`
159 |
160 | Default: `'@pmmmwh/react-refresh-webpack-plugin/client/ErrorOverlayEntry'`
161 |
162 | The **PATH** to a file/module that sets up the error overlay integration.
163 | Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
164 |
165 | When set to `false`, no code will be injected for this stage.
166 |
167 | #### `module`
168 |
169 | Type: `string | false`
170 |
171 | Default: `'@pmmmwh/react-refresh-webpack-plugin/overlay'`
172 |
173 | The **PATH** to a file/module to be used as an error overlay (e.g. `react-error-overlay`).
174 | Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
175 |
176 | The provided file should contain two **NAMED** exports:
177 |
178 | ```ts
179 | function handleRuntimeError(error: Error) {}
180 | function clearRuntimeErrors() {}
181 | ```
182 |
183 | - `handleRuntimeError` is invoked when a **RUNTIME** error is **CAUGHT** (e.g. during module initialisation or execution);
184 | - `clearRuntimeErrors` is invoked when a module is **RE-INITIALISED** via "Fast Refresh".
185 |
186 | If the default `entry` is used, the file should contain two more **NAMED** exports:
187 |
188 | ```ts
189 | function showCompileError(webpackErrorMessage: string) {}
190 | function clearCompileError() {}
191 | ```
192 |
193 | - `showCompileError` is invoked when an error occurred during a Webpack compilation
194 | (NOTE: `webpackErrorMessage` might be ANSI encoded depending on the integration);
195 | - `clearCompileError` is invoked when a new Webpack compilation is started (i.e. HMR rebuild).
196 |
197 | > Note: if you want to use `react-error-overlay` as a value to this option,
198 | > you should instead use `react-dev-utils/refreshOverlayInterop` or implement a similar interop.
199 | > The APIs expected by this plugin is slightly different from what `react-error-overlay` provides out-of-the-box.
200 |
201 | #### `sockIntegration`
202 |
203 | Default: `wds`
204 |
205 | Type: `wds`, `whm`, `wps`, `false` or `string`
206 |
207 | The HMR integration that the error overlay will interact with -
208 | it can either be a short form name of the integration (`wds`, `whm`, `wps`),
209 | or a **PATH** to a file/module that sets up a connection to receive Webpack build messages.
210 | Both **ABSOLUTE** and **RELATIVE** paths are acceptable, but it is recommended to use the **FORMER**.
211 |
212 | Common HMR integrations (for Webpack) are support by this plugin out-of-the-box:
213 |
214 | - For `webpack-dev-server`, you can skip this option or set it to `wds`;
215 | - For `webpack-hot-middleware`, set this option to `whm`;
216 | - For `webpack-plugin-serve`, set this option to `wps`.
217 |
218 | If you use any other HMR integrations (e.g. custom ones), or if you want to customise how the connection is being setup,
219 | you will need to implement a message client in the provided file/module.
220 | You can reference implementations inside the [`sockets`](https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/main/sockets) directory.
221 |
--------------------------------------------------------------------------------
/test/loader/validateOptions.test.js:
--------------------------------------------------------------------------------
1 | describe('validateOptions', () => {
2 | let getCompilation;
3 |
4 | beforeEach(() => {
5 | jest.isolateModules(() => {
6 | getCompilation = require('../helpers/compilation');
7 | });
8 | });
9 |
10 | it('should accept "const" when it is true', async () => {
11 | const compilation = await getCompilation('cjs', { loaderOptions: { const: true } });
12 | expect(compilation.errors).toStrictEqual([]);
13 | });
14 |
15 | it('should accept "const" when it is false', async () => {
16 | const compilation = await getCompilation('cjs', { loaderOptions: { const: false } });
17 | expect(compilation.errors).toStrictEqual([]);
18 | });
19 |
20 | it('should reject "const" when it is not a boolean', async () => {
21 | const compilation = await getCompilation('cjs', { loaderOptions: { const: 1 } });
22 | expect(compilation.errors).toHaveLength(1);
23 | expect(compilation.errors[0]).toMatchInlineSnapshot(`
24 | "Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
25 | - options.const should be a boolean."
26 | `);
27 | });
28 |
29 | it('should accept "esModule" when it is true', async () => {
30 | const compilation = await getCompilation('esm', {
31 | loaderOptions: { esModule: true },
32 | });
33 | expect(compilation.errors).toStrictEqual([]);
34 | });
35 |
36 | it('should accept "esModule" when it is false', async () => {
37 | const compilation = await getCompilation('cjs', {
38 | loaderOptions: { esModule: false },
39 | });
40 | expect(compilation.errors).toStrictEqual([]);
41 | });
42 |
43 | it('should accept "esModule" when it is undefined', async () => {
44 | const compilation = await getCompilation('cjs', { loaderOptions: {} });
45 | expect(compilation.errors).toStrictEqual([]);
46 | });
47 |
48 | it('should accept "esModule" when it is an empty object', async () => {
49 | const compilation = await getCompilation('cjs', { loaderOptions: { esModule: {} } });
50 | expect(compilation.errors).toStrictEqual([]);
51 | });
52 |
53 | it('should reject "esModule" when it is not a boolean nor an object', async () => {
54 | const compilation = await getCompilation('cjs', {
55 | loaderOptions: { esModule: 'esModule' },
56 | });
57 | expect(compilation.errors).toHaveLength(1);
58 | expect(compilation.errors[0]).toMatchInlineSnapshot(`
59 | "Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
60 | - options.esModule should be one of these:
61 | boolean | object { exclude?, include? }
62 | Details:
63 | * options.esModule should be a boolean.
64 | * options.esModule should be an object:
65 | object { exclude?, include? }"
66 | `);
67 | });
68 |
69 | it('should accept "esModule.exclude" when it is a RegExp', async () => {
70 | const compilation = await getCompilation('cjs', {
71 | loaderOptions: { esModule: { exclude: /index\.js/ } },
72 | });
73 | expect(compilation.errors).toStrictEqual([]);
74 | });
75 |
76 | it('should accept "esModule.exclude" when it is an absolute path string', async () => {
77 | const compilation = await getCompilation('cjs', {
78 | loaderOptions: { esModule: { exclude: '/index.js' } },
79 | });
80 | expect(compilation.errors).toStrictEqual([]);
81 | });
82 |
83 | it('should accept "esModule.exclude" when it is a string', async () => {
84 | const compilation = await getCompilation('cjs', {
85 | loaderOptions: { esModule: { exclude: 'index.js' } },
86 | });
87 | expect(compilation.errors).toStrictEqual([]);
88 | });
89 |
90 | it('should accept "esModule.exclude" when it is an array of RegExp or strings', async () => {
91 | const compilation = await getCompilation('cjs', {
92 | loaderOptions: { esModule: { exclude: [/index\.js/, 'index.js'] } },
93 | });
94 | expect(compilation.errors).toStrictEqual([]);
95 | });
96 |
97 | it('should reject "esModule.exclude" when it is an object', async () => {
98 | const compilation = await getCompilation('cjs', {
99 | loaderOptions: { esModule: { exclude: {} } },
100 | });
101 | expect(compilation.errors).toHaveLength(1);
102 | expect(compilation.errors[0]).toMatchInlineSnapshot(`
103 | "Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
104 | - options.esModule should be one of these:
105 | boolean | object { exclude?, include? }
106 | Details:
107 | * options.esModule.exclude should be one of these:
108 | RegExp | string
109 | Details:
110 | * options.esModule.exclude should be an instance of RegExp.
111 | * options.esModule.exclude should be a string.
112 | * options.esModule.exclude should be an array:
113 | [RegExp | string, ...] (should not have fewer than 1 item)
114 | * options.esModule.exclude should be one of these:
115 | RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)"
116 | `);
117 | });
118 |
119 | it('should accept "esModule.include" when it is a RegExp', async () => {
120 | const compilation = await getCompilation('esm', {
121 | loaderOptions: { esModule: { include: /index\.js/ } },
122 | });
123 | expect(compilation.errors).toStrictEqual([]);
124 | });
125 |
126 | it('should accept "esModule.include" when it is an absolute path string', async () => {
127 | const compilation = await getCompilation('esm', {
128 | loaderOptions: { esModule: { include: '/index.js' } },
129 | });
130 | expect(compilation.errors).toStrictEqual([]);
131 | });
132 |
133 | it('should accept "esModule.include" when it is a string', async () => {
134 | const compilation = await getCompilation('esm', {
135 | loaderOptions: { esModule: { include: 'index.js' } },
136 | });
137 | expect(compilation.errors).toStrictEqual([]);
138 | });
139 |
140 | it('should accept "esModule.include" when it is an array of RegExp or strings', async () => {
141 | const compilation = await getCompilation('esm', {
142 | loaderOptions: { esModule: { include: [/index\.js/, 'index.js'] } },
143 | });
144 | expect(compilation.errors).toStrictEqual([]);
145 | });
146 |
147 | it('should reject "esModule.include" when it is an object', async () => {
148 | const compilation = await getCompilation('esm', {
149 | loaderOptions: { esModule: { include: {} } },
150 | });
151 | expect(compilation.errors).toHaveLength(1);
152 | expect(compilation.errors[0]).toMatchInlineSnapshot(`
153 | "Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
154 | - options.esModule should be one of these:
155 | boolean | object { exclude?, include? }
156 | Details:
157 | * options.esModule.include should be one of these:
158 | RegExp | string
159 | Details:
160 | * options.esModule.include should be an instance of RegExp.
161 | * options.esModule.include should be a string.
162 | * options.esModule.include should be an array:
163 | [RegExp | string, ...] (should not have fewer than 1 item)
164 | * options.esModule.include should be one of these:
165 | RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item)"
166 | `);
167 | });
168 |
169 | it('should reject any unknown options', async () => {
170 | const compilation = await getCompilation('cjs', {
171 | loaderOptions: { unknown: 'unknown' },
172 | });
173 | expect(compilation.errors).toHaveLength(1);
174 | expect(compilation.errors[0]).toMatchInlineSnapshot(`
175 | "Invalid options object. React Refresh Loader has been initialized using an options object that does not match the API schema.
176 | - options has an unknown property 'unknown'. These properties are valid:
177 | object { const?, esModule? }"
178 | `);
179 | });
180 | });
181 |
--------------------------------------------------------------------------------
/test/helpers/sandbox/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fse = require('fs-extra');
3 | const { nanoid } = require('nanoid');
4 | const { getIndexHTML, getPackageJson, getWDSConfig } = require('./configs');
5 | const { killTestProcess, spawnWebpackServe } = require('./spawn');
6 |
7 | // Extends the timeout for tests using the sandbox
8 | jest.setTimeout(1000 * 60);
9 |
10 | // Setup a global "queue" of cleanup handlers to allow auto-teardown of tests,
11 | // even when they did not run the cleanup function.
12 | /** @type {Map>} */
13 | const cleanupHandlers = new Map();
14 | afterEach(async () => {
15 | for (const [, callback] of cleanupHandlers) {
16 | await callback();
17 | }
18 | });
19 |
20 | /**
21 | * Logs output to the console (only in debug mode).
22 | * @param {...*} args
23 | * @returns {void}
24 | */
25 | const log = (...args) => {
26 | if (__DEBUG__) {
27 | console.log(...args);
28 | }
29 | };
30 |
31 | /**
32 | * Pause current asynchronous execution for provided milliseconds.
33 | * @param {number} ms
34 | * @returns {Promise}
35 | */
36 | const sleep = (ms) => {
37 | return new Promise((resolve) => {
38 | setTimeout(resolve, ms);
39 | });
40 | };
41 |
42 | /**
43 | * @typedef {Object} SandboxSession
44 | * @property {boolean} didFullRefresh
45 | * @property {*[]} errors
46 | * @property {*[]} logs
47 | * @property {function(): void} resetState
48 | * @property {function(string, string): Promise} write
49 | * @property {function(string, string): Promise} patch
50 | * @property {function(string): Promise} remove
51 | * @property {function(*, ...*=): Promise<*>} evaluate
52 | * @property {function(): Promise} reload
53 | */
54 |
55 | const rootSandboxDir = path.join(__dirname, '../..', '__tmp__');
56 |
57 | /**
58 | * Creates a Webpack and Puppeteer backed sandbox to execute HMR operations on.
59 | * @param {Object} [options]
60 | * @param {boolean} [options.esModule]
61 | * @param {string} [options.id]
62 | * @param {Map} [options.initialFiles]
63 | * @returns {Promise<[SandboxSession, function(): Promise]>}
64 | */
65 | async function getSandbox({ esModule = false, id = nanoid(), initialFiles = new Map() } = {}) {
66 | const { default: getPort } = await import('get-port');
67 | const port = await getPort();
68 |
69 | // Get sandbox directory paths
70 | const sandboxDir = path.join(rootSandboxDir, id);
71 | const srcDir = path.join(sandboxDir, 'src');
72 | const publicDir = path.join(sandboxDir, 'public');
73 | // In case of an ID clash, remove the existing sandbox directory
74 | await fse.remove(sandboxDir);
75 | // Create the sandbox source directory
76 | await fse.mkdirp(srcDir);
77 | // Create the sandbox public directory
78 | await fse.mkdirp(publicDir);
79 |
80 | // Write necessary files to sandbox
81 | await fse.writeFile(path.join(sandboxDir, 'webpack.config.js'), getWDSConfig(srcDir));
82 | await fse.writeFile(path.join(publicDir, 'index.html'), getIndexHTML(port));
83 | await fse.writeFile(path.join(srcDir, 'package.json'), getPackageJson(esModule));
84 | await fse.writeFile(
85 | path.join(srcDir, 'index.js'),
86 | esModule
87 | ? `export default function Sandbox() { return 'new sandbox'; }`
88 | : "module.exports = function Sandbox() { return 'new sandbox'; };"
89 | );
90 |
91 | // Write initial files to sandbox
92 | for (const [filePath, fileContent] of initialFiles.entries()) {
93 | await fse.writeFile(path.join(srcDir, filePath), fileContent);
94 | }
95 |
96 | // TODO: Add handling for webpack-hot-middleware and webpack-plugin-serve
97 | const app = await spawnWebpackServe(port, { public: publicDir, root: sandboxDir, src: srcDir });
98 | /** @type {import('puppeteer').Page} */
99 | const page = await browser.newPage();
100 |
101 | await page.goto(`http://localhost:${port}/`);
102 |
103 | let didFullRefresh = false;
104 | /** @type {string[]} */
105 | let errors = [];
106 | /** @type {string[]} */
107 | let logs = [];
108 |
109 | // Expose logging and hot callbacks to the page
110 | await Promise.all([
111 | page.exposeFunction('log', (...args) => {
112 | logs.push(args.join(' '));
113 | }),
114 | page.exposeFunction('onHotAcceptError', (errorMessage) => {
115 | errors.push(errorMessage);
116 | }),
117 | page.exposeFunction('onHotSuccess', () => {
118 | page.emit('hotSuccess');
119 | }),
120 | ]);
121 |
122 | // Reset testing logs and errors on any navigation.
123 | // This is done for the main frame only,
124 | // because child frames (e.g. iframes) might attach to the document,
125 | // which will cause this event to fire.
126 | page.on('framenavigated', (frame) => {
127 | if (frame === page.mainFrame()) {
128 | resetState();
129 | }
130 | });
131 |
132 | /** @returns {void} */
133 | function resetState() {
134 | errors = [];
135 | logs = [];
136 | }
137 |
138 | async function cleanupSandbox() {
139 | try {
140 | await page.close();
141 | await killTestProcess(app);
142 |
143 | if (!__DEBUG__) {
144 | await fse.remove(sandboxDir);
145 | }
146 |
147 | // Remove current cleanup handler from the global queue since it has been called
148 | cleanupHandlers.delete(id);
149 | } catch (e) {
150 | // Do nothing
151 | }
152 | }
153 |
154 | // Cache the cleanup handler for global cleanup
155 | // This is done in case tests fail and async handlers are kept alive
156 | cleanupHandlers.set(id, cleanupSandbox);
157 |
158 | return [
159 | {
160 | /** @type {boolean} */
161 | get didFullRefresh() {
162 | return didFullRefresh;
163 | },
164 | /** @type {*[]} */
165 | get errors() {
166 | return errors;
167 | },
168 | /** @type {*[]} */
169 | get logs() {
170 | return logs;
171 | },
172 | /** @returns {void} */
173 | resetState,
174 | /**
175 | * @param {string} fileName
176 | * @param {string} content
177 | * @returns {Promise}
178 | */
179 | async write(fileName, content) {
180 | // Update the file on filesystem
181 | const fullFileName = path.join(srcDir, fileName);
182 | const directory = path.dirname(fullFileName);
183 | await fse.mkdirp(directory);
184 | await fse.writeFile(fullFileName, content);
185 | },
186 | /**
187 | * @param {string} fileName
188 | * @param {string} content
189 | * @returns {Promise}
190 | */
191 | async patch(fileName, content) {
192 | // Register an event for HMR completion
193 | let hmrStatus = 'pending';
194 | // Parallelize file writing and event listening to prevent race conditions
195 | await Promise.all([
196 | this.write(fileName, content),
197 | new Promise((resolve) => {
198 | const hmrTimeout = setTimeout(() => {
199 | hmrStatus = 'timeout';
200 | resolve();
201 | }, 30 * 1000);
202 |
203 | // Frame Navigate and Hot Success events have to be exclusive,
204 | // so we remove the other listener when one of them is triggered.
205 |
206 | /**
207 | * @param {import('puppeteer').Frame} frame
208 | * @returns {void}
209 | */
210 | const onFrameNavigate = (frame) => {
211 | if (frame === page.mainFrame()) {
212 | page.off('hotSuccess', onHotSuccess);
213 | clearTimeout(hmrTimeout);
214 | hmrStatus = 'reloaded';
215 | resolve();
216 | }
217 | };
218 |
219 | /**
220 | * @returns {void}
221 | */
222 | const onHotSuccess = () => {
223 | page.off('framenavigated', onFrameNavigate);
224 | clearTimeout(hmrTimeout);
225 | hmrStatus = 'success';
226 | resolve();
227 | };
228 |
229 | // Make sure that the event listener is bound to trigger only once
230 | page.once('framenavigated', onFrameNavigate);
231 | page.once('hotSuccess', onHotSuccess);
232 | }),
233 | ]);
234 |
235 | if (hmrStatus === 'reloaded') {
236 | log('Application reloaded.');
237 | didFullRefresh = didFullRefresh || true;
238 | } else if (hmrStatus === 'success') {
239 | log('Hot update complete.');
240 | } else {
241 | throw new Error(`Application is in an inconsistent state: ${hmrStatus}.`);
242 | }
243 |
244 | // Slow down tests to wait for re-rendering
245 | await sleep(1000);
246 | },
247 | /**
248 | * @param {string} fileName
249 | * @returns {Promise}
250 | */
251 | async remove(fileName) {
252 | const fullFileName = path.join(srcDir, fileName);
253 | await fse.remove(fullFileName);
254 | },
255 | /**
256 | * @param {*} fn
257 | * @param {...*} restArgs
258 | * @returns {Promise<*>}
259 | */
260 | async evaluate(fn, ...restArgs) {
261 | if (typeof fn === 'function') {
262 | return await page.evaluate(fn, ...restArgs);
263 | } else {
264 | throw new Error('You must pass a function to be evaluated in the browser!');
265 | }
266 | },
267 | /** @returns {Promise} */
268 | async reload() {
269 | await page.reload({ waitUntil: 'networkidle2' });
270 | didFullRefresh = false;
271 | },
272 | },
273 | cleanupSandbox,
274 | ];
275 | }
276 |
277 | module.exports = getSandbox;
278 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const { validate: validateOptions } = require('schema-utils');
2 | const { getRefreshGlobalScope } = require('./globals');
3 | const {
4 | getAdditionalEntries,
5 | getIntegrationEntry,
6 | getSocketIntegration,
7 | injectRefreshLoader,
8 | makeRefreshRuntimeModule,
9 | normalizeOptions,
10 | } = require('./utils');
11 | const schema = require('./options.json');
12 |
13 | class ReactRefreshPlugin {
14 | /**
15 | * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
16 | */
17 | constructor(options = {}) {
18 | validateOptions(schema, options, {
19 | name: 'React Refresh Plugin',
20 | baseDataPath: 'options',
21 | });
22 |
23 | /**
24 | * @readonly
25 | * @type {import('./types').NormalizedPluginOptions}
26 | */
27 | this.options = normalizeOptions(options);
28 | }
29 |
30 | /**
31 | * Applies the plugin.
32 | * @param {import('webpack').Compiler} compiler A webpack compiler object.
33 | * @returns {void}
34 | */
35 | apply(compiler) {
36 | // Skip processing in non-development mode, but allow manual force-enabling
37 | if (
38 | // Webpack do not set process.env.NODE_ENV, so we need to check for mode.
39 | // Ref: https://github.com/webpack/webpack/issues/7074
40 | (compiler.options.mode !== 'development' ||
41 | // We also check for production process.env.NODE_ENV,
42 | // in case it was set and mode is non-development (e.g. 'none')
43 | (process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
44 | !this.options.forceEnable
45 | ) {
46 | return;
47 | }
48 |
49 | const logger = compiler.getInfrastructureLogger(this.constructor.name);
50 |
51 | // Get Webpack imports from compiler instance (if available) -
52 | // this allow mono-repos to use different versions of Webpack without conflicts.
53 | const webpack = compiler.webpack || require('webpack');
54 | const {
55 | DefinePlugin,
56 | EntryDependency,
57 | EntryPlugin,
58 | ModuleFilenameHelpers,
59 | NormalModule,
60 | ProvidePlugin,
61 | RuntimeGlobals,
62 | Template,
63 | } = webpack;
64 |
65 | // Inject react-refresh context to all Webpack entry points.
66 | const { overlayEntries, prependEntries } = getAdditionalEntries(this.options);
67 | // Prepended entries does not care about injection order,
68 | // so we can utilise EntryPlugin for simpler logic.
69 | for (const entry of prependEntries) {
70 | new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
71 | }
72 |
73 | const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
74 | const socketEntryData = [];
75 | compiler.hooks.make.tap(
76 | { name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
77 | (compilation) => {
78 | // Exhaustively search all entries for `integrationEntry`.
79 | // If found, mark those entries and the index of `integrationEntry`.
80 | for (const [name, entryData] of compilation.entries.entries()) {
81 | const index = entryData.dependencies.findIndex(
82 | (dep) => dep.request && dep.request.includes(integrationEntry)
83 | );
84 | if (index !== -1) {
85 | socketEntryData.push({ name, index });
86 | }
87 | }
88 | }
89 | );
90 |
91 | // Overlay entries need to be injected AFTER integration's entry,
92 | // so we will loop through everything in `finishMake` instead of `make`.
93 | // This ensures we can traverse all entry points and inject stuff with the correct order.
94 | for (const [idx, entry] of overlayEntries.entries()) {
95 | compiler.hooks.finishMake.tapPromise(
96 | {
97 | name: this.constructor.name,
98 | stage: Number.MIN_SAFE_INTEGER + (overlayEntries.length - idx - 1),
99 | },
100 | (compilation) => {
101 | // Only hook into the current compiler
102 | if (compilation.compiler !== compiler) {
103 | return Promise.resolve();
104 | }
105 |
106 | const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }];
107 | return Promise.all(
108 | injectData.map(({ name, index }) => {
109 | return new Promise((resolve, reject) => {
110 | const options = { name };
111 | const dep = EntryPlugin.createDependency(entry, options);
112 | compilation.addEntry(compiler.context, dep, options, (err) => {
113 | if (err) return reject(err);
114 |
115 | // If the entry is not a global one,
116 | // and we have registered the index for integration entry,
117 | // we will reorder all entry dependencies to our desired order.
118 | // That is, to have additional entries DIRECTLY behind integration entry.
119 | if (name && typeof index !== 'undefined') {
120 | const entryData = compilation.entries.get(name);
121 | entryData.dependencies.splice(
122 | index + 1,
123 | 0,
124 | entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
125 | );
126 | }
127 |
128 | resolve();
129 | });
130 | });
131 | })
132 | );
133 | }
134 | );
135 | }
136 |
137 | // Inject necessary modules and variables to bundle's global scope
138 | const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {});
139 | /** @type {Record}*/
140 | const definedModules = {
141 | // Mapping of react-refresh globals to Webpack runtime globals
142 | $RefreshReg$: `${refreshGlobal}.register`,
143 | $RefreshSig$: `${refreshGlobal}.signature`,
144 | 'typeof $RefreshReg$': 'function',
145 | 'typeof $RefreshSig$': 'function',
146 |
147 | // Library mode
148 | __react_refresh_library__: JSON.stringify(
149 | Template.toIdentifier(
150 | this.options.library ||
151 | compiler.options.output.uniqueName ||
152 | compiler.options.output.library
153 | )
154 | ),
155 | };
156 | /** @type {Record} */
157 | const providedModules = {
158 | __react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
159 | };
160 |
161 | if (this.options.overlay === false) {
162 | // Stub errorOverlay module so their calls can be erased
163 | definedModules.__react_refresh_error_overlay__ = false;
164 | definedModules.__react_refresh_socket__ = false;
165 | } else {
166 | if (this.options.overlay.module) {
167 | providedModules.__react_refresh_error_overlay__ = require.resolve(
168 | this.options.overlay.module
169 | );
170 | }
171 | if (this.options.overlay.sockIntegration) {
172 | providedModules.__react_refresh_socket__ = getSocketIntegration(
173 | this.options.overlay.sockIntegration
174 | );
175 | }
176 | }
177 |
178 | new DefinePlugin(definedModules).apply(compiler);
179 | new ProvidePlugin(providedModules).apply(compiler);
180 |
181 | const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
182 | let loggedHotWarning = false;
183 | compiler.hooks.compilation.tap(
184 | this.constructor.name,
185 | (compilation, { normalModuleFactory }) => {
186 | // Only hook into the current compiler
187 | if (compilation.compiler !== compiler) {
188 | return;
189 | }
190 |
191 | // Set factory for EntryDependency which is used to initialise the module
192 | compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);
193 |
194 | const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
195 | compilation.hooks.additionalTreeRuntimeRequirements.tap(
196 | this.constructor.name,
197 | // Setup react-refresh globals with a Webpack runtime module
198 | (chunk, runtimeRequirements) => {
199 | runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
200 | runtimeRequirements.add(RuntimeGlobals.moduleCache);
201 | runtimeRequirements.add(refreshGlobal);
202 | compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule());
203 | }
204 | );
205 |
206 | normalModuleFactory.hooks.afterResolve.tap(
207 | this.constructor.name,
208 | // Add react-refresh loader to process files that matches specified criteria
209 | (resolveData) => {
210 | injectRefreshLoader(resolveData.createData, {
211 | match,
212 | options: {
213 | const: compilation.runtimeTemplate.supportsConst(),
214 | esModule: this.options.esModule,
215 | },
216 | });
217 | }
218 | );
219 |
220 | NormalModule.getCompilationHooks(compilation).loader.tap(
221 | // `Infinity` ensures this check will run only after all other taps
222 | { name: this.constructor.name, stage: Infinity },
223 | // Check for existence of the HMR runtime -
224 | // it is the foundation to this plugin working correctly
225 | (context) => {
226 | if (!context.hot && !loggedHotWarning) {
227 | logger.warn(
228 | [
229 | 'Hot Module Replacement (HMR) is not enabled!',
230 | 'React Refresh requires HMR to function properly.',
231 | ].join(' ')
232 | );
233 | loggedHotWarning = true;
234 | }
235 | }
236 | );
237 | }
238 | );
239 | }
240 | }
241 |
242 | module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
243 | module.exports = ReactRefreshPlugin;
244 |
--------------------------------------------------------------------------------