Skip to content

Commit bc34596

Browse files
authored
Merge branch 'main' into renovate/major-netlify-packages
2 parents bd7d0fb + aedb279 commit bc34596

File tree

10 files changed

+133
-25
lines changed

10 files changed

+133
-25
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
os: [ubuntu-latest, macOS-latest, windows-latest]
19-
node-version: ['*']
19+
node-version: ['24']
2020
include:
2121
- os: ubuntu-latest
2222
# Many of our dependencies require 20.6.0, so we use the same minimum.
@@ -42,7 +42,7 @@ jobs:
4242
# Use npm@10 on Node 22+ due to https://github.com/npm/cli/issues/8489
4343
- name: Setup npm version
4444
run: npm install -g npm@10
45-
if: "${{ matrix.node-version == '*' }}"
45+
if: "${{ matrix.node-version == '24' }}"
4646
- name: Setup Deno
4747
uses: denoland/setup-deno@v1
4848
with:

package-lock.json

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export interface DeleteStoreResponse {
22
blobs_deleted: number
3+
has_more: boolean
34
}

packages/blobs/src/main.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,7 @@ describe('deleteAll', () => {
15001500
const mockStore = new MockFetch()
15011501
.delete({
15021502
headers: { authorization: `Bearer ${apiToken}` },
1503-
response: Response.json({ blobs_deleted: 3 }),
1503+
response: Response.json({ blobs_deleted: 3, has_more: false }),
15041504
url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production`,
15051505
})
15061506
.inject()
@@ -1542,7 +1542,7 @@ describe('deleteAll', () => {
15421542
const mockStore = new MockFetch()
15431543
.delete({
15441544
headers: { authorization: `Bearer ${apiToken}` },
1545-
response: Response.json({ blobs_deleted: 3 }),
1545+
response: Response.json({ blobs_deleted: 3, has_more: false }),
15461546
url: `https://api.netlify.com/api/v1/blobs/${siteID}/oldie`,
15471547
})
15481548
.inject()
@@ -1565,7 +1565,7 @@ describe('deleteAll', () => {
15651565
const mockStore = new MockFetch()
15661566
.delete({
15671567
headers: { authorization: `Bearer ${edgeToken}` },
1568-
response: Response.json({ blobs_deleted: 3 }),
1568+
response: Response.json({ blobs_deleted: 3, has_more: false }),
15691569
url: `${edgeURL}/${siteID}/site:production`,
15701570
})
15711571
.inject()
@@ -1605,6 +1605,33 @@ describe('deleteAll', () => {
16051605

16061606
expect(mockStore.fulfilled).toBeTruthy()
16071607
})
1608+
1609+
test('Handles paginated deletion with multiple batches', async () => {
1610+
const mockStore = new MockFetch()
1611+
.delete({
1612+
headers: { authorization: `Bearer ${edgeToken}` },
1613+
response: Response.json({ blobs_deleted: 100, has_more: true }),
1614+
url: `${edgeURL}/${siteID}/site:production`,
1615+
})
1616+
.delete({
1617+
headers: { authorization: `Bearer ${edgeToken}` },
1618+
response: Response.json({ blobs_deleted: 50, has_more: false }),
1619+
url: `${edgeURL}/${siteID}/site:production`,
1620+
})
1621+
.inject()
1622+
1623+
const blobs = getStore({
1624+
edgeURL,
1625+
name: 'production',
1626+
token: edgeToken,
1627+
siteID,
1628+
})
1629+
1630+
const res = await blobs.deleteAll()
1631+
1632+
expect(mockStore.fulfilled).toBeTruthy()
1633+
expect(res.deletedBlobs).toBe(150)
1634+
})
16081635
})
16091636
})
16101637

packages/blobs/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export class BlobsServer {
180180
}
181181
}
182182

183-
return Response.json({ blobs_deleted: blobsDeleted })
183+
return Response.json({ blobs_deleted: blobsDeleted, has_more: false })
184184
}
185185

186186
private async get(req: Request): Promise<Response> {

packages/blobs/src/store.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,20 +152,28 @@ export class Store {
152152
}
153153

154154
async deleteAll(): Promise<DeleteStoreResult> {
155-
const res = await this.client.makeRequest({ method: HTTPMethod.DELETE, storeName: this.name })
155+
let totalDeletedBlobs = 0
156+
let hasMore = true
156157

157-
if (res.status !== 200) {
158-
throw new BlobsInternalError(res)
159-
}
158+
while (hasMore) {
159+
const res = await this.client.makeRequest({ method: HTTPMethod.DELETE, storeName: this.name })
160+
161+
if (res.status !== 200) {
162+
throw new BlobsInternalError(res)
163+
}
160164

161-
const data = (await res.json()) as DeleteStoreResponse
165+
const data = (await res.json()) as DeleteStoreResponse
162166

163-
if (typeof data.blobs_deleted !== 'number') {
164-
throw new BlobsInternalError(res)
167+
if (typeof data.blobs_deleted !== 'number') {
168+
throw new BlobsInternalError(res)
169+
}
170+
171+
totalDeletedBlobs += data.blobs_deleted
172+
hasMore = typeof data.has_more === 'boolean' && data.has_more
165173
}
166174

167175
return {
168-
deletedBlobs: data.blobs_deleted,
176+
deletedBlobs: totalDeletedBlobs,
169177
}
170178
}
171179

packages/dev-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"parse-gitignore": "^2.0.0",
6969
"semver": "^7.7.2",
7070
"tmp-promise": "^3.0.3",
71-
"uuid": "^11.1.0",
71+
"uuid": "^13.0.0",
7272
"write-file-atomic": "^5.0.1"
7373
}
7474
}

packages/dev/src/lib/reqres.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
import { IncomingHttpHeaders, IncomingMessage } from 'node:http'
1+
import { IncomingMessage } from 'node:http'
22
import { Readable } from 'node:stream'
33

4-
export const normalizeHeaders = (headers: IncomingHttpHeaders): HeadersInit => {
4+
export const normalizeHeaders = (request: IncomingMessage) => {
55
const result: [string, string][] = []
6+
let headers = request.headers
7+
8+
// Handle HTTP/2 pseudo-headers: https://www.rfc-editor.org/rfc/rfc9113.html#name-request-pseudo-header-field
9+
// In certain versions of Node.js, the built-in `Request` constructor from undici throws
10+
// if a header starts with a colon.
11+
if (request.httpVersionMajor >= 2) {
12+
headers = { ...headers }
13+
delete headers[':authority']
14+
delete headers[':method']
15+
delete headers[':path']
16+
delete headers[':scheme']
17+
}
618

719
for (const [key, value] of Object.entries(headers)) {
820
if (Array.isArray(value)) {
@@ -43,11 +55,14 @@ export const getNormalizedRequestFromNodeRequest = (
4355
? null
4456
: (Readable.toWeb(input) as unknown as ReadableStream<unknown>)
4557

58+
const normalizedHeaders = normalizeHeaders(input)
59+
normalizedHeaders.push(['x-nf-request-id', requestID])
60+
4661
return new Request(fullUrl, {
4762
body,
4863
// @ts-expect-error Not typed!
4964
duplex: 'half',
50-
headers: normalizeHeaders({ ...input.headers, 'x-nf-request-id': requestID }),
65+
headers: normalizedHeaders,
5166
method,
5267
})
5368
}

packages/dev/src/main.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { readFile } from 'node:fs/promises'
2+
import { IncomingMessage } from 'node:http'
3+
import { Socket } from 'node:net'
24
import { resolve } from 'node:path'
35

46
import { createImageServerHandler, Fixture, generateImage, getImageResponseSize, HTTPServer } from '@netlify/dev-utils'
@@ -14,6 +16,48 @@ describe('Handling requests', () => {
1416
vi.unstubAllEnvs()
1517
})
1618

19+
test('Handles HTTP/2 Node.js requests', async () => {
20+
const fixture = new Fixture()
21+
.withFile(
22+
'netlify.toml',
23+
`[build]
24+
publish = "public"
25+
`,
26+
)
27+
.withFile('public/index.html', 'Hello from static file')
28+
const directory = await fixture.create()
29+
const dev = new NetlifyDev({
30+
projectRoot: directory,
31+
geolocation: { enabled: false },
32+
})
33+
await dev.start()
34+
35+
const nodeReq = new IncomingMessage(new Socket())
36+
nodeReq.httpVersionMajor = 2
37+
nodeReq.httpVersionMinor = 0
38+
nodeReq.method = 'GET'
39+
nodeReq.url = '/index.html'
40+
nodeReq.headers = {
41+
accept: 'text/html',
42+
host: 'example.netlify.app',
43+
'user-agent': 'test-agent',
44+
// These four HTTP/2 pseudo request headers are required per the HTTP/2 spec:
45+
// https://www.rfc-editor.org/rfc/rfc9113.html#name-request-pseudo-header-field
46+
// These show up here like any other header on Node.js IncomingMessage objects,
47+
':method': 'GET',
48+
':path': '/index.html',
49+
':scheme': 'https',
50+
':authority': 'example.netlify.app',
51+
}
52+
const result = await dev.handleAndIntrospectNodeRequest(nodeReq)
53+
54+
expect(result?.response.ok).toBe(true)
55+
expect(await result?.response.text()).toBe('Hello from static file')
56+
57+
await dev.stop()
58+
await fixture.destroy()
59+
})
60+
1761
describe('No linked site', () => {
1862
test('Same-site rewrite to a static file', async () => {
1963
const fixture = new Fixture()

packages/vite-plugin-tanstack-start/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Vite plugin for TanStack Start on Netlify",
55
"type": "module",
66
"engines": {
7-
"node": "^22.12.0"
7+
"node": "^22.12.0 || >=24.0.0"
88
},
99
"main": "./dist/main.js",
1010
"exports": "./dist/main.js",

0 commit comments

Comments
 (0)