Skip to content

Commit c0208e7

Browse files
committed
refactor: enable type checking
1 parent 90d783a commit c0208e7

19 files changed

+152
-98
lines changed

lib/index.js

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { getRefreshGlobalScope } = require('./globals');
33
const {
44
getAdditionalEntries,
55
getIntegrationEntry,
6+
getLibraryNamespace,
67
getSocketIntegration,
78
injectRefreshLoader,
89
makeRefreshRuntimeModule,
@@ -15,7 +16,7 @@ class ReactRefreshPlugin {
1516
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
1617
*/
1718
constructor(options = {}) {
18-
validateOptions(schema, options, {
19+
validateOptions(/** @type {Parameters<typeof validateOptions>[0]} */ (schema), options, {
1920
name: 'React Refresh Plugin',
2021
baseDataPath: 'options',
2122
});
@@ -53,13 +54,13 @@ class ReactRefreshPlugin {
5354
const webpack = compiler.webpack || require('webpack');
5455
const {
5556
DefinePlugin,
56-
EntryDependency,
5757
EntryPlugin,
5858
ModuleFilenameHelpers,
5959
NormalModule,
6060
ProvidePlugin,
6161
RuntimeGlobals,
6262
Template,
63+
dependencies: { ModuleDependency },
6364
} = webpack;
6465

6566
// Inject react-refresh context to all Webpack entry points.
@@ -70,23 +71,33 @@ class ReactRefreshPlugin {
7071
new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
7172
}
7273

73-
const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
74+
/** @type {{ name?: string; index?: number }[]} */
7475
const socketEntryData = [];
75-
compiler.hooks.make.tap(
76-
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
77-
(compilation) => {
78-
// Exhaustively search all entries for `integrationEntry`.
79-
// If found, mark those entries and the index of `integrationEntry`.
80-
for (const [name, entryData] of compilation.entries.entries()) {
81-
const index = entryData.dependencies.findIndex(
82-
(dep) => dep.request && dep.request.includes(integrationEntry)
83-
);
84-
if (index !== -1) {
85-
socketEntryData.push({ name, index });
76+
77+
/** @type {string | undefined} */
78+
let integrationEntry;
79+
if (this.options.overlay && this.options.overlay.sockIntegration) {
80+
integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
81+
}
82+
83+
if (integrationEntry) {
84+
compiler.hooks.make.tap(
85+
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
86+
(compilation) => {
87+
// Exhaustively search all entries for `integrationEntry`.
88+
// If found, mark those entries and the index of `integrationEntry`.
89+
for (const [name, entryData] of compilation.entries.entries()) {
90+
const index = entryData.dependencies.findIndex((dep) => {
91+
if (!(dep instanceof ModuleDependency)) return false;
92+
return dep.request.includes(integrationEntry);
93+
});
94+
if (index !== -1) {
95+
socketEntryData.push({ name, index });
96+
}
8697
}
8798
}
88-
}
89-
);
99+
);
100+
}
90101

91102
// Overlay entries need to be injected AFTER integration's entry,
92103
// so we will loop through everything in `finishMake` instead of `make`.
@@ -97,37 +108,39 @@ class ReactRefreshPlugin {
97108
name: this.constructor.name,
98109
stage: Number.MIN_SAFE_INTEGER + (overlayEntries.length - idx - 1),
99110
},
100-
(compilation) => {
111+
async (compilation) => {
101112
// Only hook into the current compiler
102-
if (compilation.compiler !== compiler) {
103-
return Promise.resolve();
104-
}
113+
if (compilation.compiler !== compiler) return;
105114

106115
const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }];
107-
return Promise.all(
116+
await Promise.all(
108117
injectData.map(({ name, index }) => {
109-
return new Promise((resolve, reject) => {
110-
const options = { name };
111-
const dep = EntryPlugin.createDependency(entry, options);
112-
compilation.addEntry(compiler.context, dep, options, (err) => {
113-
if (err) return reject(err);
114-
115-
// If the entry is not a global one,
116-
// and we have registered the index for integration entry,
117-
// we will reorder all entry dependencies to our desired order.
118-
// That is, to have additional entries DIRECTLY behind integration entry.
119-
if (name && typeof index !== 'undefined') {
120-
const entryData = compilation.entries.get(name);
121-
entryData.dependencies.splice(
122-
index + 1,
123-
0,
124-
entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
125-
);
126-
}
127-
128-
resolve();
129-
});
130-
});
118+
return /** @type {Promise<void>} */ (
119+
new Promise((resolve, reject) => {
120+
const options = { name };
121+
const dep = EntryPlugin.createDependency(entry, options);
122+
compilation.addEntry(compiler.context, dep, options, (err) => {
123+
if (err) return reject(err);
124+
125+
// If the entry is not a global one,
126+
// and we have registered the index for integration entry,
127+
// we will reorder all entry dependencies to our desired order.
128+
// That is, to have additional entries DIRECTLY behind integration entry.
129+
if (name && typeof index !== 'undefined') {
130+
const entryData = compilation.entries.get(name);
131+
if (entryData) {
132+
entryData.dependencies.splice(
133+
index + 1,
134+
0,
135+
entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
136+
);
137+
}
138+
}
139+
140+
resolve();
141+
});
142+
})
143+
);
131144
})
132145
);
133146
}
@@ -143,21 +156,20 @@ class ReactRefreshPlugin {
143156
$RefreshSig$: `${refreshGlobal}.signature`,
144157
'typeof $RefreshReg$': 'function',
145158
'typeof $RefreshSig$': 'function',
146-
147-
// Library mode
148-
__react_refresh_library__: JSON.stringify(
149-
Template.toIdentifier(
150-
this.options.library ||
151-
compiler.options.output.uniqueName ||
152-
compiler.options.output.library
153-
)
154-
),
155159
};
156160
/** @type {Record<string, string>} */
157161
const providedModules = {
158162
__react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
159163
};
160164

165+
// Library mode
166+
const libraryNamespace = getLibraryNamespace(this.options, compiler.options.output);
167+
if (libraryNamespace) {
168+
definedModules['__react_refresh_library__'] = JSON.stringify(
169+
Template.toIdentifier(libraryNamespace)
170+
);
171+
}
172+
161173
if (this.options.overlay === false) {
162174
// Stub errorOverlay module so their calls can be erased
163175
definedModules.__react_refresh_error_overlay__ = false;
@@ -178,7 +190,15 @@ class ReactRefreshPlugin {
178190
new DefinePlugin(definedModules).apply(compiler);
179191
new ProvidePlugin(providedModules).apply(compiler);
180192

181-
const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
193+
/**
194+
* @param {string} [str]
195+
* @returns boolean
196+
*/
197+
const match = (str) => {
198+
if (str == null) return false;
199+
return ModuleFilenameHelpers.matchObject(this.options, str);
200+
};
201+
182202
let loggedHotWarning = false;
183203
compiler.hooks.compilation.tap(
184204
this.constructor.name,
@@ -188,9 +208,6 @@ class ReactRefreshPlugin {
188208
return;
189209
}
190210

191-
// Set factory for EntryDependency which is used to initialise the module
192-
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);
193-
194211
const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
195212
compilation.hooks.additionalTreeRuntimeRequirements.tap(
196213
this.constructor.name,

lib/utils/getAdditionalEntries.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function getAdditionalEntries(options) {
1818
const overlayEntries = [
1919
// Error overlay runtime
2020
options.overlay && options.overlay.entry && require.resolve(options.overlay.entry),
21-
].filter(Boolean);
21+
].filter((x) => typeof x === 'string');
2222

2323
return { prependEntries, overlayEntries };
2424
}

lib/utils/getIntegrationEntry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Gets entry point of a supported socket integration.
3-
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
3+
* @param {false | 'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
44
* @returns {string | undefined} Path to the resolved integration entry point.
55
*/
66
function getIntegrationEntry(integrationType) {

lib/utils/getLibraryNamespace.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Gets the library namespace for React Refresh injection.
3+
* @param {import('../types').NormalizedPluginOptions} pluginOptions Configuration options for this plugin.
4+
* @param {import('webpack').Compilation["options"]["output"]} outputOptions Configuration options for Webpack output.
5+
* @returns {string | undefined} The library namespace for React Refresh injection.
6+
*/
7+
function getLibraryNamespace(pluginOptions, outputOptions) {
8+
if (pluginOptions.library) return pluginOptions.library;
9+
if (outputOptions.uniqueName) return outputOptions.uniqueName;
10+
11+
if (!outputOptions.library || !outputOptions.library.name) return;
12+
13+
const libraryName = outputOptions.library.name;
14+
if (Array.isArray(libraryName)) return libraryName[0];
15+
if (typeof libraryName === 'string') return libraryName;
16+
if (Array.isArray(libraryName.root)) return libraryName.root[0];
17+
if (typeof libraryName.root === 'string') return libraryName.root;
18+
}
19+
20+
module.exports = getLibraryNamespace;

lib/utils/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const getAdditionalEntries = require('./getAdditionalEntries');
22
const getIntegrationEntry = require('./getIntegrationEntry');
3+
const getLibraryNamespace = require('./getLibraryNamespace');
34
const getSocketIntegration = require('./getSocketIntegration');
45
const injectRefreshLoader = require('./injectRefreshLoader');
56
const makeRefreshRuntimeModule = require('./makeRefreshRuntimeModule');
@@ -8,6 +9,7 @@ const normalizeOptions = require('./normalizeOptions');
89
module.exports = {
910
getAdditionalEntries,
1011
getIntegrationEntry,
12+
getLibraryNamespace,
1113
getSocketIntegration,
1214
injectRefreshLoader,
1315
makeRefreshRuntimeModule,

lib/utils/injectRefreshLoader.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,48 @@ const refreshUtilsPath = path.join(__dirname, '../runtime/RefreshUtils');
1818

1919
/**
2020
* Injects refresh loader to all JavaScript-like and user-specified files.
21-
* @param {*} moduleData Module factory creation data.
21+
* @param {import('webpack').ResolveData["createData"]} moduleData Module factory creation data.
2222
* @param {InjectLoaderOptions} injectOptions Options to alter how the loader is injected.
23-
* @returns {*} The injected module factory creation data.
23+
* @returns {import('webpack').ResolveData["createData"]} The injected module factory creation data.
2424
*/
2525
function injectRefreshLoader(moduleData, injectOptions) {
2626
const { match, options } = injectOptions;
2727

2828
// Include and exclude user-specified files
29-
if (!match(moduleData.matchResource || moduleData.resource)) return moduleData;
29+
if (!match(moduleData.matchResource || moduleData.resource || '')) return moduleData;
3030
// Include and exclude dynamically generated modules from other loaders
3131
if (moduleData.matchResource && !match(moduleData.request)) return moduleData;
3232
// Exclude files referenced as assets
33-
if (moduleData.type.includes('asset')) return moduleData;
33+
if (moduleData.type != null && moduleData.type.includes('asset')) return moduleData;
3434
// Check to prevent double injection
35-
if (moduleData.loaders.find(({ loader }) => loader === resolvedLoader)) return moduleData;
35+
if (
36+
moduleData.loaders != null &&
37+
moduleData.loaders.find(({ loader }) => loader === resolvedLoader)
38+
) {
39+
return moduleData;
40+
}
3641
// Skip react-refresh and the plugin's runtime utils to prevent self-referencing -
3742
// this is useful when using the plugin as a direct dependency,
3843
// or when node_modules are specified to be processed.
3944
if (
40-
moduleData.resource.includes(reactRefreshPath) ||
41-
moduleData.resource.includes(refreshUtilsPath)
45+
moduleData.resource != null &&
46+
(moduleData.resource.includes(reactRefreshPath) ||
47+
moduleData.resource.includes(refreshUtilsPath))
4248
) {
4349
return moduleData;
4450
}
4551

52+
if (moduleData.loaders == null) moduleData.loaders = [];
53+
4654
// As we inject runtime code for each module,
4755
// it is important to run the injected loader after everything.
4856
// This way we can ensure that all code-processing have been done,
4957
// and we won't risk breaking tools like Flow or ESLint.
5058
moduleData.loaders.unshift({
5159
loader: resolvedLoader,
5260
options,
61+
ident: null,
62+
type: null,
5363
});
5464

5565
return moduleData;

lib/utils/makeRefreshRuntimeModule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* This module creates an isolated `__webpack_require__` function for each module,
44
* and injects a `$Refresh$` object into it for use by React Refresh.
55
* @param {import('webpack')} webpack The Webpack exports.
6-
* @returns {typeof import('webpack').RuntimeModule} The runtime module class.
6+
* @returns {new () => import('webpack').RuntimeModule} The runtime module class.
77
*/
88
function makeRefreshRuntimeModule(webpack) {
99
return class ReactRefreshRuntimeModule extends webpack.RuntimeModule {

lib/utils/normalizeOptions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const normalizeOptions = (options) => {
3333
return overlay;
3434
});
3535

36-
return options;
36+
return /** @type {import('../types').NormalizedPluginOptions} */ (options);
3737
};
3838

3939
module.exports = normalizeOptions;

loader/index.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// That check, however, will break when `fetch` polyfills are used for SSR setups.
44
// We "reset" the polyfill here to ensure it won't interfere with source-map generation.
55
const originalFetch = global.fetch;
6+
// @ts-expect-error
67
delete global.fetch;
78

89
const { validate: validateOptions } = require('schema-utils');
@@ -31,17 +32,22 @@ const RefreshRuntimePath = require
3132
* @returns {void}
3233
*/
3334
function ReactRefreshLoader(source, inputSourceMap, meta) {
34-
let options = this.getOptions();
35-
validateOptions(schema, options, {
35+
const _options = this.getOptions();
36+
validateOptions(/** @type {Parameters<typeof validateOptions>[0]} */ (schema), _options, {
3637
baseDataPath: 'options',
3738
name: 'React Refresh Loader',
3839
});
3940

40-
options = normalizeOptions(options);
41+
const options = normalizeOptions(_options);
4142

4243
const callback = this.async();
4344

44-
const { ModuleFilenameHelpers, Template } = this._compiler.webpack || require('webpack');
45+
/** @type {import('webpack')} */
46+
let webpack;
47+
if (this._compiler != null && this._compiler.webpack) webpack = this._compiler.webpack;
48+
else webpack = require('webpack');
49+
50+
const { ModuleFilenameHelpers, Template } = webpack;
4551

4652
const RefreshSetupRuntimes = {
4753
cjs: Template.asString(
@@ -57,7 +63,7 @@ function ReactRefreshLoader(source, inputSourceMap, meta) {
5763
* @this {import('webpack').LoaderContext<import('./types').ReactRefreshLoaderOptions>}
5864
* @param {string} source
5965
* @param {import('source-map').RawSourceMap} [inputSourceMap]
60-
* @returns {Promise<[string, import('source-map').RawSourceMap]>}
66+
* @returns {Promise<[string, import('source-map').RawSourceMap?]>}
6167
*/
6268
async function _loader(source, inputSourceMap) {
6369
/** @type {'esm' | 'cjs'} */

loader/utils/getModuleSystem.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let packageJsonTypeMap = new Map();
66

77
/**
88
* Infers the current active module system from loader context and options.
9-
* @this {import('webpack').loader.LoaderContext}
9+
* @this {import('webpack').LoaderContext<import('../types').ReactRefreshLoaderOptions>}
1010
* @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
1111
* @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
1212
* @return {Promise<'esm' | 'cjs'>} The inferred module system.
@@ -73,7 +73,7 @@ async function getModuleSystem(ModuleFilenameHelpers, options) {
7373
// from the `resourcePath` folder up to the matching `searchPath`,
7474
// to avoid retracing these steps when processing sibling resources.
7575
if (packageJsonTypeMap.has(searchPath)) {
76-
packageJsonType = packageJsonTypeMap.get(searchPath);
76+
packageJsonType = /** @type {string} */ (packageJsonTypeMap.get(searchPath));
7777

7878
let currentPath = resourceContext;
7979
while (currentPath !== searchPath) {

0 commit comments

Comments
 (0)