├── .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 | --------------------------------------------------------------------------------