Skip to content

Commit d8d09d2

Browse files
authored
feat: error integration (#3)
feat: error integration
2 parents c6e66e9 + 90c6320 commit d8d09d2

29 files changed

+2417
-595
lines changed

.prettierrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"endOfLine": "lf",
3+
"printWidth": 100,
34
"singleQuote": true,
45
"trailingComma": "es5"
56
}

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"babel-loader": "^8.0.6",
2020
"cross-env": "^6.0.3",
2121
"html-webpack-plugin": "^3.2.0",
22-
"react-refresh": "^0.6.0",
22+
"react-refresh": "^0.7.0",
2323
"react-refresh-webpack-plugin": "https://github.com/pmmmwh/react-refresh-webpack-plugin#master",
2424
"webpack": "^4.41.2",
2525
"webpack-cli": "^3.3.9",

example/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const HtmlWebpackPlugin = require('html-webpack-plugin');
2-
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');
2+
const ReactRefreshPlugin = require('../src');
33

44
module.exports = {
55
entry: './src/index.js',

example/yarn.lock

Lines changed: 517 additions & 504 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@
88
"lint": "prettier --check \"**/*.{js,jsx}\"",
99
"lint:fix": "prettier --write \"**/*.{js,jsx}\""
1010
},
11+
"dependencies": {
12+
"ansi-html": "^0.0.7",
13+
"error-stack-parser": "^2.0.4",
14+
"html-entities": "^1.2.1",
15+
"lodash.debounce": "^4.0.8",
16+
"react-dev-utils": "^9.1.0",
17+
"sockjs-client": "^1.4.0"
18+
},
1119
"devDependencies": {
1220
"prettier": "^1.18.2",
13-
"react-refresh": "^0.6.0",
21+
"react-refresh": "^0.7.0",
1422
"webpack": "^4.41.2"
1523
},
1624
"peerDependencies": {
17-
"react-refresh": "*"
25+
"react-refresh": ">= 0.7"
1826
},
1927
"engines": {
20-
"node": "8.x || 9.x || 10.x || 11.x || 12.x || 13.x"
28+
"node": ">= 8.x"
2129
}
2230
}

src/helpers/createRefreshTemplate.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { Template } = require('webpack');
88
* [Ref](https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js#L233)
99
*/
1010
const beforeModule = `
11-
var cleanup = function NoOp() {};
11+
let cleanup = function NoOp() {};
1212
1313
if (window && window.$RefreshSetup$) {
1414
cleanup = window.$RefreshSetup$(module.i);
@@ -33,10 +33,7 @@ const afterModule = `
3333
function createRefreshTemplate(source, chunk) {
3434
// If a chunk is injected with the plugin,
3535
// our custom entry for react-refresh musts be injected
36-
if (
37-
!chunk.entryModule ||
38-
!/ReactRefreshEntry/.test(chunk.entryModule._identifier || '')
39-
) {
36+
if (!chunk.entryModule || !/ReactRefreshEntry/.test(chunk.entryModule._identifier || '')) {
4037
return source;
4138
}
4239

src/helpers/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const createRefreshTemplate = require('./createRefreshTemplate');
22
const injectRefreshEntry = require('./injectRefreshEntry');
33

4-
module.exports = { createRefreshTemplate, injectRefreshEntry };
4+
module.exports = {
5+
createRefreshTemplate,
6+
injectRefreshEntry,
7+
};

src/helpers/injectRefreshEntry.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,22 @@
77
* @returns {WebpackEntry} An injected entry object.
88
*/
99
const injectRefreshEntry = originalEntry => {
10-
const ReactRefreshEntry = require.resolve('../runtime/ReactRefreshEntry');
10+
const entryInjects = [
11+
// React-refresh runtime
12+
require.resolve('../runtime/ReactRefreshEntry'),
13+
// Error overlay runtime
14+
require.resolve('../runtime/ErrorOverlayEntry'),
15+
// React-refresh Babel transform detection
16+
require.resolve('../runtime/BabelDetectComponent'),
17+
];
1118

1219
// Single string entry point
1320
if (typeof originalEntry === 'string') {
14-
return [ReactRefreshEntry, originalEntry];
21+
return [...entryInjects, originalEntry];
1522
}
1623
// Single array entry point
1724
if (Array.isArray(originalEntry)) {
18-
return [ReactRefreshEntry, ...originalEntry];
25+
return [...entryInjects, ...originalEntry];
1926
}
2027
// Multiple entry points
2128
if (typeof originalEntry === 'object') {

src/index.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@ const webpack = require('webpack');
33
const { createRefreshTemplate, injectRefreshEntry } = require('./helpers');
44
const { refreshUtils } = require('./runtime/globals');
55

6+
/**
7+
* @typedef {Object} ReactRefreshPluginOptions
8+
* @property {boolean} [disableRefreshCheck] A flag to disable detection of the react-refresh Babel plugin.
9+
* @property {boolean} [forceEnable] A flag to enable the plugin forcefully.
10+
*/
11+
12+
/** @type {ReactRefreshPluginOptions} */
13+
const defaultOptions = {
14+
disableRefreshCheck: false,
15+
forceEnable: false,
16+
};
17+
618
class ReactRefreshPlugin {
719
/**
8-
* @param {*} [options] Options for react-refresh-plugin.
9-
* @param {boolean} [options.forceEnable] A flag to enable the plugin forcefully.
20+
* @param {ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
1021
* @returns {void}
1122
*/
1223
constructor(options) {
13-
this.options = options || {};
24+
this.options = Object.assign(defaultOptions, options);
1425
}
1526

1627
/**
@@ -59,11 +70,14 @@ class ReactRefreshPlugin {
5970
/\.([jt]sx?|flow)$/.test(data.resource) &&
6071
// Skip all files from node_modules
6172
!/node_modules/.test(data.resource) &&
62-
// Skip runtime refresh utilities (to prevent self-referencing)
73+
// Skip files related to refresh runtime (to prevent self-referencing)
6374
// This is useful when using the plugin as a direct dependency
64-
data.resource !== path.join(__dirname, './runtime/utils.js')
75+
!data.resource.includes(path.join(__dirname, './runtime'))
6576
) {
66-
data.loaders.unshift(require.resolve('./loader'));
77+
data.loaders.unshift({
78+
loader: require.resolve('./loader'),
79+
options: undefined,
80+
});
6781
}
6882

6983
return data;
@@ -76,6 +90,33 @@ class ReactRefreshPlugin {
7690
// Constructs the correct module template for react-refresh
7791
createRefreshTemplate
7892
);
93+
94+
compilation.hooks.finishModules.tap(this.constructor.name, modules => {
95+
if (!this.options.disableRefreshCheck) {
96+
const refreshPluginInjection = /\$RefreshReg\$/;
97+
const RefreshDetectionModule = modules.find(
98+
module => module.resource === require.resolve('./runtime/BabelDetectComponent.js')
99+
);
100+
101+
// In most cases, if we cannot find the injected detection module,
102+
// there are other compilation instances injected by other plugins.
103+
// We will have to bail out in those cases.
104+
if (!RefreshDetectionModule) {
105+
return;
106+
}
107+
108+
// Check for the function transform by the Babel plugin.
109+
if (!refreshPluginInjection.test(RefreshDetectionModule._source.source())) {
110+
throw new Error(
111+
[
112+
'The plugin is unable to detect transformed code from react-refresh.',
113+
'Did you forget to include "react-refresh/babel" in your list of Babel plugins?',
114+
'Note: you can disable this check by setting "disableRefreshCheck: true".',
115+
].join(' ')
116+
);
117+
}
118+
}
119+
});
79120
});
80121
}
81122
}

src/loader.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const path = require('path');
12
const { Template } = require('webpack');
23
const { refreshUtils } = require('./runtime/globals');
34

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

78
/**
89
* A simple Webpack loader to inject react-refresh HMR code into modules.
10+
*
11+
* [Reference for Loader API](https://webpack.js.org/api/loaders/)
912
* @param {string} source The original module source code.
13+
* @param {*} [inputSourceMap] The source map of the module.
14+
* @property {function(string): void} addDependency Adds a dependency for Webpack to watch.
15+
* @property {function(Error | null, string | Buffer, *?, *?): void} callback Sends loader results to Webpack.
1016
* @returns {string} The injected module source code.
1117
*/
12-
function RefreshHotLoader(source) {
13-
// Only apply transform if the source code contains a React import
14-
return reactModule.test(source)
15-
? source +
16-
Template.getFunctionContent(require('./runtime/RefreshModuleRuntime'))
17-
.trim()
18-
.replace(/\$RefreshUtils\$/g, refreshUtils)
19-
: source;
18+
function RefreshHotLoader(source, inputSourceMap) {
19+
// Add dependency to allow caching and invalidations
20+
this.addDependency(path.resolve('./runtime/RefreshModuleRuntime'));
21+
22+
// Use callback to allow source maps to pass through
23+
this.callback(
24+
null,
25+
// Only apply transform if the source code contains a React import
26+
reactModule.test(source)
27+
? source +
28+
'\n\n' +
29+
Template.getFunctionContent(require('./runtime/RefreshModuleRuntime'))
30+
.trim()
31+
.replace(/^ {2}/gm, '')
32+
.replace(/\$RefreshUtils\$/g, refreshUtils)
33+
: source,
34+
inputSourceMap
35+
);
2036
}
2137

2238
module.exports = RefreshHotLoader;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const ansiHTML = require('ansi-html');
2+
const { Html5Entities } = require('html-entities');
3+
const theme = require('../theme');
4+
const formatFilename = require('../utils/formatFilename');
5+
6+
ansiHTML.setColors(theme);
7+
8+
const entities = new Html5Entities();
9+
10+
/**
11+
* @typedef {Object} CompileErrorTraceProps
12+
* @property {string} errorMessage
13+
*/
14+
15+
/**
16+
* A formatter that turns Webpack compile error messages into highlighted HTML source traces.
17+
* @param {Document} document
18+
* @param {HTMLElement} root
19+
* @param {CompileErrorTraceProps} props
20+
* @returns {void}
21+
*/
22+
function CompileErrorTrace(document, root, props) {
23+
const errorParts = props.errorMessage.split('\n');
24+
const errorMessage = errorParts
25+
.splice(1, 1)[0]
26+
// Strip filename from the error message
27+
.replace(/^(.*:)\s.*:(\s.*)$/, '$1$2');
28+
errorParts[0] = formatFilename(errorParts[0]);
29+
errorParts.unshift(errorMessage);
30+
31+
const stackContainer = document.createElement('pre');
32+
stackContainer.innerHTML = ansiHTML(entities.encode(errorParts.join('\n')));
33+
stackContainer.style.fontFamily = [
34+
'"Operator Mono SSm"',
35+
'"Operator Mono"',
36+
'"Fira Code Retina"',
37+
'"Fira Code"',
38+
'"FiraCode-Retina"',
39+
'"Andale Mono"',
40+
'"Lucida Console"',
41+
'Menlo',
42+
'Consolas',
43+
'Monaco',
44+
'monospace',
45+
].join(', ');
46+
stackContainer.style.margin = '0';
47+
stackContainer.style.whiteSpace = 'pre-wrap';
48+
49+
root.appendChild(stackContainer);
50+
}
51+
52+
module.exports = CompileErrorTrace;

src/overlay/components/PageHeader.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const theme = require('../theme');
2+
const Spacer = require('./Spacer');
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.dimgrey;
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.padding = '1rem 1.5rem';
25+
pageHeaderContainer.style.position = 'fixed';
26+
pageHeaderContainer.style.top = props.topOffset || '0';
27+
pageHeaderContainer.style.width = 'calc(100vw - 3rem)';
28+
29+
const title = document.createElement('h3');
30+
title.innerText = props.title;
31+
title.style.color = '#' + theme.red;
32+
title.style.fontSize = '1.125rem';
33+
title.style.lineHeight = '1.3';
34+
title.style.margin = '0';
35+
pageHeaderContainer.appendChild(title);
36+
37+
if (props.message) {
38+
title.style.margin = '0 0 0.5rem';
39+
40+
const message = document.createElement('span');
41+
message.innerText = props.message;
42+
message.style.color = '#' + theme.white;
43+
message.style.wordBreak = 'break-word';
44+
pageHeaderContainer.appendChild(message);
45+
}
46+
47+
root.appendChild(pageHeaderContainer);
48+
49+
// This has to run after appending elements to root
50+
// because we need to actual mounted height.
51+
Spacer(document, root, {
52+
space: pageHeaderContainer.offsetHeight.toString(10),
53+
});
54+
}
55+
56+
module.exports = PageHeader;

0 commit comments

Comments
 (0)