diff --git a/plugin/src/helpers/config.ts b/plugin/src/helpers/config.ts index 3b55de10..732b0a8b 100644 --- a/plugin/src/helpers/config.ts +++ b/plugin/src/helpers/config.ts @@ -3,11 +3,12 @@ import { EOL } from 'os' import path from 'path' import process from 'process' -import { stripIndent } from 'common-tags' +import { NetlifyConfig } from '@netlify/build' import fs, { existsSync } from 'fs-extra' import type { GatsbyConfig, PluginRef } from 'gatsby' import { checkPackageVersion } from './files' +import type { FunctionList } from './functions' export async function spliceConfig({ startMarker, @@ -114,64 +115,126 @@ export async function checkConfig({ utils, netlifyConfig }): Promise { } } +export async function modifyConfig({ + netlifyConfig, + cacheDir, + neededFunctions, +}: { + netlifyConfig: NetlifyConfig + cacheDir: string + neededFunctions: FunctionList +}): Promise { + mutateConfig({ netlifyConfig, cacheDir, neededFunctions }) + + if (neededFunctions.includes('API')) { + // Editing _redirects so it works with ntl dev + await spliceConfig({ + startMarker: '# @netlify/plugin-gatsby redirects start', + endMarker: '# @netlify/plugin-gatsby redirects end', + contents: '/api/* /.netlify/functions/__api 200', + fileName: path.join(netlifyConfig.build.publish, '_redirects'), + }) + } +} + export function mutateConfig({ netlifyConfig, - compiledFunctionsDir, cacheDir, + neededFunctions, +}: { + netlifyConfig: NetlifyConfig + cacheDir: string + neededFunctions: FunctionList }): void { /* eslint-disable no-underscore-dangle, no-param-reassign */ - netlifyConfig.functions.__api = { - included_files: [path.posix.join(compiledFunctionsDir, '**')], - external_node_modules: ['msgpackr-extract'], + if (neededFunctions.includes('API')) { + netlifyConfig.functions.__api = { + included_files: [path.posix.join(cacheDir, 'functions', '**')], + external_node_modules: ['msgpackr-extract'], + } } - netlifyConfig.functions.__dsg = { - included_files: [ - 'public/404.html', - 'public/500.html', - path.posix.join(cacheDir, 'data', '**'), - path.posix.join(cacheDir, 'query-engine', '**'), - path.posix.join(cacheDir, 'page-ssr', '**'), - '!**/*.js.map', - ], - external_node_modules: ['msgpackr-extract'], - node_bundler: 'esbuild', + if (neededFunctions.includes('DSG')) { + netlifyConfig.functions.__dsg = { + included_files: [ + 'public/404.html', + 'public/500.html', + path.posix.join(cacheDir, 'data', '**'), + path.posix.join(cacheDir, 'query-engine', '**'), + path.posix.join(cacheDir, 'page-ssr', '**'), + '!**/*.js.map', + ], + external_node_modules: ['msgpackr-extract'], + node_bundler: 'esbuild', + } } - netlifyConfig.functions.__ssr = { ...netlifyConfig.functions.__dsg } + if (neededFunctions.includes('SSR')) { + netlifyConfig.functions.__ssr = { + included_files: [ + 'public/404.html', + 'public/500.html', + path.posix.join(cacheDir, 'data', '**'), + path.posix.join(cacheDir, 'query-engine', '**'), + path.posix.join(cacheDir, 'page-ssr', '**'), + '!**/*.js.map', + ], + external_node_modules: ['msgpackr-extract'], + node_bundler: 'esbuild', + } + } /* eslint-enable no-underscore-dangle, no-param-reassign */ } -export function shouldSkipFunctions(cacheDir: string): boolean { - if ( - process.env.NETLIFY_SKIP_GATSBY_FUNCTIONS === 'true' || - process.env.NETLIFY_SKIP_GATSBY_FUNCTIONS === '1' - ) { - console.log( - 'Skipping Gatsby Functions and SSR/DSG support because the environment variable NETLIFY_SKIP_GATSBY_FUNCTIONS is set to true', - ) - return true - } +export async function getNeededFunctions( + cacheDir: string, +): Promise { + if (!existsSync(path.join(cacheDir, 'functions'))) return [] - if (!existsSync(path.join(cacheDir, 'functions'))) { - console.log( - `Skipping Gatsby Functions and SSR/DSG support because the site's Gatsby version does not support them`, - ) - return true + const neededFunctions = overrideNeededFunctions( + await readFunctionSkipFile(cacheDir), + ) + + const functionList = Object.keys(neededFunctions).filter( + (name) => neededFunctions[name] === true, + ) as FunctionList + + if (functionList.length === 0) { + console.log('Skipping Gatsby Functions and SSR/DSG support') + } else { + console.log(`Enabling Gatsby ${functionList.join('/')} support`) } - const skipFile = path.join(cacheDir, '.nf-skip-gatsby-functions') + return functionList +} + +async function readFunctionSkipFile(cacheDir: string) { + try { + // read skip file from gatsby-plugin-netlify + return await fs.readJson(path.join(cacheDir, '.nf-skip-gatsby-functions')) + } catch (error) { + // missing skip file = all functions needed + // empty or invalid skip file = no functions needed + return error.code === 'ENOENT' ? { API: true, SSR: true, DSG: true } : {} + } +} - if (existsSync(skipFile)) { - console.log( - stripIndent` - Skipping Gatsby Functions and SSR/DSG support because gatsby-plugin-netlify reported that this site does not use them. - If this is incorrect, remove the file "${skipFile}" and try again.`, - ) - return true +// eslint-disable-next-line complexity +function overrideNeededFunctions(neededFunctions) { + const skipAll = isEnvSet('NETLIFY_SKIP_GATSBY_FUNCTIONS') + const skipAPI = isEnvSet('NETLIFY_SKIP_API_FUNCTION') + const skipSSR = isEnvSet('NETLIFY_SKIP_SSR_FUNCTION') + const skipDSG = isEnvSet('NETLIFY_SKIP_DSG_FUNCTION') + + return { + API: skipAll || skipAPI ? false : neededFunctions.API, + SSR: skipAll || skipSSR ? false : neededFunctions.SSR, + DSG: skipAll || skipDSG ? false : neededFunctions.DSG, } +} - return false +function isEnvSet(envVar: string) { + return process.env[envVar] === 'true' || process.env[envVar] === '1' } export function getGatsbyRoot(publish: string): string { diff --git a/plugin/src/helpers/files.ts b/plugin/src/helpers/files.ts index 714041a0..1e5646aa 100644 --- a/plugin/src/helpers/files.ts +++ b/plugin/src/helpers/files.ts @@ -1,6 +1,7 @@ import os from 'os' import process from 'process' +import { NetlifyConfig } from '@netlify/build' import { copyFile, ensureDir, @@ -12,6 +13,8 @@ import { import { dirname, join, resolve } from 'pathe' import semver from 'semver' +import type { FunctionList } from './functions' + const DEFAULT_LAMBDA_PLATFORM = 'linux' const DEFAULT_LAMBDA_ABI = '83' const DEFAULT_LAMBDA_ARCH = 'x64' @@ -23,6 +26,20 @@ const RELOCATABLE_BINARIES = [ `node.abi${DEFAULT_LAMBDA_ABI}.glibc.node`, ] +export const modifyFiles = async ({ + netlifyConfig, + neededFunctions, +}: { + netlifyConfig: NetlifyConfig + neededFunctions: FunctionList +}): Promise => { + if (neededFunctions.includes('SSR') || neededFunctions.includes('DSG')) { + const root = dirname(netlifyConfig.build.publish) + await patchFile(root) + await relocateBinaries(root) + } +} + /** * Manually patching the bundle to work around various incompatibilities in some versions. */ diff --git a/plugin/src/helpers/functions.ts b/plugin/src/helpers/functions.ts index 3525b135..c6842760 100644 --- a/plugin/src/helpers/functions.ts +++ b/plugin/src/helpers/functions.ts @@ -6,6 +6,8 @@ import { makeApiHandler, makeHandler } from '../templates/handlers' import { getGatsbyRoot } from './config' +export type FunctionList = Array<'API' | 'SSR' | 'DSG'> + const writeFunction = async ({ renderMode, handlerName, @@ -34,30 +36,40 @@ const writeApiFunction = async ({ appDir, functionDir }) => { export const writeFunctions = async ({ constants, netlifyConfig, + neededFunctions, }: { constants: NetlifyPluginConstants netlifyConfig: NetlifyConfig + neededFunctions: FunctionList }): Promise => { const { PUBLISH_DIR, INTERNAL_FUNCTIONS_SRC } = constants const siteRoot = getGatsbyRoot(PUBLISH_DIR) const functionDir = resolve(INTERNAL_FUNCTIONS_SRC, '__api') const appDir = relative(functionDir, siteRoot) - await writeFunction({ - renderMode: 'SSR', - handlerName: '__ssr', - appDir, - functionsSrc: INTERNAL_FUNCTIONS_SRC, - }) + if (neededFunctions.includes('SSR')) { + await writeFunction({ + renderMode: 'SSR', + handlerName: '__ssr', + appDir, + functionsSrc: INTERNAL_FUNCTIONS_SRC, + }) + } + + if (neededFunctions.includes('DSG')) { + await writeFunction({ + renderMode: 'DSG', + handlerName: '__dsg', + appDir, + functionsSrc: INTERNAL_FUNCTIONS_SRC, + }) + } - await writeFunction({ - renderMode: 'DSG', - handlerName: '__dsg', - appDir, - functionsSrc: INTERNAL_FUNCTIONS_SRC, - }) await setupImageCdn({ constants, netlifyConfig }) - await writeApiFunction({ appDir, functionDir }) + + if (neededFunctions.includes('API')) { + await writeApiFunction({ appDir, functionDir }) + } } export const setupImageCdn = async ({ diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 09d96d0f..24c3f30e 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -1,4 +1,4 @@ -import path, { dirname, join } from 'path' +import path from 'path' import process from 'process' import { NetlifyPluginOptions } from '@netlify/build' @@ -6,13 +6,8 @@ import { stripIndent } from 'common-tags' import { existsSync } from 'fs-extra' import { normalizedCacheDir, restoreCache, saveCache } from './helpers/cache' -import { - checkConfig, - mutateConfig, - shouldSkipFunctions, - spliceConfig, -} from './helpers/config' -import { patchFile, relocateBinaries } from './helpers/files' +import { checkConfig, getNeededFunctions, modifyConfig } from './helpers/config' +import { modifyFiles } from './helpers/files' import { deleteFunctions, writeFunctions } from './helpers/functions' import { checkZipSize } from './helpers/verification' @@ -58,27 +53,15 @@ export async function onBuild({ The plugin no longer uses this and it should be deleted to avoid conflicts.\n`) } - if (shouldSkipFunctions(cacheDir)) { - await deleteFunctions(constants) - return - } - const compiledFunctionsDir = path.join(cacheDir, '/functions') + const neededFunctions = await getNeededFunctions(cacheDir) - await writeFunctions({ constants, netlifyConfig }) + await deleteFunctions(constants) - mutateConfig({ netlifyConfig, cacheDir, compiledFunctionsDir }) + await writeFunctions({ constants, netlifyConfig, neededFunctions }) - const root = dirname(netlifyConfig.build.publish) - await patchFile(root) - await relocateBinaries(root) + await modifyConfig({ netlifyConfig, cacheDir, neededFunctions }) - // Editing _redirects so it works with ntl dev - spliceConfig({ - startMarker: '# @netlify/plugin-gatsby redirects start', - endMarker: '# @netlify/plugin-gatsby redirects end', - contents: '/api/* /.netlify/functions/__api 200', - fileName: join(netlifyConfig.build.publish, '_redirects'), - }) + await modifyFiles({ netlifyConfig, neededFunctions }) } export async function onPostBuild({ @@ -86,7 +69,12 @@ export async function onPostBuild({ utils, }): Promise { await saveCache({ publish: PUBLISH_DIR, utils }) - for (const func of ['api', 'dsg', 'ssr']) { - await checkZipSize(path.join(FUNCTIONS_DIST, `__${func}.zip`)) + + const cacheDir = normalizedCacheDir(PUBLISH_DIR) + + const neededFunctions = await getNeededFunctions(cacheDir) + + for (const func of neededFunctions) { + await checkZipSize(path.join(FUNCTIONS_DIST, `__${func.toLowerCase()}.zip`)) } }