index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. const validateOptions = require('schema-utils');
  2. const { DefinePlugin, ModuleFilenameHelpers, ProvidePlugin, Template } = require('webpack');
  3. const ConstDependency = require('webpack/lib/dependencies/ConstDependency');
  4. const { refreshGlobal, webpackRequire, webpackVersion } = require('./globals');
  5. const {
  6. createError,
  7. getParserHelpers,
  8. getRefreshGlobal,
  9. getSocketIntegration,
  10. injectRefreshEntry,
  11. injectRefreshLoader,
  12. normalizeOptions,
  13. } = require('./utils');
  14. const schema = require('./options.json');
  15. // Mapping of react-refresh globals to Webpack runtime globals
  16. const REPLACEMENTS = {
  17. $RefreshRuntime$: {
  18. expr: `${refreshGlobal}.runtime`,
  19. req: [webpackRequire, `${refreshGlobal}.runtime`],
  20. type: 'object',
  21. },
  22. $RefreshSetup$: {
  23. expr: `${refreshGlobal}.setup`,
  24. req: [webpackRequire, `${refreshGlobal}.setup`],
  25. type: 'function',
  26. },
  27. $RefreshCleanup$: {
  28. expr: `${refreshGlobal}.cleanup`,
  29. req: [webpackRequire, `${refreshGlobal}.cleanup`],
  30. type: 'function',
  31. },
  32. $RefreshReg$: {
  33. expr: `${refreshGlobal}.register`,
  34. req: [webpackRequire, `${refreshGlobal}.register`],
  35. type: 'function',
  36. },
  37. $RefreshSig$: {
  38. expr: `${refreshGlobal}.signature`,
  39. req: [webpackRequire, `${refreshGlobal}.signature`],
  40. type: 'function',
  41. },
  42. };
  43. class ReactRefreshPlugin {
  44. /**
  45. * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
  46. */
  47. constructor(options = {}) {
  48. validateOptions(schema, options, {
  49. name: 'React Refresh Plugin',
  50. baseDataPath: 'options',
  51. });
  52. /**
  53. * @readonly
  54. * @type {import('./types').NormalizedPluginOptions}
  55. */
  56. this.options = normalizeOptions(options);
  57. }
  58. /**
  59. * Applies the plugin.
  60. * @param {import('webpack').Compiler} compiler A webpack compiler object.
  61. * @returns {void}
  62. */
  63. apply(compiler) {
  64. // Throw if we encounter an unsupported Webpack version,
  65. // since things will most likely not work.
  66. if (webpackVersion !== 4 && webpackVersion !== 5) {
  67. throw createError(`Webpack v${webpackVersion} is not supported!`);
  68. }
  69. // Skip processing in non-development mode, but allow manual force-enabling
  70. if (
  71. // Webpack do not set process.env.NODE_ENV, so we need to check for mode.
  72. // Ref: https://github.com/webpack/webpack/issues/7074
  73. (compiler.options.mode !== 'development' ||
  74. // We also check for production process.env.NODE_ENV,
  75. // in case it was set and mode is non-development (e.g. 'none')
  76. (process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
  77. !this.options.forceEnable
  78. ) {
  79. return;
  80. }
  81. // Inject react-refresh context to all Webpack entry points
  82. compiler.options.entry = injectRefreshEntry(compiler.options.entry, this.options);
  83. // Inject necessary modules to bundle's global scope
  84. /** @type {Record<string, string>} */
  85. let providedModules = {
  86. __react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
  87. };
  88. if (this.options.overlay === false) {
  89. // Stub errorOverlay module so calls to it can be erased
  90. const definePlugin = new DefinePlugin({
  91. __react_refresh_error_overlay__: false,
  92. __react_refresh_init_socket__: false,
  93. });
  94. definePlugin.apply(compiler);
  95. } else {
  96. providedModules = {
  97. ...providedModules,
  98. ...(this.options.overlay.module && {
  99. __react_refresh_error_overlay__: require.resolve(this.options.overlay.module),
  100. }),
  101. ...(this.options.overlay.sockIntegration && {
  102. __react_refresh_init_socket__: getSocketIntegration(this.options.overlay.sockIntegration),
  103. }),
  104. };
  105. }
  106. const providePlugin = new ProvidePlugin(providedModules);
  107. providePlugin.apply(compiler);
  108. const matchObject = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
  109. const { evaluateToString, toConstantDependency } = getParserHelpers();
  110. compiler.hooks.compilation.tap(
  111. this.constructor.name,
  112. (compilation, { normalModuleFactory }) => {
  113. // Only hook into the current compiler
  114. if (compilation.compiler !== compiler) {
  115. return;
  116. }
  117. // Set template for ConstDependency which is used by parser hooks
  118. compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template());
  119. // Tap into version-specific compilation hooks
  120. switch (webpackVersion) {
  121. case 4: {
  122. const outputOptions = compilation.mainTemplate.outputOptions;
  123. compilation.mainTemplate.hooks.require.tap(
  124. this.constructor.name,
  125. // Constructs the module template for react-refresh
  126. (source, chunk, hash) => {
  127. // Check for the output filename
  128. // This is to ensure we are processing a JS-related chunk
  129. let filename = outputOptions.filename;
  130. if (typeof filename === 'function') {
  131. // Only usage of the `chunk` property is documented by Webpack.
  132. // However, some internal Webpack plugins uses other properties,
  133. // so we also pass them through to be on the safe side.
  134. filename = filename({
  135. contentHashType: 'javascript',
  136. chunk,
  137. hash,
  138. });
  139. }
  140. // Check whether the current compilation is outputting to JS,
  141. // since other plugins can trigger compilations for other file types too.
  142. // If we apply the transform to them, their compilation will break fatally.
  143. // One prominent example of this is the HTMLWebpackPlugin.
  144. // If filename is falsy, something is terribly wrong and there's nothing we can do.
  145. if (!filename || !filename.includes('.js')) {
  146. return source;
  147. }
  148. // Split template source code into lines for easier processing
  149. const lines = source.split('\n');
  150. // Webpack generates this line when the MainTemplate is called
  151. const moduleInitializationLineNumber = lines.findIndex((line) =>
  152. line.includes('modules[moduleId].call(')
  153. );
  154. // Unable to find call to module execution -
  155. // this happens if the current module does not call MainTemplate.
  156. // In this case, we will return the original source and won't mess with it.
  157. if (moduleInitializationLineNumber === -1) {
  158. return source;
  159. }
  160. const moduleInterceptor = Template.asString([
  161. `${refreshGlobal}.init();`,
  162. 'try {',
  163. Template.indent(lines[moduleInitializationLineNumber]),
  164. '} finally {',
  165. Template.indent(`${refreshGlobal}.cleanup(moduleId);`),
  166. '}',
  167. ]);
  168. return Template.asString([
  169. ...lines.slice(0, moduleInitializationLineNumber),
  170. '',
  171. outputOptions.strictModuleExceptionHandling
  172. ? Template.indent(moduleInterceptor)
  173. : moduleInterceptor,
  174. '',
  175. ...lines.slice(moduleInitializationLineNumber + 1, lines.length),
  176. ]);
  177. }
  178. );
  179. compilation.mainTemplate.hooks.requireExtensions.tap(
  180. this.constructor.name,
  181. // Setup react-refresh globals as extensions to Webpack's require function
  182. (source) => {
  183. return Template.asString([source, '', getRefreshGlobal()]);
  184. }
  185. );
  186. normalModuleFactory.hooks.afterResolve.tap(
  187. this.constructor.name,
  188. // Add react-refresh loader to process files that matches specified criteria
  189. (data) => {
  190. return injectRefreshLoader(data, matchObject);
  191. }
  192. );
  193. compilation.hooks.normalModuleLoader.tap(
  194. // `Infinity` ensures this check will run only after all other taps
  195. { name: this.constructor.name, stage: Infinity },
  196. // Check for existence of the HMR runtime -
  197. // it is the foundation to this plugin working correctly
  198. (context) => {
  199. if (!context.hot) {
  200. throw createError(
  201. [
  202. 'Hot Module Replacement (HMR) is not enabled!',
  203. 'React Refresh requires HMR to function properly.',
  204. ].join(' ')
  205. );
  206. }
  207. }
  208. );
  209. break;
  210. }
  211. case 5: {
  212. const NormalModule = require('webpack/lib/NormalModule');
  213. const RuntimeGlobals = require('webpack/lib/RuntimeGlobals');
  214. const ReactRefreshRuntimeModule = require('./runtime/RefreshRuntimeModule');
  215. compilation.hooks.additionalTreeRuntimeRequirements.tap(
  216. this.constructor.name,
  217. // Setup react-refresh globals with a Webpack runtime module
  218. (chunk, runtimeRequirements) => {
  219. runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
  220. compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule());
  221. }
  222. );
  223. normalModuleFactory.hooks.afterResolve.tap(
  224. this.constructor.name,
  225. // Add react-refresh loader to process files that matches specified criteria
  226. (resolveData) => {
  227. injectRefreshLoader(resolveData.createData, matchObject);
  228. }
  229. );
  230. NormalModule.getCompilationHooks(compilation).loader.tap(
  231. // `Infinity` ensures this check will run only after all other taps
  232. { name: this.constructor.name, stage: Infinity },
  233. // Check for existence of the HMR runtime -
  234. // it is the foundation to this plugin working correctly
  235. (context) => {
  236. if (!context.hot) {
  237. throw createError(
  238. [
  239. 'Hot Module Replacement (HMR) is not enabled!',
  240. 'React Refresh requires HMR to function properly.',
  241. ].join(' ')
  242. );
  243. }
  244. }
  245. );
  246. break;
  247. }
  248. default: {
  249. throw createError(`Encountered unexpected Webpack version (v${webpackVersion})`);
  250. }
  251. }
  252. /**
  253. * Transform global calls into Webpack runtime calls.
  254. * @param {*} parser
  255. * @returns {void}
  256. */
  257. const parserHandler = (parser) => {
  258. Object.entries(REPLACEMENTS).forEach(([key, info]) => {
  259. parser.hooks.expression
  260. .for(key)
  261. .tap(this.constructor.name, toConstantDependency(parser, info.expr, info.req));
  262. if (info.type) {
  263. parser.hooks.evaluateTypeof
  264. .for(key)
  265. .tap(this.constructor.name, evaluateToString(info.type));
  266. }
  267. });
  268. };
  269. normalModuleFactory.hooks.parser
  270. .for('javascript/auto')
  271. .tap(this.constructor.name, parserHandler);
  272. normalModuleFactory.hooks.parser
  273. .for('javascript/dynamic')
  274. .tap(this.constructor.name, parserHandler);
  275. normalModuleFactory.hooks.parser
  276. .for('javascript/esm')
  277. .tap(this.constructor.name, parserHandler);
  278. }
  279. );
  280. }
  281. }
  282. module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
  283. module.exports = ReactRefreshPlugin;