Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
134f9e8
feat: add function to create client socket
pmmmwh Sep 23, 2019
ecfa08e
feat: add global constant for runtime logic hooks
pmmmwh Sep 23, 2019
429a6b1
feat: bootstrap error overlay entry using react-error-overlay
pmmmwh Sep 23, 2019
cb83557
feat: allow dismissal of runtime errors during refresh
pmmmwh Sep 23, 2019
a0b9b9b
feat: inject error box entry to bundle
pmmmwh Sep 23, 2019
525a034
feat: add necessary dependencies for error box
pmmmwh Sep 23, 2019
830203b
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Sep 23, 2019
5b9eac1
feat: re-add code to properly dismiss error overlay
pmmmwh Sep 23, 2019
b201442
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Oct 3, 2019
6675359
docs: add a hint to not use the plugin in production
pmmmwh Oct 3, 2019
c92c3ef
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Oct 3, 2019
3353f22
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Nov 1, 2019
ccbd2f0
fix: update dated lockfile
pmmmwh Nov 1, 2019
1b3a9ac
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Nov 2, 2019
3c1d837
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Nov 4, 2019
7594cef
fix: ignore all runtime files when plugin is used as direct dependency
pmmmwh Nov 4, 2019
fbf5216
chore: update dependencies
pmmmwh Nov 4, 2019
4698478
fix: make socket creator fully ES5 compliant
pmmmwh Nov 4, 2019
e1af727
chore: update plugin dependencies
pmmmwh Nov 4, 2019
24f6833
fix: fix typos, make error overlay dismissal work
pmmmwh Nov 4, 2019
918b5a9
Merge remote-tracking branch 'origin/master' into feat/error-overlay
pmmmwh Nov 5, 2019
0f93e34
fix: move enqueueUpdate to only run after first reload
pmmmwh Nov 10, 2019
6d2bb34
fix: switch all vars in runtime files to const/let
pmmmwh Nov 10, 2019
506eef6
feat: add event listener generators to capture runtime errors
pmmmwh Nov 10, 2019
c0d76b2
feat(overlay): implement overlay building blocks as "components"
pmmmwh Nov 11, 2019
4a856af
fix: fix missed cleanup of var to let
pmmmwh Nov 11, 2019
4e352e1
feat(overlay): add utils to handle runtime requirements of the overlay
pmmmwh Nov 11, 2019
b933f2d
feat(overlay): define overlay ANSI theme
pmmmwh Nov 11, 2019
66a6790
feat(overlay): implement container for compilation errors
pmmmwh Nov 11, 2019
0accda8
feat(overlay): implement container for runtime errors
pmmmwh Nov 11, 2019
b0a0f66
feat(overlay): implement state logic and exports for overlay
pmmmwh Nov 11, 2019
22ec6ba
chore: add dependencies for the overlay
pmmmwh Nov 11, 2019
745eb17
docs(overlay): add missing docs for containers
pmmmwh Nov 11, 2019
c74f557
docs(overlay): document root exports and state manipulations
pmmmwh Nov 11, 2019
4f47391
feat: apply overlay in runtime utils to dismiss compile errors
pmmmwh Nov 11, 2019
297ecc7
feat: rework integration with error overlay within plugin
pmmmwh Nov 11, 2019
c2632ae
docs: add minor docs to create socket
pmmmwh Nov 11, 2019
faac777
chore: update demo to use self version
pmmmwh Nov 11, 2019
427a4e2
refactor: remove global hook in favour of a noop when no errors are c…
pmmmwh Nov 13, 2019
4ba68f9
chore: update error overlay title display
pmmmwh Nov 16, 2019
259bf7e
fix: update removeAllChildren to ignore some children
pmmmwh Nov 27, 2019
04c4e69
fix: handle the case when module.hot.data.module is undefined
pmmmwh Nov 27, 2019
9ee9e3e
refactor: move runtime error handlers into overlay so that timing err…
pmmmwh Nov 27, 2019
8093385
chore: remove strip-ansi
pmmmwh Nov 27, 2019
4b3dcc4
refactor: update loader integration to be more accurate
pmmmwh Nov 27, 2019
303c42c
fix: make sourcemaps work with the plugin
pmmmwh Nov 27, 2019
cd308a3
fix: make buttons larger and fix footer overflow issue
pmmmwh Nov 30, 2019
cd8221e
feat: add babel transform detection and cleanup plugin code
pmmmwh Dec 1, 2019
7c20ad6
chore: update dependencies
pmmmwh Dec 1, 2019
0e94db3
fix: re-add code after broken refactoring and file renaming
pmmmwh Dec 3, 2019
a5159c0
Merge branch 'master' into feat/error-overlay
pmmmwh Dec 3, 2019
893f5fc
chore: update dependencies for example
pmmmwh Dec 6, 2019
7c485ad
chore: update prettier config
pmmmwh Dec 6, 2019
efdce8c
chore: lint files
pmmmwh Dec 6, 2019
344cc42
fix: module hot data on refresh might be an object with no keys
pmmmwh Dec 6, 2019
81ea5ca
feat: update code/overlay theme
pmmmwh Dec 6, 2019
f0a7342
fix: update error overlay entry to handle runtime race conditions
pmmmwh Dec 6, 2019
c11360d
chore: bump and add debounce as dependency
pmmmwh Dec 6, 2019
eafb632
refactor: update overlay footer to align more to desired theme
pmmmwh Dec 6, 2019
f08d28c
chore: implement spacer component
pmmmwh Dec 6, 2019
6a89ff6
refactor: implement overlay/error header to align more to desired theme
pmmmwh Dec 6, 2019
7085951
refactor: update stack trace components
pmmmwh Dec 6, 2019
b024824
refactor: update main error containers
pmmmwh Dec 6, 2019
ef9f79d
docs: update types/docs for overlay components
pmmmwh Dec 6, 2019
2a402e0
refactor: cleanup use of white color in overlay
pmmmwh Dec 6, 2019
90c6320
refactor: update error overlay interface to be more robust
pmmmwh Dec 6, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"endOfLine": "lf",
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5"
}
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"babel-loader": "^8.0.6",
"cross-env": "^6.0.3",
"html-webpack-plugin": "^3.2.0",
"react-refresh": "^0.6.0",
"react-refresh": "^0.7.0",
"react-refresh-webpack-plugin": "https://github.com/pmmmwh/react-refresh-webpack-plugin#master",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
Expand Down
2 changes: 1 addition & 1 deletion example/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');
const ReactRefreshPlugin = require('../src');

module.exports = {
entry: './src/index.js',
Expand Down
1,021 changes: 517 additions & 504 deletions example/yarn.lock

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@
"lint": "prettier --check \"**/*.{js,jsx}\"",
"lint:fix": "prettier --write \"**/*.{js,jsx}\""
},
"dependencies": {
"ansi-html": "^0.0.7",
"error-stack-parser": "^2.0.4",
"html-entities": "^1.2.1",
"lodash.debounce": "^4.0.8",
"react-dev-utils": "^9.1.0",
"sockjs-client": "^1.4.0"
},
"devDependencies": {
"prettier": "^1.18.2",
"react-refresh": "^0.6.0",
"react-refresh": "^0.7.0",
"webpack": "^4.41.2"
},
"peerDependencies": {
"react-refresh": "*"
"react-refresh": ">= 0.7"
},
"engines": {
"node": "8.x || 9.x || 10.x || 11.x || 12.x || 13.x"
"node": ">= 8.x"
}
}
7 changes: 2 additions & 5 deletions src/helpers/createRefreshTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { Template } = require('webpack');
* [Ref](https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js#L233)
*/
const beforeModule = `
var cleanup = function NoOp() {};
let cleanup = function NoOp() {};

if (window && window.$RefreshSetup$) {
cleanup = window.$RefreshSetup$(module.i);
Expand All @@ -33,10 +33,7 @@ const afterModule = `
function createRefreshTemplate(source, chunk) {
// If a chunk is injected with the plugin,
// our custom entry for react-refresh musts be injected
if (
!chunk.entryModule ||
!/ReactRefreshEntry/.test(chunk.entryModule._identifier || '')
) {
if (!chunk.entryModule || !/ReactRefreshEntry/.test(chunk.entryModule._identifier || '')) {
return source;
}

Expand Down
5 changes: 4 additions & 1 deletion src/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const createRefreshTemplate = require('./createRefreshTemplate');
const injectRefreshEntry = require('./injectRefreshEntry');

module.exports = { createRefreshTemplate, injectRefreshEntry };
module.exports = {
createRefreshTemplate,
injectRefreshEntry,
};
13 changes: 10 additions & 3 deletions src/helpers/injectRefreshEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@
* @returns {WebpackEntry} An injected entry object.
*/
const injectRefreshEntry = originalEntry => {
const ReactRefreshEntry = require.resolve('../runtime/ReactRefreshEntry');
const entryInjects = [
// React-refresh runtime
require.resolve('../runtime/ReactRefreshEntry'),
// Error overlay runtime
require.resolve('../runtime/ErrorOverlayEntry'),
// React-refresh Babel transform detection
require.resolve('../runtime/BabelDetectComponent'),
];

// Single string entry point
if (typeof originalEntry === 'string') {
return [ReactRefreshEntry, originalEntry];
return [...entryInjects, originalEntry];
}
// Single array entry point
if (Array.isArray(originalEntry)) {
return [ReactRefreshEntry, ...originalEntry];
return [...entryInjects, ...originalEntry];
}
// Multiple entry points
if (typeof originalEntry === 'object') {
Expand Down
53 changes: 47 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ const webpack = require('webpack');
const { createRefreshTemplate, injectRefreshEntry } = require('./helpers');
const { refreshUtils } = require('./runtime/globals');

/**
* @typedef {Object} ReactRefreshPluginOptions
* @property {boolean} [disableRefreshCheck] A flag to disable detection of the react-refresh Babel plugin.
* @property {boolean} [forceEnable] A flag to enable the plugin forcefully.
*/

/** @type {ReactRefreshPluginOptions} */
const defaultOptions = {
disableRefreshCheck: false,
forceEnable: false,
};

class ReactRefreshPlugin {
/**
* @param {*} [options] Options for react-refresh-plugin.
* @param {boolean} [options.forceEnable] A flag to enable the plugin forcefully.
* @param {ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
* @returns {void}
*/
constructor(options) {
this.options = options || {};
this.options = Object.assign(defaultOptions, options);
}

/**
Expand Down Expand Up @@ -59,11 +70,14 @@ class ReactRefreshPlugin {
/\.([jt]sx?|flow)$/.test(data.resource) &&
// Skip all files from node_modules
!/node_modules/.test(data.resource) &&
// Skip runtime refresh utilities (to prevent self-referencing)
// Skip files related to refresh runtime (to prevent self-referencing)
// This is useful when using the plugin as a direct dependency
data.resource !== path.join(__dirname, './runtime/utils.js')
!data.resource.includes(path.join(__dirname, './runtime'))
) {
data.loaders.unshift(require.resolve('./loader'));
data.loaders.unshift({
loader: require.resolve('./loader'),
options: undefined,
});
}

return data;
Expand All @@ -76,6 +90,33 @@ class ReactRefreshPlugin {
// Constructs the correct module template for react-refresh
createRefreshTemplate
);

compilation.hooks.finishModules.tap(this.constructor.name, modules => {
if (!this.options.disableRefreshCheck) {
const refreshPluginInjection = /\$RefreshReg\$/;
const RefreshDetectionModule = modules.find(
module => module.resource === require.resolve('./runtime/BabelDetectComponent.js')
);

// In most cases, if we cannot find the injected detection module,
// there are other compilation instances injected by other plugins.
// We will have to bail out in those cases.
if (!RefreshDetectionModule) {
return;
}

// Check for the function transform by the Babel plugin.
if (!refreshPluginInjection.test(RefreshDetectionModule._source.source())) {
throw new Error(
[
'The plugin is unable to detect transformed code from react-refresh.',
'Did you forget to include "react-refresh/babel" in your list of Babel plugins?',
'Note: you can disable this check by setting "disableRefreshCheck: true".',
].join(' ')
);
}
}
});
});
}
}
Expand Down
32 changes: 24 additions & 8 deletions src/loader.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const path = require('path');
const { Template } = require('webpack');
const { refreshUtils } = require('./runtime/globals');

Expand All @@ -6,17 +7,32 @@ const reactModule = /['"]react['"]/;

/**
* A simple Webpack loader to inject react-refresh HMR code into modules.
*
* [Reference for Loader API](https://webpack.js.org/api/loaders/)
* @param {string} source The original module source code.
* @param {*} [inputSourceMap] The source map of the module.
* @property {function(string): void} addDependency Adds a dependency for Webpack to watch.
* @property {function(Error | null, string | Buffer, *?, *?): void} callback Sends loader results to Webpack.
* @returns {string} The injected module source code.
*/
function RefreshHotLoader(source) {
// Only apply transform if the source code contains a React import
return reactModule.test(source)
? source +
Template.getFunctionContent(require('./runtime/RefreshModuleRuntime'))
.trim()
.replace(/\$RefreshUtils\$/g, refreshUtils)
: source;
function RefreshHotLoader(source, inputSourceMap) {
// Add dependency to allow caching and invalidations
this.addDependency(path.resolve('./runtime/RefreshModuleRuntime'));

// Use callback to allow source maps to pass through
this.callback(
null,
// Only apply transform if the source code contains a React import
reactModule.test(source)
? source +
'\n\n' +
Template.getFunctionContent(require('./runtime/RefreshModuleRuntime'))
.trim()
.replace(/^ {2}/gm, '')
.replace(/\$RefreshUtils\$/g, refreshUtils)
: source,
inputSourceMap
);
}

module.exports = RefreshHotLoader;
52 changes: 52 additions & 0 deletions src/overlay/components/CompileErrorTrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const ansiHTML = require('ansi-html');
const { Html5Entities } = require('html-entities');
const theme = require('../theme');
const formatFilename = require('../utils/formatFilename');

ansiHTML.setColors(theme);

const entities = new Html5Entities();

/**
* @typedef {Object} CompileErrorTraceProps
* @property {string} errorMessage
*/

/**
* A formatter that turns Webpack compile error messages into highlighted HTML source traces.
* @param {Document} document
* @param {HTMLElement} root
* @param {CompileErrorTraceProps} props
* @returns {void}
*/
function CompileErrorTrace(document, root, props) {
const errorParts = props.errorMessage.split('\n');
const errorMessage = errorParts
.splice(1, 1)[0]
// Strip filename from the error message
.replace(/^(.*:)\s.*:(\s.*)$/, '$1$2');
errorParts[0] = formatFilename(errorParts[0]);
errorParts.unshift(errorMessage);

const stackContainer = document.createElement('pre');
stackContainer.innerHTML = ansiHTML(entities.encode(errorParts.join('\n')));
stackContainer.style.fontFamily = [
'"Operator Mono SSm"',
'"Operator Mono"',
'"Fira Code Retina"',
'"Fira Code"',
'"FiraCode-Retina"',
'"Andale Mono"',
'"Lucida Console"',
'Menlo',
'Consolas',
'Monaco',
'monospace',
].join(', ');
stackContainer.style.margin = '0';
stackContainer.style.whiteSpace = 'pre-wrap';

root.appendChild(stackContainer);
}

module.exports = CompileErrorTrace;
56 changes: 56 additions & 0 deletions src/overlay/components/PageHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const theme = require('../theme');
const Spacer = require('./Spacer');

/**
* @typedef {Object} PageHeaderProps
* @property {string} [message]
* @property {string} title
* @property {string} [topOffset]
*/

/**
* The header of the overlay.
* @param {Document} document
* @param {HTMLElement} root
* @param {PageHeaderProps} props
* @returns {void}
*/
function PageHeader(document, root, props) {
const pageHeaderContainer = document.createElement('div');
pageHeaderContainer.style.background = '#' + theme.dimgrey;
pageHeaderContainer.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.3)';
pageHeaderContainer.style.color = '#' + theme.white;
pageHeaderContainer.style.left = '0';
pageHeaderContainer.style.padding = '1rem 1.5rem';
pageHeaderContainer.style.position = 'fixed';
pageHeaderContainer.style.top = props.topOffset || '0';
pageHeaderContainer.style.width = 'calc(100vw - 3rem)';

const title = document.createElement('h3');
title.innerText = props.title;
title.style.color = '#' + theme.red;
title.style.fontSize = '1.125rem';
title.style.lineHeight = '1.3';
title.style.margin = '0';
pageHeaderContainer.appendChild(title);

if (props.message) {
title.style.margin = '0 0 0.5rem';

const message = document.createElement('span');
message.innerText = props.message;
message.style.color = '#' + theme.white;
message.style.wordBreak = 'break-word';
pageHeaderContainer.appendChild(message);
}

root.appendChild(pageHeaderContainer);

// This has to run after appending elements to root
// because we need to actual mounted height.
Spacer(document, root, {
space: pageHeaderContainer.offsetHeight.toString(10),
});
}

module.exports = PageHeader;
Loading