Skip to content

feat: enable API, SSR and DSG functions individually as required #375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 18, 2022
145 changes: 104 additions & 41 deletions plugin/src/helpers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -114,64 +115,126 @@ export async function checkConfig({ utils, netlifyConfig }): Promise<void> {
}
}

export async function modifyConfig({
netlifyConfig,
cacheDir,
neededFunctions,
}: {
netlifyConfig: NetlifyConfig
cacheDir: string
neededFunctions: FunctionList
}): Promise<void> {
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<FunctionList> {
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 {
Expand Down
17 changes: 17 additions & 0 deletions plugin/src/helpers/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os from 'os'
import process from 'process'

import { NetlifyConfig } from '@netlify/build'
import {
copyFile,
ensureDir,
Expand All @@ -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'
Expand All @@ -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<void> => {
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.
*/
Expand Down
38 changes: 25 additions & 13 deletions plugin/src/helpers/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -34,30 +36,40 @@ const writeApiFunction = async ({ appDir, functionDir }) => {
export const writeFunctions = async ({
constants,
netlifyConfig,
neededFunctions,
}: {
constants: NetlifyPluginConstants
netlifyConfig: NetlifyConfig
neededFunctions: FunctionList
}): Promise<void> => {
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 ({
Expand Down
42 changes: 15 additions & 27 deletions plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import path, { dirname, join } from 'path'
import path from 'path'
import process from 'process'

import { NetlifyPluginOptions } from '@netlify/build'
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'

Expand Down Expand Up @@ -58,35 +53,28 @@ 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({
constants: { PUBLISH_DIR, FUNCTIONS_DIST },
utils,
}): Promise<void> {
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`))
}
}