Skip to content

Commit

Permalink
feat: error integration (#3)
Browse files Browse the repository at this point in the history
feat: error integration
  • Loading branch information
pmmmwh authored Dec 6, 2019
2 parents c6e66e9 + 90c6320 commit d8d09d2
Show file tree
Hide file tree
Showing 29 changed files with 2,417 additions and 595 deletions.
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

0 comments on commit d8d09d2

Please sign in to comment.