Skip to content

Loader - capabilities and module system detection #338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3ec32c4
refactor: extract loader utilities into directory
pmmmwh Mar 27, 2021
6b17a0f
chore(deps): add loader-utils to process options
pmmmwh Mar 28, 2021
b38f15b
feat: define loader options and option types
pmmmwh Mar 29, 2021
2ccdd0f
feat: implement loader options normalization
pmmmwh Mar 29, 2021
53883ea
feat: implement heuristic to determine current module system
pmmmwh Mar 29, 2021
625d1b1
feat: implement simple template renderer, export utils
pmmmwh Mar 29, 2021
4deef22
feat: hoist current module id in refresh global scope
pmmmwh Mar 29, 2021
b52cf4a
refactor: make refresh module runtime template-able with full ES5 compat
pmmmwh Mar 29, 2021
20071f1
feat: make loader accept options
pmmmwh Mar 29, 2021
08cfc5f
feat: render templates according to current capabilities and module s…
pmmmwh Mar 29, 2021
a442ac7
chore: generate loader types
pmmmwh Mar 29, 2021
dd35c2d
chore: add missing path type in options
pmmmwh Mar 29, 2021
8a72f6a
test: fix jest-resolver to include js suffix
pmmmwh Mar 29, 2021
8844d20
refactor: rename blockIdentifier to const
pmmmwh Mar 29, 2021
b638410
refactor: remove templating and use webpack template instead
pmmmwh Mar 29, 2021
7d0325e
test: update loader snapshots
pmmmwh Mar 29, 2021
355e5b7
test: update refresh global snapshots
pmmmwh Mar 29, 2021
38a070f
test: enable devtools by default in debug mode
pmmmwh Mar 29, 2021
b901eda
chore: remove renderTemplate
pmmmwh Mar 29, 2021
9af61b3
test: add tests for getIdentitySourceMap
pmmmwh Mar 30, 2021
458e63f
test: add tests for normalizeOptions
pmmmwh Mar 30, 2021
0d91ebe
test: update tests for getRefreshGlobal to check moduleId
pmmmwh Mar 30, 2021
06a04df
test: use toBe for comparing primitives
pmmmwh Mar 30, 2021
4c15e5b
test: add tests for getRefreshModuleRuntime and fix typo
pmmmwh Mar 30, 2021
79110b1
fix: use node10 compatible fs promises
pmmmwh Mar 30, 2021
aab268f
refactor: remove unneeded parser replacements
pmmmwh Mar 30, 2021
53510c8
test: update loader snapshots
pmmmwh Mar 30, 2021
f56bc03
chore: regenerate types
pmmmwh Mar 30, 2021
f18fbc6
test: ensure loader compilations does not cache
pmmmwh Apr 1, 2021
621a2a6
test: refactor location mock and add fetch mock
pmmmwh Apr 1, 2021
45d28da
test: move fixtures into module system folders
pmmmwh Apr 1, 2021
fd4c823
test: add fetch polyfill test for loader
pmmmwh Apr 1, 2021
4e080eb
test: test include/exclude options of esModule
pmmmwh Apr 1, 2021
7669a76
test: add test for getModuleSystem
pmmmwh Apr 1, 2021
ffda273
test: fix broken impl of getModuleSystem
pmmmwh Apr 1, 2021
7a1c43f
test: fix ESLint for loader fixtures
pmmmwh Apr 1, 2021
a5c821b
docs: update JSDoc for loader code
pmmmwh Apr 1, 2021
b5571ee
test: add loader validate options test
pmmmwh Apr 1, 2021
c172a66
chore: regenerate types for loader
pmmmwh Apr 1, 2021
702a124
test: change compilation to accept context instead of filename
pmmmwh Apr 1, 2021
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
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