Skip to content

Commit 5ad4ebd

Browse files
authored
feat: support turbopack build on Next.js canaries (#14845)
Fixes #14786 In the latest Next.js 16 canaries, the Next.js team introduced support for externalizing transitive dependencies, which unlocks compatibility with Turbopack Build. This PR updates our withPayload.js wrapper to take advantage of that capability. It adds a version check that automatically enables Turbopack Build support when the project is using Next.js ≥ `16.1.0-canary.3`.
1 parent d50b60e commit 5ad4ebd

File tree

9 files changed

+293
-88
lines changed

9 files changed

+293
-88
lines changed

next.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import bundleAnalyzer from '@next/bundle-analyzer'
22
import { withSentryConfig } from '@sentry/nextjs'
3-
import { withPayload } from './packages/next/src/withPayload.js'
3+
import { withPayload } from './packages/next/src/withPayload/withPayload.js'
44
import path from 'path'
55
import { fileURLToPath } from 'url'
66

packages/next/bundleWithPayload.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* This file creates a cjs-compatible bundle of the withPayload function.
3+
*/
4+
5+
import * as esbuild from 'esbuild'
6+
import path from 'path'
7+
8+
await esbuild.build({
9+
entryPoints: ['dist/withPayload/withPayload.js'],
10+
bundle: true,
11+
platform: 'node',
12+
format: 'cjs',
13+
outfile: `dist/cjs/withPayload.cjs`,
14+
splitting: false,
15+
minify: true,
16+
metafile: true,
17+
tsconfig: path.resolve(import.meta.dirname, 'tsconfig.json'),
18+
sourcemap: true,
19+
minify: false,
20+
// 18.20.2 is the lowest version of node supported by Payload
21+
target: 'node18.20.2',
22+
})
23+
console.log('withPayload cjs bundle created successfully')

packages/next/eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export const index = [
2121
// See comment in packages/eslint-config/index.mjs
2222
allowDefaultProject: [
2323
'bundleScss.js',
24-
'createStubScss.js',
2524
'bundle.js',
2625
'babel.config.cjs',
26+
'bundleWithPayload.js',
27+
'createStubScss.js',
2728
],
2829
},
2930
},

packages/next/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
"default": "./src/index.js"
3333
},
3434
"./withPayload": {
35-
"import": "./src/withPayload.js",
36-
"default": "./src/withPayload.js"
35+
"import": "./src/withPayload/withPayload.ts",
36+
"default": "./src/withPayload/withPayload.ts"
3737
},
3838
"./layouts": {
3939
"import": "./src/exports/layouts.ts",
@@ -85,7 +85,7 @@
8585
"build": "pnpm build:reactcompiler",
8686
"build:babel": "rm -rf dist_optimized && babel dist --out-dir dist_optimized --source-maps --extensions .ts,.js,.tsx,.jsx,.cjs,.mjs && rm -rf dist && mv dist_optimized dist",
8787
"build:bundle-for-analysis": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm build:swc && pnpm build:babel && pnpm copyfiles && node ./bundle.js esbuild",
88-
"build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs --strip-leading-paths",
88+
"build:cjs": "node ./bundleWithPayload.js",
8989
"build:debug": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm build:swc:debug && pnpm copyfiles:debug && pnpm build:types && pnpm build:cjs && node createStubScss.js",
9090
"build:esbuild": "node bundleScss.js",
9191
"build:reactcompiler": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm build:swc && pnpm build:babel && pnpm copyfiles && pnpm build:types && pnpm build:esbuild && pnpm build:cjs",
@@ -156,9 +156,9 @@
156156
"default": "./dist/prod/styles.css"
157157
},
158158
"./withPayload": {
159-
"import": "./dist/withPayload.js",
159+
"import": "./dist/withPayload/withPayload.js",
160160
"require": "./dist/cjs/withPayload.cjs",
161-
"default": "./dist/withPayload.js"
161+
"default": "./dist/withPayload/withPayload.js"
162162
},
163163
"./layouts": {
164164
"import": "./dist/exports/layouts.js",

packages/next/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default as withPayload } from './withPayload.js'
1+
export { default as withPayload } from './withPayload/withPayload.js'

packages/next/src/withPayload.js renamed to packages/next/src/withPayload/withPayload.ts

Lines changed: 53 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1+
/* eslint-disable no-console */
2+
/* eslint-disable no-restricted-exports */
3+
import type { NextConfig } from 'next'
4+
5+
import {
6+
getNextjsVersion,
7+
supportsTurbopackExternalizeTransitiveDependencies,
8+
} from './withPayload.utils.js'
9+
import { withPayloadLegacy } from './withPayloadLegacy.js'
10+
11+
const poweredByHeader = {
12+
key: 'X-Powered-By',
13+
value: 'Next.js, Payload',
14+
}
15+
116
/**
217
* @param {import('next').NextConfig} nextConfig
318
* @param {Object} [options] - Optional configuration options
419
* @param {boolean} [options.devBundleServerPackages] - Whether to bundle server packages in development mode. @default false
5-
*
6-
* @returns {import('next').NextConfig}
720
* */
8-
export const withPayload = (nextConfig = {}, options = {}) => {
21+
export const withPayload = (
22+
nextConfig: NextConfig = {},
23+
options: { devBundleServerPackages?: boolean } = {},
24+
): NextConfig => {
25+
const nextjsVersion = getNextjsVersion()
26+
27+
const supportsTurbopackBuild = supportsTurbopackExternalizeTransitiveDependencies(nextjsVersion)
28+
929
const env = nextConfig.env || {}
1030

1131
if (nextConfig.experimental?.staleTimes?.dynamic) {
@@ -15,67 +35,9 @@ export const withPayload = (nextConfig = {}, options = {}) => {
1535
env.NEXT_PUBLIC_ENABLE_ROUTER_CACHE_REFRESH = 'true'
1636
}
1737

18-
if (process.env.PAYLOAD_PATCH_TURBOPACK_WARNINGS !== 'false') {
19-
// TODO: This warning is thrown because we cannot externalize the entry-point package for client-s3, so we patch the warning to not show it.
20-
// We can remove this once Next.js implements https://github.com/vercel/next.js/discussions/76991
21-
const turbopackWarningText =
22-
'Packages that should be external need to be installed in the project directory, so they can be resolved from the output files.\nTry to install it into the project directory by running'
23-
24-
// TODO 4.0: Remove this once we drop support for Next.js 15.2.x
25-
const turbopackConfigWarningText = "Unrecognized key(s) in object: 'turbopack'"
26-
27-
const consoleWarn = console.warn
28-
console.warn = (...args) => {
29-
// Force to disable serverExternalPackages warnings: https://github.com/vercel/next.js/issues/68805
30-
if (
31-
(typeof args[1] === 'string' && args[1].includes(turbopackWarningText)) ||
32-
(typeof args[0] === 'string' && args[0].includes(turbopackWarningText))
33-
) {
34-
return
35-
}
36-
37-
// Add Payload-specific message after turbopack config warning in Next.js 15.2.x or lower.
38-
// TODO 4.0: Remove this once we drop support for Next.js 15.2.x
39-
const hasTurbopackConfigWarning =
40-
(typeof args[1] === 'string' && args[1].includes(turbopackConfigWarningText)) ||
41-
(typeof args[0] === 'string' && args[0].includes(turbopackConfigWarningText))
42-
43-
if (hasTurbopackConfigWarning) {
44-
consoleWarn(...args)
45-
consoleWarn(
46-
'Payload: You can safely ignore the "Invalid next.config" warning above. This only occurs on Next.js 15.2.x or lower. We recommend upgrading to Next.js 15.4.7 to resolve this warning.',
47-
)
48-
return
49-
}
50-
51-
consoleWarn(...args)
52-
}
53-
}
54-
55-
const isBuild = process.env.NODE_ENV === 'production'
56-
const isTurbopackNextjs15 = process.env.TURBOPACK === '1'
57-
const isTurbopackNextjs16 = process.env.TURBOPACK === 'auto'
58-
59-
if (isBuild && (isTurbopackNextjs15 || isTurbopackNextjs16)) {
60-
throw new Error(
61-
'Payload does not support using Turbopack for production builds. If you are using Next.js 16, please use `next build --webpack` instead.',
62-
)
63-
}
64-
65-
const poweredByHeader = {
66-
key: 'X-Powered-By',
67-
value: 'Next.js, Payload',
68-
}
69-
70-
/**
71-
* @type {import('next').NextConfig}
72-
*/
73-
const toReturn = {
38+
const baseConfig: NextConfig = {
7439
...nextConfig,
7540
env,
76-
turbopack: {
77-
...(nextConfig.turbopack || {}),
78-
},
7941
outputFileTracingExcludes: {
8042
...(nextConfig.outputFileTracingExcludes || {}),
8143
'**/*': [
@@ -88,6 +50,9 @@ export const withPayload = (nextConfig = {}, options = {}) => {
8850
...(nextConfig.outputFileTracingIncludes || {}),
8951
'**/*': [...(nextConfig.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'],
9052
},
53+
turbopack: {
54+
...(nextConfig.turbopack || {}),
55+
},
9156
// We disable the poweredByHeader here because we add it manually in the headers function below
9257
...(nextConfig.poweredByHeader !== false ? { poweredByHeader: false } : {}),
9358
headers: async () => {
@@ -96,7 +61,6 @@ export const withPayload = (nextConfig = {}, options = {}) => {
9661
return [
9762
...(headersFromConfig || []),
9863
{
99-
source: '/:path*',
10064
headers: [
10165
{
10266
key: 'Accept-CH',
@@ -112,20 +76,14 @@ export const withPayload = (nextConfig = {}, options = {}) => {
11276
},
11377
...(nextConfig.poweredByHeader !== false ? [poweredByHeader] : []),
11478
],
79+
source: '/:path*',
11580
},
11681
]
11782
},
11883
serverExternalPackages: [
119-
// serverExternalPackages = webpack.externals, but with turbopack support and an additional check
120-
// for whether the package is resolvable from the project root
121-
...(nextConfig.serverExternalPackages || []),
122-
// Can be externalized, because we require users to install graphql themselves - we only rely on it as a peer dependency => resolvable from the project root.
123-
//
12484
// WHY: without externalizing graphql, a graphql version error will be thrown
12585
// during runtime ("Ensure that there is only one instance of \"graphql\" in the node_modules\ndirectory.")
12686
'graphql',
127-
// External, because it installs import-in-the-middle and require-in-the-middle - both in the default serverExternalPackages list.
128-
'@sentry/nextjs',
12987
...(process.env.NODE_ENV === 'development' && options.devBundleServerPackages !== true
13088
? /**
13189
* Unless explicitly disabled by the user, by passing `devBundleServerPackages: true` to withPayload, we
@@ -208,6 +166,13 @@ export const withPayload = (nextConfig = {}, options = {}) => {
208166
'libsql',
209167
'require-in-the-middle',
210168
],
169+
plugins: [
170+
...(incomingWebpackConfig?.plugins || []),
171+
// Fix cloudflare:sockets error: https://github.com/vercel/next.js/discussions/50177
172+
new webpackOptions.webpack.IgnorePlugin({
173+
resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
174+
}),
175+
],
211176
resolve: {
212177
...(incomingWebpackConfig?.resolve || {}),
213178
alias: {
@@ -237,22 +202,31 @@ export const withPayload = (nextConfig = {}, options = {}) => {
237202
aws4: false,
238203
},
239204
},
240-
plugins: [
241-
...(incomingWebpackConfig?.plugins || []),
242-
// Fix cloudflare:sockets error: https://github.com/vercel/next.js/discussions/50177
243-
new webpackOptions.webpack.IgnorePlugin({
244-
resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
245-
}),
246-
],
247205
}
248206
},
249207
}
250208

251209
if (nextConfig.basePath) {
252-
toReturn.env.NEXT_BASE_PATH = nextConfig.basePath
210+
baseConfig.env.NEXT_BASE_PATH = nextConfig.basePath
253211
}
254212

255-
return toReturn
213+
if (!supportsTurbopackBuild) {
214+
return withPayloadLegacy(baseConfig)
215+
} else {
216+
return {
217+
...baseConfig,
218+
serverExternalPackages: [
219+
...(baseConfig.serverExternalPackages || []),
220+
'drizzle-kit',
221+
'drizzle-kit/api',
222+
'sharp',
223+
'libsql',
224+
'require-in-the-middle',
225+
// Prevents turbopack build errors by the thread-stream package which is installed by pino
226+
'pino',
227+
],
228+
}
229+
}
256230
}
257231

258232
export default withPayload

0 commit comments

Comments
 (0)