Skip to content

Commit 8b3f65b

Browse files
mrstorkpiehserhalp
authoredApr 22, 2024
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 <philippe.serhal@gmail.com> * test: rename unit test for blobs directory --------- Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com> Co-authored-by: Philippe Serhal <philippe.serhal@gmail.com>
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+
})

‎tests/e2e/page-router.test.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,17 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
230230
expect(data3?.pageProps?.time).toBe(date3)
231231
})
232232

233-
test('requesting a non existing page route that needs to be fetched from the blob store like 404.html', async ({
233+
test('should serve 404 page when requesting non existing page (no matching route)', async ({
234234
page,
235235
pageRouter,
236236
}) => {
237+
// 404 page is built and uploaded to blobs at build time
238+
// when Next.js serves 404 it will try to fetch it from the blob store
239+
// if request handler function is unable to get from blob store it will
240+
// fail request handling and serve 500 error.
241+
// This implicitly tests that request handler function is able to read blobs
242+
// that are uploaded as part of site deploy.
243+
237244
const response = await page.goto(new URL('non-existing', pageRouter.url).href)
238245
const headers = response?.headers() || {}
239246
expect(response?.status()).toBe(404)
@@ -246,7 +253,7 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
246253
expect(headers['cache-control']).toBe('no-cache,no-store,max-age=0,must-revalidate')
247254
})
248255

249-
test('requesting a non existing page route that needs to be fetched from the blob store like 404.html (notFound: true)', async ({
256+
test('should serve 404 page when requesting non existing page (marked with notFound: true in getStaticProps)', async ({
250257
page,
251258
pageRouter,
252259
}) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
eslint: {
4+
ignoreDuringBuilds: true,
5+
},
6+
}
7+
8+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "old-cli",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"postinstall": "next build",
7+
"dev": "next dev",
8+
"build": "next build"
9+
},
10+
"dependencies": {
11+
"next": "latest",
12+
"netlify-cli": "17.21.1",
13+
"react": "^18.2.0",
14+
"react-dom": "^18.2.0"
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function Home({ ssr }) {
2+
return (
3+
<main>
4+
<div data-testid="smoke">SSR: {ssr ? 'yes' : 'no'}</div>
5+
</main>
6+
)
7+
}
8+
9+
export const getServerSideProps = async () => {
10+
return {
11+
props: {
12+
ssr: true,
13+
},
14+
}
15+
}

‎tests/integration/build/copy-next-code.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { join } from 'node:path'
33
import { NetlifyPluginOptions } from '@netlify/build'
44
import { expect, test } from 'vitest'
55

6+
import { readFile, readdir } from 'node:fs/promises'
67
import { copyNextServerCode } from '../../../src/build/content/server.js'
78
import { PluginContext, RequiredServerFilesManifest } from '../../../src/build/plugin-context.js'
8-
import { FixtureTestContext, createFsFixture } from '../../utils/fixture.js'
9-
import { readFile, readdir } from 'node:fs/promises'
9+
import { type FixtureTestContext } from '../../utils/contexts.js'
10+
import { createFsFixture } from '../../utils/fixture.js'
1011

1112
test<FixtureTestContext>('should copy the next standalone folder correctly for a simple site', async (ctx) => {
1213
const reqServerFiles = JSON.stringify({

‎tests/integration/cache-handler.test.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import { load } from 'cheerio'
22
import { getLogger } from 'lambda-local'
33
import { v4 } from 'uuid'
44
import { beforeEach, describe, expect, test, vi } from 'vitest'
5-
import {
6-
createFixture,
7-
invokeFunction,
8-
runPlugin,
9-
type FixtureTestContext,
10-
} from '../utils/fixture.js'
5+
import { type FixtureTestContext } from '../utils/contexts.js'
6+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
117
import {
128
countOfBlobServerGetsForKey,
139
decodeBlobKey,

‎tests/integration/edge-handler.test.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { v4 } from 'uuid'
22
import { beforeEach, describe, expect, test, vi } from 'vitest'
3-
import {
4-
createFixture,
5-
invokeEdgeFunction,
6-
runPlugin,
7-
type FixtureTestContext,
8-
} from '../utils/fixture.js'
3+
import { type FixtureTestContext } from '../utils/contexts.js'
4+
import { createFixture, invokeEdgeFunction, runPlugin } from '../utils/fixture.js'
95
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
106
import { LocalServer } from '../utils/local-server.js'
117

‎tests/integration/fetch-handler.test.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,8 @@ import { HttpResponse, http, passthrough } from 'msw'
44
import { setupServer } from 'msw/node'
55
import { v4 } from 'uuid'
66
import { afterAll, beforeAll, beforeEach, expect, test, vi } from 'vitest'
7-
import {
8-
createFixture,
9-
invokeFunction,
10-
runPlugin,
11-
runPluginStep,
12-
type FixtureTestContext,
13-
} from '../utils/fixture.js'
7+
import { type FixtureTestContext } from '../utils/contexts.js'
8+
import { createFixture, invokeFunction, runPlugin, runPluginStep } from '../utils/fixture.js'
149
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
1510

1611
// Disable the verbose logging of the lambda-local runtime

‎tests/integration/page-router.test.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@ import { load } from 'cheerio'
22
import { getLogger } from 'lambda-local'
33
import { HttpResponse, http, passthrough } from 'msw'
44
import { setupServer } from 'msw/node'
5+
import { platform } from 'node:process'
56
import { v4 } from 'uuid'
67
import { afterAll, afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest'
7-
import {
8-
createFixture,
9-
invokeFunction,
10-
runPlugin,
11-
type FixtureTestContext,
12-
} from '../utils/fixture.js'
8+
import { type FixtureTestContext } from '../utils/contexts.js'
9+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
1310
import { encodeBlobKey, generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
14-
import { platform } from 'node:process'
1511

1612
// Disable the verbose logging of the lambda-local runtime
1713
getLogger().level = 'alert'

‎tests/integration/pnpm.test.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { load } from 'cheerio'
22
import { getLogger } from 'lambda-local'
3+
import { platform } from 'node:process'
34
import { v4 } from 'uuid'
45
import { beforeEach, expect, test, vi } from 'vitest'
5-
import {
6-
createFixture,
7-
invokeFunction,
8-
runPlugin,
9-
type FixtureTestContext,
10-
} from '../utils/fixture.js'
6+
import { type FixtureTestContext } from '../utils/contexts.js'
7+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
118
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
12-
import { platform } from 'node:process'
139

1410
// Disable the verbose logging of the lambda-local runtime
1511
getLogger().level = 'alert'

‎tests/integration/request-context.test.ts

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { getLogger } from 'lambda-local'
2+
import cjsMock from 'mock-require'
3+
import type { CacheHandler } from 'next/dist/server/lib/incremental-cache/index.js'
4+
import { realpathSync } from 'node:fs'
5+
import { join } from 'node:path'
26
import { v4 } from 'uuid'
37
import { beforeEach, describe, expect, test, vi } from 'vitest'
4-
import {
5-
createFixture,
6-
invokeFunction,
7-
runPlugin,
8-
type FixtureTestContext,
9-
} from '../utils/fixture.js'
8+
import { SERVER_HANDLER_NAME } from '../../src/build/plugin-context.js'
9+
import { type FixtureTestContext } from '../utils/contexts.js'
10+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
1011
import {
1112
countOfBlobServerGetsForKey,
1213
encodeBlobKey,
1314
generateRandomObjectID,
1415
startMockBlobStore,
1516
} from '../utils/helpers.js'
16-
import cjsMock from 'mock-require'
17-
import type { CacheHandler } from 'next/dist/server/lib/incremental-cache/index.js'
18-
import { SERVER_HANDLER_NAME } from '../../src/build/plugin-context.js'
19-
import { realpathSync } from 'node:fs'
20-
import { join } from 'node:path'
2117

2218
// Disable the verbose logging of the lambda-local runtime
2319
getLogger().level = 'alert'

‎tests/integration/revalidate-path.test.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import { load } from 'cheerio'
22
import { getLogger } from 'lambda-local'
33
import { v4 } from 'uuid'
44
import { beforeEach, expect, test, vi } from 'vitest'
5-
import {
6-
createFixture,
7-
invokeFunction,
8-
runPlugin,
9-
type FixtureTestContext,
10-
} from '../utils/fixture.js'
5+
import { type FixtureTestContext } from '../utils/contexts.js'
6+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
117
import {
128
encodeBlobKey,
139
generateRandomObjectID,

‎tests/integration/revalidate-tags.test.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import { load } from 'cheerio'
22
import { getLogger } from 'lambda-local'
33
import { v4 } from 'uuid'
44
import { beforeEach, expect, test, vi } from 'vitest'
5-
import {
6-
createFixture,
7-
invokeFunction,
8-
runPlugin,
9-
type FixtureTestContext,
10-
} from '../utils/fixture.js'
5+
import { type FixtureTestContext } from '../utils/contexts.js'
6+
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
117
import {
128
encodeBlobKey,
139
generateRandomObjectID,

‎tests/integration/simple-app.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import { gunzipSync } from 'node:zlib'
77
import { gt, prerelease } from 'semver'
88
import { v4 } from 'uuid'
99
import { Mock, afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
10+
import { getPatchesToApply } from '../../src/build/content/server.js'
11+
import { type FixtureTestContext } from '../utils/contexts.js'
1012
import {
1113
createFixture,
14+
getFixtureSourceDirectory,
1215
invokeFunction,
1316
runPlugin,
14-
getFixtureSourceDirectory,
15-
type FixtureTestContext,
1617
} from '../utils/fixture.js'
1718
import {
1819
decodeBlobKey,
1920
generateRandomObjectID,
2021
getBlobEntries,
2122
startMockBlobStore,
2223
} from '../utils/helpers.js'
23-
import { getPatchesToApply } from '../../src/build/content/server.js'
2424

2525
const mockedCp = cp as Mock<
2626
Parameters<(typeof import('node:fs/promises'))['cp']>,

‎tests/integration/static.test.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@ import { existsSync } from 'node:fs'
55
import { join } from 'node:path'
66
import { v4 } from 'uuid'
77
import { beforeEach, expect, test, vi } from 'vitest'
8-
import {
9-
createFixture,
10-
invokeFunction,
11-
runPlugin,
12-
runPluginStep,
13-
type FixtureTestContext,
14-
} from '../utils/fixture.js'
8+
import { type FixtureTestContext } from '../utils/contexts.js'
9+
import { createFixture, invokeFunction, runPlugin, runPluginStep } from '../utils/fixture.js'
1510
import {
1611
decodeBlobKey,
1712
generateRandomObjectID,

‎tests/integration/turborepo.test.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { getLogger } from 'lambda-local'
2+
import { existsSync } from 'node:fs'
3+
import { rm } from 'node:fs/promises'
4+
import { join } from 'node:path'
25
import { v4 } from 'uuid'
36
import { beforeEach, expect, test, vi } from 'vitest'
4-
import { createFixture, runPlugin, type FixtureTestContext } from '../utils/fixture.js'
7+
import { type FixtureTestContext } from '../utils/contexts.js'
8+
import { createFixture, runPlugin } from '../utils/fixture.js'
59
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
6-
import { glob } from 'fast-glob'
7-
import { existsSync } from 'node:fs'
8-
import { join } from 'node:path'
9-
import { rm } from 'node:fs/promises'
1010

1111
// Disable the verbose logging of the lambda-local runtime
1212
getLogger().level = 'alert'

‎tests/integration/wasm.test.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { load } from 'cheerio'
21
import { getLogger } from 'lambda-local'
32
import { v4 } from 'uuid'
4-
import { beforeEach, expect, test, vi, describe } from 'vitest'
5-
import {
6-
createFixture,
7-
invokeFunction,
8-
runPlugin,
9-
type FixtureTestContext,
10-
invokeEdgeFunction,
11-
} from '../utils/fixture.js'
3+
import { beforeEach, describe, expect, test, vi } from 'vitest'
4+
import { type FixtureTestContext } from '../utils/contexts.js'
5+
import { createFixture, invokeEdgeFunction, invokeFunction, runPlugin } from '../utils/fixture.js'
126
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
137
import { LocalServer } from '../utils/local-server.js'
148

‎tests/test-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { afterEach, vi } from 'vitest'
2-
import { FixtureTestContext } from './utils/fixture'
31
import fs from 'node:fs'
2+
import { afterEach } from 'vitest'
3+
import { type FixtureTestContext } from './utils/contexts'
44

55
// cleanup after each test as a fallback if someone forgot to call it
66
afterEach<FixtureTestContext>(async ({ cleanup }) => {

‎tests/utils/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const BLOB_TOKEN = 'secret-token'

‎tests/utils/contexts.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type getStore } from '@netlify/blobs'
2+
import { BlobsServer } from '@netlify/blobs/server'
3+
import { type WriteStream } from 'node:fs'
4+
import { MockInstance, TestContext } from 'vitest'
5+
6+
export interface FixtureTestContext extends TestContext {
7+
cwd: string
8+
siteID: string
9+
deployID: string
10+
blobStoreHost: string
11+
blobStorePort: number
12+
blobServer: BlobsServer
13+
blobServerGetSpy: MockInstance<Parameters<BlobsServer['get']>, ReturnType<BlobsServer['get']>>
14+
blobStore: ReturnType<typeof getStore>
15+
functionDist: string
16+
edgeFunctionPort: number
17+
edgeFunctionOutput: WriteStream
18+
cleanup?: (() => Promise<void>)[]
19+
}

‎tests/utils/create-e2e-fixture.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ interface E2EConfig {
3535
* If runtime should be installed in a custom location and not in cwd / packagePath
3636
*/
3737
runtimeInstallationPath?: string
38+
/**
39+
* Some fixtures might pin to non-latest CLI versions. This is used to verify the used CLI version matches expected one
40+
*/
41+
expectedCliVersion?: string
3842
}
3943

4044
/**
@@ -71,6 +75,8 @@ export const createE2EFixture = async (fixture: string, config: E2EConfig = {})
7175
await setNextVersionInFixture(isolatedFixtureRoot, NEXT_VERSION)
7276
}
7377
await installRuntime(packageName, isolatedFixtureRoot, config)
78+
await verifyFixture(isolatedFixtureRoot, config)
79+
7480
const result = await deploySite(isolatedFixtureRoot, config)
7581

7682
console.log(`🌍 Deployed site is live: ${result.url}`)
@@ -226,14 +232,38 @@ async function installRuntime(
226232
}
227233
}
228234

235+
async function verifyFixture(isolatedFixtureRoot: string, { expectedCliVersion }: E2EConfig) {
236+
if (expectedCliVersion) {
237+
const { stdout } = await execaCommand('npx ntl --version', { cwd: isolatedFixtureRoot })
238+
239+
const match = stdout.match(/netlify-cli\/(?<version>\S+)/)
240+
241+
if (!match) {
242+
throw new Error(`Could not extract the Netlify CLI version from the build logs`)
243+
}
244+
245+
const extractedVersion = match.groups?.version
246+
247+
if (!extractedVersion) {
248+
throw new Error(`Could not extract the Netlify CLI version from the build logs`)
249+
}
250+
251+
if (extractedVersion !== expectedCliVersion) {
252+
throw new Error(
253+
`Using unexpected CLI version "${extractedVersion}". Expected "${expectedCliVersion}"`,
254+
)
255+
}
256+
}
257+
}
258+
229259
async function deploySite(
230260
isolatedFixtureRoot: string,
231261
{ packagePath, cwd = '' }: E2EConfig,
232262
): Promise<DeployResult> {
233263
console.log(`🚀 Building and deploying site...`)
234264

235265
const outputFile = 'deploy-output.txt'
236-
let cmd = `ntl deploy --build --site ${SITE_ID}`
266+
let cmd = `npx ntl deploy --build --site ${SITE_ID}`
237267

238268
if (packagePath) {
239269
cmd += ` --filter ${packagePath}`
@@ -319,6 +349,10 @@ export const fixtureFactories = {
319349
buildCommand: 'nx run custom-dist-dir:build',
320350
publishDirectory: 'dist/apps/custom-dist-dir/dist',
321351
}),
352+
cliBeforeRegionalBlobsSupport: () =>
353+
createE2EFixture('cli-before-regional-blobs-support', {
354+
expectedCliVersion: '17.21.1',
355+
}),
322356
yarnMonorepoWithPnpmLinker: () =>
323357
createE2EFixture('yarn-monorepo-with-pnpm-linker', {
324358
packageManger: 'berry',

‎tests/utils/fixture.ts

+6-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { type getStore } from '@netlify/blobs'
2-
import { BlobsServer } from '@netlify/blobs/server'
3-
import { TestContext, assert, vi, MockInstance } from 'vitest'
1+
import { assert, vi } from 'vitest'
42

53
import { type NetlifyPluginConstants, type NetlifyPluginOptions } from '@netlify/build'
64
import { bundle, serve } from '@netlify/edge-bundler'
@@ -9,7 +7,7 @@ import { zipFunctions } from '@netlify/zip-it-and-ship-it'
97
import { execaCommand } from 'execa'
108
import getPort from 'get-port'
119
import { execute } from 'lambda-local'
12-
import { createWriteStream, existsSync, type WriteStream } from 'node:fs'
10+
import { createWriteStream, existsSync } from 'node:fs'
1311
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
1412
import { tmpdir } from 'node:os'
1513
import { basename, dirname, join, parse, relative } from 'node:path'
@@ -25,25 +23,11 @@ import {
2523
PluginContext,
2624
SERVER_HANDLER_NAME,
2725
} from '../../src/build/plugin-context.js'
26+
import { BLOB_TOKEN } from './constants.js'
27+
import { type FixtureTestContext } from './contexts.js'
28+
import { createBlobContext } from './helpers.js'
2829
import { setNextVersionInFixture } from './next-version-helpers.mjs'
2930

30-
export interface FixtureTestContext extends TestContext {
31-
cwd: string
32-
siteID: string
33-
deployID: string
34-
blobStoreHost: string
35-
blobStorePort: number
36-
blobServer: BlobsServer
37-
blobServerGetSpy: MockInstance<Parameters<BlobsServer['get']>, ReturnType<BlobsServer['get']>>
38-
blobStore: ReturnType<typeof getStore>
39-
functionDist: string
40-
edgeFunctionPort: number
41-
edgeFunctionOutput: WriteStream
42-
cleanup?: (() => Promise<void>)[]
43-
}
44-
45-
export const BLOB_TOKEN = 'secret-token'
46-
4731
const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts'
4832
const actualCwd = await vi.importActual<typeof import('process')>('process').then((p) => p.cwd())
4933
const eszipHelper = join(actualCwd, 'tools/deno/eszip.ts')
@@ -373,15 +357,7 @@ export async function invokeFunction(
373357
// The environment variables available during execution
374358
const environment = {
375359
NODE_ENV: 'production',
376-
NETLIFY_BLOBS_CONTEXT: Buffer.from(
377-
JSON.stringify({
378-
edgeURL: `http://${ctx.blobStoreHost}`,
379-
uncachedEdgeURL: `http://${ctx.blobStoreHost}`,
380-
token: BLOB_TOKEN,
381-
siteID: ctx.siteID,
382-
deployID: ctx.deployID,
383-
}),
384-
).toString('base64'),
360+
NETLIFY_BLOBS_CONTEXT: createBlobContext(ctx),
385361
...(env || {}),
386362
}
387363
const response = (await execute({

‎tests/utils/helpers.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import getPort from 'get-port'
2-
import { BLOB_TOKEN, type FixtureTestContext } from './fixture.js'
32

43
import { getDeployStore } from '@netlify/blobs'
54
import { BlobsServer } from '@netlify/blobs/server'
@@ -10,6 +9,8 @@ import { mkdtemp } from 'node:fs/promises'
109
import { tmpdir } from 'node:os'
1110
import { join } from 'node:path'
1211
import { assert, vi } from 'vitest'
12+
import { BLOB_TOKEN } from './constants'
13+
import { type FixtureTestContext } from './contexts'
1314

1415
/**
1516
* Uses next.js incremental cache to compute the same cache key for a URL that is automatically generated
@@ -48,6 +49,7 @@ export const createBlobContext = (ctx: FixtureTestContext) =>
4849
token: BLOB_TOKEN,
4950
siteID: ctx.siteID,
5051
deployID: ctx.deployID,
52+
primaryRegion: 'us-test-1',
5153
}),
5254
).toString('base64')
5355

@@ -74,6 +76,7 @@ export const startMockBlobStore = async (ctx: FixtureTestContext) => {
7476
deployID: ctx.deployID,
7577
siteID: ctx.siteID,
7678
token: BLOB_TOKEN,
79+
experimentalRegion: 'context',
7780
})
7881
}
7982

@@ -88,6 +91,7 @@ export const getBlobEntries = async (ctx: FixtureTestContext) => {
8891
deployID: ctx.deployID,
8992
siteID: ctx.siteID,
9093
token: BLOB_TOKEN,
94+
experimentalRegion: 'context',
9195
})
9296

9397
const { blobs } = await ctx.blobStore.list()
@@ -101,14 +105,17 @@ export function getBlobServerGets(ctx: FixtureTestContext, predicate?: (key: str
101105
if (typeof request.url !== 'string') {
102106
return undefined
103107
}
104-
// request url is /:siteID/:deployID/:key for get
105-
// /:siteID/:deployID for list
106-
// we only want gets
107-
const urlSegments = request.url.split('/')
108-
if (urlSegments.length === 4) {
109-
return decodeBlobKey(urlSegments[3])
108+
109+
let urlSegments = request.url.split('/').slice(1)
110+
111+
// ignore region url component when using `experimentalRegion`
112+
const REGION_PREFIX = 'region:'
113+
if (urlSegments[0].startsWith(REGION_PREFIX)) {
114+
urlSegments = urlSegments.slice(1)
110115
}
111-
return undefined
116+
117+
const [_siteID, _deployID, key] = urlSegments
118+
return key && decodeBlobKey(key)
112119
})
113120
.filter(isString)
114121
.filter((key) => !predicate || predicate(key))

‎tools/build.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,22 @@ async function bundle(entryPoints, format, watch) {
2727
platform: 'node',
2828
target: 'node18',
2929
format,
30-
external: ['next', '*.cjs'], // don't try to bundle the cjs files as we are providing them separately
30+
external: ['next'], // don't try to bundle next
3131
allowOverwrite: watch,
32+
plugins: [
33+
{
34+
// runtime modules are all entrypoints, so importing them should mark them as external
35+
// to avoid duplicating them in the bundle (which also can cause import path issues)
36+
name: 'mark-runtime-modules-as-external',
37+
setup(pluginBuild) {
38+
pluginBuild.onResolve({ filter: /^\..*\.c?js$/ }, (args) => {
39+
if (args.importer.includes(join('next-runtime-minimal', 'src'))) {
40+
return { path: args.path, external: true }
41+
}
42+
})
43+
},
44+
},
45+
],
3246
}
3347

3448
if (format === 'esm') {

‎vitest.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,7 @@ export default defineConfig({
7676
sequencer: Sequencer,
7777
},
7878
},
79+
esbuild: {
80+
include: ['**/*.ts', '**/*.cts'],
81+
},
7982
})

0 commit comments

Comments
 (0)
Please sign in to comment.