This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
// This is a patch for mozilla/source-map#349 -
// internally, it uses the existence of the `fetch` global to toggle browser behaviours.
// That check, however, will break when `fetch` polyfills are used for SSR setups.
// We "reset" the polyfill here to ensure it won't interfere with source-map generation.
const originalFetch = global.fetch;
delete global.fetch;
const { getOptions } = require('loader-utils');
const { validate: validateOptions } = require('schema-utils');
const { SourceMapConsumer, SourceNode } = require('source-map');
const {
getIdentitySourceMap,
getModuleSystem,
getRefreshModuleRuntime,
normalizeOptions,
} = require('./utils');
const schema = require('./options.json');
const RefreshRuntimePath = require
.resolve('react-refresh')
.replace(/\\/g, '/')
.replace(/'/g, "\\'");
/**
* A simple Webpack loader to inject react-refresh HMR code into modules.
*
* [Reference for Loader API](https://webpack.js.org/api/loaders/)
* @this {import('webpack').LoaderContext<import('./types').ReactRefreshLoaderOptions>}
* @param {string} source The original module source code.
* @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
* @param {*} [meta] The loader metadata passed in.
* @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();
const { ModuleFilenameHelpers, Template } = this._compiler.webpack || require('webpack');
const RefreshSetupRuntimes = {
cjs: Template.asString(
`__webpack_require__.$Refresh$.runtime = require('${RefreshRuntimePath}');`
),
esm: Template.asString([
`import * as __react_refresh_runtime__ from '${RefreshRuntimePath}';`,
`__webpack_require__.$Refresh$.runtime = __react_refresh_runtime__;`,
]),
};
/**
* @this {import('webpack').LoaderContext<import('./types').ReactRefreshLoaderOptions>}
* @param {string} source
* @param {import('source-map').RawSourceMap} [inputSourceMap]
* @returns {Promise<[string, import('source-map').RawSourceMap]>}
*/
async function _loader(source, inputSourceMap) {
/** @type {'esm' | 'cjs'} */
const moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);
const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
const RefreshModuleRuntime = getRefreshModuleRuntime(Template, {
const: options.const,
moduleSystem,
});
if (this.sourceMap) {
let originalSourceMap = inputSourceMap;
if (!originalSourceMap) {
originalSourceMap = getIdentitySourceMap(source, this.resourcePath);
}
return SourceMapConsumer.with(originalSourceMap, undefined, (consumer) => {
const node = SourceNode.fromStringWithSourceMap(source, consumer);
node.prepend([RefreshSetupRuntime, '\n\n']);
node.add(['\n\n', RefreshModuleRuntime]);
const { code, map } = node.toStringWithSourceMap();
return [code, map.toJSON()];
});
} else {
return [[RefreshSetupRuntime, source, RefreshModuleRuntime].join('\n\n'), inputSourceMap];
}
}
_loader.call(this, source, inputSourceMap).then(
([code, map]) => {
callback(null, code, map, meta);
},
(error) => {
callback(error);
}
);
}
module.exports = ReactRefreshLoader;
// Restore the original value of the `fetch` global, if it exists
if (originalFetch) {
global.fetch = originalFetch;
}

View File

@@ -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" }] }
}
}

View File

@@ -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 = {};

View File

@@ -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;

View File

@@ -0,0 +1,128 @@
const { promises: fsPromises } = require('fs');
const path = require('path');
/** @type {Map<string, string | undefined>} */
let packageJsonTypeMap = new Map();
/**
* Infers the current active module system from loader context and options.
* @this {import('webpack').loader.LoaderContext}
* @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
* @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
* @return {Promise<'esm' | 'cjs'>} The inferred module system.
*/
async function getModuleSystem(ModuleFilenameHelpers, options) {
// Check loader options -
// if `esModule` is set we don't have to do extra guess work.
switch (typeof options.esModule) {
case 'boolean': {
return options.esModule ? 'esm' : 'cjs';
}
case 'object': {
if (
options.esModule.include &&
ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.include)
) {
return 'esm';
}
if (
options.esModule.exclude &&
ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.exclude)
) {
return 'cjs';
}
break;
}
default: // Do nothing
}
// Check current resource's extension
if (/\.mjs$/.test(this.resourcePath)) return 'esm';
if (/\.cjs$/.test(this.resourcePath)) return 'cjs';
if (typeof this.addMissingDependency !== 'function') {
// This is Webpack 4 which does not support `import.meta`.
// We assume `.js` files are CommonJS because the output cannot be ESM anyway.
return 'cjs';
}
// We will assume CommonJS if we cannot determine otherwise
let packageJsonType = '';
// We begin our search for relevant `package.json` files,
// at the directory of the resource being loaded.
// These paths should already be resolved,
// but we resolve them again to ensure we are dealing with an aboslute path.
const resourceContext = path.dirname(this.resourcePath);
let searchPath = resourceContext;
let previousSearchPath = '';
// We start our search just above the root context of the webpack compilation
const stopPath = path.dirname(this.rootContext);
// If the module context is a resolved symlink outside the `rootContext` path,
// then we will never find the `stopPath` - so we also halt when we hit the root.
// Note that there is a potential that the wrong `package.json` is found in some pathalogical cases,
// such as a folder that is conceptually a package + does not have an ancestor `package.json`,
// but there exists a `package.json` higher up.
// This might happen if you have a folder of utility JS files that you symlink but did not organize as a package.
// We consider this an unsupported edge case for now.
while (searchPath !== stopPath && searchPath !== previousSearchPath) {
// If we have already determined the `package.json` type for this path we can stop searching.
// We do however still need to cache the found value,
// from the `resourcePath` folder up to the matching `searchPath`,
// to avoid retracing these steps when processing sibling resources.
if (packageJsonTypeMap.has(searchPath)) {
packageJsonType = packageJsonTypeMap.get(searchPath);
let currentPath = resourceContext;
while (currentPath !== searchPath) {
// We set the found type at least level from `resourcePath` folder up to the matching `searchPath`
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
break;
}
let packageJsonPath = path.join(searchPath, 'package.json');
try {
const packageSource = await fsPromises.readFile(packageJsonPath, 'utf-8');
try {
const packageObject = JSON.parse(packageSource);
// Any package.json is sufficient as long as it can be parsed.
// If it does not explicitly have a `type: "module"` it will be assumed to be CommonJS.
packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
packageJsonTypeMap.set(searchPath, packageJsonType);
// We set the type in the cache for all paths from the `resourcePath` folder,
// up to the matching `searchPath` to avoid retracing these steps when processing sibling resources.
let currentPath = resourceContext;
while (currentPath !== searchPath) {
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
} catch (e) {
// `package.json` exists but could not be parsed.
// We track it as a dependency so we can reload if this file changes.
}
this.addDependency(packageJsonPath);
break;
} catch (e) {
// `package.json` does not exist.
// We track it as a missing dependency so we can reload if this file is added.
this.addMissingDependency(packageJsonPath);
}
// Try again at the next level up
previousSearchPath = searchPath;
searchPath = path.dirname(searchPath);
}
// Check `package.json` for the `type` field -
// fallback to use `cjs` for anything ambiguous.
return packageJsonType === 'module' ? 'esm' : 'cjs';
}
module.exports = getModuleSystem;

View File

@@ -0,0 +1,63 @@
/**
* @typedef ModuleRuntimeOptions {Object}
* @property {boolean} const Use ES6 `const` and `let` in generated runtime code.
* @property {'cjs' | 'esm'} moduleSystem The module system to be used.
*/
/**
* Generates code appended to each JS-like module for react-refresh capabilities.
*
* `__react_refresh_utils__` will be replaced with actual utils during source parsing by `webpack.ProvidePlugin`.
*
* [Reference for Runtime Injection](https://github.com/webpack/webpack/blob/b07d3b67d2252f08e4bb65d354a11c9b69f8b434/lib/HotModuleReplacementPlugin.js#L419)
* [Reference for HMR Error Recovery](https://github.com/webpack/webpack/issues/418#issuecomment-490296365)
*
* @param {import('webpack').Template} Webpack's templating helpers.
* @param {ModuleRuntimeOptions} options The refresh module runtime options.
* @returns {string} The refresh module runtime template.
*/
function getRefreshModuleRuntime(Template, options) {
const constDeclaration = options.const ? 'const' : 'var';
const letDeclaration = options.const ? 'let' : 'var';
const webpackHot = options.moduleSystem === 'esm' ? 'import.meta.webpackHot' : 'module.hot';
return Template.asString([
`${constDeclaration} $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;`,
`${constDeclaration} $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(`,
Template.indent('$ReactRefreshModuleId$'),
');',
'',
'function $ReactRefreshModuleRuntime$(exports) {',
Template.indent([
`if (${webpackHot}) {`,
Template.indent([
`${letDeclaration} errorOverlay;`,
"if (typeof __react_refresh_error_overlay__ !== 'undefined') {",
Template.indent('errorOverlay = __react_refresh_error_overlay__;'),
'}',
`${letDeclaration} testMode;`,
"if (typeof __react_refresh_test__ !== 'undefined') {",
Template.indent('testMode = __react_refresh_test__;'),
'}',
'return __react_refresh_utils__.executeRuntime(',
Template.indent([
'exports,',
'$ReactRefreshModuleId$,',
`${webpackHot},`,
'errorOverlay,',
'testMode',
]),
');',
]),
'}',
]),
'}',
'',
"if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {",
Template.indent('$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);'),
'} else {',
Template.indent('$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);'),
'}',
]);
}
module.exports = getRefreshModuleRuntime;

View File

@@ -0,0 +1,11 @@
const getIdentitySourceMap = require('./getIdentitySourceMap');
const getModuleSystem = require('./getModuleSystem');
const getRefreshModuleRuntime = require('./getRefreshModuleRuntime');
const normalizeOptions = require('./normalizeOptions');
module.exports = {
getIdentitySourceMap,
getModuleSystem,
getRefreshModuleRuntime,
normalizeOptions,
};

View File

@@ -0,0 +1,25 @@
const { d, n } = require('../../options');
/**
* Normalizes the options for the loader.
* @param {import('../types').ReactRefreshLoaderOptions} options Non-normalized loader options.
* @returns {import('../types').NormalizedLoaderOptions} Normalized loader options.
*/
const normalizeOptions = (options) => {
d(options, 'const', false);
n(options, 'esModule', (esModule) => {
if (typeof esModule === 'boolean' || typeof esModule === 'undefined') {
return esModule;
}
d(esModule, 'include');
d(esModule, 'exclude');
return esModule;
});
return options;
};
module.exports = normalizeOptions;