Hot Module Replacement (HMR) Integration

solving issue #1255

This project implements Hot Module Replacement (HMR) for a React-based Chrome extension, allowing developers to see real-time changes in their popup component without needing to refresh the entire extension. The following outlines the configuration and structure necessary for achieving this setup.

#### Key Components

1. **Webpack Configuration (`webpack.config.js`)**:
   - Configures Webpack for development and production modes, specifying the entry point and output settings.
   - Enables HMR in the development server for instant updates.

2. **Entry Point (`src/popup.js`)**:
   - Contains the main logic for the popup component, integrating the HMR logic to allow for module updates without a full reload.

3. **Index File (`src/index.js`)**:
   - Updated to support HMR with checks for `module.hot`, ensuring that the component re-renders on updates without refreshing the entire popup.

4. **Package Configuration (`package.json`)**:
   - Includes scripts for building and serving the application, specifying configurations needed for both development and production.

5. **Development Server**:
   - The command `npm start` launches a development server with HMR enabled, providing a smooth development experience.

#### Summary of Changes

- **HMR Logic**:
  - Added in `popup.js` using `if (module.hot) { ... }` to ensure updates are reflected in real-time.
  - Implemented in `index.js` to facilitate automatic re-rendering of the popup component on code changes.

- **Webpack Dev Server**: Configured with `hot: true` to support HMR functionality.

- **File Structure**: Organized files into a clear structure, facilitating maintainability and ease of access.

### Benefits

Implementing HMR improves the development workflow by reducing the time spent on refreshing and waiting for the extension to reload. This results in a more productive environment, allowing for faster iteration and debugging of features.
This commit is contained in:
minowau 2024-07-14 10:39:18 +05:30
parent d205f0e272
commit 508feef8a1
4 changed files with 89 additions and 65 deletions

View file

@ -9,8 +9,8 @@
"lint:css": "stylelint source/**/*.css", "lint:css": "stylelint source/**/*.css",
"lint-fix": "run-p 'lint:* -- --fix'", "lint-fix": "run-p 'lint:* -- --fix'",
"test": "run-s lint:* build", "test": "run-s lint:* build",
"build": "webpack --mode=production", "start": "webpack serve --mode=development --hot --open",
"start": "webpack --mode=development --watch", "build": "webpack --mode=production",
"release:cws": "webstore upload --source=dist --auto-publish", "release:cws": "webstore upload --source=dist --auto-publish",
"release:amo": "web-ext-submit --source-dir dist", "release:amo": "web-ext-submit --source-dir dist",
"release": "run-s build release:*" "release": "run-s build release:*"

View file

@ -133,3 +133,11 @@ ReactDOM.render(
isChrome() ? <URLOpenerChrome /> : <URLOpenerNonChrome />, isChrome() ? <URLOpenerChrome /> : <URLOpenerNonChrome />,
document.getElementById("app") document.getElementById("app")
); );
// HMR integration
if (module.hot) {
module.hot.accept('./popup', () => {
const NextPopup = require('./popup').default;
ReactDOM.render(<NextPopup />, document.getElementById('app'));
});
}

View file

@ -1,3 +1,4 @@
const webpack = require('webpack');
const path = require('path'); const path = require('path');
const SizePlugin = require('size-plugin'); const SizePlugin = require('size-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
@ -5,17 +6,23 @@ const WebExtWebpackPlugin = require('@ianwalter/web-ext-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
module.exports = { module.exports = {
devtool: 'source-map', devtool: 'source-map',
stats: 'errors-only', stats: 'errors-only',
entry: { entry: {
background: './src/background', background: './src/background',
popup: './src/popup', popup: './src/popup',
}, // Add HMR client
output: { main: [
path: path.join(__dirname, 'dist'), 'webpack-hot-middleware/client?reload=true', // Use 'reload=true' for CSS
filename: '[name].js' './src/index.js', // Adjust to your main file
}, ],
module: { },
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/', // Required for HMR
},
module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.js$/,
@ -34,41 +41,45 @@ module.exports = {
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime", "@babel/plugin-transform-runtime",
'react-refresh/babel', // Add React Refresh for HMR
], ],
}, },
}, },
}, },
{ {
test: /\.(svg|gif|png|jpg)$/, test: /\.(svg|gif|png|jpg)$/,
use: 'url-loader', use: 'url-loader',
} }
], ],
}, },
plugins: [ plugins: [
new SizePlugin(), new webpack.HotModuleReplacementPlugin(), // Enable HMR
new CopyWebpackPlugin([ new SizePlugin(),
{ new CopyWebpackPlugin({
from: '**/*', patterns: [
context: 'public', {
}, from: '**/*',
{ context: 'public',
from: 'node_modules/webextension-polyfill/dist/browser-polyfill.min.js' },
} {
]), from: 'node_modules/webextension-polyfill/dist/browser-polyfill.min.js'
new WebExtWebpackPlugin({ sourceDir: path.join(__dirname, 'dist'), verbose: true }), }
], ],
optimization: { }),
minimizer: [ new WebExtWebpackPlugin({ sourceDir: path.join(__dirname, 'dist'), verbose: true }),
new TerserPlugin({ ],
terserOptions: { optimization: {
mangle: false, minimizer: [
compress: false, new TerserPlugin({
output: { terserOptions: {
beautify: true, mangle: false,
indent_level: 2 // eslint-disable-line camelcase compress: false,
} output: {
} beautify: true,
}) indent_level: 2
] }
} }
})
]
}
}; };

View file

@ -1,6 +1,6 @@
import React, {useMemo, useState, useEffect, useRef} from 'react'; import React, { useMemo, useState, useEffect, useRef } from 'react';
import {Rnd} from 'react-rnd'; import { Rnd } from 'react-rnd';
import {useSelector, useDispatch} from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import cx from 'classnames'; import cx from 'classnames';
import pubsub from 'pubsub.js'; import pubsub from 'pubsub.js';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
@ -16,7 +16,7 @@ import useCommonStyles from '../useCommonStyles';
import useStyles from './useStyles'; import useStyles from './useStyles';
import TextAreaWithCopyButton from '../../utils/TextAreaWithCopyButton'; import TextAreaWithCopyButton from '../../utils/TextAreaWithCopyButton';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import {APPLY_CSS} from '../../constants/pubsubEvents'; import { APPLY_CSS } from '../../constants/pubsubEvents';
import { import {
CSS_EDITOR_MODES, CSS_EDITOR_MODES,
DEVTOOLS_MODES, DEVTOOLS_MODES,
@ -24,21 +24,21 @@ import {
isVeriticallyStacked, isVeriticallyStacked,
} from '../../constants/previewerLayouts'; } from '../../constants/previewerLayouts';
import KebabMenu from '../KebabMenu'; import KebabMenu from '../KebabMenu';
import {Tooltip} from '@material-ui/core'; import { Tooltip } from '@material-ui/core';
import DockRight from '../icons/DockRight'; import DockRight from '../icons/DockRight';
import {debounce} from 'lodash'; import { debounce } from 'lodash';
import CSSEditor from '../icons/CSSEditor'; import CSSEditor from '../icons/CSSEditor';
const getResizingDirections = position => { const getResizingDirections = position => {
switch (position) { switch (position) {
case CSS_EDITOR_MODES.LEFT: case CSS_EDITOR_MODES.LEFT:
return {right: true}; return { right: true };
case CSS_EDITOR_MODES.RIGHT: case CSS_EDITOR_MODES.RIGHT:
return {left: true}; return { left: true };
case CSS_EDITOR_MODES.TOP: case CSS_EDITOR_MODES.TOP:
return {bottom: true}; return { bottom: true };
case CSS_EDITOR_MODES.BOTTOM: case CSS_EDITOR_MODES.BOTTOM:
return {top: true}; return { top: true };
default: default:
return true; return true;
} }
@ -49,12 +49,7 @@ const computeHeight = (position, devToolsConfig) => {
return null; return null;
} }
return isVeriticallyStacked(position) return isVeriticallyStacked(position)
? `calc(100vh - ${10 + ? `calc(100vh - ${10 + headerHeight + statusBarHeight + (devToolsConfig.open && devToolsConfig.mode === DEVTOOLS_MODES.BOTTOM ? devToolsConfig.size.height : 0)}px)`
headerHeight +
statusBarHeight +
(devToolsConfig.open && devToolsConfig.mode === DEVTOOLS_MODES.BOTTOM
? devToolsConfig.size.height
: 0)}px)`
: 300; : 300;
}; };
@ -106,7 +101,7 @@ const LiveCssEditor = ({
if (!content) { if (!content) {
return; return;
} }
pubsub.publish(APPLY_CSS, [{css: content}]); pubsub.publish(APPLY_CSS, [{ css: content }]);
}; };
useEffect(() => { useEffect(() => {
@ -139,13 +134,13 @@ const LiveCssEditor = ({
const disableDragging = useMemo(() => !isUndocked, [isUndocked]); const disableDragging = useMemo(() => !isUndocked, [isUndocked]);
return ( return (
<div className={classes.wrapper} style={{height, width}}> <div className={classes.wrapper} style={{ height, width }}>
<Rnd <Rnd
ref={rndRef} ref={rndRef}
dragHandleClassName={classes.titleBar} dragHandleClassName={classes.titleBar}
disableDragging={disableDragging} disableDragging={disableDragging}
enableResizing={enableResizing} enableResizing={enableResizing}
style={{zIndex: 100}} style={{ zIndex: 100 }}
default={{ default={{
...getDefaultPosition(isUndocked), ...getDefaultPosition(isUndocked),
...getDefaultSize(isUndocked), ...getDefaultSize(isUndocked),
@ -155,7 +150,7 @@ const LiveCssEditor = ({
if (isUndocked) { if (isUndocked) {
return; return;
} }
const {width: _width, height: _height} = ref.getBoundingClientRect(); const { width: _width, height: _height } = ref.getBoundingClientRect();
if (width !== _width) { if (width !== _width) {
setWidth(_width); setWidth(_width);
} }
@ -209,7 +204,7 @@ const LiveCssEditor = ({
mode="css" mode="css"
theme="twilight" theme="twilight"
name="css" name="css"
onChange={debounce(onCSSEditorContentChange, 25, {maxWait: 50})} onChange={debounce(onCSSEditorContentChange, 25, { maxWait: 50 })}
fontSize={14} fontSize={14}
showPrintMargin={true} showPrintMargin={true}
showGutter={true} showGutter={true}
@ -253,4 +248,14 @@ const LiveCssEditor = ({
</div> </div>
); );
}; };
export default LiveCssEditor; export default LiveCssEditor;
// HMR integration
if (module.hot) {
module.hot.accept('./LiveCssEditor', () => {
const NextLiveCssEditor = require('./LiveCssEditor').default;
ReactDOM.render(<NextLiveCssEditor />, document.getElementById('app'));
});
}