Skip to content

Commit

Permalink
feat: feature detect Webpack compiler version (#415)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmmmwh authored Jun 3, 2021
1 parent 185e251 commit a88fd0f
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 244 deletions.
42 changes: 0 additions & 42 deletions lib/RefreshRuntimeModule.js

This file was deleted.

31 changes: 20 additions & 11 deletions lib/globals.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
const { version } = require('webpack');
/**
* Gets current bundle's global scope identifier for React Refresh.
* @param {Record<string, string>} runtimeGlobals The Webpack runtime globals.
* @returns {string} The React Refresh global scope within the Webpack bundle.
*/
module.exports.getRefreshGlobalScope = (runtimeGlobals) => {
return `${runtimeGlobals.require || '__webpack_require__'}.$Refresh$`;
};

// Parse the major version of Webpack: x.y.z => x
const webpackVersion = parseInt(version || '', 10);
/**
* Gets current Webpack version according to features on the compiler instance.
* @param {import('webpack').Compiler} compiler The current Webpack compiler instance.
* @returns {number} The current Webpack version.
*/
module.exports.getWebpackVersion = (compiler) => {
if (!compiler.hooks) {
throw new Error(`[ReactRefreshPlugin] Webpack version is not supported!`);
}

let webpackGlobals = {};
if (webpackVersion === 5) {
webpackGlobals = require('webpack/lib/RuntimeGlobals');
}

module.exports.webpackVersion = webpackVersion;
module.exports.webpackRequire = webpackGlobals.require || '__webpack_require__';
module.exports.refreshGlobal = `${module.exports.webpackRequire}.$Refresh$`;
// Webpack v5+ implements compiler caching
return 'cache' in compiler ? 5 : 4;
};
108 changes: 33 additions & 75 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
const { validate: validateOptions } = require('schema-utils');
const {
DefinePlugin,
EntryPlugin,
ModuleFilenameHelpers,
ProvidePlugin,
Template,
} = require('webpack');
const ConstDependency = require('webpack/lib/dependencies/ConstDependency');
const { refreshGlobal, webpackRequire, webpackVersion } = require('./globals');
const { getRefreshGlobalScope, getWebpackVersion } = require('./globals');
const {
getAdditionalEntries,
getIntegrationEntry,
getParserHelpers,
getRefreshGlobal,
getSocketIntegration,
injectRefreshEntry,
injectRefreshLoader,
makeRefreshRuntimeModule,
normalizeOptions,
} = require('./utils');
const schema = require('./options.json');

// Mapping of react-refresh globals to Webpack runtime globals
const REPLACEMENTS = {
$RefreshReg$: {
expr: `${refreshGlobal}.register`,
req: [webpackRequire, `${refreshGlobal}.register`],
type: 'function',
},
$RefreshSig$: {
expr: `${refreshGlobal}.signature`,
req: [webpackRequire, `${refreshGlobal}.signature`],
type: 'function',
},
};

class ReactRefreshPlugin {
/**
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
Expand All @@ -57,12 +35,6 @@ class ReactRefreshPlugin {
* @returns {void}
*/
apply(compiler) {
// Throw if we encounter an unsupported Webpack version,
// since things will most likely not work.
if (webpackVersion !== 4 && webpackVersion !== 5) {
throw new Error(`[ReactRefreshPlugin] Webpack v${webpackVersion} is not supported!`);
}

// Skip processing in non-development mode, but allow manual force-enabling
if (
// Webpack do not set process.env.NODE_ENV, so we need to check for mode.
Expand All @@ -76,20 +48,34 @@ class ReactRefreshPlugin {
return;
}

const webpackVersion = getWebpackVersion(compiler);
const logger = compiler.getInfrastructureLogger(this.constructor.name);
let loggedHotWarning = false;

// Get Webpack imports from compiler instance (if available) -
// this allow mono-repos to use different versions of Webpack without conflicts.
const webpack = compiler.webpack || require('webpack');
const {
DefinePlugin,
EntryDependency,
EntryPlugin,
ModuleFilenameHelpers,
NormalModule,
ProvidePlugin,
RuntimeGlobals,
Template,
} = webpack;

// Inject react-refresh context to all Webpack entry points.
// This should create `EntryDependency` objects when available,
// and fallback to patching the `entry` object for legacy workflows.
const additional = getAdditionalEntries({
const addEntries = getAdditionalEntries({
devServer: compiler.options.devServer,
options: this.options,
});
if (EntryPlugin) {
// Prepended entries does not care about injection order,
// so we can utilise EntryPlugin for simpler logic.
additional.prependEntries.forEach((entry) => {
addEntries.prependEntries.forEach((entry) => {
new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
});

Expand All @@ -114,7 +100,7 @@ class ReactRefreshPlugin {
// Overlay entries need to be injected AFTER integration's entry,
// so we will loop through everything in `finishMake` instead of `make`.
// This ensures we can traverse all entry points and inject stuff with the correct order.
additional.overlayEntries.forEach((entry, idx, arr) => {
addEntries.overlayEntries.forEach((entry, idx, arr) => {
compiler.hooks.finishMake.tapPromise(
{ name: this.constructor.name, stage: Number.MIN_SAFE_INTEGER + (arr.length - idx - 1) },
(compilation) => {
Expand Down Expand Up @@ -154,12 +140,20 @@ class ReactRefreshPlugin {
);
});
} else {
compiler.options.entry = injectRefreshEntry(compiler.options.entry, additional);
compiler.options.entry = injectRefreshEntry(compiler.options.entry, addEntries);
}

// Inject necessary modules to bundle's global scope
// Inject necessary modules and variables to bundle's global scope
const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {});
/** @type {Record<string, string | boolean>}*/
const definedModules = {
// Mapping of react-refresh globals to Webpack runtime globals
$RefreshReg$: `${refreshGlobal}.register`,
$RefreshSig$: `${refreshGlobal}.signature`,
'typeof $RefreshReg$': 'function',
'typeof $RefreshSig$': 'function',

// Library mode
__react_refresh_library__: JSON.stringify(
Template.toIdentifier(
this.options.library ||
Expand Down Expand Up @@ -197,7 +191,7 @@ class ReactRefreshPlugin {
new ProvidePlugin(providedModules).apply(compiler);

const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
const { evaluateToString, toConstantDependency } = getParserHelpers();
let loggedHotWarning = false;
compiler.hooks.compilation.tap(
this.constructor.name,
(compilation, { normalModuleFactory }) => {
Expand All @@ -206,9 +200,6 @@ class ReactRefreshPlugin {
return;
}

// Set template for ConstDependency which is used by parser hooks
compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template());

// Tap into version-specific compilation hooks
switch (webpackVersion) {
case 4: {
Expand Down Expand Up @@ -278,7 +269,7 @@ class ReactRefreshPlugin {
this.constructor.name,
// Setup react-refresh globals as extensions to Webpack's require function
(source) => {
return Template.asString([source, '', getRefreshGlobal()]);
return Template.asString([source, '', getRefreshGlobal(Template)]);
}
);

Expand Down Expand Up @@ -314,14 +305,10 @@ class ReactRefreshPlugin {
break;
}
case 5: {
const EntryDependency = require('webpack/lib/dependencies/EntryDependency');
const NormalModule = require('webpack/lib/NormalModule');
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals');
const ReactRefreshRuntimeModule = require('./RefreshRuntimeModule');

// Set factory for EntryDependency which is used to initialise the module
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);

const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
compilation.hooks.additionalTreeRuntimeRequirements.tap(
this.constructor.name,
// Setup react-refresh globals with a Webpack runtime module
Expand Down Expand Up @@ -371,35 +358,6 @@ class ReactRefreshPlugin {
// Do nothing - this should be an impossible case
}
}

/**
* Transform global calls into Webpack runtime calls.
* @param {*} parser
* @returns {void}
*/
const parserHandler = (parser) => {
Object.entries(REPLACEMENTS).forEach(([key, info]) => {
parser.hooks.expression
.for(key)
.tap(this.constructor.name, toConstantDependency(parser, info.expr, info.req));

if (info.type) {
parser.hooks.evaluateTypeof
.for(key)
.tap(this.constructor.name, evaluateToString(info.type));
}
});
};

normalModuleFactory.hooks.parser
.for('javascript/auto')
.tap(this.constructor.name, parserHandler);
normalModuleFactory.hooks.parser
.for('javascript/dynamic')
.tap(this.constructor.name, parserHandler);
normalModuleFactory.hooks.parser
.for('javascript/esm')
.tap(this.constructor.name, parserHandler);
}
);
}
Expand Down
44 changes: 0 additions & 44 deletions lib/utils/getParserHelpers.js

This file was deleted.

Loading

0 comments on commit a88fd0f

Please sign in to comment.