Skip to content

Commit 1b6c98d

Browse files
committed
Merge branch 'main' into tn/blob
2 parents 31e4078 + 7904318 commit 1b6c98d

File tree

7 files changed

+116
-95
lines changed

7 files changed

+116
-95
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
{
2222
files: ['src/handlers/**'],
2323
rules: {
24+
'max-statements': ['error', 30],
2425
'import/no-anonymous-default-export': 'off',
2526
},
2627
},

src/handlers/server.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@
22
/* eslint-disable max-statements */
33
import { toComputeResponse, toReqRes } from '@fastly/http-compute-js'
44
import { HandlerEvent, type Handler, HandlerContext } from "@netlify/functions"
5+
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
56
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
67

78
import { netliBlob } from '../helpers/blobs/blobs.cjs'
8-
import { TASK_DIR } from '../helpers/constants.js'
9+
import { RUN_DIR } from '../helpers/constants.js'
10+
import { setCacheControlHeaders, setVaryHeaders } from '../helpers/headers.js'
911

10-
let nextHandler: WorkerRequestHandler
12+
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
1113

1214
export default async (request: Request) => {
1315
if (!nextHandler) {
1416
// set the server config
15-
const { setRequestConfig } = await import('../helpers/config.js')
16-
await setRequestConfig()
17+
const { getRunConfig, setRunConfig } = await import('../helpers/config.js')
18+
nextConfig = await getRunConfig()
19+
setRunConfig(nextConfig)
1720

1821
// let Next.js initialize and create the request handler
1922
const { getRequestHandlers } = await import('next/dist/server/lib/start-server.js')
2023
;[nextHandler] = await getRequestHandlers({
2124
port: 3000,
2225
hostname: 'localhost',
23-
dir: TASK_DIR,
26+
dir: RUN_DIR,
2427
isDev: false,
2528
})
2629
}
@@ -40,6 +43,9 @@ export default async (request: Request) => {
4043
const response = { headers: res.getHeaders(), statusCode: res.statusCode }
4144
console.log('Next server response:', JSON.stringify(response, null, 2))
4245

46+
setCacheControlHeaders(res)
47+
setVaryHeaders(res, req, nextConfig)
48+
4349
return toComputeResponse(res)
4450
}
4551
// Commenting out for now

src/helpers/config.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
import { readFile } from 'node:fs/promises'
22

3-
import { TASK_DIR } from './constants.js'
3+
import { NextConfigComplete } from 'next/dist/server/config-shared.js'
4+
5+
import { RUN_DIR } from './constants.js'
46

57
/**
6-
* Enable standalone mode at build-time
8+
* Enable Next.js standalone mode at build time
79
*/
810
export const setBuildConfig = () => {
911
process.env.NEXT_PRIVATE_STANDALONE = 'true'
1012
}
1113

1214
/**
13-
* Configure the request-time custom cache handler
15+
* Get Next.js config from the build output
1416
*/
15-
export const setRequestConfig = async () => {
17+
export const getRunConfig = async () => {
1618
// get config from the build output
17-
const runtimeConfig = JSON.parse(await readFile(`${TASK_DIR}/.next/required-server-files.json`, 'utf-8'))
19+
return JSON.parse(await readFile(`${RUN_DIR}/.next/required-server-files.json`, 'utf-8')).config
20+
}
1821

22+
/**
23+
* Configure the custom cache handler at request time
24+
*/
25+
export const setRunConfig = (config: NextConfigComplete) => {
1926
// set the path to the cache handler
20-
runtimeConfig.config.experimental = {
21-
...runtimeConfig.config.experimental,
22-
incrementalCacheHandlerPath: `${TASK_DIR}/dist/handlers/cache.cjs`,
27+
config.experimental = {
28+
...config.experimental,
29+
incrementalCacheHandlerPath: `${RUN_DIR}/dist/handlers/cache.cjs`,
2330
}
2431

2532
// set config
26-
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(runtimeConfig.config)
33+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config)
2734
}

src/helpers/constants.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import { fileURLToPath } from 'node:url'
33

44
export const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
55
export const PLUGIN_DIR = resolve(`${MODULE_DIR}../..`)
6-
export const TASK_DIR = process.cwd()
6+
export const WORKING_DIR = process.cwd()
77

8-
export const BUILD_DIR = `${TASK_DIR}/.netlify/.next`
8+
export const BUILD_DIR = `${WORKING_DIR}/.netlify`
9+
export const RUN_DIR = WORKING_DIR
910
export const SERVER_DIR = `${BUILD_DIR}/server`
1011
export const STANDALONE_BUILD_DIR = `${BUILD_DIR}/standalone`
1112
export const FETCH_CACHE_DIR = `${BUILD_DIR}/cache/fetch-cache`
1213

13-
export const FUNCTIONS_DIR = `${TASK_DIR}/.netlify/functions-internal`
14-
export const EDGE_FUNCTIONS_DIR = `${TASK_DIR}/.netlify/edge-functions`
15-
14+
export const SERVER_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/functions-internal`
1615
export const SERVER_HANDLER_NAME = '___netlify-server-handler'
17-
export const SERVER_HANDLER_DIR = `${FUNCTIONS_DIR}/${SERVER_HANDLER_NAME}`
16+
export const SERVER_HANDLER_DIR = `${SERVER_FUNCTIONS_DIR}/${SERVER_HANDLER_NAME}`
17+
18+
export const EDGE_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/edge-functions`
19+
export const EDGE_HANDLER_NAME = '___netlify-edge-handler'
20+
export const EDGE_HANDLER_DIR = `${EDGE_FUNCTIONS_DIR}/${EDGE_HANDLER_NAME}`

src/helpers/files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { BUILD_DIR, STANDALONE_BUILD_DIR } from './constants.js'
1616
* Move the Next.js build output from the publish dir to a temp dir
1717
*/
1818
export const stashBuildOutput = async ({ PUBLISH_DIR }: NetlifyPluginConstants) => {
19-
await move(PUBLISH_DIR, BUILD_DIR, { overwrite: true })
19+
await move(PUBLISH_DIR, `${BUILD_DIR}/.next`, { overwrite: true })
2020

2121
// remove prerendered content from the standalone build (it's also in the main build dir)
2222
const prerenderedContent = await getPrerenderedContent(STANDALONE_BUILD_DIR)
@@ -88,6 +88,6 @@ export const storePrerenderedContent = async ({ NETLIFY_API_TOKEN, SITE_ID }:
8888
export const publishStaticAssets = ({ PUBLISH_DIR }: NetlifyPluginConstants) => {
8989
return Promise.all([
9090
copy('public', PUBLISH_DIR),
91-
copy(`${BUILD_DIR}/static/`, `${PUBLISH_DIR}/_next/static`),
91+
copy(`${BUILD_DIR}/.next/static/`, `${PUBLISH_DIR}/_next/static`),
9292
])
9393
}

src/helpers/functions.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { writeFile } from 'fs/promises'
33
import { nodeFileTrace } from '@vercel/nft'
44
import { copy, emptyDir, readJson, writeJSON } from 'fs-extra/esm'
55

6-
import { BUILD_DIR, SERVER_HANDLER_DIR, SERVER_HANDLER_NAME, PLUGIN_DIR } from './constants.js'
6+
import {
7+
BUILD_DIR,
8+
EDGE_HANDLER_DIR,
9+
PLUGIN_DIR,
10+
SERVER_HANDLER_DIR,
11+
SERVER_HANDLER_NAME,
12+
} from './constants.js'
713

814
const pkg = await readJson(`${PLUGIN_DIR}/package.json`)
915

@@ -26,8 +32,8 @@ export const createServerHandler = async () => {
2632
)
2733

2834
// copy the next.js standalone build output to the handler directory
29-
await copy(`${BUILD_DIR}/standalone/.next`, `${SERVER_HANDLER_DIR}/.next`)
30-
await copy(`${BUILD_DIR}/standalone/node_modules`, `${SERVER_HANDLER_DIR}/node_modules`)
35+
await copy(`${BUILD_DIR}/.next/standalone/.next`, `${SERVER_HANDLER_DIR}/.next`)
36+
await copy(`${BUILD_DIR}/.next/standalone/node_modules`, `${SERVER_HANDLER_DIR}/node_modules`)
3137

3238
// create the handler metadata file
3339
await writeJSON(`${SERVER_HANDLER_DIR}/${SERVER_HANDLER_NAME}.json`, {
@@ -61,5 +67,6 @@ export const createServerHandler = async () => {
6167
* Create a Netlify edge function to run the Next.js server
6268
*/
6369
export const createEdgeHandler = async () => {
64-
// TODO: implement
70+
// reset the handler directory
71+
await emptyDir(EDGE_HANDLER_DIR)
6572
}

src/helpers/headers.ts

Lines changed: 66 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,94 @@
1-
import type { HandlerEvent } from '@netlify/functions'
1+
import type { IncomingMessage, ServerResponse } from 'http'
2+
23
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
34

4-
export interface NetlifyVaryHeaderBuilder {
5+
type HeaderValue = number | string | string[]
6+
7+
interface NetlifyVaryDirectives {
58
headers: string[]
69
languages: string[]
710
cookies: string[]
811
}
912

10-
const generateNetlifyVaryHeaderValue = ({ headers, languages, cookies }: NetlifyVaryHeaderBuilder): string => {
11-
let NetlifyVaryHeader = ``
12-
if (headers && headers.length !== 0) {
13-
NetlifyVaryHeader += `header=${headers.join(`|`)}`
13+
const generateNetlifyVaryDirectives = ({
14+
headers,
15+
languages,
16+
cookies,
17+
}: NetlifyVaryDirectives): string[] => {
18+
const directives = []
19+
if (headers.length !== 0) {
20+
directives.push(`header=${headers.join(`|`)}`)
1421
}
15-
if (languages && languages.length !== 0) {
16-
if (NetlifyVaryHeader.length !== 0) {
17-
NetlifyVaryHeader += `,`
18-
}
19-
NetlifyVaryHeader += `language=${languages.join(`|`)}`
22+
if (languages.length !== 0) {
23+
directives.push(`language=${languages.join(`|`)}`)
2024
}
21-
if (cookies && cookies.length !== 0) {
22-
if (NetlifyVaryHeader.length !== 0) {
23-
NetlifyVaryHeader += `,`
24-
}
25-
NetlifyVaryHeader += `cookie=${cookies.join(`|`)}`
25+
if (cookies.length !== 0) {
26+
directives.push(`cookie=${cookies.join(`|`)}`)
2627
}
27-
28-
return NetlifyVaryHeader
28+
return directives
2929
}
3030

31-
const getDirectives = (headerValue: string): string[] => headerValue.split(',').map((directive) => directive.trim())
32-
33-
const removeSMaxAgeAndStaleWhileRevalidate = (headerValue: string): string =>
34-
getDirectives(headerValue)
35-
.filter((directive) => {
36-
if (directive.startsWith('s-maxage')) {
37-
return false
38-
}
39-
if (directive.startsWith('stale-while-revalidate')) {
40-
return false
41-
}
42-
return true
43-
})
44-
.join(`,`)
45-
46-
export const handleVary = (headers: Record<string, string>, event: HandlerEvent, nextConfig: NextConfigComplete) => {
47-
const netlifyVaryBuilder: NetlifyVaryHeaderBuilder = {
31+
/**
32+
* Parse a header value into an array of directives
33+
*/
34+
const getDirectives = (headerValue: HeaderValue): string[] => {
35+
const directives = Array.isArray(headerValue) ? headerValue : String(headerValue).split(',')
36+
return directives.map((directive) => directive.trim())
37+
}
38+
/**
39+
* Ensure the Netlify CDN varies on things that Next.js varies on,
40+
* e.g. i18n, preview mode, etc.
41+
*/
42+
export const setVaryHeaders = (
43+
res: ServerResponse,
44+
req: IncomingMessage,
45+
{ basePath, i18n }: NextConfigComplete,
46+
) => {
47+
const netlifyVaryDirectives: NetlifyVaryDirectives = {
4848
headers: [],
4949
languages: [],
5050
cookies: ['__prerender_bypass', '__next_preview_data'],
5151
}
5252

53-
if (headers.vary.length !== 0) {
54-
netlifyVaryBuilder.headers.push(...getDirectives(headers.vary))
53+
const vary = res.getHeader('vary')
54+
if (vary !== undefined) {
55+
netlifyVaryDirectives.headers.push(...getDirectives(vary))
5556
}
5657

57-
const autoDetectedLocales = getAutoDetectedLocales(nextConfig)
58-
59-
if (autoDetectedLocales.length > 1) {
60-
const logicalPath =
61-
nextConfig.basePath && event.path.startsWith(nextConfig.basePath)
62-
? event.path.slice(nextConfig.basePath.length)
63-
: event.path
58+
const path = new URL(req.url ?? '/', `http://${req.headers.host}`).pathname
59+
const locales = i18n && i18n.localeDetection !== false ? i18n.locales : []
6460

61+
if (locales.length > 1) {
62+
const logicalPath = basePath && path.startsWith(basePath) ? path.slice(basePath.length) : path
6563
if (logicalPath === `/`) {
66-
netlifyVaryBuilder.languages.push(...autoDetectedLocales)
67-
netlifyVaryBuilder.cookies.push(`NEXT_LOCALE`)
64+
netlifyVaryDirectives.languages.push(...locales)
65+
netlifyVaryDirectives.cookies.push(`NEXT_LOCALE`)
6866
}
6967
}
7068

71-
const NetlifyVaryHeader = generateNetlifyVaryHeaderValue(netlifyVaryBuilder)
72-
if (NetlifyVaryHeader.length !== 0) {
73-
headers[`netlify-vary`] = NetlifyVaryHeader
74-
}
75-
}
76-
77-
export const handleCacheControl = (headers: Record<string, string>) => {
78-
if (headers['cache-control'] && !headers['cdn-cache-control'] && !headers['netlify-cdn-cache-control']) {
79-
headers['netlify-cdn-cache-control'] = headers['cache-control']
80-
81-
const filteredCacheControlDirectives = removeSMaxAgeAndStaleWhileRevalidate(headers['cache-control'])
82-
83-
// use default cache-control if no directives are left
84-
headers['cache-control'] =
85-
filteredCacheControlDirectives.length === 0
86-
? 'public, max-age=0, must-revalidate'
87-
: filteredCacheControlDirectives
88-
}
69+
res.setHeader(`netlify-vary`, generateNetlifyVaryDirectives(netlifyVaryDirectives))
8970
}
9071

91-
export const getAutoDetectedLocales = (config: NextConfigComplete): Array<string> => {
92-
if (config.i18n && config.i18n.localeDetection !== false && config.i18n.locales.length > 1) {
93-
return config.i18n.locales
72+
/**
73+
* Ensure stale-while-revalidate and s-maxage don't leak to the client, but
74+
* assume the user knows what they are doing if CDN cache controls are set
75+
*/
76+
export const setCacheControlHeaders = (res: ServerResponse) => {
77+
const cacheControl = res.getHeader('cache-control')
78+
if (
79+
cacheControl !== undefined &&
80+
!res.hasHeader('cdn-cache-control') &&
81+
!res.hasHeader('netlify-cdn-cache-control')
82+
) {
83+
const directives = getDirectives(cacheControl).filter(
84+
(directive) =>
85+
!directive.startsWith('s-maxage') && !directive.startsWith('stale-while-revalidate'),
86+
)
87+
88+
res.setHeader('netlify-cdn-cache-control', cacheControl)
89+
res.setHeader(
90+
'cache-control',
91+
directives.length === 0 ? 'public, max-age=0, must-revalidate' : directives,
92+
)
9493
}
95-
96-
return []
9794
}

0 commit comments

Comments
 (0)