├── fixtures ├── vite │ ├── src │ │ ├── count.js │ │ ├── Counter.jsx │ │ ├── index.jsx │ │ └── App.jsx │ ├── vite.config.js │ ├── package.json │ └── index.html ├── parcel │ ├── src │ │ ├── count.js │ │ ├── Counter.jsx │ │ ├── index.jsx │ │ ├── index.html │ │ └── App.jsx │ └── package.json ├── webpack-cjs │ ├── src │ │ ├── count.js │ │ ├── index.js │ │ ├── Counter.js │ │ └── App.js │ ├── config │ │ ├── webpack │ │ │ └── persistentCache │ │ │ │ └── createEnvironmentHash.js │ │ ├── webpackDevServer.config.js │ │ ├── env.js │ │ ├── paths.js │ │ ├── modules.js │ │ └── webpack.config.js │ ├── public │ │ └── index.html │ ├── .gitignore │ ├── package.json │ └── scripts │ │ ├── start.js │ │ └── build.js ├── webpack │ ├── src │ │ ├── count.js │ │ ├── Counter.js │ │ ├── index.js │ │ └── App.js │ ├── public │ │ └── index.html │ ├── .gitignore │ └── package.json ├── webpack-esm │ ├── src │ │ ├── count.js │ │ ├── Counter.js │ │ ├── index.js │ │ └── App.js │ ├── public │ │ └── index.html │ ├── .gitignore │ └── package.json ├── jest-cjs │ ├── package.json │ └── test.js └── jest-esm │ ├── test.js │ └── package.json ├── use-hot-module-reload ├── .npmignore ├── tsup.cjs.ts ├── tsup.mjs.ts ├── tsconfig.json ├── src │ ├── env.d.ts │ ├── use-hot-module-reload.cts │ └── use-hot-module-reload.mts └── package.json ├── .editorconfig ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── LICENSE └── package.json /fixtures/vite/src/count.js: -------------------------------------------------------------------------------- 1 | export const count = 0 2 | -------------------------------------------------------------------------------- /fixtures/parcel/src/count.js: -------------------------------------------------------------------------------- 1 | export const count = 3 2 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/src/count.js: -------------------------------------------------------------------------------- 1 | exports.count = 0 2 | -------------------------------------------------------------------------------- /fixtures/webpack/src/count.js: -------------------------------------------------------------------------------- 1 | export const count = 0 2 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/src/count.js: -------------------------------------------------------------------------------- 1 | export const count = 0 2 | -------------------------------------------------------------------------------- /use-hot-module-reload/.npmignore: -------------------------------------------------------------------------------- 1 | tsup.cjs.ts 2 | tsup.mjs.ts 3 | src 4 | -------------------------------------------------------------------------------- /fixtures/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/webpack/persistentCache/createEnvironmentHash.js: -------------------------------------------------------------------------------- 1 | const {createHash} = require('crypto') 2 | 3 | module.exports = (env) => { 4 | const hash = createHash('md5') 5 | hash.update(JSON.stringify(env)) 6 | 7 | return hash.digest('hex') 8 | } 9 | -------------------------------------------------------------------------------- /use-hot-module-reload/tsup.cjs.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/use-hot-module-reload.cts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: false, 8 | format: 'cjs', 9 | dts: true, 10 | }) 11 | -------------------------------------------------------------------------------- /use-hot-module-reload/tsup.mjs.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/use-hot-module-reload.mts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: false, 8 | format: 'esm', 9 | dts: true, 10 | }) 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /fixtures/parcel/src/Counter.jsx: -------------------------------------------------------------------------------- 1 | import {count} from './count' 2 | 3 | const counterStyle = { 4 | position: 'absolute', 5 | bottom: '10px', 6 | right: '10px', 7 | fontSize: '20px', 8 | } 9 | 10 | export function Counter() { 11 | return
{count}
12 | } 13 | -------------------------------------------------------------------------------- /fixtures/parcel/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')) 6 | root.render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /fixtures/vite/src/Counter.jsx: -------------------------------------------------------------------------------- 1 | import {count} from './count' 2 | 3 | const counterStyle = { 4 | position: 'absolute', 5 | bottom: '10px', 6 | right: '10px', 7 | fontSize: '20px', 8 | } 9 | 10 | export function Counter() { 11 | return
{count}
12 | } 13 | -------------------------------------------------------------------------------- /fixtures/vite/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')) 6 | root.render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /fixtures/webpack/src/Counter.js: -------------------------------------------------------------------------------- 1 | import {count} from './count' 2 | 3 | const counterStyle = { 4 | position: 'absolute', 5 | bottom: '10px', 6 | right: '10px', 7 | fontSize: '20px', 8 | } 9 | 10 | export function Counter() { 11 | return
{count}
12 | } 13 | -------------------------------------------------------------------------------- /fixtures/webpack/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')) 6 | root.render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/src/Counter.js: -------------------------------------------------------------------------------- 1 | import {count} from './count.js' 2 | 3 | const counterStyle = { 4 | position: 'absolute', 5 | bottom: '10px', 6 | right: '10px', 7 | fontSize: '20px', 8 | } 9 | 10 | export function Counter() { 11 | return
{count}
12 | } 13 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.js' 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')) 6 | root.render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/src/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const ReactDOM = require('react-dom/client') 3 | const App = require('./App') 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')) 6 | root.render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/src/Counter.js: -------------------------------------------------------------------------------- 1 | const {count} = require('./count') 2 | 3 | const counterStyle = { 4 | position: 'absolute', 5 | bottom: '10px', 6 | right: '10px', 7 | fontSize: '20px', 8 | } 9 | 10 | function Counter() { 11 | return
{count}
12 | } 13 | 14 | module.exports = {Counter} 15 | -------------------------------------------------------------------------------- /fixtures/webpack/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /fixtures/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "vite build", 7 | "clean": "rimraf dist", 8 | "start": "vite dev" 9 | }, 10 | "dependencies": { 11 | "@vitejs/plugin-react": "^2.0.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "vite": "^3.0.9" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /use-hot-module-reload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "strict": true, 6 | "module": "Preserve", 7 | "lib": ["ES2018", "DOM"], 8 | "noUncheckedIndexedAccess": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/webpack/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /fixtures/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "plugin:@typescript-eslint/recommended", 6 | "sanity/typescript", 7 | "prettier/@typescript-eslint", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "prettier/prettier": "error" 12 | }, 13 | "env": { 14 | "node": true, 15 | "browser": true 16 | }, 17 | "parserOptions": { 18 | "ecmaVersion": 2018 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/parcel/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Parcel React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Compiled files 5 | dist 6 | lib 7 | 8 | # Coverage directories used by tools like istanbul 9 | coverage 10 | .nyc_output 11 | 12 | # Dependency directories 13 | node_modules 14 | 15 | # MacOS 16 | .DS_Store 17 | 18 | # Non-yarn lockfiles 19 | package-lock.json 20 | pnpm-lock.json 21 | 22 | # Readme that is copied pre-publish 23 | use-hot-module-reload/README.md 24 | 25 | # Bundler caches 26 | .parcel-cache 27 | .vite 28 | -------------------------------------------------------------------------------- /fixtures/jest-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-cjs-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "jest", 7 | "clean": "echo 'nothing to do'" 8 | }, 9 | "jest": { 10 | "testEnvironment": "jsdom" 11 | }, 12 | "devDependencies": { 13 | "@testing-library/jest-dom": "^5.16.5", 14 | "@testing-library/react": "^14.0.0", 15 | "jest": "^29.5.0", 16 | "jest-environment-jsdom": "^29.5.0", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "use-hot-module-reload": "^1.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/jest-esm/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render, screen} from '@testing-library/react' 3 | import {useHotModuleReload} from 'use-hot-module-reload' 4 | 5 | import '@testing-library/jest-dom' 6 | 7 | function SomeComponent() { 8 | useHotModuleReload(() => console.log('it reloaded')) 9 | return React.createElement('div', {role: 'test'}, 'hello') 10 | } 11 | 12 | test('does not crash when using', async () => { 13 | render(React.createElement(SomeComponent, {})) 14 | await screen.findByRole('test') 15 | expect(screen.getByRole('test')).toHaveTextContent('hello') 16 | }) 17 | -------------------------------------------------------------------------------- /fixtures/jest-cjs/test.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const {render, screen} = require('@testing-library/react') 3 | const {useHotModuleReload} = require('use-hot-module-reload') 4 | 5 | require('@testing-library/jest-dom') 6 | 7 | function SomeComponent() { 8 | useHotModuleReload(() => console.log('it reloaded')) 9 | return React.createElement('div', {role: 'test'}, 'hello') 10 | } 11 | 12 | test('does not crash when using', async () => { 13 | render(React.createElement(SomeComponent, {})) 14 | await screen.findByRole('test') 15 | expect(screen.getByRole('test')).toHaveTextContent('hello') 16 | }) 17 | -------------------------------------------------------------------------------- /fixtures/jest-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-esm-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "NODE_OPTIONS=--experimental-vm-modules jest", 8 | "clean": "echo 'nothing to do'" 9 | }, 10 | "jest": { 11 | "testEnvironment": "jsdom", 12 | "transform": {} 13 | }, 14 | "devDependencies": { 15 | "@testing-library/jest-dom": "^5.16.5", 16 | "@testing-library/react": "^14.0.0", 17 | "jest": "^29.5.0", 18 | "jest-environment-jsdom": "^29.5.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "use-hot-module-reload": "^1.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "parcel build src/index.html", 7 | "clean": "rimraf dist", 8 | "start": "parcel src/index.html" 9 | }, 10 | "browserslist": { 11 | "production": [ 12 | ">0.2%", 13 | "not dead", 14 | "not op_mini all" 15 | ], 16 | "development": [ 17 | "last 1 chrome version", 18 | "last 1 firefox version", 19 | "last 1 safari version" 20 | ] 21 | }, 22 | "dependencies": { 23 | "parcel": "^2.7.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fixtures/vite/src/App.jsx: -------------------------------------------------------------------------------- 1 | import {useState, useCallback} from 'react' 2 | import {useHotModuleReload} from 'use-hot-module-reload' 3 | import {Counter} from './Counter' 4 | 5 | function App() { 6 | const [lastHMRed, setLastHMRed] = useState('') 7 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 8 | useHotModuleReload(updateHMRTime) 9 | 10 | return ( 11 | <> 12 |

useHotModuleReload() demo

13 | 14 |

15 | {lastHMRed 16 | ? `Last hot module reload at: ${lastHMRed}` 17 | : 'Edit some file to trigger an updated lastHMR'} 18 |

19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /fixtures/parcel/src/App.jsx: -------------------------------------------------------------------------------- 1 | import {useState, useCallback} from 'react' 2 | import {useHotModuleReload} from 'use-hot-module-reload' 3 | import {Counter} from './Counter' 4 | 5 | function App() { 6 | const [lastHMRed, setLastHMRed] = useState('') 7 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 8 | useHotModuleReload(updateHMRTime) 9 | 10 | return ( 11 | <> 12 |

useHotModuleReload() demo

13 | 14 |

15 | {lastHMRed 16 | ? `Last hot module reload at: ${lastHMRed}` 17 | : 'Edit some file to trigger an updated lastHMR'} 18 |

19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /fixtures/webpack/src/App.js: -------------------------------------------------------------------------------- 1 | import {useState, useCallback} from 'react' 2 | import {useHotModuleReload} from 'use-hot-module-reload' 3 | import {Counter} from './Counter' 4 | 5 | function App() { 6 | const [lastHMRed, setLastHMRed] = useState('') 7 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 8 | useHotModuleReload(updateHMRTime) 9 | 10 | return ( 11 | <> 12 |

useHotModuleReload() demo

13 | 14 |

15 | {lastHMRed 16 | ? `Last hot module reload at: ${lastHMRed}` 17 | : 'Edit some file to trigger an updated lastHMR'} 18 |

19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/src/App.js: -------------------------------------------------------------------------------- 1 | import {useState, useCallback} from 'react' 2 | import {useHotModuleReload} from 'use-hot-module-reload' 3 | import {Counter} from './Counter.js' 4 | 5 | function App() { 6 | const [lastHMRed, setLastHMRed] = useState('') 7 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 8 | useHotModuleReload(updateHMRTime) 9 | 10 | return ( 11 | <> 12 |

useHotModuleReload() demo

13 | 14 |

15 | {lastHMRed 16 | ? `Last hot module reload at: ${lastHMRed}` 17 | : 'Edit some file to trigger an updated lastHMR'} 18 |

19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/src/App.js: -------------------------------------------------------------------------------- 1 | const {useState, useCallback} = require('react') 2 | const {useHotModuleReload} = require('use-hot-module-reload') 3 | const {Counter} = require('./Counter') 4 | 5 | function App() { 6 | const [lastHMRed, setLastHMRed] = useState('') 7 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 8 | useHotModuleReload(updateHMRTime) 9 | 10 | return ( 11 | <> 12 |

useHotModuleReload() demo

13 | 14 |

15 | {lastHMRed 16 | ? `Last hot module reload at: ${lastHMRed}` 17 | : 'Edit some file to trigger an updated lastHMR'} 18 |

19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | module.exports = App 26 | -------------------------------------------------------------------------------- /fixtures/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "react-scripts build", 7 | "clean": "rimraf build", 8 | "start": "react-scripts start" 9 | }, 10 | "browserslist": { 11 | "production": [ 12 | ">0.2%", 13 | "not dead", 14 | "not op_mini all" 15 | ], 16 | "development": [ 17 | "last 1 chrome version", 18 | "last 1 firefox version", 19 | "last 1 safari version" 20 | ] 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app" 25 | ] 26 | }, 27 | "dependencies": { 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-scripts": "5.0.1", 31 | "use-hot-module-reload": "^1.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /use-hot-module-reload/src/env.d.ts: -------------------------------------------------------------------------------- 1 | // Added by `vite-plugin-react` 2 | interface Window { 3 | // eslint-disable-next-line camelcase 4 | __vite_plugin_react_timeout: number 5 | } 6 | 7 | // Added by webpack in CommonJS mode 8 | interface NodeModule { 9 | hot?: { 10 | addStatusHandler: (handler: (status: string) => void) => void 11 | removeStatusHandler: (handler: (status: string) => void) => void 12 | } 13 | } 14 | 15 | interface ImportMeta { 16 | // Added by webpack in ESM mode 17 | webpackHot?: { 18 | addStatusHandler: (handler: (status: string) => void) => void 19 | removeStatusHandler: (handler: (status: string) => void) => void 20 | } 21 | 22 | // Added by vite 23 | hot?: { 24 | on: (event: 'vite:beforeUpdate', handler: () => void) => void 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fixtures/webpack-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-esm-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "react-scripts build", 8 | "clean": "rimraf build", 9 | "start": "react-scripts start" 10 | }, 11 | "browserslist": { 12 | "production": [ 13 | ">0.2%", 14 | "not dead", 15 | "not op_mini all" 16 | ], 17 | "development": [ 18 | "last 1 chrome version", 19 | "last 1 firefox version", 20 | "last 1 safari version" 21 | ] 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app" 26 | ] 27 | }, 28 | "dependencies": { 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-scripts": "5.0.1", 32 | "use-hot-module-reload": "^1.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [2.0.0](https://github.com/sanity-io/ui/compare/v1.0.3...v2.0.0) (2024-03-22) 9 | 10 | ### ⚠ BREAKING CHANGES 11 | 12 | - Changed the way ESM/CJS is handled, defaulting to ESM mode with `type: 'module'` and using the `.js` extension by default. CommonJS imports are now done through a `.cjs` extension implicitly. While this _shouldn't_ be breaking, getting bundlers and environments conditional loading "just right" is a complex and sometimes brittle process. If you run into issues, please let us know. 13 | 14 | ### Bug Fixes 15 | 16 | - correct hasHMR check in ESM mode ([92a69e2](https://github.com/rexxars/use-hot-module-reload/commit/92a69e2df1252cfdf8079b560f96ececda1ca282)) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-hot-module-reload 2 | 3 | React hook that triggers a callback after hot-module reloading has been performed (for any module, not just the one it was used in). 4 | 5 | Works with modern versions of Webpack and Vite. 6 | Other bundlers may be added if they expose a way to listen for updates. 7 | Unsupported bundlers will not trigger the callback, but the hook should not break anything. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save use-hot-module-reload 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```tsx 18 | import {useState, useCallback} from 'react' 19 | import {useHotModuleReload} from 'use-hot-module-reload' 20 | 21 | export function MyComponent() { 22 | const [lastHMRed, setLastHMRed] = useState('') 23 | const updateHMRTime = useCallback(() => setLastHMRed(new Date().toISOString()), []) 24 | useHotModuleReload(updateHMRTime) 25 | 26 | return
{lastHMRed && `Last hot module reload at: ${lastHMRed}`}
27 | } 28 | ``` 29 | 30 | ## License 31 | 32 | MIT © [Espen Hovlandsdal](https://espen.codes/) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Espen Hovlandsdal 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 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware') 2 | const ignoredFiles = require('react-dev-utils/ignoredFiles') 3 | const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware') 4 | const paths = require('./paths') 5 | 6 | const host = process.env.HOST || '0.0.0.0' 7 | 8 | module.exports = function () { 9 | return { 10 | headers: { 11 | 'Access-Control-Allow-Origin': '*', 12 | 'Access-Control-Allow-Methods': '*', 13 | 'Access-Control-Allow-Headers': '*', 14 | }, 15 | static: { 16 | directory: paths.appPublic, 17 | publicPath: [paths.publicUrlOrPath], 18 | watch: { 19 | ignored: ignoredFiles(paths.appSrc), 20 | }, 21 | }, 22 | client: { 23 | overlay: { 24 | errors: true, 25 | warnings: false, 26 | }, 27 | }, 28 | devMiddleware: { 29 | publicPath: paths.publicUrlOrPath.slice(0, -1), 30 | }, 31 | host, 32 | historyApiFallback: { 33 | disableDotRule: true, 34 | index: paths.publicUrlOrPath, 35 | }, 36 | onBeforeSetupMiddleware(devServer) { 37 | devServer.app.use(evalSourceMapMiddleware(devServer)) 38 | }, 39 | onAfterSetupMiddleware(devServer) { 40 | devServer.app.use(redirectServedPath(paths.publicUrlOrPath)) 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /use-hot-module-reload/src/use-hot-module-reload.cts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react' 2 | 3 | // Allow us to short-circuit in production/non-HMR environments 4 | const hasHMR = (() => { 5 | try { 6 | return Boolean(typeof module !== 'undefined' && module.hot) 7 | } catch (err) { 8 | return false 9 | } 10 | })() 11 | 12 | /** 13 | * Trigger a callback after hot-module reloads (any, not only the module using the hook). 14 | * Use it to force recomputation of stale values and state that do not automatically update. 15 | * This should be an escape hatch - ideally you shouldn't need this. 16 | * 17 | * @param callback - The callback to be triggered after hot-module reloads. 18 | */ 19 | export function useHotModuleReload(callback: () => void): void { 20 | if (!hasHMR) { 21 | return undefined 22 | } 23 | 24 | return useCJSHotModuleReload(callback) 25 | } 26 | 27 | function useCJSHotModuleReload(callback: () => void): void { 28 | useEffect(() => { 29 | if (typeof module.hot === 'undefined' || typeof module.hot?.addStatusHandler !== 'function') { 30 | return undefined 31 | } 32 | 33 | // Webpack in CommonJS mode 34 | const statusHandler = (status: string): void => (status === 'idle' ? callback() : undefined) 35 | module.hot.addStatusHandler(statusHandler) 36 | 37 | return () => module.hot?.removeStatusHandler(statusHandler) 38 | }, [callback]) 39 | } 40 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/env.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Make sure that including paths.js after env.js will read .env variables. 5 | delete require.cache[require.resolve('./paths')] 6 | 7 | const NODE_ENV = process.env.NODE_ENV 8 | if (!NODE_ENV) { 9 | throw new Error('The NODE_ENV environment variable is required but was not specified.') 10 | } 11 | 12 | const appDirectory = fs.realpathSync(process.cwd()) 13 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 14 | .split(path.delimiter) 15 | .filter((folder) => folder && !path.isAbsolute(folder)) 16 | .map((folder) => path.resolve(appDirectory, folder)) 17 | .join(path.delimiter) 18 | 19 | const REACT_APP = /^REACT_APP_/i 20 | 21 | function getClientEnvironment(publicUrl) { 22 | const raw = Object.keys(process.env) 23 | .filter((key) => REACT_APP.test(key)) 24 | .reduce( 25 | (env, key) => { 26 | env[key] = process.env[key] 27 | return env 28 | }, 29 | { 30 | NODE_ENV: process.env.NODE_ENV || 'development', 31 | PUBLIC_URL: publicUrl, 32 | FAST_REFRESH: true, 33 | } 34 | ) 35 | 36 | const stringified = { 37 | 'process.env': Object.keys(raw).reduce((env, key) => { 38 | env[key] = JSON.stringify(raw[key]) 39 | return env 40 | }, {}), 41 | } 42 | 43 | return {raw, stringified} 44 | } 45 | 46 | module.exports = getClientEnvironment 47 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-cjs-fixture", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/build.js", 7 | "clean": "rimraf build", 8 | "start": "node scripts/start.js" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "react-app" 13 | ] 14 | }, 15 | "browserslist": { 16 | "production": [ 17 | ">0.2%", 18 | "not dead", 19 | "not op_mini all" 20 | ], 21 | "development": [ 22 | "last 1 chrome version", 23 | "last 1 firefox version", 24 | "last 1 safari version" 25 | ] 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app" 30 | ] 31 | }, 32 | "dependencies": { 33 | "@babel/core": "^7.16.0", 34 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", 35 | "babel-loader": "^8.2.3", 36 | "browserslist": "^4.18.1", 37 | "case-sensitive-paths-webpack-plugin": "^2.4.0", 38 | "fs-extra": "^10.0.0", 39 | "html-webpack-plugin": "^5.5.0", 40 | "react": "^18.2.0", 41 | "react-dev-utils": "^12.0.1", 42 | "react-dom": "^18.2.0", 43 | "react-refresh": "^0.11.0", 44 | "resolve": "^1.20.0", 45 | "resolve-url-loader": "^4.0.0", 46 | "source-map-loader": "^3.0.0", 47 | "terser-webpack-plugin": "^5.2.5", 48 | "use-hot-module-reload": "^1.0.1", 49 | "webpack": "^5.64.4", 50 | "webpack-dev-server": "^4.6.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath') 4 | 5 | const appDirectory = fs.realpathSync(process.cwd()) 6 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath) 7 | 8 | const publicUrlOrPath = getPublicUrlOrPath( 9 | process.env.NODE_ENV === 'development', 10 | require(resolveApp('package.json')).homepage, 11 | process.env.PUBLIC_URL 12 | ) 13 | 14 | const buildPath = process.env.BUILD_PATH || 'build' 15 | 16 | const moduleFileExtensions = [ 17 | 'web.mjs', 18 | 'mjs', 19 | 'web.js', 20 | 'js', 21 | 'web.ts', 22 | 'ts', 23 | 'web.tsx', 24 | 'tsx', 25 | 'json', 26 | 'web.jsx', 27 | 'jsx', 28 | ] 29 | 30 | // Resolve file paths in the same order as webpack 31 | const resolveModule = (resolveFn, filePath) => { 32 | const extension = moduleFileExtensions.find((extension) => 33 | fs.existsSync(resolveFn(`${filePath}.${extension}`)) 34 | ) 35 | 36 | if (extension) { 37 | return resolveFn(`${filePath}.${extension}`) 38 | } 39 | 40 | return resolveFn(`${filePath}.js`) 41 | } 42 | 43 | module.exports = { 44 | appPath: resolveApp('.'), 45 | appBuild: resolveApp(buildPath), 46 | appPublic: resolveApp('public'), 47 | appHtml: resolveApp('public/index.html'), 48 | appIndexJs: resolveModule(resolveApp, 'src/index'), 49 | appPackageJson: resolveApp('package.json'), 50 | appSrc: resolveApp('src'), 51 | appNodeModules: resolveApp('node_modules'), 52 | appWebpackCache: resolveApp('node_modules/.cache'), 53 | publicUrlOrPath, 54 | } 55 | 56 | module.exports.moduleFileExtensions = moduleFileExtensions 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "use-hot-module-reload", 5 | "fixtures/jest-cjs", 6 | "fixtures/jest-esm", 7 | "fixtures/parcel", 8 | "fixtures/vite", 9 | "fixtures/webpack", 10 | "fixtures/webpack-cjs", 11 | "fixtures/webpack-esm" 12 | ], 13 | "scripts": { 14 | "build": "yarn workspace use-hot-module-reload build", 15 | "clean": "rimraf fixtures/*/node_modules && yarn workspaces run clean && rimraf node_modules .parcel-cache", 16 | "jest-cjs": "yarn workspace jest-cjs-fixture build", 17 | "jest-esm": "yarn workspace jest-esm-fixture build", 18 | "parcel": "yarn workspace parcel-fixture start", 19 | "publish": "yarn test && yarn workspace use-hot-module-reload publish", 20 | "start": "yarn workspace use-hot-module-reload start", 21 | "test": "yarn workspaces run build", 22 | "vite": "yarn workspace vite-fixture start", 23 | "webpack": "yarn workspace webpack-fixture start", 24 | "webpack-cjs": "yarn workspace webpack-cjs-fixture start", 25 | "webpack-esm": "yarn workspace webpack-esm-fixture start" 26 | }, 27 | "prettier": { 28 | "bracketSpacing": false, 29 | "plugins": [ 30 | "prettier-plugin-packagejson" 31 | ], 32 | "printWidth": 100, 33 | "semi": false, 34 | "singleQuote": true 35 | }, 36 | "devDependencies": { 37 | "@typescript-eslint/eslint-plugin": "^5.33.1", 38 | "@typescript-eslint/parser": "^5.33.1", 39 | "eslint": "^8.22.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-config-sanity": "^6.0.0", 42 | "prettier": "^2.7.1", 43 | "prettier-plugin-packagejson": "^2.4.12", 44 | "rimraf": "^4.4.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /use-hot-module-reload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-hot-module-reload", 3 | "version": "2.0.0", 4 | "description": "React hook that triggers a callback after hot-module reloading has been performed (for any module, not just the one it was used in)", 5 | "keywords": [ 6 | "hmr", 7 | "hot-module-reload", 8 | "webpack-vite" 9 | ], 10 | "homepage": "https://github.com/rexxars/use-hot-module-reload#readme", 11 | "bugs": { 12 | "url": "https://github.com/rexxars/use-hot-module-reload/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://git@github.com/rexxars/use-hot-module-reload.git" 17 | }, 18 | "license": "MIT", 19 | "author": "Espen Hovlandsdal ", 20 | "sideEffects": false, 21 | "type": "module", 22 | "exports": { 23 | ".": { 24 | "require": "./dist/use-hot-module-reload.cjs", 25 | "default": "./dist/use-hot-module-reload.js" 26 | } 27 | }, 28 | "main": "./dist/use-hot-module-reload.cjs", 29 | "module": "./dist/use-hot-module-reload.js", 30 | "types": "dist/use-hot-module-reload.d.ts", 31 | "scripts": { 32 | "prebuild": "rimraf dist", 33 | "build": "concurrently \"tsup --config tsup.cjs.ts\" \"tsup --config tsup.mjs.ts\"", 34 | "clean": "rimraf dist", 35 | "prepublishOnly": "cp ../README.md . && npm run build", 36 | "test": "jest", 37 | "watch": "concurrently --kill-others \"tsup --watch --config tsup.cjs.ts\" \"tsup --watch --config tsup.mjs.ts\"" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^14.0.0", 41 | "@types/react": "^18.2.67", 42 | "concurrently": "^7.3.0", 43 | "react": "^18.2.0", 44 | "rimraf": "^3.0.2", 45 | "tsup": "^8.0.2", 46 | "typescript": "^5.4.2" 47 | }, 48 | "peerDependencies": { 49 | "react": ">=17.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/modules.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const paths = require('./paths') 3 | 4 | /** 5 | * Get additional module paths based on the baseUrl of a compilerOptions object. 6 | * 7 | * @param {Object} options 8 | */ 9 | function getAdditionalModulePaths(options = {}) { 10 | const baseUrl = options.baseUrl 11 | 12 | if (!baseUrl) { 13 | return '' 14 | } 15 | 16 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl) 17 | 18 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 19 | // the default behavior. 20 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 21 | return null 22 | } 23 | 24 | // Allow the user set the `baseUrl` to `appSrc`. 25 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 26 | return [paths.appSrc] 27 | } 28 | 29 | // If the path is equal to the root directory we ignore it here. 30 | // We don't want to allow importing from the root directly as source files are 31 | // not transpiled outside of `src`. We do allow importing them with the 32 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 33 | // an alias. 34 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 35 | return null 36 | } 37 | 38 | // Otherwise, throw an error. 39 | throw new Error( 40 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 41 | ' Create React App does not support other values at this time.' 42 | ) 43 | } 44 | 45 | /** 46 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 47 | * 48 | * @param {*} options 49 | */ 50 | function getWebpackAliases(options = {}) { 51 | const baseUrl = options.baseUrl 52 | 53 | if (!baseUrl) { 54 | return {} 55 | } 56 | 57 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl) 58 | 59 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 60 | return { 61 | src: paths.appSrc, 62 | } 63 | } 64 | } 65 | 66 | function getModules() { 67 | return { 68 | additionalModulePaths: getAdditionalModulePaths({}), 69 | webpackAliases: getWebpackAliases({}), 70 | } 71 | } 72 | 73 | module.exports = getModules() 74 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') 4 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') 5 | const TerserPlugin = require('terser-webpack-plugin') 6 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin') 7 | const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin') 8 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') 9 | const paths = require('./paths') 10 | const modules = require('./modules') 11 | const getClientEnvironment = require('./env') 12 | 13 | module.exports = function (webpackEnv) { 14 | const isEnvDevelopment = webpackEnv === 'development' 15 | const isEnvProduction = webpackEnv === 'production' 16 | const isEnvProductionProfile = isEnvProduction && process.argv.includes('--profile') 17 | const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)) 18 | 19 | const shouldUseReactRefresh = env.raw.FAST_REFRESH 20 | 21 | return { 22 | target: ['browserslist'], 23 | stats: 'errors-warnings', 24 | mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', 25 | bail: isEnvProduction, 26 | devtool: isEnvProduction ? 'source-map' : 'cheap-module-source-map', 27 | entry: paths.appIndexJs, 28 | output: { 29 | path: paths.appBuild, 30 | pathinfo: isEnvDevelopment, 31 | filename: isEnvProduction 32 | ? 'static/js/[name].[contenthash:8].js' 33 | : isEnvDevelopment && 'static/js/bundle.js', 34 | publicPath: paths.publicUrlOrPath, 35 | }, 36 | optimization: { 37 | minimize: isEnvProduction, 38 | minimizer: [ 39 | new TerserPlugin({ 40 | terserOptions: {parse: {ecma: 8}}, 41 | }), 42 | ], 43 | }, 44 | resolve: { 45 | modules: ['node_modules', paths.appNodeModules].concat(modules.additionalModulePaths || []), 46 | extensions: paths.moduleFileExtensions 47 | .map((ext) => `.${ext}`) 48 | .filter((ext) => !ext.includes('ts')), 49 | alias: { 50 | ...(isEnvProductionProfile && { 51 | 'react-dom$': 'react-dom/profiling', 52 | 'scheduler/tracing': 'scheduler/tracing-profiling', 53 | }), 54 | ...(modules.webpackAliases || {}), 55 | }, 56 | }, 57 | module: { 58 | strictExportPresence: true, 59 | rules: [ 60 | isEnvProduction && { 61 | enforce: 'pre', 62 | exclude: /@babel(?:\/|\\{1,2})runtime/, 63 | test: /\.(js|mjs|jsx|ts|tsx|css)$/, 64 | loader: require.resolve('source-map-loader'), 65 | }, 66 | { 67 | test: /\.(js|mjs|jsx|ts|tsx)$/, 68 | include: paths.appSrc, 69 | loader: require.resolve('babel-loader'), 70 | options: { 71 | sourceType: 'script', 72 | presets: [ 73 | '@babel/preset-env', 74 | [ 75 | '@babel/preset-react', 76 | { 77 | runtime: 'automatic', 78 | }, 79 | ], 80 | ], 81 | plugins: [ 82 | '@babel/plugin-transform-runtime', 83 | isEnvDevelopment && shouldUseReactRefresh && require.resolve('react-refresh/babel'), 84 | ].filter(Boolean), 85 | }, 86 | }, 87 | ].filter(Boolean), 88 | }, 89 | plugins: [ 90 | new HtmlWebpackPlugin({ 91 | inject: true, 92 | template: paths.appHtml, 93 | }), 94 | isEnvProduction && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]), 95 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), 96 | new ModuleNotFoundPlugin(paths.appPath), 97 | new webpack.DefinePlugin(env.stringified), 98 | isEnvDevelopment && 99 | new ReactRefreshWebpackPlugin({ 100 | overlay: false, 101 | }), 102 | new CaseSensitivePathsPlugin(), 103 | ].filter(Boolean), 104 | performance: false, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/scripts/start.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'development' 3 | process.env.NODE_ENV = 'development' 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | process.on('unhandledRejection', (err) => { 9 | throw err 10 | }) 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env') 14 | 15 | const fs = require('fs') 16 | const chalk = require('react-dev-utils/chalk') 17 | const webpack = require('webpack') 18 | const WebpackDevServer = require('webpack-dev-server') 19 | const clearConsole = require('react-dev-utils/clearConsole') 20 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles') 21 | const { 22 | choosePort, 23 | createCompiler, 24 | prepareProxy, 25 | prepareUrls, 26 | } = require('react-dev-utils/WebpackDevServerUtils') 27 | const openBrowser = require('react-dev-utils/openBrowser') 28 | const paths = require('../config/paths') 29 | const configFactory = require('../config/webpack.config') 30 | const createDevServerConfig = require('../config/webpackDevServer.config') 31 | const getClientEnvironment = require('../config/env') 32 | const react = require(require.resolve('react', {paths: [paths.appPath]})) 33 | 34 | const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)) 35 | const useYarn = true 36 | const isInteractive = process.stdout.isTTY 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1) 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000 45 | const HOST = process.env.HOST || '0.0.0.0' 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ) 55 | console.log(`If this was unintentional, check that you haven't mistakenly set it in your shell.`) 56 | console.log(`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`) 57 | console.log() 58 | } 59 | 60 | // We require that you explicitly set browsers and do not fall back to 61 | // browserslist defaults. 62 | const {checkBrowsers} = require('react-dev-utils/browsersHelper') 63 | checkBrowsers(paths.appPath, isInteractive) 64 | .then(() => { 65 | // We attempt to use the default port but if it is busy, we offer the user to 66 | // run on a different port. `choosePort()` Promise resolves to the next free port. 67 | return choosePort(HOST, DEFAULT_PORT) 68 | }) 69 | .then((port) => { 70 | if (port == null) { 71 | // We have not found a port. 72 | return 73 | } 74 | 75 | const config = configFactory('development') 76 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' 77 | const appName = require(paths.appPackageJson).name 78 | 79 | const urls = prepareUrls(protocol, HOST, port, paths.publicUrlOrPath.slice(0, -1)) 80 | // Create a webpack compiler that is configured with custom messages. 81 | const compiler = createCompiler({ 82 | appName, 83 | config, 84 | urls, 85 | useYarn, 86 | useTypeScript: false, 87 | webpack, 88 | }) 89 | // Load proxy config 90 | const proxySetting = require(paths.appPackageJson).proxy 91 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic, paths.publicUrlOrPath) 92 | // Serve webpack assets generated by the compiler over a web server. 93 | const serverConfig = { 94 | ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig), 95 | host: HOST, 96 | port, 97 | } 98 | const devServer = new WebpackDevServer(serverConfig, compiler) 99 | // Launch WebpackDevServer. 100 | devServer.startCallback(() => { 101 | if (isInteractive) { 102 | clearConsole() 103 | } 104 | 105 | console.log(chalk.cyan('Starting the development server...\n')) 106 | openBrowser(urls.localUrlForBrowser) 107 | }) 108 | ;['SIGINT', 'SIGTERM'].forEach(function (sig) { 109 | process.on(sig, function () { 110 | devServer.close() 111 | process.exit() 112 | }) 113 | }) 114 | 115 | if (process.env.CI !== 'true') { 116 | // Gracefully exit when stdin ends 117 | process.stdin.on('end', function () { 118 | devServer.close() 119 | process.exit() 120 | }) 121 | } 122 | }) 123 | .catch((err) => { 124 | if (err && err.message) { 125 | console.log(err.message) 126 | } 127 | process.exit(1) 128 | }) 129 | -------------------------------------------------------------------------------- /use-hot-module-reload/src/use-hot-module-reload.mts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react' 2 | 3 | // Even accessing `import` for a `typeof` check will throw in non-ESM mode, 4 | // thus the try/catch 5 | const isModule = (() => { 6 | try { 7 | return typeof import.meta !== 'undefined' 8 | } catch (err) { 9 | return false 10 | } 11 | })() 12 | 13 | // Allow us to short-circuit in production/non-HMR environments 14 | const hasHMR = (() => { 15 | try { 16 | return Boolean(isModule && (import.meta.hot || import.meta.webpackHot)) 17 | } catch (err) { 18 | return false 19 | } 20 | })() 21 | 22 | /** 23 | * Trigger a callback after hot-module reloads (any, not only the module using the hook). 24 | * Use it to force recomputation of stale values and state that do not automatically update. 25 | * This should be an escape hatch - ideally you shouldn't need this. 26 | * 27 | * @param callback - The callback to be triggered after hot-module reloads. 28 | */ 29 | export function useHotModuleReload(callback: () => void): void { 30 | if (!hasHMR) { 31 | return undefined 32 | } 33 | 34 | return typeof import.meta.webpackHot === 'undefined' 35 | ? // eslint-disable-next-line react-hooks/rules-of-hooks -- Vite vs Webpack won't change at runtime 36 | useViteHotModuleReload(callback) 37 | : // eslint-disable-next-line react-hooks/rules-of-hooks -- Vite vs Webpack won't change at runtime 38 | useWebpackHotModuleReload(callback) 39 | } 40 | 41 | function useWebpackHotModuleReload(callback: () => void): void { 42 | useEffect(() => { 43 | if (import.meta.webpackHot) { 44 | // Webpack in CommonJS mode 45 | const statusHandler = (status: string): void => (status === 'idle' ? callback() : undefined) 46 | import.meta.webpackHot.addStatusHandler(statusHandler) 47 | 48 | return () => import.meta.webpackHot?.removeStatusHandler(statusHandler) 49 | } 50 | 51 | return undefined 52 | }, [callback]) 53 | } 54 | 55 | function useViteHotModuleReload(callback: () => void): void { 56 | useEffect(() => { 57 | // Note: not using early return here in order to optimize tree-shaking 58 | if (import.meta.hot) { 59 | /** 60 | * Unfortunately, there is currently no `vite:afterUpdate` event 61 | * (see https://github.com/vitejs/vite/pull/9810), and `react-refresh` 62 | * does not expose a way to listen for flushed updates (the vite HMR only 63 | * _schedules_ an update - the update is then done asynchronously). 64 | * 65 | * Many attempts were done to find a way to know that React actually updated 66 | * with the new code, but because of layers of timeouts, debouncing, partially 67 | * updated trees and whatnot; this is complicated. 68 | * 69 | * What this actually does is to wait for a `beforeUpdate` event, and then 70 | * probe for the `__vite_plugin_react_timeout` window global to become non-zero. 71 | * When it is, it means an update is _scheduled_, which has a 30ms timeout and 72 | * a 16 ms debounce - so in essence it should be non-zero for about 46ms. 73 | * 74 | * Once it goes back to 0, it means the update is done, and we can trigger the 75 | * callback. Since this is a bit best-effort and not 100% reliable, we also add 76 | * a 1000ms timeout to make sure we don't wait forever should the update already 77 | * have been applied before we observed the change, or if vite changes the 78 | * implementation details (I have requested a callback/event from the vite crew). 79 | */ 80 | const disposers = new Set<() => void>() 81 | import.meta.hot.on('vite:beforeUpdate', () => { 82 | let flushTimeout: number | ReturnType 83 | let hasSeenScheduledUpdate = window.__vite_plugin_react_timeout > 0 84 | 85 | const refreshProber = setInterval(() => { 86 | const now = window.__vite_plugin_react_timeout 87 | if (hasSeenScheduledUpdate && now === 0) { 88 | complete() 89 | } else if (!hasSeenScheduledUpdate && now > 0) { 90 | hasSeenScheduledUpdate = true 91 | } 92 | }, 10) 93 | 94 | const fallbackTimeout = setTimeout(complete, 1000, 'fallback') 95 | 96 | function clear() { 97 | clearInterval(refreshProber) 98 | clearTimeout(fallbackTimeout) 99 | clearTimeout(flushTimeout) 100 | } 101 | 102 | function complete() { 103 | clear() 104 | 105 | // While the react refresh has been _triggered_ by this point, it may 106 | // not be flushed yet. Wait for an additional 50ms to make sure it is. 107 | flushTimeout = setTimeout(callback, 50) 108 | } 109 | 110 | disposers.add(clear) 111 | }) 112 | 113 | return () => { 114 | disposers.forEach((disposer) => disposer()) 115 | disposers.clear() 116 | } 117 | } 118 | 119 | return undefined 120 | }, [callback]) 121 | } 122 | -------------------------------------------------------------------------------- /fixtures/webpack-cjs/scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'production' 3 | process.env.NODE_ENV = 'production' 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | process.on('unhandledRejection', (err) => { 9 | throw err 10 | }) 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env') 14 | 15 | const path = require('path') 16 | const chalk = require('react-dev-utils/chalk') 17 | const fs = require('fs-extra') 18 | const webpack = require('webpack') 19 | const configFactory = require('../config/webpack.config') 20 | const paths = require('../config/paths') 21 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles') 22 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages') 23 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions') 24 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter') 25 | const printBuildError = require('react-dev-utils/printBuildError') 26 | 27 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild 28 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild 29 | const useYarn = true 30 | 31 | // These sizes are pretty large. We'll warn for bundles exceeding them. 32 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024 33 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024 34 | 35 | const isInteractive = process.stdout.isTTY 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1) 40 | } 41 | 42 | // Generate configuration 43 | const config = configFactory('production') 44 | 45 | // We require that you explicitly set browsers and do not fall back to 46 | // browserslist defaults. 47 | const {checkBrowsers} = require('react-dev-utils/browsersHelper') 48 | checkBrowsers(paths.appPath, isInteractive) 49 | .then(() => { 50 | // First, read the current file sizes in build directory. 51 | // This lets us display how much they changed later. 52 | return measureFileSizesBeforeBuild(paths.appBuild) 53 | }) 54 | .then((previousFileSizes) => { 55 | // Remove all content but keep the directory so that 56 | // if you're in it, you don't end up in Trash 57 | fs.emptyDirSync(paths.appBuild) 58 | // Merge with the public folder 59 | copyPublicFolder() 60 | // Start the webpack build 61 | return build(previousFileSizes) 62 | }) 63 | .then( 64 | ({stats, previousFileSizes, warnings}) => { 65 | if (warnings.length) { 66 | console.log(chalk.yellow('Compiled with warnings.\n')) 67 | console.log(warnings.join('\n\n')) 68 | console.log( 69 | '\nSearch for the ' + 70 | chalk.underline(chalk.yellow('keywords')) + 71 | ' to learn more about each warning.' 72 | ) 73 | console.log( 74 | 'To ignore, add ' + chalk.cyan('// eslint-disable-next-line') + ' to the line before.\n' 75 | ) 76 | } else { 77 | console.log(chalk.green('Compiled successfully.\n')) 78 | } 79 | 80 | console.log('File sizes after gzip:\n') 81 | printFileSizesAfterBuild( 82 | stats, 83 | previousFileSizes, 84 | paths.appBuild, 85 | WARN_AFTER_BUNDLE_GZIP_SIZE, 86 | WARN_AFTER_CHUNK_GZIP_SIZE 87 | ) 88 | console.log() 89 | 90 | const appPackage = require(paths.appPackageJson) 91 | const publicUrl = paths.publicUrlOrPath 92 | const publicPath = config.output.publicPath 93 | const buildFolder = path.relative(process.cwd(), paths.appBuild) 94 | printHostingInstructions(appPackage, publicUrl, publicPath, buildFolder, useYarn) 95 | }, 96 | (err) => { 97 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true' 98 | if (tscCompileOnError) { 99 | console.log( 100 | chalk.yellow( 101 | 'Compiled with the following type errors (you may want to check these before deploying your app):\n' 102 | ) 103 | ) 104 | printBuildError(err) 105 | } else { 106 | console.log(chalk.red('Failed to compile.\n')) 107 | printBuildError(err) 108 | process.exit(1) 109 | } 110 | } 111 | ) 112 | .catch((err) => { 113 | if (err && err.message) { 114 | console.log(err.message) 115 | } 116 | process.exit(1) 117 | }) 118 | 119 | // Create the production build and print the deployment instructions. 120 | function build(previousFileSizes) { 121 | console.log('Creating an optimized production build...') 122 | 123 | const compiler = webpack(config) 124 | return new Promise((resolve, reject) => { 125 | compiler.run((err, stats) => { 126 | let messages 127 | if (err) { 128 | if (!err.message) { 129 | return reject(err) 130 | } 131 | 132 | let errMessage = err.message 133 | 134 | // Add additional information for postcss errors 135 | if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { 136 | errMessage += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector 137 | } 138 | 139 | messages = formatWebpackMessages({ 140 | errors: [errMessage], 141 | warnings: [], 142 | }) 143 | } else { 144 | messages = formatWebpackMessages(stats.toJson({all: false, warnings: true, errors: true})) 145 | } 146 | if (messages.errors.length) { 147 | // Only keep the first error. Others are often indicative 148 | // of the same problem, but confuse the reader with noise. 149 | if (messages.errors.length > 1) { 150 | messages.errors.length = 1 151 | } 152 | return reject(new Error(messages.errors.join('\n\n'))) 153 | } 154 | if ( 155 | process.env.CI && 156 | (typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') && 157 | messages.warnings.length 158 | ) { 159 | // Ignore sourcemap warnings in CI builds. See #8227 for more info. 160 | const filteredWarnings = messages.warnings.filter( 161 | (w) => !/Failed to parse source map/.test(w) 162 | ) 163 | if (filteredWarnings.length) { 164 | console.log( 165 | chalk.yellow( 166 | '\nTreating warnings as errors because process.env.CI = true.\n' + 167 | 'Most CI servers set it automatically.\n' 168 | ) 169 | ) 170 | return reject(new Error(filteredWarnings.join('\n\n'))) 171 | } 172 | } 173 | 174 | const resolveArgs = { 175 | stats, 176 | previousFileSizes, 177 | warnings: messages.warnings, 178 | } 179 | 180 | return resolve(resolveArgs) 181 | }) 182 | }) 183 | } 184 | 185 | function copyPublicFolder() { 186 | fs.copySync(paths.appPublic, paths.appBuild, { 187 | dereference: true, 188 | filter: (file) => file !== paths.appHtml, 189 | }) 190 | } 191 | --------------------------------------------------------------------------------