Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
173 lines
5.0 KiB
JavaScript
173 lines
5.0 KiB
JavaScript
import { fileURLToPath } from 'url'
|
|
import { dirname } from 'path'
|
|
import { spawn } from 'child_process'
|
|
import { createServer } from 'node:http'
|
|
import httpProxy from 'http-proxy'
|
|
import process from 'node:process'
|
|
import { createGunzip } from 'zlib'
|
|
|
|
const dir = dirname(fileURLToPath(import.meta.url))
|
|
|
|
// Redirects that happen in the proxy layer, rather than in Next.js itself. This
|
|
// is used to test that the client is still able to fully prefetch the
|
|
// target page.
|
|
const proxyRedirects = {
|
|
'/redirect-to-target-page': '/target-page',
|
|
}
|
|
|
|
async function spawnNext(port) {
|
|
const child = spawn('pnpm', ['next', 'start', '-p', port, dir], {
|
|
env: process.env,
|
|
stdio: ['inherit', 'pipe', 'inherit'],
|
|
})
|
|
|
|
child.stdout.pipe(process.stdout)
|
|
|
|
// Wait until the server is listening.
|
|
return new Promise((resolve, reject) => {
|
|
child.stdout.on('data', (data) => {
|
|
if (data.toString().includes('Ready')) {
|
|
resolve(child)
|
|
}
|
|
})
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
resolve(child)
|
|
} else {
|
|
reject(new Error(`Next.js server exited with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
function isCacheableRequest(req) {
|
|
return (
|
|
req.method === 'GET' && !req.headers['cache-control']?.includes('no-store')
|
|
)
|
|
}
|
|
|
|
function isCacheableResponse(res) {
|
|
return !res.headers['cache-control']?.includes('no-store')
|
|
}
|
|
|
|
async function createFakeCDN(destPort) {
|
|
const fakeCDNCache = new Map()
|
|
|
|
const proxy = httpProxy.createProxyServer()
|
|
const cdnServer = createServer(async (req, res) => {
|
|
const pathname = new URL(req.url, `http://localhost`).pathname
|
|
const redirectUrl = proxyRedirects[pathname]
|
|
if (redirectUrl) {
|
|
console.log('Redirecting to:', redirectUrl)
|
|
res.writeHead(307, {
|
|
Location: redirectUrl,
|
|
})
|
|
res.end()
|
|
return
|
|
}
|
|
|
|
if (isCacheableRequest(req)) {
|
|
// Serve from our fake CDN if there's a matching entry.
|
|
const entry = await fakeCDNCache.get(req.url)
|
|
if (entry) {
|
|
console.log('Serving from fake CDN:', req.url)
|
|
res.writeHead(entry.statusCode, entry.statusMessage, entry.headers)
|
|
res.end(entry.data)
|
|
return
|
|
} else {
|
|
// No existing entry. Proxy to the Next.js server and then store
|
|
// the response in the cache.
|
|
proxy.web(req, res, {
|
|
target: `http://localhost:${destPort}`,
|
|
selfHandleResponse: true,
|
|
})
|
|
}
|
|
} else {
|
|
// This request isn't cacheable.
|
|
proxy.web(req, res, { target: `http://localhost:${destPort}` })
|
|
}
|
|
})
|
|
|
|
proxy.on('proxyRes', function (proxyRes, req, res) {
|
|
// If the response is cacheable, store it in a map to simulate a CDN.
|
|
//
|
|
// Note that we only key the entry on the URL, not any of the headers. i.e.
|
|
// we don't respect the Vary header. This is true of certain real CDNs, so
|
|
// Next.js must not rely on Vary.
|
|
//
|
|
// For the purposes of this test we don't respect max-age et al. Every
|
|
// entry is cached indefinitely.
|
|
if (isCacheableRequest(req) && isCacheableResponse(proxyRes)) {
|
|
let resolveCDNEntry
|
|
fakeCDNCache.set(req.url, new Promise((res) => (resolveCDNEntry = res)))
|
|
|
|
// Decompress the original response stream, if needed
|
|
let source
|
|
if (proxyRes.headers['content-encoding'] === 'gzip') {
|
|
source = proxyRes.pipe(createGunzip())
|
|
// Just store the uncompressed body and serve that from cache.
|
|
// Good enough for the purposes of this test app.
|
|
delete proxyRes.headers['content-encoding']
|
|
} else {
|
|
source = proxyRes
|
|
}
|
|
|
|
const chunks = []
|
|
source.on('data', (chunk) => {
|
|
chunks.push(chunk)
|
|
})
|
|
source.on('end', () => {
|
|
const data = Buffer.concat(chunks)
|
|
// Send response after we've collected all chunks
|
|
res.writeHead(
|
|
proxyRes.statusCode || 200,
|
|
proxyRes.statusMessage,
|
|
proxyRes.headers
|
|
)
|
|
res.end(data)
|
|
|
|
// Store the raw data for later use
|
|
const entry = {
|
|
data,
|
|
statusCode: proxyRes.statusCode,
|
|
statusMessage: proxyRes.statusMessage,
|
|
headers: proxyRes.headers,
|
|
}
|
|
resolveCDNEntry(entry)
|
|
})
|
|
return
|
|
}
|
|
// If the response isn't cacheable, pipe it through to the client.
|
|
proxyRes.pipe(res)
|
|
return
|
|
})
|
|
|
|
return cdnServer
|
|
}
|
|
|
|
export async function start(cdnPort = 3000, nextPort = cdnPort + 1) {
|
|
const next = await spawnNext(nextPort)
|
|
const cdnServer = await createFakeCDN(nextPort)
|
|
|
|
const onTerminate = () => {
|
|
cdnServer.close()
|
|
next.kill()
|
|
process.exit(0)
|
|
}
|
|
process.on('SIGINT', onTerminate)
|
|
process.on('SIGTERM', onTerminate)
|
|
|
|
const cleanup = async () => {
|
|
next.kill()
|
|
await new Promise((resolve) => cdnServer.close(resolve))
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
cdnServer.on('error', reject)
|
|
cdnServer.listen(cdnPort, () => {
|
|
console.log('Server is listening', cdnPort)
|
|
resolve(cleanup)
|
|
})
|
|
})
|
|
}
|