Skip to content

Commit 06bc4f3

Browse files
committed
Merge branch 'main' into tn/blob
2 parents c6c46b4 + a0e594c commit 06bc4f3

File tree

6 files changed

+106
-68
lines changed

6 files changed

+106
-68
lines changed

package-lock.json

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
},
3535
"homepage": "https://github.com/netlify/next-runtime-minimal#readme",
3636
"dependencies": {
37-
"@fastly/http-compute-js": "github:orinokai/http-compute-js",
38-
"@netlify/blobs": "^3.0.0",
37+
"@fastly/http-compute-js": "1.1.1",
38+
"@netlify/blobs": "^3.2.0",
3939
"@netlify/build": "^29.20.6",
4040
"@netlify/functions": "^2.0.1",
4141
"@vercel/nft": "^0.24.3",

src/handlers/cache.cts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
import { getStore } from '@netlify/blobs'
2+
import { purgeCache } from '@netlify/functions'
13
import type { CacheHandler, CacheHandlerContext } from 'next/dist/server/lib/incremental-cache/index.js'
24

5+
type TagManifest = { revalidatedAt: number }
6+
7+
const tagsManifestPath = '_netlify-cache/tags'
8+
const blobStore = getStore('TODO')
9+
310
/**
411
* Netlify Cache Handler
512
* (CJS format because Next.js doesn't support ESM yet)
613
*/
714
export default class NetlifyCacheHandler implements CacheHandler {
815
options: CacheHandlerContext
16+
revalidatedTags: string[]
917

1018
constructor(options: CacheHandlerContext) {
1119
this.options = options
20+
this.revalidatedTags = options.revalidatedTags
1221
}
1322

1423
// eslint-disable-next-line require-await, class-methods-use-this
@@ -25,5 +34,30 @@ export default class NetlifyCacheHandler implements CacheHandler {
2534
// eslint-disable-next-line class-methods-use-this, require-await
2635
public async revalidateTag(tag: string) {
2736
console.log('NetlifyCacheHandler.revalidateTag', tag)
37+
38+
const data: TagManifest = {
39+
revalidatedAt: Date.now()
40+
}
41+
42+
try {
43+
blobStore.setJSON(this.tagManifestPath(tag), data)
44+
} catch (error: any) {
45+
console.warn(`Failed to update tag manifest for ${tag}`, error)
46+
}
47+
48+
purgeCache({ tags: [tag] })
49+
}
50+
51+
private async loadTagManifest(tag: string) {
52+
try {
53+
return await blobStore.get(this.tagManifestPath(tag), {type: 'json'})
54+
} catch (error: any) {
55+
console.warn(`Failed to fetch tag manifest for ${tag}`, error)
56+
}
57+
}
58+
59+
// eslint-disable-next-line class-methods-use-this
60+
private tagManifestPath(tag: string) {
61+
return [tagsManifestPath, tag].join('/')
2862
}
2963
}

src/handlers/server.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable n/prefer-global/buffer */
22
import { toComputeResponse, toReqRes } from '@fastly/http-compute-js'
3+
import type { HeadersSentEvent } from '@fastly/http-compute-js/dist/http-compute-js/http-outgoing.js'
34
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
45
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
56

67
import { RUN_DIR } from '../helpers/constants.js'
7-
import { setCacheControlHeaders, setVaryHeaders } from '../helpers/headers.js'
8+
import { setCacheControlHeaders, setCacheTagsHeaders, setVaryHeaders } from '../helpers/headers.js'
89

910
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
1011

@@ -27,6 +28,15 @@ export default async (request: Request) => {
2728

2829
const { req, res } = toReqRes(request)
2930

31+
res.prependListener('_headersSent', (event: HeadersSentEvent) => {
32+
const headers = new Headers(event.headers)
33+
setCacheControlHeaders(headers)
34+
setCacheTagsHeaders(headers)
35+
setVaryHeaders(headers, request, nextConfig)
36+
event.headers = Object.fromEntries(headers.entries())
37+
console.log('Modified response headers:', JSON.stringify(event.headers, null, 2))
38+
})
39+
3040
try {
3141
console.log('Next server request:', req.url)
3242
await nextHandler(req, res)
@@ -40,8 +50,5 @@ export default async (request: Request) => {
4050
const response = { headers: res.getHeaders(), statusCode: res.statusCode }
4151
console.log('Next server response:', JSON.stringify(response, null, 2))
4252

43-
setCacheControlHeaders(res)
44-
setVaryHeaders(res, req, nextConfig)
45-
4653
return toComputeResponse(res)
4754
}

src/helpers/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ export const WORKING_DIR = process.cwd()
88
export const BUILD_DIR = `${WORKING_DIR}/.netlify`
99
export const RUN_DIR = WORKING_DIR
1010

11-
export const SERVER_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/functions-internal`
11+
export const SERVER_FUNCTIONS_DIR = `${BUILD_DIR}/functions-internal`
1212
export const SERVER_HANDLER_NAME = '___netlify-server-handler'
1313
export const SERVER_HANDLER_DIR = `${SERVER_FUNCTIONS_DIR}/${SERVER_HANDLER_NAME}`
1414

15-
export const EDGE_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/edge-functions`
15+
export const EDGE_FUNCTIONS_DIR = `${BUILD_DIR}/edge-functions`
1616
export const EDGE_HANDLER_NAME = '___netlify-edge-handler'
1717
export const EDGE_HANDLER_DIR = `${EDGE_FUNCTIONS_DIR}/${EDGE_HANDLER_NAME}`

src/helpers/headers.ts

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,91 @@
1-
import type { IncomingMessage, ServerResponse } from 'http'
2-
31
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
42

5-
type HeaderValue = number | string | string[]
6-
7-
interface NetlifyVaryDirectives {
3+
interface NetlifyVaryValues {
84
headers: string[]
95
languages: string[]
106
cookies: string[]
117
}
128

13-
const generateNetlifyVaryDirectives = ({
14-
headers,
15-
languages,
16-
cookies,
17-
}: NetlifyVaryDirectives): string[] => {
18-
const directives = []
9+
const generateNetlifyVaryValues = ({ headers, languages, cookies }: NetlifyVaryValues): string => {
10+
const values = []
1911
if (headers.length !== 0) {
20-
directives.push(`header=${headers.join(`|`)}`)
12+
values.push(`header=${headers.join(`|`)}`)
2113
}
2214
if (languages.length !== 0) {
23-
directives.push(`language=${languages.join(`|`)}`)
15+
values.push(`language=${languages.join(`|`)}`)
2416
}
2517
if (cookies.length !== 0) {
26-
directives.push(`cookie=${cookies.join(`|`)}`)
18+
values.push(`cookie=${cookies.join(`|`)}`)
2719
}
28-
return directives
20+
return values.join(',')
2921
}
3022

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())
23+
const getHeaderValueArray = (header: string): string[] => {
24+
return header.split(',').map((value) => value.trim())
25+
}
26+
27+
const removeHeaderValues = (header: string, values: string[]): string => {
28+
const headerValues = getHeaderValueArray(header)
29+
const filteredValues = headerValues.filter(
30+
(value) => !values.some((val) => value.startsWith(val)),
31+
)
32+
return filteredValues.join(', ')
3733
}
34+
3835
/**
3936
* Ensure the Netlify CDN varies on things that Next.js varies on,
4037
* e.g. i18n, preview mode, etc.
4138
*/
4239
export const setVaryHeaders = (
43-
res: ServerResponse,
44-
req: IncomingMessage,
40+
headers: Headers,
41+
request: Request,
4542
{ basePath, i18n }: NextConfigComplete,
4643
) => {
47-
const netlifyVaryDirectives: NetlifyVaryDirectives = {
44+
const netlifyVaryValues: NetlifyVaryValues = {
4845
headers: [],
4946
languages: [],
5047
cookies: ['__prerender_bypass', '__next_preview_data'],
5148
}
5249

53-
const vary = res.getHeader('vary')
54-
if (vary !== undefined) {
55-
netlifyVaryDirectives.headers.push(...getDirectives(vary))
50+
const vary = headers.get('vary')
51+
if (vary !== null) {
52+
netlifyVaryValues.headers.push(...getHeaderValueArray(vary))
5653
}
5754

58-
const path = new URL(req.url ?? '/', `http://${req.headers.host}`).pathname
55+
const path = new URL(request.url).pathname
5956
const locales = i18n && i18n.localeDetection !== false ? i18n.locales : []
6057

6158
if (locales.length > 1) {
6259
const logicalPath = basePath && path.startsWith(basePath) ? path.slice(basePath.length) : path
6360
if (logicalPath === `/`) {
64-
netlifyVaryDirectives.languages.push(...locales)
65-
netlifyVaryDirectives.cookies.push(`NEXT_LOCALE`)
61+
netlifyVaryValues.languages.push(...locales)
62+
netlifyVaryValues.cookies.push(`NEXT_LOCALE`)
6663
}
6764
}
6865

69-
res.setHeader(`netlify-vary`, generateNetlifyVaryDirectives(netlifyVaryDirectives))
66+
headers.set(`netlify-vary`, generateNetlifyVaryValues(netlifyVaryValues))
7067
}
7168

7269
/**
7370
* Ensure stale-while-revalidate and s-maxage don't leak to the client, but
7471
* assume the user knows what they are doing if CDN cache controls are set
7572
*/
76-
export const setCacheControlHeaders = (res: ServerResponse) => {
77-
const cacheControl = res.getHeader('cache-control')
73+
export const setCacheControlHeaders = (headers: Headers) => {
74+
const cacheControl = headers.get('cache-control')
7875
if (
79-
cacheControl !== undefined &&
80-
!res.hasHeader('cdn-cache-control') &&
81-
!res.hasHeader('netlify-cdn-cache-control')
76+
cacheControl !== null &&
77+
!headers.has('cdn-cache-control') &&
78+
!headers.has('netlify-cdn-cache-control')
8279
) {
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-
)
80+
const clientCacheControl = removeHeaderValues(cacheControl, [
81+
's-maxage',
82+
'stale-while-revalidate',
83+
])
84+
headers.set('cache-control', clientCacheControl || 'public, max-age=0, must-revalidate')
85+
headers.set('netlify-cdn-cache-control', cacheControl)
9386
}
9487
}
88+
89+
export const setCacheTagsHeaders = (headers: Headers) => {
90+
// TODO: implement
91+
}

0 commit comments

Comments
 (0)