Skip to content

Commit

Permalink
Merge branch 'main' into fix-dynamic-api-routes
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Jun 12, 2023
2 parents d19f422 + 9195192 commit 8f946a9
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 234 deletions.
554 changes: 346 additions & 208 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@babel/preset-env": "^7.15.8",
"@babel/preset-typescript": "^7.16.0",
"@delucis/if-env": "^1.1.2",
"@netlify/build": "^29.11.8",
"@netlify/build": "^29.12.3",
"@netlify/eslint-config-node": "^7.0.1",
"@testing-library/cypress": "^9.0.0",
"@types/fs-extra": "^9.0.13",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"devDependencies": {
"@delucis/if-env": "^1.1.2",
"@netlify/build": "^29.11.8",
"@netlify/build": "^29.12.3",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/merge-stream": "^1.1.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import destr from 'destr'

export const HANDLER_FUNCTION_NAME = '___netlify-handler'
export const ODB_FUNCTION_NAME = '___netlify-odb-handler'
export const API_FUNCTION_NAME = '___netlify-api-handler'
export const IMAGE_FUNCTION_NAME = '_ipx'
export const NEXT_PLUGIN_NAME = '@netlify/next-runtime'
export const NEXT_PLUGIN = '@netlify/plugin-nextjs'
export const HANDLER_FUNCTION_TITLE = 'Next.js SSR handler'
export const ODB_FUNCTION_TITLE = 'Next.js ISR handler'
export const API_FUNCTION_TITLE = 'Next.js API handler'
export const IMAGE_FUNCTION_TITLE = 'next/image handler'
// These are paths in .next that shouldn't be publicly accessible
export const HIDDEN_PATHS = destr(process.env.NEXT_KEEP_METADATA_FILES)
Expand Down
8 changes: 4 additions & 4 deletions packages/runtime/src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,19 +404,19 @@ const baseServerReplacements: Array<[string, string]> = [
const nextServerReplacements: Array<[string, string]> = [
[
`getMiddlewareManifest() {\n if (this.minimalMode) return null;`,
`getMiddlewareManifest() {\n if (this.minimalMode || (process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return null;`,
`getMiddlewareManifest() {\n if (this.minimalMode || (process.env.NETLIFY && process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return null;`,
],
[
`generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode) return []`,
`generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode || (process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return [];`,
`generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode || (process.env.NETLIFY && process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return [];`,
],
[
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode) return undefined;`,
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || (process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return undefined;`,
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || (process.env.NETLIFY && process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1')) return undefined;`,
],
[
`getMiddlewareManifest() {\n if (this.minimalMode) {`,
`getMiddlewareManifest() {\n if (!this.minimalMode && (process.env.NEXT_DISABLE_NETLIFY_EDGE === 'true' || process.env.NEXT_DISABLE_NETLIFY_EDGE === '1')) {`,
`getMiddlewareManifest() {\n if (!this.minimalMode && (process.env.NETLIFY && process.env.NEXT_DISABLE_NETLIFY_EDGE === 'true' || process.env.NEXT_DISABLE_NETLIFY_EDGE === '1')) {`,
],
]

Expand Down
63 changes: 50 additions & 13 deletions packages/runtime/src/helpers/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
HANDLER_FUNCTION_TITLE,
ODB_FUNCTION_TITLE,
IMAGE_FUNCTION_TITLE,
API_FUNCTION_TITLE,
API_FUNCTION_NAME,
LAMBDA_WARNING_SIZE,
} from '../constants'
import { getApiHandler } from '../templates/getApiHandler'
import { getHandler } from '../templates/getHandler'
Expand All @@ -31,6 +34,7 @@ import { getFunctionNameForPage } from './utils'

export interface ApiRouteConfig {
functionName: string
functionTitle?: string
route: string
config: ApiConfig
compiled: string
Expand All @@ -39,6 +43,7 @@ export interface ApiRouteConfig {

export interface APILambda {
functionName: string
functionTitle: string
routes: ApiRouteConfig[]
includedFiles: string[]
type?: ApiRouteType
Expand All @@ -60,7 +65,7 @@ export const generateFunctions = async (
: undefined

for (const apiLambda of apiLambdas) {
const { functionName, routes, type, includedFiles } = apiLambda
const { functionName, functionTitle, routes, type, includedFiles } = apiLambda

const apiHandlerSource = getApiHandler({
// most api lambdas serve multiple routes, but scheduled functions need to be in separate lambdas.
Expand Down Expand Up @@ -102,6 +107,8 @@ export const generateFunctions = async (
})
await writeFile(join(functionsDir, functionName, 'pages.js'), resolverSource)

await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })

const nfInternalFiles = await glob(join(functionsDir, functionName, '**'))
includedFiles.push(...nfInternalFiles)
}
Expand All @@ -128,7 +135,7 @@ export const generateFunctions = async (
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'),
join(functionsDir, functionName, 'handlerUtils.js'),
)
writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
}

await writeHandler(HANDLER_FUNCTION_NAME, HANDLER_FUNCTION_TITLE, false)
Expand Down Expand Up @@ -314,16 +321,14 @@ const getBundleWeight = async (patterns: string[]) => {
return sum(sizes.flat(1))
}

const MB = 1024 * 1024

export const getAPILambdas = async (
publish: string,
baseDir: string,
pageExtensions: string[],
): Promise<APILambda[]> => {
const commonDependencies = await getAPIPRouteCommonDependencies(publish)

const threshold = 50 * MB - (await getBundleWeight(commonDependencies))
const threshold = LAMBDA_WARNING_SIZE - (await getBundleWeight(commonDependencies))

const apiRoutes = await getApiRouteConfigs(publish, baseDir, pageExtensions)

Expand All @@ -334,12 +339,41 @@ export const getAPILambdas = async (

const bins = pack(weighedRoutes, threshold)

return bins.map((bin, index) => ({
functionName: bin.length === 1 ? bin[0].functionName : `api-${index}`,
routes: bin,
includedFiles: [...commonDependencies, ...routes.flatMap((route) => route.includedFiles)],
type,
}))
return bins.map((bin) => {
if (bin.length === 1) {
const [func] = bin
const { functionName, functionTitle, config, includedFiles } = func
return {
functionName,
functionTitle,
routes: [func],
includedFiles: [...commonDependencies, ...includedFiles],
type: config.type,
}
}

const includedFiles = [...commonDependencies, ...bin.flatMap((route) => route.includedFiles)]
const nonSingletonBins = bins.filter((b) => b.length > 1)
if (nonSingletonBins.length === 1) {
return {
functionName: API_FUNCTION_NAME,
functionTitle: API_FUNCTION_TITLE,
includedFiles,
routes: bin,
type,
}
}

const indexInNonSingletonBins = nonSingletonBins.indexOf(bin)

return {
functionName: `${API_FUNCTION_NAME}-${indexInNonSingletonBins + 1}`,
functionTitle: `${API_FUNCTION_TITLE} ${indexInNonSingletonBins + 1}/${nonSingletonBins.length}`,
includedFiles,
routes: bin,
type,
}
})
}

const standardFunctions = apiRoutes.filter(
Expand All @@ -366,7 +400,7 @@ export const getAPILambdas = async (
export const getApiRouteConfigs = async (
publish: string,
appDir: string,
pageExtensions: string[],
pageExtensions?: string[],
): Promise<Array<ApiRouteConfig>> => {
const pages = await readJSON(join(publish, 'server', 'pages-manifest.json'))
const apiRoutes = Object.keys(pages).filter((page) => page.startsWith('/api/'))
Expand All @@ -381,6 +415,7 @@ export const getApiRouteConfigs = async (
const config = await extractConfigFromFile(filePath, appDir)

const functionName = getFunctionNameForPage(apiRoute, config.type === ApiRouteType.BACKGROUND)
const functionTitle = `${API_FUNCTION_TITLE} ${apiRoute}`

const compiled = pages[apiRoute]
const compiledPath = join(publish, 'server', compiled)
Expand All @@ -390,6 +425,7 @@ export const getApiRouteConfigs = async (

return {
functionName,
functionTitle,
route: apiRoute,
config,
compiled,
Expand All @@ -405,7 +441,7 @@ export const getApiRouteConfigs = async (
export const getExtendedApiRouteConfigs = async (
publish: string,
appDir: string,
pageExtensions: string[],
pageExtensions?: string[],
): Promise<Array<ApiRouteConfig>> => {
const settledApiRoutes = await getApiRouteConfigs(publish, appDir, pageExtensions)

Expand All @@ -415,6 +451,7 @@ export const getExtendedApiRouteConfigs = async (

export const packSingleFunction = (func: ApiRouteConfig): APILambda => ({
functionName: func.functionName,
functionTitle: func.functionTitle,
includedFiles: func.includedFiles,
routes: [func],
type: func.config.type,
Expand Down
12 changes: 6 additions & 6 deletions test/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2109,17 +2109,17 @@ Array [
Object {
"from": "/api/enterPreview",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"from": "/api/exitPreview",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"from": "/api/hello",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"from": "/api/hello-background",
Expand All @@ -2134,17 +2134,17 @@ Array [
Object {
"from": "/api/revalidate",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"from": "/api/shows/:id",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"from": "/api/shows/:params/*",
"status": 200,
"to": "/.netlify/functions/api-0",
"to": "/.netlify/functions/___netlify-api-handler",
},
Object {
"force": false,
Expand Down
13 changes: 12 additions & 1 deletion test/helpers/functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { describeCwdTmpDir, moveNextDist } from '../test-utils'
describeCwdTmpDir('api route file analysis', () => {
it('extracts correct route configs from source files', async () => {
await moveNextDist()
const configs = await getApiRouteConfigs('.next', process.cwd(), ['js', 'jsx', 'ts', 'tsx'])
const configs = await getApiRouteConfigs('.next', process.cwd())
// Using a Set means the order doesn't matter
expect(new Set(configs.map(({ includedFiles, ...rest }) => rest))).toEqual(
new Set([
{
functionName: '_api_og-handler',
functionTitle: 'Next.js API handler /api/og',
compiled: 'pages/api/og.js',
config: {
runtime: 'edge',
Expand All @@ -18,48 +19,56 @@ describeCwdTmpDir('api route file analysis', () => {
},
{
functionName: '_api_enterPreview-handler',
functionTitle: 'Next.js API handler /api/enterPreview',
compiled: 'pages/api/enterPreview.js',
config: {},
route: '/api/enterPreview',
},
{
functionName: '_api_exitPreview-handler',
functionTitle: 'Next.js API handler /api/exitPreview',
compiled: 'pages/api/exitPreview.js',
config: {},
route: '/api/exitPreview',
},
{
functionName: '_api_hello-handler',
functionTitle: 'Next.js API handler /api/hello',
compiled: 'pages/api/hello.js',
config: {},
route: '/api/hello',
},
{
functionName: '_api_shows_params-SPLAT-handler',
functionTitle: 'Next.js API handler /api/shows/[...params]',
compiled: 'pages/api/shows/[...params].js',
config: {},
route: '/api/shows/[...params]',
},
{
functionName: '_api_shows_id-PARAM-handler',
functionTitle: 'Next.js API handler /api/shows/[id]',
compiled: 'pages/api/shows/[id].js',
config: {},
route: '/api/shows/[id]',
},
{
functionName: '_api_hello-background-background',
functionTitle: 'Next.js API handler /api/hello-background',
compiled: 'pages/api/hello-background.js',
config: { type: 'experimental-background' },
route: '/api/hello-background',
},
{
functionName: '_api_hello-scheduled-handler',
functionTitle: 'Next.js API handler /api/hello-scheduled',
compiled: 'pages/api/hello-scheduled.js',
config: { schedule: '@hourly', type: 'experimental-scheduled' },
route: '/api/hello-scheduled',
},
{
functionName: '_api_revalidate-handler',
functionTitle: 'Next.js API handler /api/revalidate',
compiled: 'pages/api/revalidate.js',
config: {},
route: '/api/revalidate',
Expand All @@ -76,12 +85,14 @@ describeCwdTmpDir('api route file analysis', () => {
new Set([
{
functionName: '_api_hello-background-background',
functionTitle: 'Next.js API handler /api/hello-background',
compiled: 'pages/api/hello-background.js',
config: { type: 'experimental-background' },
route: '/api/hello-background',
},
{
functionName: '_api_hello-scheduled-handler',
functionTitle: 'Next.js API handler /api/hello-scheduled',
compiled: 'pages/api/hello-scheduled.js',
config: { schedule: '@hourly', type: 'experimental-scheduled' },
route: '/api/hello-scheduled',
Expand Down
17 changes: 17 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,23 @@ describe('onBuild()', () => {
expect(netlifyConfig.functions['_api_*'].node_bundler).toEqual('nft')
})

it('provides displayname for split api routes', async () => {
await moveNextDist()
await nextRuntime.onBuild(defaultArgs)

const functionsManifest = await readJson(
path.join('.netlify', 'functions-internal', '___netlify-api-handler', '___netlify-api-handler.json'),
)

expect(functionsManifest).toEqual({
config: {
generator: '@netlify/next-runtime@unknown',
name: 'Next.js API handler',
},
version: 1,
})
})

// eslint-disable-next-line jest/expect-expect
it('works when `relativeAppDir` is undefined', async () => {
await moveNextDist()
Expand Down

0 comments on commit 8f946a9

Please sign in to comment.