Skip to content

Commit

Permalink
feat: enable API, SSR and DSG functions individually as required (#375)
Browse files Browse the repository at this point in the history
* feat: enable API, SSR and DSG functions individually as required

* chore: edit variable names for clarity

* fix: run setupImageCdn independently of functions

* chore: abstract file modification functions to reduce complexity in onBuild

* chore: abstract config modification functions to reduce complexity in onBuild

* chore: reduce complexity in onBuild handler

* fix: missing function invocation in getNeededFunctions

* feat: add env vars for disabling api/ssr/dsg individually

* chore: abstract function skip env var checks
  • Loading branch information
orinokai committed May 18, 2022
1 parent ea90a6f commit aa36bc2
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 81 deletions.
145 changes: 104 additions & 41 deletions plugin/src/helpers/config.ts
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
@@ -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
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
@@ -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`))
}
}

0 comments on commit aa36bc2

Please sign in to comment.