Skip to content

Commit 0f4eb7a

Browse files
authored
feat: use new blob store api (#100)
* feat: use new blob store api * chore: fix tests * chore: install deno for e2e * chore: change blobs dir
1 parent d53739f commit 0f4eb7a

14 files changed

+209
-220
lines changed

.github/workflows/run-tests.yml

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ jobs:
1717
- uses: oven-sh/setup-bun@v1
1818
- name: Setup PNPM
1919
uses: ./.github/actions/pnpm
20+
- name: Install Deno
21+
uses: denoland/setup-deno@v1
22+
with:
23+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/e55f825bd985d3c92e21d1b765d71e70d5628fba/node/bridge.ts#L17
24+
deno-version: v1.37.0
2025
- name: 'Install dependencies'
2126
run: npm ci
2227
- name: 'Netlify Login'

package-lock.json

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

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@netlify/next-runtime",
3-
"version": "5.0.0-alpha.26",
3+
"version": "5.0.0-alpha.27",
44
"description": "Run Next.js seamlessly on Netlify",
55
"main": "./dist/index.js",
66
"type": "module",
@@ -42,7 +42,7 @@
4242
"devDependencies": {
4343
"@fastly/http-compute-js": "1.1.1",
4444
"@netlify/blobs": "^6.3.1",
45-
"@netlify/build": "^29.29.4",
45+
"@netlify/build": "^29.31.0",
4646
"@netlify/edge-functions": "^2.2.0",
4747
"@netlify/eslint-config-node": "^7.0.1",
4848
"@netlify/functions": "^2.4.0",

playwright.config.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { defineConfig, devices } from '@playwright/test'
66
export default defineConfig({
77
testDir: './tests/e2e',
88
/* Run tests in files in parallel */
9-
fullyParallel: true,
9+
fullyParallel: false,
1010
/* Fail the build on CI if you accidentally left test.only in the source code. */
1111
forbidOnly: !!process.env.CI,
1212
/* Retry on CI only */
1313
retries: process.env.CI ? 2 : 0,
14-
/* Opt out of parallel tests on CI. */
15-
workers: process.env.CI ? 1 : undefined,
14+
/* Limit the number of workers on CI, use default locally */
15+
workers: process.env.CI ? 3 : undefined,
1616
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
1717
reporter: 'html',
1818
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

src/build/blob.ts

-17
This file was deleted.

src/build/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ const pkg = JSON.parse(readFileSync(join(PLUGIN_DIR, 'package.json'), 'utf-8'))
1010
export const PLUGIN_NAME = pkg.name
1111
export const PLUGIN_VERSION = pkg.version
1212

13+
/** The directory that is published to Netlify */
1314
export const STATIC_DIR = '.netlify/static'
1415
export const TEMP_DIR = '.netlify/temp'
16+
/** The directory inside the publish directory that will be uploaded by build to the blob store */
17+
export const BLOB_DIR = '.netlify/blobs/deploy'
1518

1619
export const SERVER_FUNCTIONS_DIR = '.netlify/functions-internal'
1720
export const SERVER_HANDLER_NAME = '___netlify-server-handler'

src/build/content/prerendered.ts

+27-43
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import type { NetlifyPluginOptions } from '@netlify/build'
22
import glob from 'fast-glob'
3-
import type { PrerenderManifest } from 'next/dist/build/index.js'
4-
import { readFile } from 'node:fs/promises'
3+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
54
import { basename, dirname, extname, resolve } from 'node:path'
65
import { join as joinPosix } from 'node:path/posix'
7-
import { cpus } from 'os'
8-
import pLimit from 'p-limit'
9-
import { getBlobStore } from '../blob.js'
106
import { getPrerenderManifest } from '../config.js'
7+
import { BLOB_DIR } from '../constants.js'
118

129
export type CacheEntry = {
1310
key: string
@@ -124,44 +121,31 @@ const buildPrerenderedContentEntries = async (
124121
* Upload prerendered content to the blob store and remove it from the bundle
125122
*/
126123
export const uploadPrerenderedContent = async ({
127-
constants: { PUBLISH_DIR, NETLIFY_API_TOKEN, NETLIFY_API_HOST, SITE_ID },
128-
}: Pick<NetlifyPluginOptions, 'constants'>) => {
129-
// limit concurrent uploads to 2x the number of CPUs
130-
const limit = pLimit(Math.max(2, cpus().length))
131-
132-
// read prerendered content and build JSON key/values for the blob store
133-
let manifest: PrerenderManifest
134-
let blob: ReturnType<typeof getBlobStore>
124+
constants: { PUBLISH_DIR },
125+
utils,
126+
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>) => {
135127
try {
136-
manifest = await getPrerenderManifest({ PUBLISH_DIR })
137-
blob = getBlobStore({ NETLIFY_API_TOKEN, NETLIFY_API_HOST, SITE_ID })
138-
} catch (error: any) {
139-
console.error(`Unable to upload prerendered content: ${error.message}`)
140-
return
128+
// read prerendered content and build JSON key/values for the blob store
129+
const manifest = await getPrerenderManifest({ PUBLISH_DIR })
130+
const entries = await Promise.all(
131+
await buildPrerenderedContentEntries(PUBLISH_DIR, Object.keys(manifest.routes)),
132+
)
133+
134+
// movce JSON content to the blob store directory for upload
135+
await Promise.all(
136+
entries
137+
.filter((entry) => entry.value.value !== undefined)
138+
.map(async (entry) => {
139+
const dest = resolve(BLOB_DIR, entry.key)
140+
await mkdir(dirname(dest), { recursive: true })
141+
await writeFile(resolve(BLOB_DIR, entry.key), JSON.stringify(entry.value), 'utf-8')
142+
}),
143+
)
144+
} catch (error) {
145+
utils.build.failBuild(
146+
'Failed assembling prerendered content for upload',
147+
error instanceof Error ? { error } : {},
148+
)
149+
throw error
141150
}
142-
const entries = await Promise.allSettled(
143-
await buildPrerenderedContentEntries(PUBLISH_DIR, Object.keys(manifest.routes)),
144-
)
145-
entries.forEach((result) => {
146-
if (result.status === 'rejected') {
147-
console.error(`Unable to read prerendered content: ${result.reason.message}`)
148-
}
149-
})
150-
151-
// upload JSON content data to the blob store
152-
const uploads = await Promise.allSettled(
153-
entries
154-
.filter((entry) => entry.status === 'fulfilled' && entry.value.value.value !== undefined)
155-
.map((entry: PromiseSettledResult<CacheEntry>) => {
156-
const result = entry as PromiseFulfilledResult<CacheEntry>
157-
const { key, value } = result.value
158-
return limit(() => blob.setJSON(key, value))
159-
}),
160-
)
161-
uploads.forEach((upload, index) => {
162-
if (upload.status === 'rejected') {
163-
const result = entries[index] as PromiseFulfilledResult<CacheEntry>
164-
console.error(`Unable to store ${result.value?.key}: ${upload.reason.message}`)
165-
}
166-
})
167151
}

src/build/content/static.test.ts

+28-33
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
1-
import type { NetlifyPluginOptions } from '@netlify/build'
1+
import type {
2+
NetlifyPluginConstants,
3+
NetlifyPluginOptions,
4+
NetlifyPluginUtils,
5+
} from '@netlify/build'
26
import glob from 'fast-glob'
37
import { Mock, afterEach, beforeEach, expect, test, vi } from 'vitest'
48
import { mockFileSystem } from '../../../tests/index.js'
59
import { FixtureTestContext, createFsFixture } from '../../../tests/utils/fixture.js'
6-
import { getBlobStore } from '../blob.js'
7-
import { STATIC_DIR } from '../constants.js'
10+
import { BLOB_DIR, STATIC_DIR } from '../constants.js'
811
import { copyStaticAssets, uploadStaticContent } from './static.js'
12+
import * as fs from 'fs'
13+
import { join } from 'path'
914

10-
afterEach(() => {
11-
vi.restoreAllMocks()
12-
})
13-
14-
vi.mock('../blob.js', () => ({
15-
getBlobStore: vi.fn(),
16-
}))
17-
18-
let mockBlobSet = vi.fn()
19-
beforeEach(() => {
20-
;(getBlobStore as Mock).mockReturnValue({
21-
set: mockBlobSet,
22-
})
23-
})
15+
const utils = {
16+
build: { failBuild: vi.fn() },
17+
} as unknown as NetlifyPluginUtils
2418

2519
test('should clear the static directory contents', async () => {
2620
const PUBLISH_DIR = '.next'
@@ -30,8 +24,8 @@ test('should clear the static directory contents', async () => {
3024
})
3125

3226
await copyStaticAssets({
33-
constants: { PUBLISH_DIR },
34-
} as Pick<NetlifyPluginOptions, 'constants'>)
27+
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
28+
})
3529

3630
expect(Object.keys(vol.toJSON())).toEqual(
3731
expect.not.arrayContaining([`${STATIC_DIR}/remove-me.js`]),
@@ -50,8 +44,8 @@ test<FixtureTestContext>('should link static content from the publish directory
5044
)
5145

5246
await copyStaticAssets({
53-
constants: { PUBLISH_DIR },
54-
} as Pick<NetlifyPluginOptions, 'constants'>)
47+
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
48+
})
5549

5650
const files = await glob('**/*', { cwd, dot: true })
5751

@@ -77,11 +71,10 @@ test<FixtureTestContext>('should link static content from the public directory t
7771
)
7872

7973
await copyStaticAssets({
80-
constants: { PUBLISH_DIR },
81-
} as Pick<NetlifyPluginOptions, 'constants'>)
74+
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
75+
})
8276

8377
const files = await glob('**/*', { cwd, dot: true })
84-
8578
expect(files).toEqual(
8679
expect.arrayContaining([
8780
'public/another-asset.json',
@@ -94,7 +87,6 @@ test<FixtureTestContext>('should link static content from the public directory t
9487

9588
test<FixtureTestContext>('should copy the static pages to the publish directory if the routes do not exist in the prerender-manifest', async (ctx) => {
9689
const PUBLISH_DIR = '.next'
97-
9890
const { cwd } = await createFsFixture(
9991
{
10092
[`${PUBLISH_DIR}/prerender-manifest.json`]: JSON.stringify({
@@ -108,12 +100,14 @@ test<FixtureTestContext>('should copy the static pages to the publish directory
108100
)
109101

110102
await uploadStaticContent({
111-
constants: { PUBLISH_DIR },
112-
} as Pick<NetlifyPluginOptions, 'constants'>)
103+
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
104+
utils,
105+
})
113106

114-
expect(mockBlobSet).toHaveBeenCalledTimes(2)
115-
expect(mockBlobSet).toHaveBeenCalledWith('server/pages/test.html', 'test-1')
116-
expect(mockBlobSet).toHaveBeenCalledWith('server/pages/test2.html', 'test-2')
107+
expect((await glob('**/*', { cwd: join(cwd, BLOB_DIR), dot: true })).sort()).toEqual([
108+
'server/pages/test.html',
109+
'server/pages/test2.html',
110+
])
117111
})
118112

119113
test<FixtureTestContext>('should not copy the static pages to the publish directory if the routes exist in the prerender-manifest', async (ctx) => {
@@ -135,8 +129,9 @@ test<FixtureTestContext>('should not copy the static pages to the publish direct
135129
)
136130

137131
await uploadStaticContent({
138-
constants: { PUBLISH_DIR },
139-
} as Pick<NetlifyPluginOptions, 'constants'>)
132+
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
133+
utils,
134+
})
140135

141-
expect(mockBlobSet).not.toHaveBeenCalled()
136+
expect(await glob('**/*', { cwd: join(cwd, BLOB_DIR), dot: true })).toHaveLength(0)
142137
})

0 commit comments

Comments
 (0)