@nadeshikon/plugin-nextjs
Version:
Run Next.js seamlessly on Netlify
215 lines (192 loc) • 8.21 kB
text/typescript
import { existsSync, promises } from 'fs'
import path, { relative, join } from 'path'
import type { NetlifyConfig, NetlifyPluginUtils } from '@netlify/build'
import { yellowBright, greenBright, blueBright, redBright, reset } from 'chalk'
import { async as StreamZip } from 'node-stream-zip'
import { outdent } from 'outdent'
import prettyBytes from 'pretty-bytes'
import { satisfies } from 'semver'
import { LAMBDA_MAX_SIZE } from '../constants'
import { isBundleSizeCheckDisabled } from './utils'
// This is when nft support was added
const REQUIRED_BUILD_VERSION = '>=18.16.0'
type FailBuild = NetlifyPluginUtils['build']['failBuild']
export const verifyNetlifyBuildVersion = ({
IS_LOCAL,
NETLIFY_BUILD_VERSION,
failBuild,
}: {
IS_LOCAL: boolean
NETLIFY_BUILD_VERSION: string
failBuild: FailBuild
}): void | never => {
// We check for build version because that's what's available to us, but prompt about the cli because that's what they can upgrade
if (IS_LOCAL && !satisfies(NETLIFY_BUILD_VERSION, REQUIRED_BUILD_VERSION, { includePrerelease: true })) {
return failBuild(outdent`
This version of the Next Runtime requires netlify-cli@6.12.4 or higher. Please upgrade and try again.
You can do this by running: "npm install -g netlify-cli@latest" or "yarn global add netlify-cli@latest"
`)
}
}
export const checkForOldFunctions = async ({ functions }: Pick<NetlifyPluginUtils, 'functions'>): Promise<void> => {
const allOldFunctions = await functions.list()
const oldFunctions = allOldFunctions.filter(({ name }) => name.startsWith('next_'))
if (oldFunctions.length !== 0) {
console.log(
yellowBright(outdent`
We have found the following functions in your site that seem to be left over from the old Next.js plugin (v3). We have guessed this because the name starts with "next_".
${reset(oldFunctions.map(({ name }) => `- ${name}`).join('\n'))}
If they were created by the old plugin, these functions are likely to cause errors so should be removed. You can do this by deleting the following directories:
${reset(
oldFunctions.map(({ mainFile }) => `- ${path.relative(process.cwd(), path.dirname(mainFile))}`).join('\n'),
)}
`),
)
}
}
export const checkNextSiteHasBuilt = ({
publish,
failBuild,
}: {
publish: string
failBuild: FailBuild
}): void | never => {
if (!existsSync(path.join(publish, 'BUILD_ID'))) {
let outWarning
if (path.basename(publish) === 'out') {
outWarning = `Your publish directory is set to "out", but in most cases it should be ".next".`
} else if (path.basename(publish) !== '.next' && existsSync(path.join('.next', 'BUILD_ID'))) {
outWarning = outdent`
However, a '.next' directory was found with a production build.
Consider changing your 'publish' directory to '.next'
`
} else {
outWarning = `In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config.`
}
return failBuild(outdent`
The directory "${publish}" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory.
${outWarning}
If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true".
`)
}
if (existsSync(path.join(publish, 'export-detail.json'))) {
return failBuild(outdent`
Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
The publish directory should be set to "out", and you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true".
`)
}
}
export const checkForRootPublish = ({
publish,
failBuild,
}: {
publish: string
failBuild: FailBuild
}): void | never => {
if (path.resolve(publish) === path.resolve('.')) {
failBuild(outdent`
Your publish directory is pointing to the base directory of your site. This is not supported for Next.js sites, and is probably a mistake.
In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config, or the Next site is in a subdirectory.
`)
}
}
export const checkZipSize = async (file: string, maxSize: number = LAMBDA_MAX_SIZE): Promise<void> => {
// Requires contacting the Netlify Support team to fully enable.
// Enabling this without contacting them can result in failed deploys.
if (isBundleSizeCheckDisabled()) {
console.warn(
'Function bundle size check was DISABLED with the DISABLE_BUNDLE_ZIP_SIZE_CHECK environment variable. Your deployment will break if it exceeds the maximum supported size of function zip files in your account.',
)
return
}
if (!existsSync(file)) {
console.warn(`Could not check zip size because ${file} does not exist`)
return
}
const fileSize = await promises.stat(file).then(({ size }) => size)
if (fileSize < maxSize) {
return
}
// We don't fail the build, because the actual hard max size is larger so it might still succeed
console.log(
redBright(outdent`
The function zip ${yellowBright(relative(process.cwd(), file))} size is ${prettyBytes(
fileSize,
)}, which is larger than the maximum supported size of ${prettyBytes(maxSize)}.
There are a few reasons this could happen. You may have accidentally bundled a large dependency, or you might have a
large number of pre-rendered pages included.
`),
)
const zip = new StreamZip({ file })
console.log(`Contains ${await zip.entriesCount} files`)
const sortedFiles = Object.values(await zip.entries()).sort((a, b) => b.size - a.size)
const largest = {}
for (let i = 0; i < 10 && i < sortedFiles.length; i++) {
largest[`${i + 1}`] = {
File: sortedFiles[i].name,
'Compressed Size': prettyBytes(sortedFiles[i].compressedSize),
'Uncompressed Size': prettyBytes(sortedFiles[i].size),
}
}
console.log(yellowBright`\n\nThese are the largest files in the zip:`)
console.table(largest)
console.log(
greenBright`\n\nFor more information on fixing this, see ${blueBright`https://ntl.fyi/large-next-functions`}`,
)
}
export const getProblematicUserRewrites = ({
redirects,
basePath,
}: {
redirects: NetlifyConfig['redirects']
basePath: string
}) => {
const userRewrites: NetlifyConfig['redirects'] = []
for (const redirect of redirects) {
// This is the first of the runtime-generated redirects so we can stop checking
if (redirect.from === `${basePath}/_next/static/*` && redirect.to === `/static/:splat` && redirect.status === 200) {
break
}
if (
// Redirects are fine
(redirect.status === 200 || redirect.status === 404) &&
// Rewriting to a function is also fine
!redirect.to.startsWith('/.netlify/') &&
// ...so is proxying
!redirect.to.startsWith('http')
) {
userRewrites.push(redirect)
}
}
return userRewrites
}
export const warnForProblematicUserRewrites = ({
redirects,
basePath,
}: {
redirects: NetlifyConfig['redirects']
basePath: string
}) => {
const userRewrites = getProblematicUserRewrites({ redirects, basePath })
if (userRewrites.length === 0) {
return
}
console.log(
yellowBright(outdent`
You have the following Netlify rewrite${
userRewrites.length === 1 ? '' : 's'
} that might cause conflicts with the Next.js Runtime:
${reset(userRewrites.map(({ from, to, status }) => `- ${from} ${to} ${status}`).join('\n'))}
For more information, see https://ntl.fyi/next-rewrites
`),
)
}
export const warnForRootRedirects = ({ appDir }: { appDir: string }) => {
if (existsSync(join(appDir, '_redirects'))) {
console.log(
yellowBright(
`You have a "_redirects" file in your root directory, which is not deployed and will be ignored. If you want it to be used, please move it into "public".`,
),
)
}
}