diff --git a/package.json b/package.json index c8865389697ba..16e9420986a95 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,8 @@ "tree-kill": "1.2.1", "typescript": "3.8.3", "wait-port": "0.2.2", - "webpack-bundle-analyzer": "3.6.1" + "webpack-bundle-analyzer": "3.6.1", + "worker-loader": "2.0.0" }, "resolutions": { "browserslist": "^4.8.3", diff --git a/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts b/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts index 2a1a6bab7655f..d749222312d2c 100644 --- a/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts +++ b/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts @@ -6,8 +6,31 @@ import { // @ts-ignore exists in webpack 5 RuntimeGlobals, version, + compilation as Compilation, } from 'webpack' +// Shared between webpack 4 and 5: +function injectRefreshFunctions(compilation: Compilation.Compilation) { + const hookVars: typeof compilation['mainTemplate']['hooks']['requireExtensions'] = (compilation + .mainTemplate.hooks as any).localVars + + hookVars.tap('ReactFreshWebpackPlugin', (source) => + Template.asString([ + source, + '', + '// noop fns to prevent runtime errors during initialization', + 'if (typeof self !== "undefined") {', + Template.indent('self.$RefreshReg$ = function () {};'), + Template.indent('self.$RefreshSig$ = function () {'), + Template.indent(Template.indent('return function (type) {')), + Template.indent(Template.indent(Template.indent('return type;'))), + Template.indent(Template.indent('};')), + Template.indent('};'), + '}', + ]) + ) +} + function webpack4(compiler: Compiler) { // Webpack 4 does not have a method to handle interception of module // execution. @@ -16,6 +39,8 @@ function webpack4(compiler: Compiler) { // https://github.com/webpack/webpack/blob/4c644bf1f7cb067c748a52614500e0e2182b2700/lib/MainTemplate.js#L200 compiler.hooks.compilation.tap('ReactFreshWebpackPlugin', (compilation) => { + injectRefreshFunctions(compilation) + const hookRequire: typeof compilation['mainTemplate']['hooks']['requireExtensions'] = (compilation .mainTemplate.hooks as any).require @@ -106,6 +131,8 @@ function webpack5(compiler: Compiler) { } compiler.hooks.compilation.tap('ReactFreshWebpackPlugin', (compilation) => { + injectRefreshFunctions(compilation) + // @ts-ignore Exists in webpack 5 compilation.hooks.additionalTreeRuntimeRequirements.tap( 'ReactFreshWebpackPlugin', diff --git a/packages/react-refresh-utils/runtime.ts b/packages/react-refresh-utils/runtime.ts index 43554e3d68c43..9ee9a8feec4f5 100644 --- a/packages/react-refresh-utils/runtime.ts +++ b/packages/react-refresh-utils/runtime.ts @@ -13,14 +13,6 @@ declare const self: Window & RefreshRuntimeGlobals // Hook into ReactDOM initialization RefreshRuntime.injectIntoGlobalHook(self) -// noop fns to prevent runtime errors during initialization -self.$RefreshReg$ = function () {} -self.$RefreshSig$ = function () { - return function (type) { - return type - } -} - // Register global helpers self.$RefreshHelpers$ = RefreshHelpers diff --git a/test/integration/worker-loader/lib/demo.worker.js b/test/integration/worker-loader/lib/demo.worker.js new file mode 100644 index 0000000000000..710fb47eabee1 --- /dev/null +++ b/test/integration/worker-loader/lib/demo.worker.js @@ -0,0 +1,4 @@ +const { Expensive } = require('./sharedCode') + +Expensive() +self.postMessage(true) diff --git a/test/integration/worker-loader/lib/sharedCode.js b/test/integration/worker-loader/lib/sharedCode.js new file mode 100644 index 0000000000000..da7cd7ab6dd3d --- /dev/null +++ b/test/integration/worker-loader/lib/sharedCode.js @@ -0,0 +1,17 @@ +export function Expensive() { + const start = performance.now() + let i = 99999 + + const bigArray = [] + while (--i) { + bigArray.push(i) + } + + const endTime = performance.now() + + if (typeof window === 'undefined') { + console.log('[WORKER] Completed expensive function in', endTime - start) + } else { + console.log('[WEB] Completed expensive function in', endTime - start) + } +} diff --git a/test/integration/worker-loader/next.config.js b/test/integration/worker-loader/next.config.js new file mode 100644 index 0000000000000..6b94ace037048 --- /dev/null +++ b/test/integration/worker-loader/next.config.js @@ -0,0 +1,18 @@ +module.exports = { + webpack: (config, { isServer }) => { + config.module.rules.unshift({ + test: /\.worker\.(js|ts|tsx)$/, + loader: 'worker-loader', + options: { + name: 'static/[hash].worker.js', + publicPath: '/_next/', + }, + }) + + if (!isServer) { + config.output.globalObject = 'self' + } + + return config + }, +} diff --git a/test/integration/worker-loader/pages/index.js b/test/integration/worker-loader/pages/index.js new file mode 100644 index 0000000000000..06bfc09ea654e --- /dev/null +++ b/test/integration/worker-loader/pages/index.js @@ -0,0 +1,39 @@ +import * as React from 'react' +import DemoWorker from '../lib/demo.worker' +import { Expensive } from '../lib/sharedCode' + +export default function Home() { + const [expensiveWebStatus, setExpensiveWebStatus] = React.useState('WAIT') + const [expensiveWorkerStatus, setExpensiveWorkerComplete] = React.useState( + 'WAIT' + ) + const worker = React.useRef() + + React.useEffect(() => { + worker.current = new DemoWorker() + worker.current.addEventListener('message', ({ data }) => { + if (data) { + setExpensiveWorkerComplete('PASS') + } + }) + worker.current.addEventListener('error', (data) => { + setExpensiveWorkerComplete('FAIL') + }) + }, [worker, setExpensiveWorkerComplete]) + React.useEffect(() => { + try { + Expensive() + setExpensiveWebStatus('PASS') + } catch { + setExpensiveWebStatus('FAIL') + } + }, []) + + return ( +
+

$RefreshRegistry repro

+
Web: {expensiveWebStatus}
+
Worker: {expensiveWorkerStatus}
+
+ ) +} diff --git a/test/integration/worker-loader/test/index.test.js b/test/integration/worker-loader/test/index.test.js new file mode 100644 index 0000000000000..8526d473397eb --- /dev/null +++ b/test/integration/worker-loader/test/index.test.js @@ -0,0 +1,35 @@ +/* eslint-env jest */ + +import { check, findPort, killApp, launchApp } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +const appDir = join(__dirname, '../') +const context = {} + +jest.setTimeout(1000 * 60 * 2) + +describe('Web Workers with Fast Refresh', () => { + beforeAll(async () => { + context.appPort = await findPort() + context.server = await launchApp(appDir, context.appPort) + }) + afterAll(() => { + killApp(context.server) + }) + + it('should pass on both client and worker', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/') + await browser.waitForElementByCss('#web-status') + await check(() => browser.elementByCss('#web-status').text(), /PASS/i) + await browser.waitForElementByCss('#worker-status') + await check(() => browser.elementByCss('#worker-status').text(), /PASS/i) + } finally { + if (browser) { + await browser.close() + } + } + }) +}) diff --git a/yarn.lock b/yarn.lock index 22030973aab30..b70548875f07b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9853,7 +9853,7 @@ loader-utils@2.0.0, loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^1.4.0: +loader-utils@^1.0.0, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" dependencies: @@ -14193,6 +14193,14 @@ schema-utils@2.6.6, schema-utils@^2.0.0, schema-utils@^2.6.1, schema-utils@^2.6. ajv "^6.12.0" ajv-keywords "^3.4.1" +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -16244,6 +16252,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"