Skip to content

Commit 4b3d3f4

Browse files
Skn0tteduardoboucasorinokaikodiakhq[bot]
authored
feat: Use none bundler for SSR Routes (#2084)
* feat: use "none bundler" for SSR * chore: bump prerelease version * chore: fix linting * chore: enable new path for e2e tests * fix: include static manifest * fix: include app-dir files from `app-paths-manifest.json` * fix: add fallback for when no app-paths-manifest was found * Update packages/runtime/src/index.ts --------- Co-authored-by: Eduardo Bouças <[email protected]> Co-authored-by: Rob Stanford <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 87391e1 commit 4b3d3f4

File tree

11 files changed

+166
-19
lines changed

11 files changed

+166
-19
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
// https://github.com/typescript-eslint/typescript-eslint/issues/2483
5252
'no-shadow': 'off',
5353
'@typescript-eslint/no-shadow': 'error',
54+
'import/max-dependencies': 'off',
5455
},
5556
},
5657
{

demos/canary/netlify.toml

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ command = "next build"
33
publish = ".next"
44
ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../..; fi;"
55

6+
[build.environment]
7+
NEXT_SPLIT_API_ROUTES = "true"
8+
NEXT_BUNDLE_BASED_ON_NFT_FILES = "true"
9+
610
[[plugins]]
711
package = "@netlify/plugin-nextjs"
812

demos/default/netlify.toml

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ CYPRESS_CACHE_FOLDER = "../node_modules/.CypressBinary"
1010
# set TERM variable for terminal output
1111
TERM = "xterm"
1212
NODE_VERSION = "16.15.1"
13+
NEXT_SPLIT_API_ROUTES = "true"
14+
NEXT_BUNDLE_BASED_ON_NFT_FILES = "true"
1315

1416
[[headers]]
1517
for = "/_next/image/*"

demos/middleware/netlify.toml

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ command = "npm run build"
33
publish = ".next"
44
ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../..; fi;"
55

6+
[build.environment]
7+
NEXT_SPLIT_API_ROUTES = "true"
8+
NEXT_BUNDLE_BASED_ON_NFT_FILES = "true"
9+
610
[[plugins]]
711
package = "@netlify/plugin-nextjs"
812

demos/nx-next-monorepo-demo/netlify.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff
55

66
[build.environment]
77
NEXT_SPLIT_API_ROUTES = "true"
8+
NEXT_BUNDLE_BASED_ON_NFT_FILES = "true"
89

910
[dev]
1011
command = "npm run start"

demos/static-root/netlify.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff
55

66
[build.environment]
77
NEXT_SPLIT_API_ROUTES = "true"
8+
NEXT_BUNDLE_BASED_ON_NFT_FILES = "true"
89

910
[[plugins]]
1011
package = "@netlify/plugin-nextjs"

packages/runtime/src/helpers/config.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import slash from 'slash'
88

99
import { HANDLER_FUNCTION_NAME, IMAGE_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'
1010

11-
import type { APILambda } from './functions'
11+
import type { APILambda, SSRLambda } from './functions'
1212
import type { RoutesManifest } from './types'
1313
import { escapeStringRegexp } from './utils'
1414

@@ -107,12 +107,14 @@ export const configureHandlerFunctions = async ({
107107
publish,
108108
ignore = [],
109109
apiLambdas,
110+
ssrLambdas,
110111
splitApiRoutes,
111112
}: {
112113
netlifyConfig: NetlifyConfig
113114
publish: string
114115
ignore: Array<string>
115116
apiLambdas: APILambda[]
117+
ssrLambdas: SSRLambda[]
116118
splitApiRoutes: boolean
117119
}) => {
118120
const config = await getRequiredServerFiles(publish)
@@ -170,17 +172,23 @@ export const configureHandlerFunctions = async ({
170172
})
171173
}
172174

173-
configureFunction(HANDLER_FUNCTION_NAME)
174-
configureFunction(ODB_FUNCTION_NAME)
175+
const configureLambda = (lambda: APILambda) => {
176+
const { functionName, includedFiles } = lambda
177+
netlifyConfig.functions[functionName] ||= { included_files: [] }
178+
netlifyConfig.functions[functionName].node_bundler = 'none'
179+
netlifyConfig.functions[functionName].included_files ||= []
180+
netlifyConfig.functions[functionName].included_files.push(...includedFiles.map(escapeGlob))
181+
}
182+
183+
if (ssrLambdas.length === 0) {
184+
configureFunction(HANDLER_FUNCTION_NAME)
185+
configureFunction(ODB_FUNCTION_NAME)
186+
} else {
187+
ssrLambdas.forEach(configureLambda)
188+
}
175189

176190
if (splitApiRoutes) {
177-
for (const apiLambda of apiLambdas) {
178-
const { functionName, includedFiles } = apiLambda
179-
netlifyConfig.functions[functionName] ||= { included_files: [] }
180-
netlifyConfig.functions[functionName].node_bundler = 'none'
181-
netlifyConfig.functions[functionName].included_files ||= []
182-
netlifyConfig.functions[functionName].included_files.push(...includedFiles.map(escapeGlob))
183-
}
191+
apiLambdas.forEach(configureLambda)
184192
} else {
185193
configureFunction('_api_*')
186194
}

packages/runtime/src/helpers/flags.ts

+7
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ export const splitApiRoutes = (featureFlags: Record<string, unknown>, publish: s
2929

3030
return isEnabled
3131
}
32+
33+
export const bundleBasedOnNftFiles = (featureFlags: Record<string, unknown>): boolean => {
34+
const isEnabled =
35+
destr(process.env.NEXT_BUNDLE_BASED_ON_NFT_FILES) ?? featureFlags.next_bundle_based_on_nft_files ?? false
36+
37+
return isEnabled
38+
}

packages/runtime/src/helpers/functions.ts

+120-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import bridgeFile from '@vercel/node-bridge'
33
import chalk from 'chalk'
44
import destr from 'destr'
55
import { copyFile, ensureDir, existsSync, readJSON, writeFile, writeJSON, stat } from 'fs-extra'
6+
import { PrerenderManifest } from 'next/dist/build'
67
import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/image-config'
78
import { outdent } from 'outdent'
8-
import { join, relative, resolve, dirname } from 'pathe'
9+
import { join, relative, resolve, dirname, basename, extname } from 'pathe'
910
import glob from 'tiny-glob'
1011

1112
import {
@@ -32,27 +33,35 @@ import { pack } from './pack'
3233
import { ApiRouteType } from './types'
3334
import { getFunctionNameForPage } from './utils'
3435

35-
export interface ApiRouteConfig {
36+
export interface RouteConfig {
3637
functionName: string
3738
functionTitle?: string
3839
route: string
39-
config: ApiConfig
4040
compiled: string
4141
includedFiles: string[]
4242
}
4343

44-
export interface APILambda {
44+
export interface ApiRouteConfig extends RouteConfig {
45+
config: ApiConfig
46+
}
47+
48+
export interface SSRLambda {
4549
functionName: string
4650
functionTitle: string
47-
routes: ApiRouteConfig[]
51+
routes: RouteConfig[]
4852
includedFiles: string[]
53+
}
54+
55+
export interface APILambda extends SSRLambda {
56+
routes: ApiRouteConfig[]
4957
type?: ApiRouteType
5058
}
5159

5260
export const generateFunctions = async (
5361
{ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }: NetlifyPluginConstants,
5462
appDir: string,
5563
apiLambdas: APILambda[],
64+
ssrLambdas: SSRLambda[],
5665
): Promise<void> => {
5766
const publish = resolve(PUBLISH_DIR)
5867
const functionsDir = resolve(INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC)
@@ -144,6 +153,12 @@ export const generateFunctions = async (
144153
join(functionsDir, functionName, 'handlerUtils.js'),
145154
)
146155
await writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
156+
157+
const nfInternalFiles = await glob(join(functionsDir, functionName, '**'))
158+
const lambda = ssrLambdas.find((l) => l.functionName === functionName)
159+
if (lambda) {
160+
lambda.includedFiles.push(...nfInternalFiles)
161+
}
147162
}
148163

149164
await writeHandler(HANDLER_FUNCTION_NAME, HANDLER_FUNCTION_TITLE, false)
@@ -295,13 +310,17 @@ export const traceNPMPackage = async (packageName: string, publish: string) => {
295310
}
296311
}
297312

298-
export const getAPIPRouteCommonDependencies = async (publish: string) => {
313+
export const getCommonDependencies = async (publish: string) => {
299314
const deps = await Promise.all([
300315
traceRequiredServerFiles(publish),
301316
traceNextServer(publish),
302317

303318
// used by our own bridge.js
304319
traceNPMPackage('follow-redirects', publish),
320+
321+
// using package.json because otherwise, we'd find some /dist/... path
322+
traceNPMPackage('@netlify/functions/package.json', publish),
323+
traceNPMPackage('is-promise', publish),
305324
])
306325

307326
return deps.flat(1)
@@ -329,12 +348,106 @@ const getBundleWeight = async (patterns: string[]) => {
329348
return sum(sizes.flat(1))
330349
}
331350

351+
const changeExtension = (file: string, extension: string) => {
352+
const base = basename(file, extname(file))
353+
return join(dirname(file), base + extension)
354+
}
355+
356+
const getSSRDependencies = async (publish: string): Promise<string[]> => {
357+
const prerenderManifest: PrerenderManifest = await readJSON(join(publish, 'prerender-manifest.json'))
358+
359+
return [
360+
...Object.entries(prerenderManifest.routes).flatMap(([route, ssgRoute]) => {
361+
if (ssgRoute.initialRevalidateSeconds === false) {
362+
return []
363+
}
364+
365+
if (ssgRoute.dataRoute.endsWith('.rsc')) {
366+
return [
367+
join(publish, 'server', 'app', ssgRoute.dataRoute),
368+
join(publish, 'server', 'app', changeExtension(ssgRoute.dataRoute, '.html')),
369+
]
370+
}
371+
372+
const trimmedPath = route === '/' ? 'index' : route.slice(1)
373+
return [
374+
join(publish, 'server', 'pages', `${trimmedPath}.html`),
375+
join(publish, 'server', 'pages', `${trimmedPath}.json`),
376+
]
377+
}),
378+
join(publish, '**', '*.html'),
379+
join(publish, 'static-manifest.json'),
380+
]
381+
}
382+
383+
export const getSSRLambdas = async (publish: string): Promise<SSRLambda[]> => {
384+
const commonDependencies = await getCommonDependencies(publish)
385+
const ssrRoutes = await getSSRRoutes(publish)
386+
387+
// TODO: for now, they're the same - but we should separate them
388+
const nonOdbRoutes = ssrRoutes
389+
const odbRoutes = ssrRoutes
390+
391+
const ssrDependencies = await getSSRDependencies(publish)
392+
393+
return [
394+
{
395+
functionName: HANDLER_FUNCTION_NAME,
396+
functionTitle: HANDLER_FUNCTION_TITLE,
397+
includedFiles: [
398+
...commonDependencies,
399+
...ssrDependencies,
400+
...nonOdbRoutes.flatMap((route) => route.includedFiles),
401+
],
402+
routes: nonOdbRoutes,
403+
},
404+
{
405+
functionName: ODB_FUNCTION_NAME,
406+
functionTitle: ODB_FUNCTION_TITLE,
407+
includedFiles: [...commonDependencies, ...ssrDependencies, ...odbRoutes.flatMap((route) => route.includedFiles)],
408+
routes: odbRoutes,
409+
},
410+
]
411+
}
412+
413+
const getSSRRoutes = async (publish: string): Promise<RouteConfig[]> => {
414+
const pageManifest = (await readJSON(join(publish, 'server', 'pages-manifest.json'))) as Record<string, string>
415+
const pageManifestRoutes = Object.entries(pageManifest).filter(
416+
([page, compiled]) => !page.startsWith('/api/') && !compiled.endsWith('.html'),
417+
)
418+
419+
const appPathsManifest: Record<string, string> = await readJSON(
420+
join(publish, 'server', 'app-paths-manifest.json'),
421+
).catch(() => ({}))
422+
const appRoutes = Object.entries(appPathsManifest)
423+
424+
const routes = [...pageManifestRoutes, ...appRoutes]
425+
426+
return await Promise.all(
427+
routes.map(async ([route, compiled]) => {
428+
const functionName = getFunctionNameForPage(route)
429+
430+
const compiledPath = join(publish, 'server', compiled)
431+
432+
const routeDependencies = await getDependenciesOfFile(compiledPath)
433+
const includedFiles = [compiledPath, ...routeDependencies]
434+
435+
return {
436+
functionName,
437+
route,
438+
compiled,
439+
includedFiles,
440+
}
441+
}),
442+
)
443+
}
444+
332445
export const getAPILambdas = async (
333446
publish: string,
334447
baseDir: string,
335448
pageExtensions: string[],
336449
): Promise<APILambda[]> => {
337-
const commonDependencies = await getAPIPRouteCommonDependencies(publish)
450+
const commonDependencies = await getCommonDependencies(publish)
338451

339452
const threshold = LAMBDA_WARNING_SIZE - (await getBundleWeight(commonDependencies))
340453

packages/runtime/src/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { onPreDev } from './helpers/dev'
1919
import { writeEdgeFunctions, loadMiddlewareManifest, cleanupEdgeFunctions } from './helpers/edge'
2020
import { moveStaticPages, movePublicFiles, removeMetadataFiles } from './helpers/files'
21-
import { splitApiRoutes } from './helpers/flags'
21+
import { bundleBasedOnNftFiles, splitApiRoutes } from './helpers/flags'
2222
import {
2323
generateFunctions,
2424
setupImageFunction,
@@ -28,6 +28,7 @@ import {
2828
packSingleFunction,
2929
getExtendedApiRouteConfigs,
3030
APILambda,
31+
getSSRLambdas,
3132
} from './helpers/functions'
3233
import { generateRedirects, generateStaticRedirects } from './helpers/redirects'
3334
import { shouldSkip, isNextAuthInstalled, getCustomImageResponseHeaders, getRemotePatterns } from './helpers/utils'
@@ -171,14 +172,17 @@ const plugin: NetlifyPlugin = {
171172
extendedRoutes.map(packSingleFunction),
172173
)
173174

174-
await generateFunctions(constants, appDir, apiLambdas)
175+
const ssrLambdas = bundleBasedOnNftFiles(featureFlags) ? await getSSRLambdas(publish) : []
176+
177+
await generateFunctions(constants, appDir, apiLambdas, ssrLambdas)
175178
await generatePagesResolver(constants)
176179

177180
await configureHandlerFunctions({
178181
netlifyConfig,
179182
ignore,
180183
publish: relative(process.cwd(), publish),
181184
apiLambdas,
185+
ssrLambdas,
182186
splitApiRoutes: splitApiRoutes(featureFlags, publish),
183187
})
184188

packages/runtime/src/templates/getHandler.ts

+2
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export const getHandler = ({
206206
throw new Error('Could not find Next.js server')
207207
}
208208
209+
process.env.NODE_ENV = 'production';
210+
209211
const { Server } = require("http");
210212
const { promises } = require("fs");
211213
// We copy the file here rather than requiring from the node module

0 commit comments

Comments
 (0)