Skip to content

Commit 8b3f65b

Browse files
mrstorkpiehserhalp
authored
feat: Update to latest blob client (7.3.0) (#398)
* chore: Add buildVersion and useRegionalBlobs to PluginContext * chore: Centralize deploy store configuration * chore: Extract FixtureTestContext and BLOB_TOKEN into their own files * chore: Prepare getBlobServerGets to handle regions * chore: Set and make use of shared build/run USE_REGIONAL_BLOBS environment variable * chore: Use latest @netlify/blobs version * chore: Pin regional blob functionality to a higher version of the cli * chore: mark all runtime modules as external * fix: Ensure ts files are compiled in unit tests * chore: linting * maybe win slash? * test: add fixture using CLI before regional blobs support * test: use createRequestContext in tests instead of manually creating request context objects * Update tests/e2e/page-router.test.ts Co-authored-by: Philippe Serhal <[email protected]> * test: rename unit test for blobs directory --------- Co-authored-by: Michal Piechowiak <[email protected]> Co-authored-by: Philippe Serhal <[email protected]>
1 parent 27ab1f3 commit 8b3f65b

39 files changed

+293
-161
lines changed

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"homepage": "https://github.com/netlify/next-runtime-minimal#readme",
5050
"devDependencies": {
5151
"@fastly/http-compute-js": "1.1.4",
52-
"@netlify/blobs": "^7.0.1",
52+
"@netlify/blobs": "^7.3.0",
5353
"@netlify/build": "^29.37.2",
5454
"@netlify/edge-bundler": "^11.4.0",
5555
"@netlify/edge-functions": "^2.5.1",

src/build/content/static.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import glob from 'fast-glob'
77
import { Mock, beforeEach, describe, expect, test, vi } from 'vitest'
88

99
import { mockFileSystem } from '../../../tests/index.js'
10-
import { FixtureTestContext, createFsFixture } from '../../../tests/utils/fixture.js'
10+
import { type FixtureTestContext } from '../../../tests/utils/contexts.js'
11+
import { createFsFixture } from '../../../tests/utils/fixture.js'
1112
import { PluginContext, RequiredServerFilesManifest } from '../plugin-context.js'
1213

1314
import { copyStaticAssets, copyStaticContent } from './static.js'

src/build/functions/server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
9797
return template
9898
.replaceAll('{{cwd}}', posixJoin(ctx.lambdaWorkingDirectory))
9999
.replace('{{nextServerHandler}}', posixJoin(ctx.nextServerHandler))
100+
.replace('{{useRegionalBlobs}}', ctx.useRegionalBlobs.toString())
100101
}
101102

102103
return await readFile(join(templatesDir, 'handler.tmpl.js'), 'utf-8')

src/build/plugin-context.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,18 @@ test('nx monorepo with package path and different distDir', () => {
195195
expect(ctx.relPublishDir).toBe('dist/apps/my-app/.next')
196196
expect(ctx.publishDir).toBe(join(cwd, 'dist/apps/my-app/.next'))
197197
})
198+
199+
test('should use deploy configuration blobs directory when @netlify/build version supports regional blob awareness', () => {
200+
const { cwd } = mockFileSystem({
201+
'.next/required-server-files.json': JSON.stringify({
202+
config: { distDir: '.next' },
203+
relativeAppDir: '',
204+
} as RequiredServerFilesManifest),
205+
})
206+
207+
const ctx = new PluginContext({
208+
constants: { NETLIFY_BUILD_VERSION: '29.39.1' },
209+
} as NetlifyPluginOptions)
210+
211+
expect(ctx.blobDir).toBe(join(cwd, '.netlify/deploy/v1/blobs/deploy'))
212+
})

src/build/plugin-context.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js'
1414
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
1515
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
16+
import { satisfies } from 'semver'
1617

1718
const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
1819
const PLUGIN_DIR = join(MODULE_DIR, '../..')
@@ -135,12 +136,27 @@ export class PluginContext {
135136

136137
/**
137138
* Absolute path of the directory that will be deployed to the blob store
138-
* `.netlify/blobs/deploy`
139+
* region aware: `.netlify/deploy/v1/blobs/deploy`
140+
* default: `.netlify/blobs/deploy`
139141
*/
140142
get blobDir(): string {
143+
if (this.useRegionalBlobs) {
144+
return this.resolveFromPackagePath('.netlify/deploy/v1/blobs/deploy')
145+
}
146+
141147
return this.resolveFromPackagePath('.netlify/blobs/deploy')
142148
}
143149

150+
get buildVersion(): string {
151+
return this.constants.NETLIFY_BUILD_VERSION || 'v0.0.0'
152+
}
153+
154+
get useRegionalBlobs(): boolean {
155+
// Region-aware blobs are only available as of CLI v17.22.1 (i.e. Build v29.39.1)
156+
const REQUIRED_BUILD_VERSION = '>=29.39.1'
157+
return satisfies(this.buildVersion, REQUIRED_BUILD_VERSION, { includePrerelease: true })
158+
}
159+
144160
/**
145161
* Absolute path of the directory containing the files for the serverless lambda function
146162
* `.netlify/functions-internal`

src/build/templates/handler-monorepo.tmpl.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import tracing from '{{cwd}}/.netlify/dist/run/handlers/tracing.js'
77

88
process.chdir('{{cwd}}')
99

10+
// Set feature flag for regional blobs
11+
process.env.USE_REGIONAL_BLOBS = '{{useRegionalBlobs}}'
12+
1013
let cachedHandler
1114
export default async function (req, context) {
1215
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {

src/build/templates/handler.tmpl.js

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import serverHandler from './.netlify/dist/run/handlers/server.js'
66
import { getTracer } from './.netlify/dist/run/handlers/tracer.cjs'
77
import tracing from './.netlify/dist/run/handlers/tracing.js'
88

9+
// Set feature flag for regional blobs
10+
process.env.USE_REGIONAL_BLOBS = '{{useRegionalBlobs}}'
11+
912
export default async function handler(req, context) {
1013
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
1114
tracing.start()

src/run/handlers/cache.cts

+3-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44
import { Buffer } from 'node:buffer'
55

6-
import { getDeployStore, Store } from '@netlify/blobs'
6+
import { Store } from '@netlify/blobs'
77
import { purgeCache } from '@netlify/functions'
88
import { type Span } from '@opentelemetry/api'
99
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'
@@ -16,6 +16,7 @@ import type {
1616
NetlifyCacheHandlerValue,
1717
NetlifyIncrementalCacheValue,
1818
} from '../../shared/cache-types.cjs'
19+
import { getRegionalBlobStore } from '../regional-blob-store.cjs'
1920

2021
import { getRequestContext } from './request-context.cjs'
2122
import { getTracer } from './tracer.cjs'
@@ -24,8 +25,6 @@ type TagManifest = { revalidatedAt: number }
2425

2526
type TagManifestBlobCache = Record<string, Promise<TagManifest>>
2627

27-
const fetchBeforeNextPatchedIt = globalThis.fetch
28-
2928
export class NetlifyCacheHandler implements CacheHandler {
3029
options: CacheHandlerContext
3130
revalidatedTags: string[]
@@ -36,7 +35,7 @@ export class NetlifyCacheHandler implements CacheHandler {
3635
constructor(options: CacheHandlerContext) {
3736
this.options = options
3837
this.revalidatedTags = options.revalidatedTags
39-
this.blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt, consistency: 'strong' })
38+
this.blobStore = getRegionalBlobStore({ consistency: 'strong' })
4039
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
4140
}
4241

src/run/headers.test.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
22
import { v4 } from 'uuid'
3-
import { afterEach, describe, expect, test, vi, beforeEach } from 'vitest'
3+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
44

5-
import { FixtureTestContext } from '../../tests/utils/fixture.js'
5+
import { type FixtureTestContext } from '../../tests/utils/contexts.js'
66
import { generateRandomObjectID, startMockBlobStore } from '../../tests/utils/helpers.js'
77

8-
import { setVaryHeaders, setCacheControlHeaders } from './headers.js'
8+
import { createRequestContext } from './handlers/request-context.cjs'
9+
import { setCacheControlHeaders, setVaryHeaders } from './headers.js'
910

1011
beforeEach<FixtureTestContext>(async (ctx) => {
1112
// set for each test a new deployID and siteID
@@ -198,7 +199,7 @@ describe('headers', () => {
198199
const request = new Request(defaultUrl)
199200
vi.spyOn(headers, 'set')
200201

201-
setCacheControlHeaders(headers, request, {})
202+
setCacheControlHeaders(headers, request, createRequestContext())
202203

203204
expect(headers.set).toHaveBeenCalledTimes(0)
204205
})
@@ -208,7 +209,10 @@ describe('headers', () => {
208209
const request = new Request(defaultUrl)
209210
vi.spyOn(headers, 'set')
210211

211-
setCacheControlHeaders(headers, request, { usedFsRead: true })
212+
const requestContext = createRequestContext()
213+
requestContext.usedFsRead = true
214+
215+
setCacheControlHeaders(headers, request, requestContext)
212216

213217
expect(headers.set).toHaveBeenNthCalledWith(
214218
1,
@@ -231,7 +235,7 @@ describe('headers', () => {
231235
const request = new Request(defaultUrl)
232236
vi.spyOn(headers, 'set')
233237

234-
setCacheControlHeaders(headers, request, {})
238+
setCacheControlHeaders(headers, request, createRequestContext())
235239

236240
expect(headers.set).toHaveBeenCalledTimes(0)
237241
})
@@ -245,7 +249,7 @@ describe('headers', () => {
245249
const request = new Request(defaultUrl)
246250
vi.spyOn(headers, 'set')
247251

248-
setCacheControlHeaders(headers, request, {})
252+
setCacheControlHeaders(headers, request, createRequestContext())
249253

250254
expect(headers.set).toHaveBeenCalledTimes(0)
251255
})
@@ -258,7 +262,7 @@ describe('headers', () => {
258262
const request = new Request(defaultUrl)
259263
vi.spyOn(headers, 'set')
260264

261-
setCacheControlHeaders(headers, request, {})
265+
setCacheControlHeaders(headers, request, createRequestContext())
262266

263267
expect(headers.set).toHaveBeenNthCalledWith(
264268
1,
@@ -280,7 +284,7 @@ describe('headers', () => {
280284
const request = new Request(defaultUrl, { method: 'HEAD' })
281285
vi.spyOn(headers, 'set')
282286

283-
setCacheControlHeaders(headers, request, {})
287+
setCacheControlHeaders(headers, request, createRequestContext())
284288

285289
expect(headers.set).toHaveBeenNthCalledWith(
286290
1,
@@ -302,7 +306,7 @@ describe('headers', () => {
302306
const request = new Request(defaultUrl, { method: 'POST' })
303307
vi.spyOn(headers, 'set')
304308

305-
setCacheControlHeaders(headers, request, {})
309+
setCacheControlHeaders(headers, request, createRequestContext())
306310

307311
expect(headers.set).toHaveBeenCalledTimes(0)
308312
})
@@ -315,7 +319,7 @@ describe('headers', () => {
315319
const request = new Request(defaultUrl)
316320
vi.spyOn(headers, 'set')
317321

318-
setCacheControlHeaders(headers, request, {})
322+
setCacheControlHeaders(headers, request, createRequestContext())
319323

320324
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'public')
321325
expect(headers.set).toHaveBeenNthCalledWith(
@@ -333,7 +337,7 @@ describe('headers', () => {
333337
const request = new Request(defaultUrl)
334338
vi.spyOn(headers, 'set')
335339

336-
setCacheControlHeaders(headers, request, {})
340+
setCacheControlHeaders(headers, request, createRequestContext())
337341

338342
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'max-age=604800')
339343
expect(headers.set).toHaveBeenNthCalledWith(
@@ -351,7 +355,7 @@ describe('headers', () => {
351355
const request = new Request(defaultUrl)
352356
vi.spyOn(headers, 'set')
353357

354-
setCacheControlHeaders(headers, request, {})
358+
setCacheControlHeaders(headers, request, createRequestContext())
355359

356360
expect(headers.set).toHaveBeenNthCalledWith(
357361
1,

src/run/headers.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { getDeployStore } from '@netlify/blobs'
21
import type { Span } from '@opentelemetry/api'
32
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
43

@@ -7,6 +6,7 @@ import { encodeBlobKey } from '../shared/blobkey.js'
76
import type { TagsManifest } from './config.js'
87
import type { RequestContext } from './handlers/request-context.cjs'
98
import type { RuntimeTracer } from './handlers/tracer.cjs'
9+
import { getRegionalBlobStore } from './regional-blob-store.cjs'
1010

1111
const ALL_VARIATIONS = Symbol.for('ALL_VARIATIONS')
1212
interface NetlifyVaryValues {
@@ -121,8 +121,6 @@ export const setVaryHeaders = (
121121
headers.set(`netlify-vary`, generateNetlifyVaryValues(netlifyVaryValues))
122122
}
123123

124-
const fetchBeforeNextPatchedIt = globalThis.fetch
125-
126124
/**
127125
* Change the date header to be the last-modified date of the blob. This means the CDN
128126
* will use the correct expiry time for the response. e.g. if the blob was last modified
@@ -173,8 +171,8 @@ export const adjustDateHeader = async ({
173171
warning: true,
174172
})
175173

174+
const blobStore = getRegionalBlobStore({ consistency: 'strong' })
176175
const blobKey = await encodeBlobKey(key)
177-
const blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt, consistency: 'strong' })
178176

179177
// TODO: use metadata for this
180178
lastModified = await tracer.withActiveSpan(

src/run/next.cts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import fs from 'fs/promises'
22
import { relative, resolve } from 'path'
33

4-
import { getDeployStore } from '@netlify/blobs'
54
// @ts-expect-error no types installed
65
import { patchFs } from 'fs-monkey'
76

87
import { getRequestContext } from './handlers/request-context.cjs'
98
import { getTracer } from './handlers/tracer.cjs'
9+
import { getRegionalBlobStore } from './regional-blob-store.cjs'
1010

1111
console.time('import next server')
1212

@@ -17,8 +17,6 @@ console.timeEnd('import next server')
1717

1818
type FS = typeof import('fs')
1919

20-
const fetchBeforeNextPatchedIt = globalThis.fetch
21-
2220
export async function getMockedRequestHandlers(...args: Parameters<typeof getRequestHandlers>) {
2321
const tracer = getTracer()
2422
return tracer.withActiveSpan('mocked request handler', async () => {
@@ -35,7 +33,7 @@ export async function getMockedRequestHandlers(...args: Parameters<typeof getReq
3533
} catch (error) {
3634
// only try to get .html files from the blob store
3735
if (typeof path === 'string' && path.endsWith('.html')) {
38-
const store = getDeployStore({ fetch: fetchBeforeNextPatchedIt })
36+
const store = getRegionalBlobStore()
3937
const relPath = relative(resolve('.next/server/pages'), path)
4038
const file = await store.get(await encodeBlobKey(relPath))
4139
if (file !== null) {

src/run/regional-blob-store.cts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getDeployStore, Store } from '@netlify/blobs'
2+
3+
const fetchBeforeNextPatchedIt = globalThis.fetch
4+
5+
export const getRegionalBlobStore = (args: Parameters<typeof getDeployStore>[0] = {}): Store => {
6+
return getDeployStore({
7+
...args,
8+
fetch: fetchBeforeNextPatchedIt,
9+
experimentalRegion:
10+
process.env.USE_REGIONAL_BLOBS?.toUpperCase() === 'TRUE' ? 'context' : undefined,
11+
})
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from '@playwright/test'
2+
import { test } from '../utils/playwright-helpers.js'
3+
4+
test('should serve 404 page when requesting non existing page (no matching route) if site is deployed with CLI not supporting regional blobs', async ({
5+
page,
6+
cliBeforeRegionalBlobsSupport,
7+
}) => {
8+
// 404 page is built and uploaded to blobs at build time
9+
// when Next.js serves 404 it will try to fetch it from the blob store
10+
// if request handler function is unable to get from blob store it will
11+
// fail request handling and serve 500 error.
12+
// This implicitly tests that request handler function is able to read blobs
13+
// that are uploaded as part of site deploy.
14+
15+
const response = await page.goto(new URL('non-existing', cliBeforeRegionalBlobsSupport.url).href)
16+
const headers = response?.headers() || {}
17+
expect(response?.status()).toBe(404)
18+
19+
expect(await page.textContent('h1')).toBe('404')
20+
21+
expect(headers['netlify-cdn-cache-control']).toBe(
22+
'no-cache, no-store, max-age=0, must-revalidate',
23+
)
24+
expect(headers['cache-control']).toBe('no-cache,no-store,max-age=0,must-revalidate')
25+
})

0 commit comments

Comments
 (0)