diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 91fcf8b8c0ce2..1e6eaff734339 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -532,6 +532,13 @@ export default async function build( await recursiveDelete(distDir, /^cache/) } + // Ensure commonjs handling is used for files in the distDir (generally .next) + // Files outside of the distDir can be "type": "module" + await promises.writeFile( + path.join(distDir, 'package.json'), + '{"type": "commonjs"}' + ) + // We need to write the manifest with rewrites before build // so serverless can import the manifest await nextBuildSpan diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 15743235edf12..a5b9391be164e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -41,6 +41,7 @@ import { DecodeError } from '../../shared/lib/utils' import { Span, trace } from '../../trace' import { getProperError } from '../../lib/is-error' import ws from 'next/dist/compiled/ws' +import { promises as fs } from 'fs' const wsServer = new ws.Server({ noServer: true }) @@ -432,6 +433,15 @@ export default class HotReloader { startSpan.stop() // Stop immediately to create an artificial parent span await this.clean(startSpan) + // Ensure distDir exists before writing package.json + await fs.mkdir(this.config.distDir, { recursive: true }) + + // Ensure commonjs handling is used for files in the distDir (generally .next) + // Files outside of the distDir can be "type": "module" + await fs.writeFile( + join(this.config.distDir, 'package.json'), + '{"type": "commonjs"}' + ) const configs = await this.getWebpackConfig(startSpan) diff --git a/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts b/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts index d4b5814bc6eb8..fc4b7cf5459f9 100644 --- a/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts +++ b/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts @@ -3,9 +3,9 @@ import { RefreshRuntimeGlobals } from '../runtime' declare const self: Window & RefreshRuntimeGlobals type Dictionary = { [key: string]: unknown } -declare const module: { +declare const __webpack_module__: { id: string - __proto__: { exports: unknown } + exports: unknown hot: { accept: () => void dispose: (onDispose: (data: Dictionary) => void) => void @@ -27,14 +27,16 @@ export default function () { // AMP / No-JS mode does not inject these helpers: '$RefreshHelpers$' in self ) { - var currentExports = module.__proto__.exports - var prevExports = module.hot.data?.prevExports ?? null + // @ts-ignore __webpack_module__ is global + var currentExports = __webpack_module__.exports + // @ts-ignore __webpack_module__ is global + var prevExports = __webpack_module__.hot.data?.prevExports ?? null // This cannot happen in MainTemplate because the exports mismatch between // templating and execution. self.$RefreshHelpers$.registerExportsForReactRefresh( currentExports, - module.id + __webpack_module__.id ) // A module can be accepted automatically based on its exports, e.g. when @@ -42,12 +44,13 @@ export default function () { if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) { // Save the previous exports on update so we can compare the boundary // signatures. - module.hot.dispose(function (data) { + __webpack_module__.hot.dispose(function (data) { data.prevExports = currentExports }) // Unconditionally accept an update to this module, we'll check if it's // still a Refresh Boundary later. - module.hot.accept() + // @ts-ignore importMeta is replaced in the loader + global.importMeta.webpackHot.accept() // This field is set when the previous version of this module was a // Refresh Boundary, letting us know we need to check for invalidation or @@ -66,7 +69,7 @@ export default function () { currentExports ) ) { - module.hot.invalidate() + __webpack_module__.hot.invalidate() } else { self.$RefreshHelpers$.scheduleUpdate() } @@ -78,7 +81,7 @@ export default function () { // because we already accepted this update (accidental side effect). var isNoLongerABoundary = prevExports !== null if (isNoLongerABoundary) { - module.hot.invalidate() + __webpack_module__.hot.invalidate() } } } diff --git a/packages/react-refresh-utils/loader.ts b/packages/react-refresh-utils/loader.ts index c44198429f665..85a321aaa51f0 100644 --- a/packages/react-refresh-utils/loader.ts +++ b/packages/react-refresh-utils/loader.ts @@ -2,16 +2,33 @@ import type { LoaderDefinition } from 'webpack' import RefreshModuleRuntime from './internal/ReactRefreshModule.runtime' let refreshModuleRuntime = RefreshModuleRuntime.toString() -refreshModuleRuntime = refreshModuleRuntime.slice( - refreshModuleRuntime.indexOf('{') + 1, - refreshModuleRuntime.lastIndexOf('}') +refreshModuleRuntime = refreshModuleRuntime + .slice( + refreshModuleRuntime.indexOf('{') + 1, + refreshModuleRuntime.lastIndexOf('}') + ) + // Given that the import above executes the module we need to make sure it does not crash on `import.meta` not being allowed. + .replace('global.importMeta', 'import.meta') + +let commonJsrefreshModuleRuntime = refreshModuleRuntime.replace( + 'import.meta.webpackHot', + 'module.hot' ) const ReactRefreshLoader: LoaderDefinition = function ReactRefreshLoader( source, inputSourceMap ) { - this.callback(null, `${source}\n\n;${refreshModuleRuntime}`, inputSourceMap) + this.callback( + null, + `${source}\n\n;${ + // Account for commonjs not supporting `import.meta + this.resourcePath.endsWith('.cjs') + ? commonJsrefreshModuleRuntime + : refreshModuleRuntime + }`, + inputSourceMap + ) } export default ReactRefreshLoader diff --git a/test/e2e/type-module-interop/index.test.ts b/test/e2e/type-module-interop/index.test.ts new file mode 100644 index 0000000000000..21e13a32586bf --- /dev/null +++ b/test/e2e/type-module-interop/index.test.ts @@ -0,0 +1,42 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { hasRedbox, renderViaHTTP } from 'next-test-utils' +import webdriver from 'next-webdriver' + +describe('Type module interop', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/index.js': ` + export default function Page() { + return
hello world
+ } + `, + }, + dependencies: {}, + }) + const contents = await next.readFile('package.json') + const pkg = JSON.parse(contents) + await next.patchFile( + 'package.json', + JSON.stringify({ + ...pkg, + type: 'module', + }) + ) + }) + afterAll(() => next.destroy()) + + it('should render server-side', async () => { + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('hello world') + }) + + it('should render client-side', async () => { + const browser = await webdriver(next.url, '/') + expect(await hasRedbox(browser)).toBe(false) + await browser.close() + }) +})