diff --git a/next.config.mjs b/next.config.mjs index a7d7748fa54..59d971b1d34 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,6 @@ import bundleAnalyzer from '@next/bundle-analyzer' import { withSentryConfig } from '@sentry/nextjs' -import { withPayload } from './packages/next/src/withPayload.js' +import { withPayload } from './packages/next/src/withPayload/withPayload.js' import path from 'path' import { fileURLToPath } from 'url' diff --git a/packages/next/bundleWithPayload.js b/packages/next/bundleWithPayload.js new file mode 100644 index 00000000000..4c1e252dba1 --- /dev/null +++ b/packages/next/bundleWithPayload.js @@ -0,0 +1,23 @@ +/** + * This file creates a cjs-compatible bundle of the withPayload function. + */ + +import * as esbuild from 'esbuild' +import path from 'path' + +await esbuild.build({ + entryPoints: ['dist/withPayload/withPayload.js'], + bundle: true, + platform: 'node', + format: 'cjs', + outfile: `dist/cjs/withPayload.cjs`, + splitting: false, + minify: true, + metafile: true, + tsconfig: path.resolve(import.meta.dirname, 'tsconfig.json'), + sourcemap: true, + minify: false, + // 18.20.2 is the lowest version of node supported by Payload + target: 'node18.20.2', +}) +console.log('withPayload cjs bundle created successfully') diff --git a/packages/next/eslint.config.js b/packages/next/eslint.config.js index 1860c6881d3..b130509f952 100644 --- a/packages/next/eslint.config.js +++ b/packages/next/eslint.config.js @@ -21,9 +21,10 @@ export const index = [ // See comment in packages/eslint-config/index.mjs allowDefaultProject: [ 'bundleScss.js', - 'createStubScss.js', 'bundle.js', 'babel.config.cjs', + 'bundleWithPayload.js', + 'createStubScss.js', ], }, }, diff --git a/packages/next/package.json b/packages/next/package.json index c0defe6c29e..9d6647f76a8 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -32,8 +32,8 @@ "default": "./src/index.js" }, "./withPayload": { - "import": "./src/withPayload.js", - "default": "./src/withPayload.js" + "import": "./src/withPayload/withPayload.ts", + "default": "./src/withPayload/withPayload.ts" }, "./layouts": { "import": "./src/exports/layouts.ts", @@ -85,7 +85,7 @@ "build": "pnpm build:reactcompiler", "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", "build:bundle-for-analysis": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm build:swc && pnpm build:babel && pnpm copyfiles && node ./bundle.js esbuild", - "build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs --strip-leading-paths", + "build:cjs": "node ./bundleWithPayload.js", "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", "build:esbuild": "node bundleScss.js", "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 @@ "default": "./dist/prod/styles.css" }, "./withPayload": { - "import": "./dist/withPayload.js", + "import": "./dist/withPayload/withPayload.js", "require": "./dist/cjs/withPayload.cjs", - "default": "./dist/withPayload.js" + "default": "./dist/withPayload/withPayload.js" }, "./layouts": { "import": "./dist/exports/layouts.js", diff --git a/packages/next/src/index.js b/packages/next/src/index.js index 0b09f2dfde7..d9154a66aa9 100644 --- a/packages/next/src/index.js +++ b/packages/next/src/index.js @@ -1 +1 @@ -export { default as withPayload } from './withPayload.js' +export { default as withPayload } from './withPayload/withPayload.js' diff --git a/packages/next/src/withPayload.js b/packages/next/src/withPayload/withPayload.ts similarity index 73% rename from packages/next/src/withPayload.js rename to packages/next/src/withPayload/withPayload.ts index 60f02432e6f..58230a6fb6c 100644 --- a/packages/next/src/withPayload.js +++ b/packages/next/src/withPayload/withPayload.ts @@ -1,11 +1,31 @@ +/* eslint-disable no-console */ +/* eslint-disable no-restricted-exports */ +import type { NextConfig } from 'next' + +import { + getNextjsVersion, + supportsTurbopackExternalizeTransitiveDependencies, +} from './withPayload.utils.js' +import { withPayloadLegacy } from './withPayloadLegacy.js' + +const poweredByHeader = { + key: 'X-Powered-By', + value: 'Next.js, Payload', +} + /** * @param {import('next').NextConfig} nextConfig * @param {Object} [options] - Optional configuration options * @param {boolean} [options.devBundleServerPackages] - Whether to bundle server packages in development mode. @default false - * - * @returns {import('next').NextConfig} * */ -export const withPayload = (nextConfig = {}, options = {}) => { +export const withPayload = ( + nextConfig: NextConfig = {}, + options: { devBundleServerPackages?: boolean } = {}, +): NextConfig => { + const nextjsVersion = getNextjsVersion() + + const supportsTurbopackBuild = supportsTurbopackExternalizeTransitiveDependencies(nextjsVersion) + const env = nextConfig.env || {} if (nextConfig.experimental?.staleTimes?.dynamic) { @@ -15,67 +35,9 @@ export const withPayload = (nextConfig = {}, options = {}) => { env.NEXT_PUBLIC_ENABLE_ROUTER_CACHE_REFRESH = 'true' } - if (process.env.PAYLOAD_PATCH_TURBOPACK_WARNINGS !== 'false') { - // 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. - // We can remove this once Next.js implements https://github.com/vercel/next.js/discussions/76991 - const turbopackWarningText = - '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' - - // TODO 4.0: Remove this once we drop support for Next.js 15.2.x - const turbopackConfigWarningText = "Unrecognized key(s) in object: 'turbopack'" - - const consoleWarn = console.warn - console.warn = (...args) => { - // Force to disable serverExternalPackages warnings: https://github.com/vercel/next.js/issues/68805 - if ( - (typeof args[1] === 'string' && args[1].includes(turbopackWarningText)) || - (typeof args[0] === 'string' && args[0].includes(turbopackWarningText)) - ) { - return - } - - // Add Payload-specific message after turbopack config warning in Next.js 15.2.x or lower. - // TODO 4.0: Remove this once we drop support for Next.js 15.2.x - const hasTurbopackConfigWarning = - (typeof args[1] === 'string' && args[1].includes(turbopackConfigWarningText)) || - (typeof args[0] === 'string' && args[0].includes(turbopackConfigWarningText)) - - if (hasTurbopackConfigWarning) { - consoleWarn(...args) - consoleWarn( - '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.', - ) - return - } - - consoleWarn(...args) - } - } - - const isBuild = process.env.NODE_ENV === 'production' - const isTurbopackNextjs15 = process.env.TURBOPACK === '1' - const isTurbopackNextjs16 = process.env.TURBOPACK === 'auto' - - if (isBuild && (isTurbopackNextjs15 || isTurbopackNextjs16)) { - throw new Error( - 'Payload does not support using Turbopack for production builds. If you are using Next.js 16, please use `next build --webpack` instead.', - ) - } - - const poweredByHeader = { - key: 'X-Powered-By', - value: 'Next.js, Payload', - } - - /** - * @type {import('next').NextConfig} - */ - const toReturn = { + const baseConfig: NextConfig = { ...nextConfig, env, - turbopack: { - ...(nextConfig.turbopack || {}), - }, outputFileTracingExcludes: { ...(nextConfig.outputFileTracingExcludes || {}), '**/*': [ @@ -88,6 +50,9 @@ export const withPayload = (nextConfig = {}, options = {}) => { ...(nextConfig.outputFileTracingIncludes || {}), '**/*': [...(nextConfig.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'], }, + turbopack: { + ...(nextConfig.turbopack || {}), + }, // We disable the poweredByHeader here because we add it manually in the headers function below ...(nextConfig.poweredByHeader !== false ? { poweredByHeader: false } : {}), headers: async () => { @@ -96,7 +61,6 @@ export const withPayload = (nextConfig = {}, options = {}) => { return [ ...(headersFromConfig || []), { - source: '/:path*', headers: [ { key: 'Accept-CH', @@ -112,20 +76,14 @@ export const withPayload = (nextConfig = {}, options = {}) => { }, ...(nextConfig.poweredByHeader !== false ? [poweredByHeader] : []), ], + source: '/:path*', }, ] }, serverExternalPackages: [ - // serverExternalPackages = webpack.externals, but with turbopack support and an additional check - // for whether the package is resolvable from the project root - ...(nextConfig.serverExternalPackages || []), - // 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. - // // WHY: without externalizing graphql, a graphql version error will be thrown // during runtime ("Ensure that there is only one instance of \"graphql\" in the node_modules\ndirectory.") 'graphql', - // External, because it installs import-in-the-middle and require-in-the-middle - both in the default serverExternalPackages list. - '@sentry/nextjs', ...(process.env.NODE_ENV === 'development' && options.devBundleServerPackages !== true ? /** * Unless explicitly disabled by the user, by passing `devBundleServerPackages: true` to withPayload, we @@ -208,6 +166,13 @@ export const withPayload = (nextConfig = {}, options = {}) => { 'libsql', 'require-in-the-middle', ], + plugins: [ + ...(incomingWebpackConfig?.plugins || []), + // Fix cloudflare:sockets error: https://github.com/vercel/next.js/discussions/50177 + new webpackOptions.webpack.IgnorePlugin({ + resourceRegExp: /^pg-native$|^cloudflare:sockets$/, + }), + ], resolve: { ...(incomingWebpackConfig?.resolve || {}), alias: { @@ -237,22 +202,31 @@ export const withPayload = (nextConfig = {}, options = {}) => { aws4: false, }, }, - plugins: [ - ...(incomingWebpackConfig?.plugins || []), - // Fix cloudflare:sockets error: https://github.com/vercel/next.js/discussions/50177 - new webpackOptions.webpack.IgnorePlugin({ - resourceRegExp: /^pg-native$|^cloudflare:sockets$/, - }), - ], } }, } if (nextConfig.basePath) { - toReturn.env.NEXT_BASE_PATH = nextConfig.basePath + baseConfig.env.NEXT_BASE_PATH = nextConfig.basePath } - return toReturn + if (!supportsTurbopackBuild) { + return withPayloadLegacy(baseConfig) + } else { + return { + ...baseConfig, + serverExternalPackages: [ + ...(baseConfig.serverExternalPackages || []), + 'drizzle-kit', + 'drizzle-kit/api', + 'sharp', + 'libsql', + 'require-in-the-middle', + // Prevents turbopack build errors by the thread-stream package which is installed by pino + 'pino', + ], + } + } } export default withPayload diff --git a/packages/next/src/withPayload/withPayload.utils.ts b/packages/next/src/withPayload/withPayload.utils.ts new file mode 100644 index 00000000000..9b8fb1d9d94 --- /dev/null +++ b/packages/next/src/withPayload/withPayload.utils.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-console */ +/** + * This was taken and modified from https://github.com/getsentry/sentry-javascript/blob/15256034ee8150a5b7dcb97d23eca1a5486f0cae/packages/nextjs/src/config/util.ts + * + * MIT License + * + * Copyright (c) 2012 Functional Software, Inc. dba Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { readFileSync } from 'fs' + +function _parseInt(input: string | undefined): number { + return parseInt(input || '', 10) +} + +/** + * Represents Semantic Versioning object + */ +type SemVer = { + buildmetadata?: string + /** + * undefined if not a canary version + */ + canaryVersion?: number + major?: number + minor?: number + patch?: number + prerelease?: string +} + +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const SEMVER_REGEXP = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-z-][0-9a-z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-z-][0-9a-z-]*))*))?(?:\+([0-9a-z-]+(?:\.[0-9a-z-]+)*))?$/i + +/** + * Parses input into a SemVer interface + * @param input string representation of a semver version + */ +export function parseSemver(input: string): SemVer { + const match = input.match(SEMVER_REGEXP) || [] + const major = _parseInt(match[1]) + const minor = _parseInt(match[2]) + const patch = _parseInt(match[3]) + + const prerelease = match[4] + const canaryVersion = prerelease?.startsWith('canary.') + ? parseInt(prerelease.split('.')[1] || '0', 10) + : undefined + + return { + buildmetadata: match[5], + canaryVersion, + major: isNaN(major) ? undefined : major, + minor: isNaN(minor) ? undefined : minor, + patch: isNaN(patch) ? undefined : patch, + prerelease: match[4], + } +} + +/** + * Returns the version of Next.js installed in the project, or undefined if it cannot be determined. + */ +export function getNextjsVersion(): SemVer | undefined { + try { + let pkgPath: string + + // Check if we're in ESM or CJS environment + if (typeof import.meta?.resolve === 'function') { + // ESM environment - use import.meta.resolve + const pkgUrl = import.meta.resolve('next/package.json') + pkgPath = new URL(pkgUrl).pathname + } else { + // CJS environment - use require.resolve + pkgPath = require.resolve('next/package.json') + } + + const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf8')) + return parseSemver(pkgJson.version) + } catch (e) { + console.error('Payload: Error getting Next.js version', e) + return undefined + } +} + +/** + * Checks if the current Next.js version supports Turbopack externalize transitive dependencies. + * This was introduced in Next.js v16.1.0-canary.3 + */ +export function supportsTurbopackExternalizeTransitiveDependencies( + version: SemVer | undefined, +): boolean { + if (!version) { + return false + } + + const { canaryVersion, major, minor } = version + + if (major === undefined || minor === undefined) { + return false + } + + if (major > 16) { + return true + } + + if (major === 16) { + if (minor > 1) { + return true + } + if (minor === 1) { + if (canaryVersion !== undefined) { + // 16.1.0-canary.3+ + return canaryVersion >= 3 + } else { + // Assume that Next.js 16.1 inherits support for this feature from the canary release + return true + } + } + } + + return false +} diff --git a/packages/next/src/withPayload/withPayloadLegacy.ts b/packages/next/src/withPayload/withPayloadLegacy.ts new file mode 100644 index 00000000000..2c2f6f9bc15 --- /dev/null +++ b/packages/next/src/withPayload/withPayloadLegacy.ts @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +import type { NextConfig } from 'next' + +/** + * Applies config options required to support Next.js versions before 16.1.0 and 16.1.0-canary.15. + */ +export const withPayloadLegacy = (nextConfig: NextConfig = {}): NextConfig => { + if (process.env.PAYLOAD_PATCH_TURBOPACK_WARNINGS !== 'false') { + // 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. + // We can remove this once Next.js implements https://github.com/vercel/next.js/discussions/76991 + const turbopackWarningText = + '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' + + // TODO 4.0: Remove this once we drop support for Next.js 15.2.x + const turbopackConfigWarningText = "Unrecognized key(s) in object: 'turbopack'" + + const consoleWarn = console.warn + console.warn = (...args) => { + // Force to disable serverExternalPackages warnings: https://github.com/vercel/next.js/issues/68805 + if ( + (typeof args[1] === 'string' && args[1].includes(turbopackWarningText)) || + (typeof args[0] === 'string' && args[0].includes(turbopackWarningText)) + ) { + return + } + + // Add Payload-specific message after turbopack config warning in Next.js 15.2.x or lower. + // TODO 4.0: Remove this once we drop support for Next.js 15.2.x + const hasTurbopackConfigWarning = + (typeof args[1] === 'string' && args[1].includes(turbopackConfigWarningText)) || + (typeof args[0] === 'string' && args[0].includes(turbopackConfigWarningText)) + + if (hasTurbopackConfigWarning) { + consoleWarn(...args) + consoleWarn( + '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 the latest supported Next.js version to resolve this warning.', + ) + return + } + + consoleWarn(...args) + } + } + + const isBuild = process.env.NODE_ENV === 'production' + const isTurbopackNextjs15 = process.env.TURBOPACK === '1' + const isTurbopackNextjs16 = process.env.TURBOPACK === 'auto' + + if (isBuild && (isTurbopackNextjs15 || isTurbopackNextjs16)) { + throw new Error( + 'Your Next.js and Payload versions do not support using Turbopack for production builds. Please upgrade to Next.js 16.1.0-canary.3 or higher if you want to use Turbopack for builds.', + ) + } + + const toReturn: NextConfig = { + ...nextConfig, + serverExternalPackages: [ + // serverExternalPackages = webpack.externals, but with turbopack support and an additional check + // for whether the package is resolvable from the project root + ...(nextConfig.serverExternalPackages || []), + // External, because it installs import-in-the-middle and require-in-the-middle - both in the default serverExternalPackages list. + '@sentry/nextjs', + ], + } + + return toReturn +} diff --git a/test/admin-root/next.config.mjs b/test/admin-root/next.config.mjs index f78bcde47e8..d2bf5e15c2e 100644 --- a/test/admin-root/next.config.mjs +++ b/test/admin-root/next.config.mjs @@ -1,6 +1,6 @@ import bundleAnalyzer from '@next/bundle-analyzer' -import withPayload from '../../packages/next/src/withPayload.js' +import { withPayload } from '../../packages/next/src/withPayload/withPayload.js' import path from 'path' import { fileURLToPath } from 'url'