Files
next.js/test/production/required-server-files-package-boundary/required-server-files-package-boundary.test.ts
Tim NeutkensandGitHub 8f132ea984 Fix "type: module" in project dir when using standalone or adapters (#93612)
### What?

Add `.next/package.json` (the distDir commonjs boundary marker) to the
`required-server-files.json` manifest.

### Why?

`next build` writes `.next/package.json` with `{"type": "commonjs"}` so
that everything in `.next/**` is loaded as CJS even when the user's
project `package.json` has `"type": "module"`. Without that file, Node
walks up to the project `package.json` when loading
`.next/server/**/*.js` and tries to evaluate the compiled server bundles
as ESM, which fails at runtime.

This file is necessary on disk in every server deployment artifact (the
standalone bundle, every adapter Node function), but it was not declared
in `required-server-files.json`. As a result, adapters consuming
`requiredServerFiles.files` did not include the boundary file.

`.next/package.json` is conceptually a required server file: any
deployment that wants to run `.next/server/**/*.js` correctly under a
`"type": "module"` project must ship it. Declaring it in the manifest is
the right place to express that contract once for every consumer.

This error case would only happen if the project package.json gets
included into the Node.js functions. For example when file tracing
analysis (node-file-trace) decides it needs to include it (i.e. glob
patterns or such).

### How?

Add `'package.json'` to the `requiredServerFilesManifest.files` array in
`packages/next/src/build/index.ts`. Every entry in that array is joined
with `config.distDir`, so this resolves to `.next/package.json`.
Both existing consumers pick it up automatically with no further
changes:
- `packages/next/src/build/adapter/build-complete.ts` iterates
`requiredServerFiles` into `sharedNodeAssets`, which is merged into
every Node function's `output.assets` regardless of bundler. The Vercel
adapter (and any other adapter) gets the file for free.
- `copyTracedFiles` in `packages/next/src/build/utils.ts` iterates
`requiredServerFiles.files` and copies each entry into
`.next/standalone/<relative>`, so the standalone output now contains
`.next/standalone/.next/package.json`.

### Tests
- `test/production/required-server-files-package-boundary/` — a focused,
non-standalone test that builds a `"type": "module"` app and asserts:
  - `.next/package.json` is `{"type": "commonjs"}` on disk
  - `.next/required-server-files.json` lists `.next/package.json`
- `test/production/standalone-mode/type-module/` — extended with a
`pages/dynamic.js` that uses `getServerSideProps`. The test now:
- Asserts `.next/standalone/.next/package.json` exists with `{"type":
"commonjs"}`
- Boots the standalone `server.js` and fetches `/dynamic`, which forces
Node to actually evaluate `.next/server/pages/dynamic.js` at runtime —
the exact code path the boundary file makes work
2026-05-08 17:49:29 +02:00

49 lines
1.8 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { join } from 'path'
describe('distDir package.json commonjs boundary', () => {
const { next } = nextTestSetup({
files: __dirname,
// Use "type": "module" in the project package.json so the boundary file
// is what determines whether `.next/server/**/*.js` is loaded as CJS.
packageJson: {
type: 'module',
},
})
it('writes the distDir package.json with the commonjs boundary', async () => {
const distPackageJson = JSON.parse(
await next.readFile('.next/package.json')
)
expect(distPackageJson).toEqual({ type: 'commonjs' })
})
it('lists the distDir package.json in required-server-files.json', async () => {
const manifest = JSON.parse(
await next.readFile('.next/required-server-files.json')
)
// Adapters consume `requiredServerFiles.files` to seed the per-page
// shared assets. The boundary marker must be in this list because
// per-page nft traces do not include it.
expect(manifest.files).toContain(join('.next', 'package.json'))
})
it('includes the distDir package.json in every adapter page output assets', async () => {
// The adapter writes per-page asset keys to `.next/adapter-page-assets.json`
// during onBuildComplete. The fix in `build/index.ts` ensures the boundary
// is in `requiredServerFiles.files`, which `handleBuildComplete` then
// merges into every page output's `assets` map.
const pageAssets = JSON.parse(
await next.readFile('.next/adapter-page-assets.json')
)
const pageIds = Object.keys(pageAssets)
expect(pageIds.length).toBeGreaterThan(0)
const boundaryPath = join('.next', 'package.json')
for (const pageId of pageIds) {
expect(pageAssets[pageId]).toContain(boundaryPath)
}
})
})