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,95 @@
const querystring = require('querystring');
/**
* @typedef {Object} AdditionalEntries
* @property {string[]} prependEntries
* @property {string[]} overlayEntries
*/
/**
* Creates an object that contains two entry arrays: the prependEntries and overlayEntries
* @param {Object} optionsContainer This is the container for the options to this function
* @param {import('../types').NormalizedPluginOptions} optionsContainer.options Configuration options for this plugin.
* @param {import('webpack').Compiler["options"]["devServer"]} [optionsContainer.devServer] The webpack devServer config
* @returns {AdditionalEntries} An object that contains the Webpack entries for prepending and the overlay feature
*/
function getAdditionalEntries({ devServer, options }) {
/** @type {Record<string, string | number>} */
let resourceQuery = {};
if (devServer) {
const { client, https, http2, sockHost, sockPath, sockPort } = devServer;
let { host, path, port } = devServer;
let protocol = https || http2 ? 'https' : 'http';
if (sockHost) host = sockHost;
if (sockPath) path = sockPath;
if (sockPort) port = sockPort;
if (client && client.webSocketURL != null) {
let parsedUrl = client.webSocketURL;
if (typeof parsedUrl === 'string') parsedUrl = new URL(parsedUrl);
let auth;
if (parsedUrl.username) {
auth = parsedUrl.username;
if (parsedUrl.password) {
auth += ':' + parsedUrl.password;
}
}
if (parsedUrl.hostname != null) {
host = [auth != null && auth, parsedUrl.hostname].filter(Boolean).join('@');
}
if (parsedUrl.pathname != null) {
path = parsedUrl.pathname;
}
if (parsedUrl.port != null) {
port = !['0', 'auto'].includes(String(parsedUrl.port)) ? parsedUrl.port : undefined;
}
if (parsedUrl.protocol != null) {
protocol = parsedUrl.protocol !== 'auto' ? parsedUrl.protocol.replace(':', '') : 'ws';
}
}
if (host) resourceQuery.sockHost = host;
if (path) resourceQuery.sockPath = path;
if (port) resourceQuery.sockPort = port;
resourceQuery.sockProtocol = protocol;
}
if (options.overlay) {
const { sockHost, sockPath, sockPort, sockProtocol } = options.overlay;
if (sockHost) resourceQuery.sockHost = sockHost;
if (sockPath) resourceQuery.sockPath = sockPath;
if (sockPort) resourceQuery.sockPort = sockPort;
if (sockProtocol) resourceQuery.sockProtocol = sockProtocol;
}
// We don't need to URI encode the resourceQuery as it will be parsed by Webpack
const queryString = querystring.stringify(resourceQuery, undefined, undefined, {
/**
* @param {string} string
* @returns {string}
*/
encodeURIComponent(string) {
return string;
},
});
const prependEntries = [
// React-refresh runtime
require.resolve('../../client/ReactRefreshEntry'),
];
const overlayEntries = [
// Error overlay runtime
options.overlay &&
options.overlay.entry &&
`${require.resolve(options.overlay.entry)}${queryString ? `?${queryString}` : ''}`,
].filter(Boolean);
return { prependEntries, overlayEntries };
}
module.exports = getAdditionalEntries;

View File

@@ -0,0 +1,22 @@
/**
* Gets entry point of a supported socket integration.
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
* @returns {string | undefined} Path to the resolved integration entry point.
*/
function getIntegrationEntry(integrationType) {
let resolvedEntry;
switch (integrationType) {
case 'whm': {
resolvedEntry = 'webpack-hot-middleware/client';
break;
}
case 'wps': {
resolvedEntry = 'webpack-plugin-serve/client';
break;
}
}
return resolvedEntry;
}
module.exports = getIntegrationEntry;

View File

@@ -0,0 +1,96 @@
const { getRefreshGlobalScope } = require('../globals');
/**
* @typedef {Object} RuntimeTemplate
* @property {function(string, string[]): string} basicFunction
* @property {function(): boolean} supportsConst
* @property {function(string, string=): string} returningFunction
*/
/**
* Generates the refresh global runtime template.
* @param {import('webpack').Template} Template The template helpers.
* @param {Record<string, string>} [RuntimeGlobals] The runtime globals.
* @param {RuntimeTemplate} [RuntimeTemplate] The runtime template helpers.
* @returns {string} The refresh global runtime template.
*/
function getRefreshGlobal(
Template,
RuntimeGlobals = {},
RuntimeTemplate = {
basicFunction(args, body) {
return `function(${args}) {\n${Template.indent(body)}\n}`;
},
supportsConst() {
return false;
},
returningFunction(returnValue, args = '') {
return `function(${args}) { return ${returnValue}; }`;
},
}
) {
const declaration = RuntimeTemplate.supportsConst() ? 'const' : 'var';
const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals);
return Template.asString([
`${refreshGlobal} = {`,
Template.indent([
// Lifecycle methods - They should be specific per module and restored after module execution.
// These stubs ensure unwanted calls (e.g. unsupported patterns, broken transform) would not error out.
// If the current module is processed by our loader,
// they will be swapped in place during module initialisation by the `setup` method below.
`register: ${RuntimeTemplate.returningFunction('undefined')},`,
`signature: ${RuntimeTemplate.returningFunction(
RuntimeTemplate.returningFunction('type', 'type')
)},`,
// Runtime - This should be a singleton and persist throughout the lifetime of the app.
// This stub ensures calls to `runtime` would not error out.
// If any module within the bundle is processed by our loader,
// it will be swapped in place via an injected import.
'runtime: {',
Template.indent([
`createSignatureFunctionForTransform: ${RuntimeTemplate.returningFunction(
RuntimeTemplate.returningFunction('type', 'type')
)},`,
`register: ${RuntimeTemplate.returningFunction('undefined')}`,
]),
'},',
// Setup - This handles initialisation of the global runtime.
// It should never be touched throughout the lifetime of the app.
`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} prevRegister = ${refreshGlobal}.register;`,
`${declaration} prevSignature = ${refreshGlobal}.signature;`,
`${declaration} prevCleanup = ${refreshGlobal}.cleanup;`,
'',
`${refreshGlobal}.moduleId = currentModuleId;`,
'',
`${refreshGlobal}.register = ${RuntimeTemplate.basicFunction('type, id', [
`${declaration} typeId = currentModuleId + " " + id;`,
`${refreshGlobal}.runtime.register(type, typeId);`,
])}`,
'',
`${refreshGlobal}.signature = ${RuntimeTemplate.returningFunction(
`${refreshGlobal}.runtime.createSignatureFunctionForTransform()`
)};`,
'',
`${refreshGlobal}.cleanup = ${RuntimeTemplate.basicFunction('cleanupModuleId', [
// Only cleanup if the module IDs match.
// In rare cases, it might get called in another module's `cleanup` phase.
'if (currentModuleId === cleanupModuleId) {',
Template.indent([
`${refreshGlobal}.moduleId = prevModuleId;`,
`${refreshGlobal}.register = prevRegister;`,
`${refreshGlobal}.signature = prevSignature;`,
`${refreshGlobal}.cleanup = prevCleanup;`,
]),
'}',
])}`,
])}`,
]),
'};',
]);
}
module.exports = getRefreshGlobal;

View File

@@ -0,0 +1,30 @@
/**
* Gets the socket integration to use for Webpack messages.
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
* @returns {string} Path to the resolved socket integration module.
*/
function getSocketIntegration(integrationType) {
let resolvedSocketIntegration;
switch (integrationType) {
case 'wds': {
resolvedSocketIntegration = require.resolve('../../sockets/WDSSocket');
break;
}
case 'whm': {
resolvedSocketIntegration = require.resolve('../../sockets/WHMEventSource');
break;
}
case 'wps': {
resolvedSocketIntegration = require.resolve('../../sockets/WPSSocket');
break;
}
default: {
resolvedSocketIntegration = require.resolve(integrationType);
break;
}
}
return resolvedSocketIntegration;
}
module.exports = getSocketIntegration;

View File

@@ -0,0 +1,19 @@
const getAdditionalEntries = require('./getAdditionalEntries');
const getIntegrationEntry = require('./getIntegrationEntry');
const getRefreshGlobal = require('./getRefreshGlobal');
const getSocketIntegration = require('./getSocketIntegration');
const injectRefreshEntry = require('./injectRefreshEntry');
const injectRefreshLoader = require('./injectRefreshLoader');
const makeRefreshRuntimeModule = require('./makeRefreshRuntimeModule');
const normalizeOptions = require('./normalizeOptions');
module.exports = {
getAdditionalEntries,
getIntegrationEntry,
getRefreshGlobal,
getSocketIntegration,
injectRefreshEntry,
injectRefreshLoader,
makeRefreshRuntimeModule,
normalizeOptions,
};

View File

@@ -0,0 +1,98 @@
/** @typedef {string | string[] | import('webpack').Entry} StaticEntry */
/** @typedef {StaticEntry | import('webpack').EntryFunc} WebpackEntry */
const EntryParseError = new Error(
[
'[ReactRefreshPlugin]',
'Failed to parse the Webpack `entry` object!',
'Please ensure the `entry` option in your Webpack config is specified.',
].join(' ')
);
/**
* Webpack entries related to socket integrations.
* They have to run before any code that sets up the error overlay.
* @type {string[]}
*/
const socketEntries = [
'webpack-dev-server/client',
'webpack-hot-middleware/client',
'webpack-plugin-serve/client',
'react-dev-utils/webpackHotDevClient',
];
/**
* Checks if a Webpack entry string is related to socket integrations.
* @param {string} entry A Webpack entry string.
* @returns {boolean} Whether the entry is related to socket integrations.
*/
function isSocketEntry(entry) {
return socketEntries.some((socketEntry) => entry.includes(socketEntry));
}
/**
* Injects an entry to the bundle for react-refresh.
* @param {WebpackEntry} [originalEntry] A Webpack entry object.
* @param {import('./getAdditionalEntries').AdditionalEntries} additionalEntries An object that contains the Webpack entries for prepending and the overlay feature
* @returns {WebpackEntry} An injected entry object.
*/
function injectRefreshEntry(originalEntry, additionalEntries) {
const { prependEntries, overlayEntries } = additionalEntries;
// Single string entry point
if (typeof originalEntry === 'string') {
if (isSocketEntry(originalEntry)) {
return [...prependEntries, originalEntry, ...overlayEntries];
}
return [...prependEntries, ...overlayEntries, originalEntry];
}
// Single array entry point
if (Array.isArray(originalEntry)) {
if (originalEntry.length === 0) {
throw EntryParseError;
}
const socketEntryIndex = originalEntry.findIndex(isSocketEntry);
let socketAndPrecedingEntries = [];
if (socketEntryIndex !== -1) {
socketAndPrecedingEntries = originalEntry.splice(0, socketEntryIndex + 1);
}
return [...prependEntries, ...socketAndPrecedingEntries, ...overlayEntries, ...originalEntry];
}
// Multiple entry points
if (typeof originalEntry === 'object') {
const entries = Object.entries(originalEntry);
if (entries.length === 0) {
throw EntryParseError;
}
return entries.reduce(
(acc, [curKey, curEntry]) => ({
...acc,
[curKey]:
typeof curEntry === 'object' && curEntry.import
? {
...curEntry,
import: injectRefreshEntry(curEntry.import, additionalEntries),
}
: injectRefreshEntry(curEntry, additionalEntries),
}),
{}
);
}
// Dynamic entry points
if (typeof originalEntry === 'function') {
return (...args) =>
Promise.resolve(originalEntry(...args)).then((resolvedEntry) =>
injectRefreshEntry(resolvedEntry, additionalEntries)
);
}
throw EntryParseError;
}
module.exports = injectRefreshEntry;
module.exports.socketEntries = socketEntries;

View File

@@ -0,0 +1,58 @@
const path = require('path');
/**
* @callback MatchObject
* @param {string} [str]
* @returns {boolean}
*/
/**
* @typedef {Object} InjectLoaderOptions
* @property {MatchObject} match A function to include/exclude files to be processed.
* @property {import('../../loader/types').ReactRefreshLoaderOptions} [options] Options passed to the loader.
*/
const resolvedLoader = require.resolve('../../loader');
const reactRefreshPath = path.dirname(require.resolve('react-refresh'));
const refreshUtilsPath = path.join(__dirname, '../runtime/RefreshUtils');
/**
* Injects refresh loader to all JavaScript-like and user-specified files.
* @param {*} moduleData Module factory creation data.
* @param {InjectLoaderOptions} injectOptions Options to alter how the loader is injected.
* @returns {*} The injected module factory creation data.
*/
function injectRefreshLoader(moduleData, injectOptions) {
const { match, options } = injectOptions;
// Include and exclude user-specified files
if (!match(moduleData.matchResource || moduleData.resource)) return moduleData;
// Include and exclude dynamically generated modules from other loaders
if (moduleData.matchResource && !match(moduleData.request)) return moduleData;
// Exclude files referenced as assets
if (moduleData.type.includes('asset')) return moduleData;
// Check to prevent double injection
if (moduleData.loaders.find(({ loader }) => loader === resolvedLoader)) return moduleData;
// Skip react-refresh and the plugin's runtime utils to prevent self-referencing -
// this is useful when using the plugin as a direct dependency,
// or when node_modules are specified to be processed.
if (
moduleData.resource.includes(reactRefreshPath) ||
moduleData.resource.includes(refreshUtilsPath)
) {
return moduleData;
}
// As we inject runtime code for each module,
// it is important to run the injected loader after everything.
// This way we can ensure that all code-processing have been done,
// and we won't risk breaking tools like Flow or ESLint.
moduleData.loaders.unshift({
loader: resolvedLoader,
options,
});
return moduleData;
}
module.exports = injectRefreshLoader;

View File

@@ -0,0 +1,96 @@
/**
* Makes a runtime module to intercept module execution for React Refresh.
* This module creates an isolated `__webpack_require__` function for each module,
* and injects a `$Refresh$` object into it for use by React Refresh.
* @param {import('webpack')} webpack The Webpack exports.
* @returns {new () => import('webpack').RuntimeModule} The runtime module class.
*/
function makeRefreshRuntimeModule(webpack) {
return class ReactRefreshRuntimeModule extends webpack.RuntimeModule {
constructor() {
// Second argument is the `stage` for this runtime module -
// we'll use the same stage as Webpack's HMR runtime module for safety.
super('react refresh', webpack.RuntimeModule.STAGE_BASIC);
}
/**
* @returns {string} runtime code
*/
generate() {
if (!this.compilation) throw new Error('Webpack compilation missing!');
const { runtimeTemplate } = this.compilation;
const declareVar = runtimeTemplate.supportsConst() ? 'const' : 'var';
return webpack.Template.asString([
`${declareVar} setup = ${runtimeTemplate.basicFunction('moduleId', [
`${declareVar} refresh = {`,
webpack.Template.indent([
`moduleId: moduleId,`,
`register: ${runtimeTemplate.basicFunction('type, id', [
`${declareVar} typeId = moduleId + " " + id;`,
`refresh.runtime.register(type, typeId);`,
])},`,
`signature: ${runtimeTemplate.returningFunction(
'refresh.runtime.createSignatureFunctionForTransform()'
)},`,
`runtime: {`,
webpack.Template.indent([
`createSignatureFunctionForTransform: ${runtimeTemplate.returningFunction(
runtimeTemplate.returningFunction('type', 'type')
)},`,
`register: ${runtimeTemplate.emptyFunction()}`,
]),
`},`,
]),
`};`,
`return refresh;`,
])}`,
'',
`${webpack.RuntimeGlobals.interceptModuleExecution}.push(${runtimeTemplate.basicFunction(
'options',
[
`${declareVar} originalFactory = options.factory;`,
// Using a function declaration -
// ensures `this` would propagate for modules relying on it
`options.factory = function(moduleObject, moduleExports, webpackRequire) {`,
webpack.Template.indent([
// Our require function delegates to the original require function
`${declareVar} hotRequire = ${runtimeTemplate.returningFunction(
'webpackRequire(request)',
'request'
)};`,
// The propery descriptor factory below ensures all properties but `$Refresh$`
// are proxied through to the original require function
`${declareVar} createPropertyDescriptor = ${runtimeTemplate.basicFunction('name', [
`return {`,
webpack.Template.indent([
`configurable: true,`,
`enumerable: true,`,
`get: ${runtimeTemplate.returningFunction('webpackRequire[name]')},`,
`set: ${runtimeTemplate.basicFunction('value', [
'webpackRequire[name] = value;',
])},`,
]),
`};`,
])};`,
`for (${declareVar} name in webpackRequire) {`,
webpack.Template.indent([
`if (Object.prototype.hasOwnProperty.call(webpackRequire, name) && name !== "$Refresh$") {`,
webpack.Template.indent([
`Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));`,
]),
`}`,
]),
`}`,
`hotRequire.$Refresh$ = setup(options.id);`,
`originalFactory.call(this, moduleObject, moduleExports, hotRequire);`,
]),
'};',
]
)});`,
]);
}
};
}
module.exports = makeRefreshRuntimeModule;

View File

@@ -0,0 +1,44 @@
const { d, n } = require('../../options');
/**
* Normalizes the options for the plugin.
* @param {import('../types').ReactRefreshPluginOptions} options Non-normalized plugin options.
* @returns {import('../types').NormalizedPluginOptions} Normalized plugin options.
*/
const normalizeOptions = (options) => {
d(options, 'exclude', /node_modules/i);
d(options, 'include', /\.([cm]js|[jt]sx?|flow)$/i);
d(options, 'forceEnable');
d(options, 'library');
n(options, 'overlay', (overlay) => {
/** @type {import('../types').NormalizedErrorOverlayOptions} */
const defaults = {
entry: require.resolve('../../client/ErrorOverlayEntry'),
module: require.resolve('../../overlay'),
sockIntegration: 'wds',
};
if (overlay === false) {
return false;
}
if (typeof overlay === 'undefined' || overlay === true) {
return defaults;
}
d(overlay, 'entry', defaults.entry);
d(overlay, 'module', defaults.module);
d(overlay, 'sockIntegration', defaults.sockIntegration);
d(overlay, 'sockHost');
d(overlay, 'sockPath');
d(overlay, 'sockPort');
d(overlay, 'sockProtocol');
d(options, 'useURLPolyfill');
return overlay;
});
return options;
};
module.exports = normalizeOptions;