Add support for "type": "module" in package.json (#33637)

- [x] Add failing test for development / production
- [x] Add failing test for client-side JavaScript
- [x] Write `.next/package.json` with `"type": "commonjs"
- [x] Fix issue with client-side JavaScript showing `module` is not defined

Production works after these changes. Development breaks on module not existing because of the Fast Refresh loader. Working with @sokra to add alternatives to what is being used in the loader to webpack so that it can be updated.

Fixes #23029, Fixes #24334



## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
This commit is contained in:
Tim Neutkens 2022-02-15 17:24:11 +01:00 committed by GitHub
parent 127f94dc13
commit 62b1704e41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 13 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()
}
}
}

View file

@ -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

View file

@ -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 <p>hello world</p>
}
`,
},
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()
})
})