diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index c772a0bcbc3bdb..bfd2d010f8de39 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1422,7 +1422,7 @@ export async function buildAppStaticPaths({ isRevalidate: false, experimental: { after: false, - dynamicIO: false, + dynamicIO, }, }, }, diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 7a39195f6cdf89..5461de93d924a6 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -115,7 +115,7 @@ export class EdgeRouteModuleWrapper { : undefined, experimental: { after: isAfterEnabled, - dynamicIO: false, + dynamicIO: !!process.env.__NEXT_DYNAMIC_IO, }, }, } diff --git a/test/e2e/app-dir/dynamic-io/app/params/generate-static-params/[slug]/layout.tsx b/test/e2e/app-dir/dynamic-io/app/params/generate-static-params/[slug]/layout.tsx new file mode 100644 index 00000000000000..8a1e8873cdfbcd --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/params/generate-static-params/[slug]/layout.tsx @@ -0,0 +1,35 @@ +// TODO once we make fetchCache inert with dynamicIO this test is expected +// to start failing. Right now the force cache causes the fetches to be identical +// and we get only one prebuilt route. once we remove the caching behavior of fetchCache +// when dynamicIO is on we will get more than one route. +// The ideal test wouldn't even use fetchCache but at the moment the default caching for fetch +// is to not cache and so we can't rely on the default to produce a differentiating result. +export const fetchCache = 'default-cache' + +export async function generateStaticParams() { + const set = new Set() + set.add(await fetchRandom('a')) + set.add(await fetchRandom('a')) + + return Array.from(set).map((value) => { + return { + slug: ('' + value).slice(2), + } + }) +} + +export default async function Layout({ children, params }) { + return ( + <> +

{await params.slug}

+
{children}
+ + ) +} + +const fetchRandom = async (entropy: string) => { + const response = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy + ) + return response.text() +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/async/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/async/route.ts new file mode 100644 index 00000000000000..3d6f8b0c15d369 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/async/route.ts @@ -0,0 +1,27 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../../getSentinelValue' + +export const runtime = 'edge' + +export async function generateStaticParams() { + return [ + { + dyn: '1', + }, + ] +} + +export async function GET( + request: NextRequest, + props: { params: Promise<{ dyn: string }> } +) { + const { dyn } = await props.params + return new Response( + JSON.stringify({ + value: getSentinelValue(), + type: 'dynamic params', + param: dyn, + }) + ) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/sync/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/sync/route.ts new file mode 100644 index 00000000000000..9b7af314b610be --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/[dyn]/sync/route.ts @@ -0,0 +1,29 @@ +import type { NextRequest, UnsafeUnwrappedParams } from 'next/server' + +import { getSentinelValue } from '../../../../getSentinelValue' + +export const runtime = 'edge' + +export async function generateStaticParams() { + return [ + { + dyn: '1', + }, + ] +} + +export async function GET( + request: NextRequest, + props: { params: Promise<{ dyn: string }> } +) { + const dyn = ( + props.params as unknown as UnsafeUnwrappedParams + ).dyn + return new Response( + JSON.stringify({ + value: getSentinelValue(), + type: 'dynamic params', + param: dyn, + }) + ) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-cookies/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-cookies/route.ts new file mode 100644 index 00000000000000..e794e66562f180 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-cookies/route.ts @@ -0,0 +1,18 @@ +import type { NextRequest } from 'next/server' + +import { cookies } from 'next/headers' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const sentinel = (await cookies()).get('x-sentinel') + return new Response( + JSON.stringify({ + value: getSentinelValue(), + type: 'cookies', + 'x-sentinel': sentinel, + }) + ) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-headers/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-headers/route.ts new file mode 100644 index 00000000000000..a5b914dd5a5569 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-headers/route.ts @@ -0,0 +1,18 @@ +import type { NextRequest } from 'next/server' + +import { headers } from 'next/headers' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const sentinel = (await headers()).get('x-sentinel') + return new Response( + JSON.stringify({ + value: getSentinelValue(), + type: 'headers', + 'x-sentinel': sentinel, + }) + ) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-stream/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-stream/route.ts new file mode 100644 index 00000000000000..54dba383f972d7 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-stream/route.ts @@ -0,0 +1,29 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const result = JSON.stringify({ + value: getSentinelValue(), + message: 'dynamic stream', + }) + const part1 = result.slice(0, result.length / 2) + const part2 = result.slice(result.length / 2) + + const encoder = new TextEncoder() + const chunks = [encoder.encode(part1), encoder.encode(part2)] + + let sent = 0 + const stream = new ReadableStream({ + async pull(controller) { + controller.enqueue(chunks[sent++]) + await new Promise((r) => setTimeout(r, 1)) + if (sent === chunks.length) { + controller.close() + } + }, + }) + return new Response(stream) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-url/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-url/route.ts new file mode 100644 index 00000000000000..174123832c08df --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/dynamic-url/route.ts @@ -0,0 +1,15 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const search = request.nextUrl.search + return new Response( + JSON.stringify({ + value: getSentinelValue(), + search, + }) + ) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-cached/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-cached/route.ts new file mode 100644 index 00000000000000..128abbc51509b2 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-cached/route.ts @@ -0,0 +1,25 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const fetcheda = await fetchRandomCached('a') + const fetchedb = await fetchRandomCached('b') + return new Response( + JSON.stringify({ + value: getSentinelValue(), + random1: fetcheda, + random2: fetchedb, + }) + ) +} + +const fetchRandomCached = async (entropy: string) => { + const response = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy, + { cache: 'force-cache' } + ) + return response.text() +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-mixed/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-mixed/route.ts new file mode 100644 index 00000000000000..4bdb6495c3d73e --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/fetch-mixed/route.ts @@ -0,0 +1,32 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const fetcheda = await fetchRandomCached('a') + const fetchedb = await fetchRandomUncached('b') + return new Response( + JSON.stringify({ + value: getSentinelValue(), + random1: fetcheda, + random2: fetchedb, + }) + ) +} + +const fetchRandomCached = async (entropy: string) => { + const response = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy, + { cache: 'force-cache' } + ) + return response.text() +} + +const fetchRandomUncached = async (entropy: string) => { + const response = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?b=' + entropy + ) + return response.text() +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-cached/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-cached/route.ts new file mode 100644 index 00000000000000..35a8e265b15c1c --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-cached/route.ts @@ -0,0 +1,27 @@ +import type { NextRequest } from 'next/server' + +import { unstable_cache as cache } from 'next/cache' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const messagea = await getCachedMessage('hello cached fast', 0) + const messageb = await getCachedMessage('hello cached slow', 20) + return new Response( + JSON.stringify({ + value: getSentinelValue(), + message1: messagea, + message2: messageb, + }) + ) +} + +async function getMessage(echo, delay) { + const tag = ((Math.random() * 10000) | 0).toString(16) + await new Promise((r) => setTimeout(r, delay)) + return `${tag}:${echo}` +} + +const getCachedMessage = cache(getMessage) diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-mixed/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-mixed/route.ts new file mode 100644 index 00000000000000..d143cc3363f8ef --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/io-mixed/route.ts @@ -0,0 +1,27 @@ +import type { NextRequest } from 'next/server' + +import { unstable_cache as cache } from 'next/cache' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const messagea = await getCachedMessage('hello cached fast', 0) + const messageb = await getMessage('hello uncached slow', 20) + return new Response( + JSON.stringify({ + value: getSentinelValue(), + message1: messagea, + message2: messageb, + }) + ) +} + +async function getMessage(echo, delay) { + const tag = ((Math.random() * 10000) | 0).toString(16) + await new Promise((r) => setTimeout(r, delay)) + return `${tag}:${echo}` +} + +const getCachedMessage = cache(getMessage) diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/microtask/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/microtask/route.ts new file mode 100644 index 00000000000000..0917cb1a1e8c05 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/microtask/route.ts @@ -0,0 +1,14 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + await Promise.resolve() + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'microtask', + }) + return new Response(response) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-async/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-async/route.ts new file mode 100644 index 00000000000000..4519ca0207cb9e --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-async/route.ts @@ -0,0 +1,28 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'stream response', + }) + const part1 = response.slice(0, Math.floor(response.length / 2)) + const part2 = response.slice(Math.floor(response.length / 2)) + + const encoder = new TextEncoder() + const chunks = [encoder.encode(part1), encoder.encode(part2)] + + let sent = 0 + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(chunks[sent++]) + if (sent === chunks.length) { + controller.close() + } + }, + }) + return new Response(stream) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-sync/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-sync/route.ts new file mode 100644 index 00000000000000..4de193e1c3ce7c --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-stream-sync/route.ts @@ -0,0 +1,28 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export function GET(request: NextRequest) { + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'stream response', + }) + const part1 = response.slice(0, Math.floor(response.length / 2)) + const part2 = response.slice(Math.floor(response.length / 2)) + + const encoder = new TextEncoder() + const chunks = [encoder.encode(part1), encoder.encode(part2)] + + let sent = 0 + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(chunks[sent++]) + if (sent === chunks.length) { + controller.close() + } + }, + }) + return new Response(stream) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-async/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-async/route.ts new file mode 100644 index 00000000000000..272c5818842441 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-async/route.ts @@ -0,0 +1,13 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'string response', + }) + return new Response(response) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-sync/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-sync/route.ts new file mode 100644 index 00000000000000..985e15b1ee06dd --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/static-string-sync/route.ts @@ -0,0 +1,13 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export function GET(request: NextRequest) { + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'string response', + }) + return new Response(response) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/-edge/task/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/-edge/task/route.ts new file mode 100644 index 00000000000000..8959b2115dc9f3 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io/app/routes/-edge/task/route.ts @@ -0,0 +1,14 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../../getSentinelValue' + +export const runtime = 'edge' + +export async function GET(request: NextRequest) { + await new Promise((r) => setTimeout(r, 10)) + const response = JSON.stringify({ + value: getSentinelValue(), + message: 'task', + }) + return new Response(response) +} diff --git a/test/e2e/app-dir/dynamic-io/app/routes/fetch-mixed/route.ts b/test/e2e/app-dir/dynamic-io/app/routes/fetch-mixed/route.ts index a1fb31abe77253..df581af02085b6 100644 --- a/test/e2e/app-dir/dynamic-io/app/routes/fetch-mixed/route.ts +++ b/test/e2e/app-dir/dynamic-io/app/routes/fetch-mixed/route.ts @@ -2,6 +2,8 @@ import type { NextRequest } from 'next/server' import { getSentinelValue } from '../../getSentinelValue' +export const runtime = 'edge' + export async function GET(request: NextRequest) { const fetcheda = await fetchRandomCached('a') const fetchedb = await fetchRandomUncached('b') diff --git a/test/e2e/app-dir/dynamic-io/dynamic-io.params.test.ts b/test/e2e/app-dir/dynamic-io/dynamic-io.params.test.ts index 517a994479231d..0ae20363cb84fa 100644 --- a/test/e2e/app-dir/dynamic-io/dynamic-io.params.test.ts +++ b/test/e2e/app-dir/dynamic-io/dynamic-io.params.test.ts @@ -2481,4 +2481,38 @@ describe('dynamic-io', () => { } }) }) + + if (!isNextDev) { + describe('generateStaticParams', () => { + it('should have dynamicIO semantics inside generateStaticParams', async () => { + // This test is named what we want but our current implementation is not actually correct yet. + // We are asserting current behavior and will update the test when we land the correct behavior + + const lines: Array = next.cliOutput.split('\n') + let i = 0 + while (true) { + const line = lines[i++] + if (typeof line !== 'string') { + throw new Error( + 'Could not find expected route output for /params/generate-static-params/[slug]/outputs/...' + ) + } + + if (line.includes('/params/generate-static-params/[slug]')) { + let nextLine = lines[i++] + expect(nextLine).toMatch( + /\/params\/generate-static-params\/\d+\/outputs/ + ) + nextLine = lines[i++] + // Because we force-cache we only end up with one prebuilt page. + // When dyanmicIO semantics are fully respected we will end up with two. + expect(nextLine).not.toMatch( + /\/params\/generate-static-params\/\d+\/outputs/ + ) + break + } + } + }) + }) + } }) diff --git a/test/e2e/app-dir/dynamic-io/dynamic-io.routes.test.ts b/test/e2e/app-dir/dynamic-io/dynamic-io.routes.test.ts index 089268c77d5838..da47ddd9b6cb58 100644 --- a/test/e2e/app-dir/dynamic-io/dynamic-io.routes.test.ts +++ b/test/e2e/app-dir/dynamic-io/dynamic-io.routes.test.ts @@ -48,14 +48,38 @@ describe('dynamic-io', () => { expect(json.value).toEqual('at runtime') expect(json.search).toEqual('?foo=bar') + + str = await next.render('/routes/-edge/dynamic-cookies', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toEqual('cookies') + + str = await next.render('/routes/-edge/dynamic-headers', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toEqual('headers') + + str = await next.render('/routes/-edge/dynamic-stream', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toEqual('dynamic stream') + + str = await next.render('/routes/-edge/dynamic-url?foo=bar', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.search).toEqual('?foo=bar') }) it('should prerender GET route handlers that have entirely cached io (fetches)', async () => { let str = await next.render('/routes/fetch-cached', {}) let json = JSON.parse(str) - const random1 = json.random1 - const random2 = json.random2 + let random1 = json.random1 + let random2 = json.random2 if (isNextDev) { expect(json.value).toEqual('at runtime') @@ -79,45 +103,67 @@ describe('dynamic-io', () => { expect(random1).toEqual(json.random1) expect(random2).toEqual(json.random2) } + + str = await next.render('/routes/-edge/fetch-cached', {}) + json = JSON.parse(str) + + random1 = json.random1 + random2 = json.random2 + + expect(json.value).toEqual('at runtime') + expect(typeof random1).toBe('string') + expect(typeof random2).toBe('string') + + str = await next.render('/routes/-edge/fetch-cached', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(random1).toEqual(json.random1) + expect(random2).toEqual(json.random2) }) it('should not prerender GET route handlers that have some uncached io (fetches)', async () => { let str = await next.render('/routes/fetch-mixed', {}) let json = JSON.parse(str) - const random1 = json.random1 - const random2 = json.random2 + let random1 = json.random1 + let random2 = json.random2 - if (isNextDev) { - expect(json.value).toEqual('at runtime') - expect(typeof random1).toBe('string') - expect(typeof random2).toBe('string') - } else { - expect(json.value).toEqual('at runtime') - expect(typeof random1).toBe('string') - expect(typeof random2).toBe('string') - } + expect(json.value).toEqual('at runtime') + expect(typeof random1).toBe('string') + expect(typeof random2).toBe('string') str = await next.render('/routes/fetch-mixed', {}) json = JSON.parse(str) - if (isNextDev) { - expect(json.value).toEqual('at runtime') - expect(random1).toEqual(json.random1) - expect(random2).not.toEqual(json.random2) - } else { - expect(json.value).toEqual('at runtime') - expect(random1).toEqual(json.random1) - expect(random2).not.toEqual(json.random2) - } + expect(json.value).toEqual('at runtime') + expect(random1).toEqual(json.random1) + expect(random2).not.toEqual(json.random2) + + str = await next.render('/routes/-edge/fetch-mixed', {}) + json = JSON.parse(str) + + random1 = json.random1 + random2 = json.random2 + + expect(json.value).toEqual('at runtime') + expect(typeof random1).toBe('string') + expect(typeof random2).toBe('string') + + str = await next.render('/routes/-edge/fetch-mixed', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(random1).toEqual(json.random1) + expect(random2).not.toEqual(json.random2) }) it('should prerender GET route handlers that have entirely cached io (unstable_cache)', async () => { let str = await next.render('/routes/io-cached', {}) let json = JSON.parse(str) - const message1 = json.message1 - const message2 = json.message2 + let message1 = json.message1 + let message2 = json.message2 if (isNextDev) { expect(json.value).toEqual('at runtime') @@ -141,37 +187,59 @@ describe('dynamic-io', () => { expect(message1).toEqual(json.message1) expect(message2).toEqual(json.message2) } + + str = await next.render('/routes/-edge/io-cached', {}) + json = JSON.parse(str) + + message1 = json.message1 + message2 = json.message2 + + expect(json.value).toEqual('at runtime') + expect(typeof message1).toBe('string') + expect(typeof message2).toBe('string') + + str = await next.render('/routes/-edge/io-cached', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(message1).toEqual(json.message1) + expect(message2).toEqual(json.message2) }) it('should not prerender GET route handlers that have some uncached io (unstable_cache)', async () => { let str = await next.render('/routes/io-mixed', {}) let json = JSON.parse(str) - const message1 = json.message1 - const message2 = json.message2 + let message1 = json.message1 + let message2 = json.message2 - if (isNextDev) { - expect(json.value).toEqual('at runtime') - expect(typeof message1).toBe('string') - expect(typeof message2).toBe('string') - } else { - expect(json.value).toEqual('at runtime') - expect(typeof message1).toBe('string') - expect(typeof message2).toBe('string') - } + expect(json.value).toEqual('at runtime') + expect(typeof message1).toBe('string') + expect(typeof message2).toBe('string') str = await next.render('/routes/io-mixed', {}) json = JSON.parse(str) - if (isNextDev) { - expect(json.value).toEqual('at runtime') - expect(message1).toEqual(json.message1) - expect(message2).not.toEqual(json.message2) - } else { - expect(json.value).toEqual('at runtime') - expect(message1).toEqual(json.message1) - expect(message2).not.toEqual(json.message2) - } + expect(json.value).toEqual('at runtime') + expect(message1).toEqual(json.message1) + expect(message2).not.toEqual(json.message2) + + str = await next.render('/routes/-edge/io-mixed', {}) + json = JSON.parse(str) + + message1 = json.message1 + message2 = json.message2 + + expect(json.value).toEqual('at runtime') + expect(typeof message1).toBe('string') + expect(typeof message2).toBe('string') + + str = await next.render('/routes/-edge/io-mixed', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(message1).toEqual(json.message1) + expect(message2).not.toEqual(json.message2) }) it('should prerender GET route handlers that complete synchronously or in a microtask', async () => { @@ -229,6 +297,38 @@ describe('dynamic-io', () => { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('string response') } + + // Edge versions are always dynamic + + str = await next.render('/routes/-edge/microtask', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('microtask') + + str = await next.render('/routes/-edge/static-stream-sync', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('stream response') + + str = await next.render('/routes/-edge/static-stream-async', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('stream response') + + str = await next.render('/routes/-edge/static-string-sync', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('string response') + + str = await next.render('/routes/-edge/static-string-async', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('string response') }) it('should not prerender GET route handlers that complete in a new Task', async () => { @@ -237,6 +337,12 @@ describe('dynamic-io', () => { expect(json.value).toEqual('at runtime') expect(json.message).toBe('task') + + str = await next.render('/routes/-edge/task', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.message).toBe('task') }) it('should prerender GET route handlers when accessing awaited params', async () => { @@ -270,6 +376,23 @@ describe('dynamic-io', () => { expect(json.param).toBe('2') expect(getLines('In route /routes/[dyn]')).toEqual([]) } + + // Edge versions are always dynamic + str = await next.render('/routes/-edge/1/async', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toBe('dynamic params') + expect(json.param).toBe('1') + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([]) + + str = await next.render('/routes/-edge/2/async', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toBe('dynamic params') + expect(json.param).toBe('2') + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([]) }) it('should prerender GET route handlers when accessing params without awaiting first', async () => { @@ -311,5 +434,38 @@ describe('dynamic-io', () => { expect(json.param).toBe('2') expect(getLines('In route /routes/[dyn]')).toEqual([]) } + + // Edge versions are always dynamic + str = await next.render('/routes/-edge/1/sync', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toBe('dynamic params') + expect(json.param).toBe('1') + if (isNextDev) { + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([ + expect.stringContaining( + 'a param property was accessed directly with `params.dyn`.' + ), + ]) + } else { + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([]) + } + + str = await next.render('/routes/-edge/2/sync', {}) + json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.type).toBe('dynamic params') + expect(json.param).toBe('2') + if (isNextDev) { + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([ + expect.stringContaining( + 'a param property was accessed directly with `params.dyn`.' + ), + ]) + } else { + expect(getLines('In route /routes/-edge/[dyn]')).toEqual([]) + } }) })