Pick esm main fields and condition names first for RSC server layer (#50548)

For RSC server layer so far we bundle all dependencies, ESM format is the better one rather than CJS to analyze and tree-shake out the unused parts. This PR changes pick the condition names that are in ESM format first for server layer.

Also fixes the misorder of condition names of edge runtime, `conditionNames` should only contain either ESM or CJS, previously the main fields are mixed with conditon names which is not expected for webpack, we separate them now.

Since we're picking ESM instead CJS now, the error of require `exports * from` doesn't exist anymore, but if you're using a CJS dependency which require a ESM package, it will error. This is the existing behavior for our webpack configuration but could happen on server layer bundling

Other related changes:

* Imports are hoisted in ESM, so migrate`enhanceGlobals` to a imported module
* Use `...` to pick the proper imports by import expression, and prefer the `react-server` / `edge-light` condition names for corresponding cases
* Remove edge SSR duplicated `middleware` export checking
This commit is contained in:
Jiachi Liu 2023-06-08 18:24:03 +02:00 committed by GitHub
parent ea63e92e87
commit a035224dc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 989 additions and 789 deletions

View file

@ -224,7 +224,7 @@
"semver": "7.3.7",
"shell-quote": "1.7.3",
"strip-ansi": "6.0.0",
"styled-components": "6.0.0-beta.5",
"styled-components": "6.0.0-rc.3",
"styled-jsx": "5.1.1",
"styled-jsx-plugin-postcss": "3.0.2",
"swr": "^2.0.0",

View file

@ -1,8 +1,7 @@
import { adapter, enhanceGlobals } from 'next/dist/server/web/adapter'
import 'next/dist/server/web/globals'
import { adapter } from 'next/dist/server/web/adapter'
import { NAME, PAGE } from 'BOOTSTRAP_CONFIG'
enhanceGlobals()
var mod = require('ENTRY')
var handler = mod.middleware || mod.default

View file

@ -5,6 +5,7 @@ import crypto from 'crypto'
import { webpack } from 'next/dist/compiled/webpack/webpack'
import path from 'path'
import semver from 'next/dist/compiled/semver'
import { escapeStringRegexp } from '../shared/lib/escape-regexp'
import {
DOT_NEXT_ALIAS,
@ -98,15 +99,23 @@ const reactPackagesRegex = /^(react|react-dom|react-server-dom-webpack)($|\/)/
const asyncStoragesRegex =
/next[\\/]dist[\\/]client[\\/]components[\\/](static-generation-async-storage|action-async-storage|request-async-storage)/
// exports.<conditionName>
const edgeConditionNames = [
'edge-light',
'worker',
// inherits the default conditions
'...',
]
// packageJson.<mainField>
const mainFieldsPerCompiler: Record<CompilerNameValues, string[]> = {
[COMPILER_NAMES.server]: ['main', 'module'],
[COMPILER_NAMES.client]: ['browser', 'module', 'main'],
[COMPILER_NAMES.edgeServer]: [
'edge-light',
'worker',
'browser',
'module',
'main',
// inherits the default conditions
'...',
],
}
@ -915,12 +924,9 @@ export default async function getBaseWebpackConfig(
const reactServerCondition = [
'react-server',
...mainFieldsPerCompiler[
isEdgeServer ? COMPILER_NAMES.edgeServer : COMPILER_NAMES.server
],
'node',
'import',
'require',
...(isEdgeServer ? edgeConditionNames : []),
// inherits the default conditions
'...',
]
const clientEntries = isClient
@ -1166,11 +1172,7 @@ export default async function getBaseWebpackConfig(
: undefined),
mainFields: mainFieldsPerCompiler[compilerType],
...(isEdgeServer && {
conditionNames: [
...mainFieldsPerCompiler[COMPILER_NAMES.edgeServer],
'import',
'node',
],
conditionNames: edgeConditionNames,
}),
plugins: [],
}

View file

@ -30,13 +30,11 @@ const nextEdgeFunctionLoader: webpack.LoaderDefinitionFunction<EdgeFunctionLoade
buildInfo.rootDir = rootDir
return `
import { adapter, enhanceGlobals } from 'next/dist/esm/server/web/adapter'
import 'next/dist/esm/server/web/globals'
import { adapter } from 'next/dist/esm/server/web/adapter'
import { IncrementalCache } from 'next/dist/esm/server/lib/incremental-cache'
enhanceGlobals()
const mod = require(${stringifiedPagePath})
const handler = mod.middleware || mod.default
import handler from ${stringifiedPagePath}
if (typeof handler !== 'function') {
throw new Error('The Edge Function "pages${page}" must export a \`default\` function');

View file

@ -99,12 +99,11 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
)}${isAppDir ? `?${WEBPACK_RESOURCE_QUERIES.edgeSSREntry}` : ''}`
const transformed = `
import { adapter, enhanceGlobals } from 'next/dist/esm/server/web/adapter'
import 'next/dist/esm/server/web/globals'
import { adapter } from 'next/dist/esm/server/web/adapter'
import { getRender } from 'next/dist/esm/build/webpack/loaders/next-edge-ssr-loader/render'
import { IncrementalCache } from 'next/dist/esm/server/lib/incremental-cache'
enhanceGlobals()
const pagesType = ${JSON.stringify(pagesType)}
${
isAppDir
@ -132,10 +131,10 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
`
}
const incrementalCacheHandler = ${
${
incrementalCacheHandlerPath
? `require("${incrementalCacheHandlerPath}")`
: 'null'
? `import incrementalCacheHandler from "${incrementalCacheHandlerPath}"`
: 'const incrementalCacheHandler = null'
}
const buildManifest = self.__BUILD_MANIFEST

View file

@ -40,12 +40,11 @@ export default function middlewareLoader(this: any) {
buildInfo.rootDir = rootDir
return `
import { adapter, enhanceGlobals } from 'next/dist/esm/server/web/adapter'
import 'next/dist/esm/server/web/globals'
import { adapter } from 'next/dist/esm/server/web/adapter'
import * as mod from ${stringifiedPagePath}
enhanceGlobals()
var mod = require(${stringifiedPagePath})
var handler = mod.middleware || mod.default;
const handler = mod.middleware || mod.default
if (typeof handler !== 'function') {
throw new Error('The Middleware "pages${page}" must export a \`middleware\` or a \`default\` function');
@ -53,9 +52,9 @@ export default function middlewareLoader(this: any) {
export default function (opts) {
return adapter({
...opts,
page: ${JSON.stringify(page)},
handler,
...opts,
page: ${JSON.stringify(page)},
handler,
})
}
`

File diff suppressed because one or more lines are too long

View file

@ -1330,10 +1330,12 @@ export default class DevServer extends Server {
if (isError(err) && err.stack) {
try {
const frames = parseStack(err.stack!)
// Filter out internal edge related runtime stack
const frame = frames.find(
({ file }) =>
!file?.startsWith('eval') &&
!file?.includes('web/adapter') &&
!file?.includes('web/globals') &&
!file?.includes('sandbox/context') &&
!file?.includes('<anonymous>')
)

View file

@ -4,6 +4,7 @@
// This module will only be loaded once per process.
const { dirname } = require('path')
const mod = require('module')
const resolveFilename = mod._resolveFilename
const hookPropertyMap = new Map()
@ -20,9 +21,9 @@ export function addHookAliases(aliases: [string, string][] = []) {
addHookAliases([
// Use `require.resolve` explicitly to make them statically analyzable
// styled-jsx needs to be resolved as the external dependency.
['styled-jsx', require.resolve('styled-jsx')],
['styled-jsx/style', require.resolve('styled-jsx/style')],
['styled-jsx', dirname(require.resolve('styled-jsx/package.json'))],
['styled-jsx/style', require.resolve('styled-jsx/style')],
['zod', dirname(require.resolve('zod/package.json'))],
])
// Override built-in React packages if necessary

View file

@ -17,8 +17,7 @@ import {
RSC,
} from '../../client/components/app-router-headers'
import { NEXT_QUERY_PARAM_PREFIX } from '../../lib/constants'
declare const _ENTRIES: any
import { ensureInstrumentationRegistered } from './globals'
class NextRequestHint extends NextRequest {
sourcePage: string
@ -59,29 +58,6 @@ export type AdapterOptions = {
IncrementalCache?: typeof import('../lib/incremental-cache').IncrementalCache
}
async function registerInstrumentation() {
if (
'_ENTRIES' in globalThis &&
_ENTRIES.middleware_instrumentation &&
_ENTRIES.middleware_instrumentation.register
) {
try {
await _ENTRIES.middleware_instrumentation.register()
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
}
}
}
let registerInstrumentationPromise: Promise<void> | null = null
function ensureInstrumentationRegistered() {
if (!registerInstrumentationPromise) {
registerInstrumentationPromise = registerInstrumentation()
}
return registerInstrumentationPromise
}
export async function adapter(
params: AdapterOptions
): Promise<FetchEventResult> {
@ -318,50 +294,3 @@ export async function adapter(
waitUntil: Promise.all(event[waitUntilSymbol]),
}
}
function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
}
function __import_unsupported(moduleName: string) {
const proxy: any = new Proxy(function () {}, {
get(_obj, prop) {
if (prop === 'then') {
return {}
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
construct() {
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
apply(_target, _this, args) {
if (typeof args[0] === 'function') {
return args[0](proxy)
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
})
return new Proxy({}, { get: () => proxy })
}
export function enhanceGlobals() {
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env
global.process = process
}
// to allow building code that import but does not use node.js modules,
// webpack will expect this function to exist in global scope
Object.defineProperty(globalThis, '__import_unsupported', {
value: __import_unsupported,
enumerable: false,
configurable: false,
})
// Eagerly fire instrumentation hook to make the startup faster.
void ensureInstrumentationRegistered()
}

View file

@ -3,9 +3,9 @@ import type { RouteDefinition } from '../future/route-definitions/route-definiti
import type { RouteModule } from '../future/route-modules/route-module'
import type { NextRequest } from './spec-extension/request'
import { adapter, enhanceGlobals, type AdapterOptions } from './adapter'
import './globals'
import { adapter, type AdapterOptions } from './adapter'
import { IncrementalCache } from '../lib/incremental-cache'
enhanceGlobals()
import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'
import { RouteMatcher } from '../future/route-matchers/route-matcher'

View file

@ -0,0 +1,73 @@
declare const _ENTRIES: any
async function registerInstrumentation() {
if (
'_ENTRIES' in globalThis &&
_ENTRIES.middleware_instrumentation &&
_ENTRIES.middleware_instrumentation.register
) {
try {
await _ENTRIES.middleware_instrumentation.register()
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
}
}
}
let registerInstrumentationPromise: Promise<void> | null = null
export function ensureInstrumentationRegistered() {
if (!registerInstrumentationPromise) {
registerInstrumentationPromise = registerInstrumentation()
}
return registerInstrumentationPromise
}
function getUnsupportedModuleErrorMessage(module: string) {
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
return `The edge runtime does not support Node.js '${module}' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
}
function __import_unsupported(moduleName: string) {
const proxy: any = new Proxy(function () {}, {
get(_obj, prop) {
if (prop === 'then') {
return {}
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
construct() {
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
apply(_target, _this, args) {
if (typeof args[0] === 'function') {
return args[0](proxy)
}
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
},
})
return new Proxy({}, { get: () => proxy })
}
function enhanceGlobals() {
// The condition is true when the "process" module is provided
if (process !== global.process) {
// prefer local process but global.process has correct "env"
process.env = global.process.env
global.process = process
}
// to allow building code that import but does not use node.js modules,
// webpack will expect this function to exist in global scope
Object.defineProperty(globalThis, '__import_unsupported', {
value: __import_unsupported,
enumerable: false,
configurable: false,
})
// Eagerly fire instrumentation hook to make the startup faster.
void ensureInstrumentationRegistered()
}
enhanceGlobals()

View file

@ -44,7 +44,7 @@ function withTaggedErrors(fn: RunnerFn): RunnerFn {
})
}
export const getRuntimeContext = async (params: {
export async function getRuntimeContext(params: {
name: string
onWarning?: any
useCache: boolean
@ -52,7 +52,7 @@ export const getRuntimeContext = async (params: {
distDir: string
paths: string[]
incrementalCache?: any
}): Promise<EdgeRuntime<any>> => {
}): Promise<EdgeRuntime<any>> {
const { runtime, evaluateInContext } = await getModuleContext({
moduleName: params.name,
onWarning: params.onWarning ?? (() => {}),
@ -71,7 +71,7 @@ export const getRuntimeContext = async (params: {
return runtime
}
export const run = withTaggedErrors(async (params) => {
export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
const runtime = await getRuntimeContext(params)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []

File diff suppressed because it is too large Load diff

View file

@ -159,6 +159,12 @@ createNextDescribe(
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
})
it('should use the same react in server app', async () => {
@ -167,6 +173,26 @@ createNextDescribe(
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
})
it('should use the same react in edge server app', async () => {
const html = await next.render('/esm/edge-server')
const v1 = html.match(/App React Version: ([^<]+)</)[1]
const v2 = html.match(/External React Version: ([^<]+)</)[1]
expect(v1).toBe(v2)
// Should work with both esm and cjs imports
expect(html).toContain(
'CJS-ESM Compat package: cjs-esm-compat/index.mjs'
)
expect(html).toContain('CJS package: cjs-lib')
})
it('should use the same react in pages', async () => {
@ -189,25 +215,26 @@ createNextDescribe(
})
if (isNextDev) {
it('should error for wildcard exports of client module references in esm', async () => {
const page = 'app/esm-client-ref/page.js'
const pageSource = await next.readFile(page)
it('should error for require ESM package in CJS package', async () => {
const page = 'app/cjs-import-esm/page.js'
// reuse esm-client-ref/page.js
const pageSource = await next.readFile('app/esm-client-ref/page.js')
try {
await next.patchFile(
page,
pageSource.replace(
"'client-esm-module'",
"'client-esm-module-wildcard'"
"'client-cjs-import-esm-wildcard'"
)
)
await next.render('/esm-client-ref')
await next.render('/cjs-import-esm')
} finally {
await next.patchFile(page, pageSource)
}
expect(next.cliOutput).toInclude(
`It's currently unsupported to use "export *" in a client boundary. Please use named exports instead.`
`ESM packages (client-esm-module-wildcard) need to be imported`
)
})
}

View file

@ -1,7 +1,9 @@
'use client'
import { version, useValue } from 'esm-with-react'
import React from 'react'
import { version, useValue } from 'esm-with-react'
import { packageEntry as compatPackageEntry } from 'cjs-esm-compat'
import { packageName } from 'cjs-lib'
export default function Index() {
const value = useValue()
@ -10,6 +12,8 @@ export default function Index() {
<h2>{'App React Version: ' + React.version}</h2>
<h2>{'External React Version: ' + version}</h2>
<h2>{'Test: ' + value}</h2>
<h2>{`CJS-ESM Compat package: ${compatPackageEntry}`}</h2>
<h2>{`CJS package: ${packageName}`}</h2>
</div>
)
}

View file

@ -0,0 +1,16 @@
import { version } from 'esm-with-react'
import { packageEntry as compatPackageEntry } from 'cjs-esm-compat'
import { packageName } from 'cjs-lib'
import React from 'react'
export default function Index() {
return (
<div>
<h2>{'App React Version: ' + React.version}</h2>
<h2>{'External React Version: ' + version}</h2>
<h2>{`CJS-ESM Compat package: ${compatPackageEntry}`}</h2>
<h2>{`CJS package: ${packageName}`}</h2>
</div>
)
}

View file

@ -1,4 +1,6 @@
import { version } from 'esm-with-react'
import { packageEntry as compatPackageEntry } from 'cjs-esm-compat'
import { packageName } from 'cjs-lib'
import React from 'react'
@ -7,6 +9,8 @@ export default function Index() {
<div>
<h2>{'App React Version: ' + React.version}</h2>
<h2>{'External React Version: ' + version}</h2>
<h2>{`CJS-ESM Compat package: ${compatPackageEntry}`}</h2>
<h2>{`CJS package: ${packageName}`}</h2>
</div>
)
}

View file

@ -1,4 +1,4 @@
import { createResponse } from 'cjs-lib'
import { createResponse } from 'next-server-cjs-lib'
import { respond } from 'compat-next-server-module'
export async function middleware(request) {

View file

@ -0,0 +1 @@
exports.packageEntry = 'cjs-esm-compat/index.cjs'

View file

@ -0,0 +1 @@
export const packageEntry = 'cjs-esm-compat/index.mjs'

View file

@ -0,0 +1,7 @@
{
"exports": {
"import": "./index.mjs",
"require": "./index.cjs",
"default": "./index.cjs"
}
}

View file

@ -1,8 +1 @@
Object.defineProperty(exports, '__esModule', { value: true })
const server_1 = require('next/server')
const createResponse = (...args) => {
return new server_1.NextResponse(...args)
}
exports.createResponse = createResponse
// Note: this is a CJS library that used the `NextResponse` export from `next/server`.
exports.packageName = 'cjs-lib'

View file

@ -0,0 +1,3 @@
'use client'
module.exports = require('client-esm-module-wildcard')

View file

@ -0,0 +1,5 @@
{
"name": "client-cjs-import-esm-wildcard",
"type": "commonjs",
"exports": "./index.js"
}

View file

@ -0,0 +1,9 @@
Object.defineProperty(exports, '__esModule', { value: true })
const server_1 = require('next/server')
const createResponse = (...args) => {
return new server_1.NextResponse(...args)
}
exports.createResponse = createResponse
exports.packageName = 'cjs-lib'
// Note: this is a CJS library that used the `NextResponse` export from `next/server`.

View file

@ -0,0 +1,4 @@
{
"name": "next-server-cjs-lib",
"exports": "./index.js"
}

View file

@ -11,8 +11,8 @@ export default function Index() {
return (
<div>
<h1>{`component:index.server`}</h1>
<div>{'env:' + envVar}</div>
<div>{'header:' + header}</div>
<div id="env">{'env:' + envVar}</div>
<div id="header">{'header:' + header}</div>
<Nav />
</div>
)

View file

@ -96,11 +96,11 @@ createNextDescribe(
'<meta name="viewport" content="width=device-width, initial-scale=1"/>'
)
expect(homeHTML).toContain('component:index.server')
expect(homeHTML).toContain('header:test-util')
const inlineFlightContents = []
const $ = cheerio.load(homeHTML)
expect($('h1').text()).toBe('component:index.server')
$('script').each((_index, tag) => {
const content = $(tag).text()
if (content) inlineFlightContents.push(content)
@ -390,9 +390,7 @@ createNextDescribe(
it('should support streaming for flight response', async () => {
await next
.fetch('/', {
headers: {
['RSC'.toString()]: '1',
},
headers: { RSC: '1' },
})
.then(async (response) => {
const result = await resolveStreamResponse(response)