diff --git a/packages/runtime/src/helpers/config.ts b/packages/runtime/src/helpers/config.ts index 64984297d5..6cf82bbb71 100644 --- a/packages/runtime/src/helpers/config.ts +++ b/packages/runtime/src/helpers/config.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { NetlifyConfig } from '@netlify/build' import destr from 'destr' import { readJSON, writeJSON } from 'fs-extra' @@ -80,7 +81,28 @@ const resolveModuleRoot = (moduleName) => { const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'] -export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [] }) => { +export const hasManuallyAddedModule = ({ + netlifyConfig, + moduleName, +}: { + netlifyConfig: NetlifyConfig + moduleName: string +}) => + /* eslint-disable camelcase */ + Object.values(netlifyConfig.functions).some(({ included_files = [] }) => + included_files.some((inc) => inc.includes(`node_modules/${moduleName}`)), + ) +/* eslint-enable camelcase */ + +export const configureHandlerFunctions = async ({ + netlifyConfig, + publish, + ignore = [], +}: { + netlifyConfig: NetlifyConfig + publish: string + ignore: Array +}) => { const config = await getRequiredServerFiles(publish) const files = config.files || [] const cssFilesToInclude = files.filter((f) => f.startsWith(`${publish}/static/css/`)) @@ -91,6 +113,11 @@ export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore netlifyConfig.functions._ipx.node_bundler = 'nft' } + // If the user has manually added the module to included_files, then don't exclude it + const excludedModules = DEFAULT_EXCLUDED_MODULES.filter( + (moduleName) => !hasManuallyAddedModule({ netlifyConfig, moduleName }), + ) + /* eslint-enable no-underscore-dangle */ ;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, '_api_*'].forEach((functionName) => { netlifyConfig.functions[functionName] ||= { included_files: [], external_node_modules: [] } @@ -109,6 +136,7 @@ export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, `!${publish}/server/**/*.js.nft.json`, + '!**/node_modules/@next/swc*/**/*', ...cssFilesToInclude, ...ignore.map((path) => `!${slash(path)}`), ) @@ -123,7 +151,7 @@ export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore ) } - DEFAULT_EXCLUDED_MODULES.forEach((moduleName) => { + excludedModules.forEach((moduleName) => { const moduleRoot = resolveModuleRoot(moduleName) if (moduleRoot) { netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`) @@ -156,11 +184,11 @@ const buildHeader = (buildHeaderParams: BuildHeaderParams) => { const sanitizePath = (path: string) => path.replace(/:[^*/]+\*$/, '*') /** - * Persist NEXT.js custom headers to the Netlify configuration so the headers work with static files + * Persist Next.js custom headers to the Netlify configuration so the headers work with static files * See {@link https://nextjs.org/docs/api-reference/next.config.js/headers} for more information on custom * headers in Next.js * - * @param nextConfig - The NextJS configuration + * @param nextConfig - The Next.js configuration * @param netlifyHeaders - Existing headers that are already configured in the Netlify configuration */ export const generateCustomHeaders = (nextConfig: NextConfig, netlifyHeaders: NetlifyHeaders = []) => { @@ -210,3 +238,4 @@ export const generateCustomHeaders = (nextConfig: NextConfig, netlifyHeaders: Ne } } } +/* eslint-enable max-lines */ diff --git a/test/index.js b/test/index.js index daa7f97581..5d0f634660 100644 --- a/test/index.js +++ b/test/index.js @@ -162,6 +162,7 @@ beforeEach(async () => { netlifyConfig.headers = [] netlifyConfig.functions[HANDLER_FUNCTION_NAME] && (netlifyConfig.functions[HANDLER_FUNCTION_NAME].included_files = []) netlifyConfig.functions[ODB_FUNCTION_NAME] && (netlifyConfig.functions[ODB_FUNCTION_NAME].included_files = []) + netlifyConfig.functions['_api_*'] && (netlifyConfig.functions['_api_*'].included_files = []) await useFixture('serverless_next_config') }) @@ -517,6 +518,7 @@ describe('onBuild()', () => { '.next/BUILD_ID', '.next/static/chunks/webpack-middleware*.js', '!.next/server/**/*.js.nft.json', + '!**/node_modules/@next/swc*/**/*', '!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*', `!node_modules/next/dist/server/lib/squoosh/**/*.wasm`, `!node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm`, @@ -533,6 +535,43 @@ describe('onBuild()', () => { expect(netlifyConfig.functions[ODB_FUNCTION_NAME].node_bundler).toEqual('nft') }) + const excludesSharp = (includedFiles) => includedFiles.some((file) => file.startsWith('!') && file.includes('sharp')) + + it("doesn't exclude sharp if manually included", async () => { + await moveNextDist() + + const functions = [HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, '_api_*'] + + await nextRuntime.onBuild(defaultArgs) + + // Should exclude by default + for (const func of functions) { + expect(excludesSharp(netlifyConfig.functions[func].included_files)).toBeTruthy() + } + + // ...but if the user has added it, we shouldn't exclude it + for (const func of functions) { + netlifyConfig.functions[func].included_files = ['node_modules/sharp/**/*'] + } + + await nextRuntime.onBuild(defaultArgs) + + for (const func of functions) { + expect(excludesSharp(netlifyConfig.functions[func].included_files)).toBeFalsy() + } + + // ...even if it's in a subdirectory + for (const func of functions) { + netlifyConfig.functions[func].included_files = ['subdirectory/node_modules/sharp/**/*'] + } + + await nextRuntime.onBuild(defaultArgs) + + for (const func of functions) { + expect(excludesSharp(netlifyConfig.functions[func].included_files)).toBeFalsy() + } + }) + it('generates a file referencing all page sources', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs)