Skip to content

Commit

Permalink
Loader - capabilities and module system detection (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmmmwh authored Apr 1, 2021
1 parent 94a6a38 commit 38e70c0
Show file tree
Hide file tree
Showing 34 changed files with 1,933 additions and 687 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}
},
{
"files": ["test/**/fixtures/*.esm.js"],
"files": ["test/**/fixtures/esm/*.js"],
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
Expand Down
10 changes: 0 additions & 10 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,6 @@ const schema = require('./options.json');

// Mapping of react-refresh globals to Webpack runtime globals
const REPLACEMENTS = {
$RefreshRuntime$: {
expr: `${refreshGlobal}.runtime`,
req: [webpackRequire, `${refreshGlobal}.runtime`],
type: 'object',
},
$RefreshCleanup$: {
expr: `${refreshGlobal}.cleanup`,
req: [webpackRequire, `${refreshGlobal}.cleanup`],
type: 'function',
},
$RefreshReg$: {
expr: `${refreshGlobal}.register`,
req: [webpackRequire, `${refreshGlobal}.register`],
Expand Down
4 changes: 4 additions & 0 deletions lib/utils/getRefreshGlobal.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ function getRefreshGlobal(runtimeTemplate = FALLBACK_RUNTIME_TEMPLATE) {
`setup: ${runtimeTemplate.basicFunction('currentModuleId', [
// Store all previous values for fields on `refreshGlobal` -
// this allows proper restoration in the `cleanup` phase.
`${declaration} prevModuleId = ${refreshGlobal}.moduleId;`,
`${declaration} prevRuntime = ${refreshGlobal}.runtime;`,
`${declaration} prevRegister = ${refreshGlobal}.register;`,
`${declaration} prevSignature = ${refreshGlobal}.signature;`,
`${declaration} prevCleanup = ${refreshGlobal}.cleanup;`,
'',
`${refreshGlobal}.moduleId = currentModuleId;`,
'',
// Initialise the runtime with stubs.
// If the module is processed by our loader,
// they will be mutated in place during module initialisation.
Expand All @@ -63,6 +66,7 @@ function getRefreshGlobal(runtimeTemplate = FALLBACK_RUNTIME_TEMPLATE) {
// In rare cases, it might get called in another module's `cleanup` phase.
'if (currentModuleId === cleanupModuleId) {',
Template.indent([
`${refreshGlobal}.moduleId = prevModuleId;`,
`${refreshGlobal}.runtime = prevRuntime;`,
`${refreshGlobal}.register = prevRegister;`,
`${refreshGlobal}.signature = prevSignature;`,
Expand Down
87 changes: 0 additions & 87 deletions loader/RefreshModule.runtime.js

This file was deleted.

13 changes: 0 additions & 13 deletions loader/RefreshSetup.runtime.js

This file was deleted.

76 changes: 35 additions & 41 deletions loader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,31 @@
const originalFetch = global.fetch;
delete global.fetch;

const { SourceMapConsumer, SourceMapGenerator, SourceNode } = require('source-map');
const { getOptions } = require('loader-utils');
const { validate: validateOptions } = require('schema-utils');
const { SourceMapConsumer, SourceNode } = require('source-map');
const { Template } = require('webpack');
const { refreshGlobal } = require('../lib/globals');
const {
getIdentitySourceMap,
getModuleSystem,
getRefreshModuleRuntime,
normalizeOptions,
} = require('./utils');
const schema = require('./options.json');

/**
* Generates an identity source map from a source file.
* @param {string} source The content of the source file.
* @param {string} resourcePath The name of the source file.
* @returns {import('source-map').RawSourceMap} The identity source map.
*/
function getIdentitySourceMap(source, resourcePath) {
const sourceMap = new SourceMapGenerator();
sourceMap.setSourceContent(resourcePath, source);

source.split('\n').forEach((line, index) => {
sourceMap.addMapping({
source: resourcePath,
original: {
line: index + 1,
column: 0,
},
generated: {
line: index + 1,
column: 0,
},
});
});
const RefreshRuntimePath = require
.resolve('react-refresh/runtime.js')
.replace(/\\/g, '/')
.replace(/'/g, "\\'");

return sourceMap.toJSON();
}

/**
* Gets a runtime template from provided function.
* @param {function(): void} fn A function containing the runtime template.
* @returns {string} The "sanitized" runtime template.
*/
function getTemplate(fn) {
return Template.getFunctionContent(fn).trim().replace(/^ {2}/gm, '');
}

const RefreshSetupRuntime = getTemplate(require('./RefreshSetup.runtime')).replace(
'$RefreshRuntimePath$',
require.resolve('react-refresh/runtime').replace(/\\/g, '/').replace(/'/g, "\\'")
);
const RefreshModuleRuntime = getTemplate(require('./RefreshModule.runtime'));
const RefreshSetupRuntimes = {
cjs: Template.asString(`${refreshGlobal}.runtime = require('${RefreshRuntimePath}');`),
esm: Template.asString([
`import * as __react_refresh_runtime__ from '${RefreshRuntimePath}';`,
`${refreshGlobal}.runtime = __react_refresh_runtime__;`,
]),
};

/**
* A simple Webpack loader to inject react-refresh HMR code into modules.
Expand All @@ -61,6 +42,14 @@ const RefreshModuleRuntime = getTemplate(require('./RefreshModule.runtime'));
* @returns {void}
*/
function ReactRefreshLoader(source, inputSourceMap, meta) {
let options = getOptions(this);
validateOptions(schema, options, {
baseDataPath: 'options',
name: 'React Refresh Loader',
});

options = normalizeOptions(options);

const callback = this.async();

/**
Expand All @@ -70,6 +59,11 @@ function ReactRefreshLoader(source, inputSourceMap, meta) {
* @returns {Promise<[string, import('source-map').RawSourceMap]>}
*/
async function _loader(source, inputSourceMap) {
const moduleSystem = await getModuleSystem(this, options);

const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
const RefreshModuleRuntime = getRefreshModuleRuntime({ const: options.const, moduleSystem });

if (this.sourceMap) {
let originalSourceMap = inputSourceMap;
if (!originalSourceMap) {
Expand Down
37 changes: 37 additions & 0 deletions loader/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"additionalProperties": false,
"type": "object",
"definitions": {
"MatchCondition": {
"anyOf": [{ "instanceof": "RegExp", "tsType": "RegExp" }, { "$ref": "#/definitions/Path" }]
},
"MatchConditions": {
"type": "array",
"items": { "$ref": "#/definitions/MatchCondition" },
"minItems": 1
},
"Path": { "type": "string" },
"ESModuleOptions": {
"additionalProperties": false,
"type": "object",
"properties": {
"exclude": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
},
"include": {
"anyOf": [
{ "$ref": "#/definitions/MatchCondition" },
{ "$ref": "#/definitions/MatchConditions" }
]
}
}
}
},
"properties": {
"const": { "type": "boolean" },
"esModule": { "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/ESModuleOptions" }] }
}
}
17 changes: 17 additions & 0 deletions loader/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @typedef {Object} ESModuleOptions
* @property {string | RegExp | Array<string | RegExp>} [exclude] Files to explicitly exclude from flagged as ES Modules.
* @property {string | RegExp | Array<string | RegExp>} [include] Files to explicitly include for flagged as ES Modules.
*/

/**
* @typedef {Object} ReactRefreshLoaderOptions
* @property {boolean} [const] Enables usage of ES6 `const` and `let` in generated runtime code.
* @property {boolean | ESModuleOptions} [esModule] Enables strict ES Modules compatible runtime.
*/

/**
* @typedef {import('type-fest').SetRequired<ReactRefreshLoaderOptions, 'const'>} NormalizedLoaderOptions
*/

module.exports = {};
30 changes: 30 additions & 0 deletions loader/utils/getIdentitySourceMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { SourceMapGenerator } = require('source-map');

/**
* Generates an identity source map from a source file.
* @param {string} source The content of the source file.
* @param {string} resourcePath The name of the source file.
* @returns {import('source-map').RawSourceMap} The identity source map.
*/
function getIdentitySourceMap(source, resourcePath) {
const sourceMap = new SourceMapGenerator();
sourceMap.setSourceContent(resourcePath, source);

source.split('\n').forEach((line, index) => {
sourceMap.addMapping({
source: resourcePath,
original: {
line: index + 1,
column: 0,
},
generated: {
line: index + 1,
column: 0,
},
});
});

return sourceMap.toJSON();
}

module.exports = getIdentitySourceMap;
Loading

0 comments on commit 38e70c0

Please sign in to comment.