Skip to content

Commit

Permalink
Add support for "type": "module" in package.json (#33637)
Browse files Browse the repository at this point in the history
- [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`
  • Loading branch information
timneutkens committed Feb 15, 2022
1 parent 127f94d commit 62b1704
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 13 deletions.
7 changes: 7 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -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 })

Expand Down Expand Up @@ -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)

Expand Down
Expand Up @@ -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
Expand All @@ -27,27 +27,30 @@ 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
// it is a Refresh Boundary.
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
Expand All @@ -66,7 +69,7 @@ export default function () {
currentExports
)
) {
module.hot.invalidate()
__webpack_module__.hot.invalidate()
} else {
self.$RefreshHelpers$.scheduleUpdate()
}
Expand All @@ -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()
}
}
}
Expand Down
25 changes: 21 additions & 4 deletions packages/react-refresh-utils/loader.ts
Expand Up @@ -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
42 changes: 42 additions & 0 deletions 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 <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()
})
})

0 comments on commit 62b1704

Please sign in to comment.