Skip to content

Commit d98efc1

Browse files
Skn0ttnickytonline
andauthored
feat: split up API Routes + use .nft.json files to make builds fast (#2058)
* feat: split up API Routes Brings back the functionality that was reverted in #1731, but under a flag. This will be utterly slow in building, so let's try to speed that up in the next step! * feat: load includedFiles for every page * refactor: extract function config logic * refactor: extract flag into own definition * feat: use "none" bundler for split-up api routes * feat: list some more dependencies * feat: use NFT to trace common required files * refactor: clean up a wee bit * fix: please don't include /sh * feat: enable flag by default, so tests use it * feat: add a naïve packing algo * feat: write rough sketch for packing lambdas * refactor: add constructor for APILambda * feat: pack handlers together into bundles * fix: linter * feat: exclude some heavy unneeded files * fix: trigger CI again, now that it supports `none` bundler * feat: remove code for old mechanism If we'll be doing a staged rollout, and our test suite covers most of the cases, we should be able to roll this out without a self-serve opt-out mechanism. * fix: remove test for deleted code * fix: ensure that react doesn't try to load development build * fix: move test directory into repo, so node_modules can be read * fix: snapshot with API redirects * fix: remove .only * fix: don't assert on _api_* * fix: another test * fix: apply NODE_ENV=prod to right generated handler * feat: remove nft tracing * feat: put change behind flag again * feat: source flag from plugin input * fix: add default value for featureflags * fix: default flag to true for testing * fix: revert changes to lockfiles * fix: eslint it/test * fix: lint * fix: revert distracting change * fix: now that we don't use nft anymore, we don't have to copy over node_modules * fix: swallow require.resolve errors for unit tests * fix: remove timing logs * fix: lint name * fix: lint * fix: isr needs _document.js * fix: add _app for ISR * fix: correct wrong output of some npm versions * fix: integration test For some reason, ZISI doesn't like relative paths in this integration test. We can fix it by using absolute paths. Since this PR moves API routes out of __netlify_handler, we can no longer make the assertion on x-nf-render-mode. * fix: assemble npm package path correctly, also for monorepos * fix: try what happens if we use next-netlify server * fix: check what happens when we skip revalidation * fix: dont let revalidate request time out * fix: send x-nextjs-cache header to prevent error * fix: update error message in test * fix: resolve relative paths based on data in required-server-files * fix: test * fix: keep manually-added `included_files` * fix: try something * fix: don't include unneeded _app pages in ISR * fix: finalize includedFiles before writing it onto netlifyConfig * chore: update comment * fix: exclude sass file in monorepos * Update packages/runtime/src/helpers/functions.ts * chore: remove comment * fix: update flag impl * refactor: use getRequiredServerFiles * chore: add comment on route[0] * fix: set NEXT_SPLIT_API_ROUTES in netlify.toml * fix: put updated revalidate behaviour behind flag * fix: supply splitApiRoutes in getHandler * fix: better run your code before committing it and embarrassing yourself --------- Co-authored-by: Nick Taylor <[email protected]>
1 parent 5ee2fce commit d98efc1

27 files changed

+547
-101
lines changed

.github/workflows/e2e-appdir.yml

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
push:
77
branches: [main]
88

9+
env:
10+
NEXT_SPLIT_API_ROUTES: true
11+
912
jobs:
1013
setup:
1114
runs-on: ubuntu-latest

.github/workflows/e2e-next.yml

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88
schedule:
99
- cron: '0 0 * * *'
1010

11+
env:
12+
NEXT_SPLIT_API_ROUTES: true
13+
1114
jobs:
1215
setup:
1316
runs-on: ubuntu-latest

.github/workflows/test-integration.yml

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ concurrency:
99
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
1010
cancel-in-progress: true
1111

12+
env:
13+
NEXT_SPLIT_API_ROUTES: true
14+
1215
jobs:
1316
build:
1417
name: Integration tests

.github/workflows/test.yml

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ concurrency:
1111
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
1212
cancel-in-progress: true
1313

14+
env:
15+
NEXT_SPLIT_API_ROUTES: true
16+
1417
jobs:
1518
build:
1619
name: Unit tests

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,6 @@ packages/*/lib
152152
cypress/screenshots
153153

154154
# Test cases have node module fixtures
155-
!test/**/node_modules
155+
!test/**/node_modules
156+
157+
/tmp

cypress/e2e/default/revalidate.cy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('On-demand revalidation', () => {
3333
cy.request({ url: '/api/revalidate/?select=5', failOnStatusCode: false }).then((res) => {
3434
expect(res.status).to.eq(500)
3535
expect(res.body).to.have.property('message')
36-
expect(res.body.message).to.include('Invalid response 404')
36+
expect(res.body.message).to.include('could not refresh content for path /getStaticProps/withRevalidate/3/, path is not handled by an odb')
3737
})
3838
})
3939
it('revalidates dynamic non-prerendered ISR route with fallback blocking', () => {

demos/default/netlify.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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"
1314

1415
[[headers]]
1516
for = "/_next/image/*"

demos/middleware/netlify.toml

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ 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+
69
[[plugins]]
710
package = "@netlify/plugin-nextjs"
811

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

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ command = "npm run build"
33
publish = "dist/apps/demo-monorepo/.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+
69
[dev]
710
command = "npm run start"
811
targetPort = 4200

demos/static-root/netlify.toml

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ 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+
69
[[plugins]]
710
package = "@netlify/plugin-nextjs"
811

packages/runtime/src/helpers/config.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import slash from 'slash'
88

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

11+
import { splitApiRoutes } from './flags'
12+
import type { APILambda } from './functions'
1113
import type { RoutesManifest } from './types'
1214
import { escapeStringRegexp } from './utils'
1315

@@ -17,6 +19,7 @@ type NetlifyHeaders = NetlifyConfig['headers']
1719

1820
export interface RequiredServerFiles {
1921
version?: number
22+
relativeAppDir?: string
2023
config?: NextConfigComplete
2124
appDir?: string
2225
files?: string[]
@@ -98,10 +101,14 @@ export const configureHandlerFunctions = async ({
98101
netlifyConfig,
99102
publish,
100103
ignore = [],
104+
apiLambdas,
105+
featureFlags,
101106
}: {
102107
netlifyConfig: NetlifyConfig
103108
publish: string
104109
ignore: Array<string>
110+
apiLambdas: APILambda[]
111+
featureFlags: Record<string, unknown>
105112
}) => {
106113
const config = await getRequiredServerFiles(publish)
107114
const files = config.files || []
@@ -117,7 +124,7 @@ export const configureHandlerFunctions = async ({
117124
(moduleName) => !hasManuallyAddedModule({ netlifyConfig, moduleName }),
118125
)
119126

120-
;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, '_api_*'].forEach((functionName) => {
127+
const configureFunction = (functionName: string) => {
121128
netlifyConfig.functions[functionName] ||= { included_files: [], external_node_modules: [] }
122129
netlifyConfig.functions[functionName].node_bundler = 'nft'
123130
netlifyConfig.functions[functionName].included_files ||= []
@@ -156,7 +163,22 @@ export const configureHandlerFunctions = async ({
156163
netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`)
157164
}
158165
})
159-
})
166+
}
167+
168+
configureFunction(HANDLER_FUNCTION_NAME)
169+
configureFunction(ODB_FUNCTION_NAME)
170+
171+
if (splitApiRoutes(featureFlags)) {
172+
for (const apiLambda of apiLambdas) {
173+
const { functionName, includedFiles } = apiLambda
174+
netlifyConfig.functions[functionName] ||= { included_files: [] }
175+
netlifyConfig.functions[functionName].node_bundler = 'none'
176+
netlifyConfig.functions[functionName].included_files ||= []
177+
netlifyConfig.functions[functionName].included_files.push(...includedFiles)
178+
}
179+
} else {
180+
configureFunction('_api_*')
181+
}
160182
}
161183

162184
interface BuildHeaderParams {

packages/runtime/src/helpers/files.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export const getDependenciesOfFile = async (file: string) => {
371371
if (!existsSync(nft)) {
372372
return []
373373
}
374-
const dependencies = await readJson(nft, 'utf8')
374+
const dependencies = (await readJson(nft, 'utf8')) as { files: string[] }
375375
return dependencies.files.map((dep) => resolve(dirname(file), dep))
376376
}
377377

packages/runtime/src/helpers/flags.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import destr from 'destr'
2+
3+
/**
4+
* If this flag is enabled, we generate individual Lambda functions for API Routes.
5+
* They're packed together in 50mb chunks to avoid hitting the Lambda size limit.
6+
*
7+
* To prevent bundling times from rising,
8+
* we use the "none" bundling strategy where we fully rely on Next.js' `.nft.json` files.
9+
* This should to a significant speedup, but is still experimental.
10+
*
11+
* If disabled, we bundle all API Routes into a single function.
12+
* This is can lead to large bundle sizes.
13+
*
14+
* Disabled by default. Can be overriden using the NEXT_SPLIT_API_ROUTES env var.
15+
*/
16+
export const splitApiRoutes = (featureFlags: Record<string, unknown>): boolean =>
17+
destr(process.env.NEXT_SPLIT_API_ROUTES) ?? featureFlags.next_split_api_routes ?? false

0 commit comments

Comments
 (0)