diff --git a/test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js similarity index 94% rename from test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js rename to test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js index 6b185f4514b5f..3c3966b5c18f9 100644 --- a/test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js +++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js @@ -1,4 +1,4 @@ -// 'use client' +'use client' import { unstable_after as after } from 'next/server' import { cliLog } from '../../utils/log' diff --git a/test/e2e/app-dir/next-after-app/app/static/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js similarity index 66% rename from test/e2e/app-dir/next-after-app/app/static/page.js rename to test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js index 80fafb550f11f..a2e1714170741 100644 --- a/test/e2e/app-dir/next-after-app/app/static/page.js +++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js @@ -1,12 +1,11 @@ import { unstable_after as after } from 'next/server' import { cliLog } from '../../utils/log' -// (patched in tests) -// export const dynamic = 'REPLACE_ME' +export const dynamic = 'error' export default function Index() { after(async () => { - cliLog({ source: '[page] /static' }) + cliLog({ source: '[page] /invalid-in-dynamic-error' }) }) return
Page with after()
} diff --git a/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js new file mode 100644 index 0000000000000..e3d86ec431c50 --- /dev/null +++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js @@ -0,0 +1,11 @@ +import { unstable_after as after } from 'next/server' +import { cliLog } from '../../utils/log' + +export const dynamic = 'force-static' + +export default function Index() { + after(async () => { + cliLog({ source: '[page] /invalid-in-dynamic-force-static' }) + }) + return
Page with after()
+} diff --git a/test/development/app-dir/next-after-app-invalid-usage/app/layout.js b/test/development/app-dir/next-after-app-invalid-usage/app/layout.js new file mode 100644 index 0000000000000..a55016aeb623b --- /dev/null +++ b/test/development/app-dir/next-after-app-invalid-usage/app/layout.js @@ -0,0 +1,10 @@ +export default function AppLayout({ children }) { + return ( + + + after + + {children} + + ) +} diff --git a/test/development/app-dir/next-after-app-invalid-usage/index.test.ts b/test/development/app-dir/next-after-app-invalid-usage/index.test.ts new file mode 100644 index 0000000000000..e9210b5326ed7 --- /dev/null +++ b/test/development/app-dir/next-after-app-invalid-usage/index.test.ts @@ -0,0 +1,50 @@ +/* eslint-env jest */ +import { nextTestSetup } from 'e2e-utils' +import * as Log from './utils/log' +import { + assertHasRedbox, + getRedboxDescription, + getRedboxSource, +} from '../../../lib/next-test-utils' + +describe('unstable_after() - invalid usages', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + const getLogs = () => { + if (next.cliOutput.length < currentCliOutputIndex) { + // cliOutput shrank since we started the test, so something (like a `sandbox`) reset the logs + currentCliOutputIndex = 0 + } + return Log.readCliLogs(next.cliOutput.slice(currentCliOutputIndex)) + } + + it.each(['error', 'force-static'])( + `errors at compile time with dynamic = "%s"`, + async (dynamicValue) => { + const pathname = '/invalid-in-dynamic-' + dynamicValue + const session = await next.browser(pathname) + + await assertHasRedbox(session) + expect(await getRedboxDescription(session)).toContain( + `Route ${pathname} with \`dynamic = "${dynamicValue}"\` couldn't be rendered statically because it used \`unstable_after\`` + ) + expect(getLogs()).toHaveLength(0) + } + ) + it('errors at compile time when used in a client module', async () => { + const session = await next.browser('/invalid-in-client') + + await assertHasRedbox(session) + expect(await getRedboxSource(session)).toMatch( + /You're importing a component that needs "?unstable_after"?\. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component\./ + ) + expect(getLogs()).toHaveLength(0) + }) +}) diff --git a/test/development/app-dir/next-after-app-invalid-usage/next.config.js b/test/development/app-dir/next-after-app-invalid-usage/next.config.js new file mode 100644 index 0000000000000..ec0f3bcc9dad4 --- /dev/null +++ b/test/development/app-dir/next-after-app-invalid-usage/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + experimental: { + after: true, + }, +} diff --git a/test/development/app-dir/next-after-app-invalid-usage/utils/log.js b/test/development/app-dir/next-after-app-invalid-usage/utils/log.js new file mode 100644 index 0000000000000..7811c609528ca --- /dev/null +++ b/test/development/app-dir/next-after-app-invalid-usage/utils/log.js @@ -0,0 +1,16 @@ +export function cliLog(/** @type {string | Record} */ data) { + console.log('' + JSON.stringify(data) + '') +} + +export function readCliLogs(/** @type {string} */ output) { + return output + .split('\n') + .map((line) => { + const match = line.match(/^(?.+?)<\/test-log>$/) + if (!match) { + return null + } + return JSON.parse(match.groups.value) + }) + .filter(Boolean) +} diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js new file mode 100644 index 0000000000000..a3fef8fa1ba42 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/[id]/dynamic/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js new file mode 100644 index 0000000000000..347eaad656059 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js @@ -0,0 +1 @@ +export { default } from '../../nodejs/[id]/layout' diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js new file mode 100644 index 0000000000000..7d4b31e8d13d5 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/[id]/setting-cookies/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js new file mode 100644 index 0000000000000..6cdab36d3a33e --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/[id]/with-action/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js new file mode 100644 index 0000000000000..7516c6ada37d1 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js @@ -0,0 +1,4 @@ +export { + default, + generateMetadata, +} from '../../../nodejs/[id]/with-metadata/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/delay/page.js b/test/e2e/app-dir/next-after-app/app/edge/delay/page.js new file mode 100644 index 0000000000000..95d978bf462c9 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/delay/page.js @@ -0,0 +1,3 @@ +export { default } from '../../nodejs/delay/page' + +export const dynamic = 'force-dynamic' diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js new file mode 100644 index 0000000000000..76c3dd48baf60 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/interrupted/calls-not-found/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js new file mode 100644 index 0000000000000..28367a391dfb8 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js @@ -0,0 +1,5 @@ +import { createPage } from '../../../nodejs/interrupted/calls-redirect/page' + +// NOTE: this page is forked from /nodejs + +export default createPage('/edge') diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js new file mode 100644 index 0000000000000..bdf37ff651bba --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/interrupted/redirect-target/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js new file mode 100644 index 0000000000000..5f2caf9a6e225 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/interrupted/throws-error/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/layout.js b/test/e2e/app-dir/next-after-app/app/edge/layout.js new file mode 100644 index 0000000000000..b19ff8efbdce8 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/layout.js @@ -0,0 +1,3 @@ +export { default } from '../nodejs/layout' + +export const runtime = 'edge' diff --git a/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js b/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js new file mode 100644 index 0000000000000..c5a458e6e3fe1 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js @@ -0,0 +1 @@ +export { default } from '../../../nodejs/middleware/redirect/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js b/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js new file mode 100644 index 0000000000000..549fd5b9e0ab5 --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js @@ -0,0 +1 @@ +export { default } from '../../nodejs/nested-after/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js b/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js new file mode 100644 index 0000000000000..a38874129d7ee --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js @@ -0,0 +1 @@ +export { default } from '../../nodejs/provided-request-context/page' diff --git a/test/e2e/app-dir/next-after-app/app/edge/route/route.js b/test/e2e/app-dir/next-after-app/app/edge/route/route.js new file mode 100644 index 0000000000000..8426f52cb33cd --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/edge/route/route.js @@ -0,0 +1,4 @@ +export { GET } from '../../nodejs/route/route' + +export const runtime = 'edge' +export const dynamic = 'force-dynamic' diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js deleted file mode 100644 index 29eb6d9d15f7e..0000000000000 --- a/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from 'next/navigation' -import { unstable_after as after } from 'next/server' -import { cliLog } from '../../../utils/log' - -export default function Page() { - after(() => { - cliLog({ - source: '[page] /interrupted/calls-redirect', - }) - }) - redirect('/interrupted/redirect-target') -} diff --git a/test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js similarity index 93% rename from test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js index 3794116ed4e8a..1903f5ce67113 100644 --- a/test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js @@ -1,6 +1,6 @@ import { unstable_after as after } from 'next/server' import { cache } from 'react' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' import { headers } from 'next/headers' const thing = cache(() => Symbol('cache me please')) diff --git a/test/e2e/app-dir/next-after-app/app/[id]/layout.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js similarity index 81% rename from test/e2e/app-dir/next-after-app/app/[id]/layout.js rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js index 9c589f027344d..9bfd4d7040361 100644 --- a/test/e2e/app-dir/next-after-app/app/[id]/layout.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js @@ -1,5 +1,5 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../utils/log' +import { cliLog } from '../../../utils/log' export default function Layout({ children }) { after(async () => { diff --git a/test/e2e/app-dir/next-after-app/app/[id]/setting-cookies/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/setting-cookies/page.js similarity index 100% rename from test/e2e/app-dir/next-after-app/app/[id]/setting-cookies/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/setting-cookies/page.js diff --git a/test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js similarity index 95% rename from test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js index 87108571b308f..37a098c12c2f8 100644 --- a/test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js @@ -1,6 +1,6 @@ import { unstable_after as after } from 'next/server' import { cache } from 'react' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' import { headers } from 'next/headers' const thing = cache(() => Symbol('cache me please')) diff --git a/test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js similarity index 87% rename from test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js index 8209df109face..696440e417d23 100644 --- a/test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js @@ -1,5 +1,5 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' export function generateMetadata({ params }) { after(() => { diff --git a/test/e2e/app-dir/next-after-app/app/delay/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js similarity index 94% rename from test/e2e/app-dir/next-after-app/app/delay/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js index bf832369c558e..a168846557490 100644 --- a/test/e2e/app-dir/next-after-app/app/delay/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js @@ -1,6 +1,6 @@ import { Suspense } from 'react' import { unstable_after as after } from 'next/server' -import { cliLog } from '../../utils/log' +import { cliLog } from '../../../utils/log' export const dynamic = 'force-dynamic' diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js similarity index 83% rename from test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js index 19fb589c8e24e..9ac1c2e1881bc 100644 --- a/test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js @@ -1,6 +1,6 @@ import { notFound } from 'next/navigation' import { unstable_after as after } from 'next/server' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' export default function Page() { after(() => { diff --git a/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js new file mode 100644 index 0000000000000..01e5824710f9a --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js @@ -0,0 +1,19 @@ +import { unstable_after as after } from 'next/server' +import { redirect } from 'next/navigation' +import { cliLog } from '../../../../utils/log' + +// NOTE: this page is forked in /edge + +export function createPage(pathPrefix) { + return function Page() { + after(() => { + cliLog({ + source: '[page] /interrupted/calls-redirect', + }) + }) + + redirect(pathPrefix + '/interrupted/redirect-target') + } +} + +export default createPage('/nodejs') diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js similarity index 81% rename from test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js index 09aea952148c6..fbf0aceaa1ad5 100644 --- a/test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js @@ -1,5 +1,5 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' export default function Page() { after(() => { diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js similarity index 81% rename from test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js index 2eee04cf2463b..ff7c1f5ff71d5 100644 --- a/test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js @@ -1,5 +1,5 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../../utils/log' +import { cliLog } from '../../../../utils/log' export default function Page() { after(() => { diff --git a/test/e2e/app-dir/next-after-app/app/layout.js b/test/e2e/app-dir/next-after-app/app/nodejs/layout.js similarity index 74% rename from test/e2e/app-dir/next-after-app/app/layout.js rename to test/e2e/app-dir/next-after-app/app/nodejs/layout.js index bece157b43025..3b3d1f37a51a0 100644 --- a/test/e2e/app-dir/next-after-app/app/layout.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/layout.js @@ -1,5 +1,4 @@ -// (patched in tests) -// export const runtime = 'REPLACE_ME' +export const runtime = 'nodejs' export default function AppLayout({ children }) { return ( diff --git a/test/e2e/app-dir/next-after-app/app/middleware/redirect/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/middleware/redirect/page.js similarity index 100% rename from test/e2e/app-dir/next-after-app/app/middleware/redirect/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/middleware/redirect/page.js diff --git a/test/e2e/app-dir/next-after-app/app/nested-after/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js similarity index 97% rename from test/e2e/app-dir/next-after-app/app/nested-after/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js index 39b1dbaffe49c..62b8460eb56cf 100644 --- a/test/e2e/app-dir/next-after-app/app/nested-after/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js @@ -1,6 +1,6 @@ import { unstable_after as after } from 'next/server' import { cache } from 'react' -import { cliLog } from '../../utils/log' +import { cliLog } from '../../../utils/log' import { headers } from 'next/headers' const thing = cache(() => Symbol('cache me please')) diff --git a/test/e2e/app-dir/next-after-app/app/provided-request-context/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js similarity index 83% rename from test/e2e/app-dir/next-after-app/app/provided-request-context/page.js rename to test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js index 4650954dec10b..b984c265810d5 100644 --- a/test/e2e/app-dir/next-after-app/app/provided-request-context/page.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js @@ -1,5 +1,5 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../utils/log' +import { cliLog } from '../../../utils/log' export default function Page() { after(() => { diff --git a/test/e2e/app-dir/next-after-app/app/route/route.js b/test/e2e/app-dir/next-after-app/app/nodejs/route/route.js similarity index 72% rename from test/e2e/app-dir/next-after-app/app/route/route.js rename to test/e2e/app-dir/next-after-app/app/nodejs/route/route.js index e8c6290fc9011..7ebdad2dd54ea 100644 --- a/test/e2e/app-dir/next-after-app/app/route/route.js +++ b/test/e2e/app-dir/next-after-app/app/nodejs/route/route.js @@ -1,9 +1,7 @@ import { unstable_after as after } from 'next/server' -import { cliLog } from '../../utils/log' - -// (patched in tests) -// export const runtime = 'REPLACE_ME' +import { cliLog } from '../../../utils/log' +export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export async function GET() { diff --git a/test/e2e/app-dir/next-after-app/index.test.ts b/test/e2e/app-dir/next-after-app/index.test.ts index 4fdc5d7dc1b03..efcba7aaa0c1d 100644 --- a/test/e2e/app-dir/next-after-app/index.test.ts +++ b/test/e2e/app-dir/next-after-app/index.test.ts @@ -4,57 +4,19 @@ import { retry } from 'next-test-utils' import { createProxyServer } from 'next/experimental/testmode/proxy' import { outdent } from 'outdent' import { sandbox } from '../../../lib/development-sandbox' -import * as fs from 'fs' -import * as path from 'path' -import * as os from 'os' import * as Log from './utils/log' const runtimes = ['nodejs', 'edge'] describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { - const logFileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logs-')) - const logFile = path.join(logFileDir, 'logs.jsonl') - - const { next, isNextDev, isNextDeploy, skipped } = nextTestSetup({ + const { next, isNextDeploy, skipped } = nextTestSetup({ files: __dirname, // `patchFile` and reading runtime logs are not supported in a deployed environment skipDeployment: true, - env: { - PERSISTENT_LOG_FILE: logFile, - }, }) if (skipped) return - - { - const originalContents: Record = {} - - beforeAll(async () => { - const placeholder = `// export const runtime = 'REPLACE_ME'` - - const filesToPatch = ['app/layout.js', 'app/route/route.js'] - - for (const file of filesToPatch) { - await next.patchFile(file, (contents) => { - if (!contents.includes(placeholder)) { - throw new Error(`Placeholder "${placeholder}" not found in ${file}`) - } - originalContents[file] = contents - - return contents.replace( - placeholder, - `export const runtime = '${runtimeValue}'` - ) - }) - } - }) - - afterAll(async () => { - for (const [file, contents] of Object.entries(originalContents)) { - await next.patchFile(file, contents) - } - }) - } + const pathPrefix = '/' + runtimeValue let currentCliOutputIndex = 0 beforeEach(() => { @@ -70,7 +32,8 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { } it('runs in dynamic pages', async () => { - await next.render('/123/dynamic') + const response = await next.fetch(pathPrefix + '/123/dynamic') + expect(response.status).toBe(200) await retry(() => { expect(getLogs()).toContainEqual({ source: '[layout] /[id]' }) expect(getLogs()).toContainEqual({ @@ -85,7 +48,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { }) it('runs in dynamic route handlers', async () => { - const res = await next.fetch('/route') + const res = await next.fetch(pathPrefix + '/route') expect(res.status).toBe(200) await retry(() => { expect(getLogs()).toContainEqual({ source: '[route handler] /route' }) @@ -93,7 +56,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { }) it('runs in server actions', async () => { - const browser = await next.browser('/123/with-action') + const browser = await next.browser(pathPrefix + '/123/with-action') expect(getLogs()).toContainEqual({ source: '[layout] /[id]', }) @@ -114,7 +77,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { }) it('runs callbacks from nested unstable_after calls', async () => { - await next.browser('/nested-after') + await next.browser(pathPrefix + '/nested-after') await retry(() => { for (const id of [1, 2, 3]) { @@ -131,7 +94,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { describe('interrupted RSC renders', () => { it('runs callbacks if redirect() was called', async () => { - await next.browser('/interrupted/calls-redirect') + await next.browser(pathPrefix + '/interrupted/calls-redirect') await retry(() => { expect(getLogs()).toContainEqual({ @@ -144,14 +107,14 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { }) it('runs callbacks if notFound() was called', async () => { - await next.browser('/interrupted/calls-not-found') + await next.browser(pathPrefix + '/interrupted/calls-not-found') expect(getLogs()).toContainEqual({ source: '[page] /interrupted/calls-not-found', }) }) it('runs callbacks if a user error was thrown in the RSC render', async () => { - await next.browser('/interrupted/throws-error') + await next.browser(pathPrefix + '/interrupted/throws-error') expect(getLogs()).toContainEqual({ source: '[page] /interrupted/throws-error', }) @@ -161,7 +124,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { it('runs in middleware', async () => { const requestId = `${Date.now()}` const res = await next.fetch( - `/middleware/redirect-source?requestId=${requestId}`, + pathPrefix + `/middleware/redirect-source?requestId=${requestId}`, { redirect: 'follow', headers: { @@ -183,10 +146,19 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { if (!isNextDeploy) { it('only runs callbacks after the response is fully sent', async () => { const pageStartedFetching = promiseWithResolvers() + pageStartedFetching.promise.catch(() => {}) const shouldSendResponse = promiseWithResolvers() + shouldSendResponse.promise.catch(() => {}) + const abort = (error: Error) => { - pageStartedFetching.reject(error) - shouldSendResponse.reject(error) + pageStartedFetching.reject( + new Error('pageStartedFetching was aborted', { cause: error }) + ) + shouldSendResponse.reject( + new Error('shouldSendResponse was aborted', { + cause: error, + }) + ) } const proxyServer = await createProxyServer({ @@ -200,25 +172,30 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { }) try { - const pendingReq = next.fetch('/delay', { - headers: { 'Next-Test-Proxy-Port': String(proxyServer.port) }, - }) - - pendingReq.then( - async (res) => { - if (res.status !== 200) { - const msg = `Got non-200 response (${res.status}), aborting` - console.error(msg + '\n', await res.text()) - abort(new Error(msg)) + const pendingReq = next + .fetch(pathPrefix + '/delay', { + headers: { 'Next-Test-Proxy-Port': String(proxyServer.port) }, + }) + .then( + async (res) => { + if (res.status !== 200) { + const err = new Error( + `Got non-200 response (${res.status}) for ${res.url}, aborting` + ) + abort(err) + throw err + } + return res + }, + (err) => { + abort(err) + throw err } - }, - (err) => { - abort(err) - } - ) + ) await Promise.race([ pageStartedFetching.promise, + pendingReq, // if the page throws before it starts fetching, we want to catch that timeoutPromise( 10_000, 'Timeout while waiting for the page to call fetch' @@ -253,7 +230,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { } it('runs in generateMetadata()', async () => { - await next.browser('/123/with-metadata') + await next.browser(pathPrefix + '/123/with-metadata') expect(getLogs()).toContainEqual({ source: '[metadata] /[id]/with-metadata', value: '123', @@ -264,7 +241,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { const EXPECTED_ERROR = /An error occurred in a function passed to `unstable_after\(\)`: .+?: Cookies can only be modified in a Server Action or Route Handler\./ - const browser = await next.browser('/123/setting-cookies') + const browser = await next.browser(pathPrefix + '/123/setting-cookies') // after() from render expect(next.cliOutput).toMatch(EXPECTED_ERROR) @@ -302,7 +279,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { `, ], ]), - '/provided-request-context' + pathPrefix + '/provided-request-context' ) try { @@ -319,67 +296,6 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { await cleanup() } }) - - if (isNextDev) { - // TODO: these are at the end because they destroy the next server. - // is there a cleaner way to do this without making the tests slower? - - describe('invalid usages', () => { - it.each(['error', 'force-static'])( - `errors at compile time with dynamic = "%s"`, - async (dynamicValue) => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/static/page.js', - (await next.readFile('app/static/page.js')).replace( - `// export const dynamic = 'REPLACE_ME'`, - `export const dynamic = '${dynamicValue}'` - ), - ], - ]), - '/static' - ) - - try { - await session.assertHasRedbox() - expect(await session.getRedboxDescription()).toContain( - `Route /static with \`dynamic = "${dynamicValue}"\` couldn't be rendered statically because it used \`unstable_after\`` - ) - expect(getLogs()).toHaveLength(0) - } finally { - await cleanup() - } - } - ) - - it('errors at compile time when used in a client module', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/invalid-in-client/page.js', - (await next.readFile('app/invalid-in-client/page.js')).replace( - `// 'use client'`, - `'use client'` - ), - ], - ]), - '/invalid-in-client' - ) - try { - await session.assertHasRedbox() - expect(await session.getRedboxSource(true)).toMatch( - /You're importing a component that needs "?unstable_after"?\. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component\./ - ) - expect(getLogs()).toHaveLength(0) - } finally { - await cleanup() - } - }) - }) - } }) function promiseWithResolvers() { diff --git a/test/e2e/app-dir/next-after-app/middleware.js b/test/e2e/app-dir/next-after-app/middleware.js index 6a1c60397228b..f18a04fb0943f 100644 --- a/test/e2e/app-dir/next-after-app/middleware.js +++ b/test/e2e/app-dir/next-after-app/middleware.js @@ -6,7 +6,12 @@ export function middleware( /** @type {import ('next/server').NextRequest} */ request ) { const url = new URL(request.url) - if (url.pathname.startsWith('/middleware/redirect-source')) { + + const match = url.pathname.match( + /^(?\/[^/]+?)\/middleware\/redirect-source/ + ) + if (match) { + const prefix = match.groups.prefix const requestId = url.searchParams.get('requestId') after(() => { cliLog({ @@ -15,10 +20,12 @@ export function middleware( cookies: { testCookie: cookies().get('testCookie')?.value }, }) }) - return NextResponse.redirect(new URL('/middleware/redirect', request.url)) + return NextResponse.redirect( + new URL(prefix + '/middleware/redirect', request.url) + ) } } export const config = { - matcher: '/middleware/:path*', + matcher: ['/:prefix/middleware/:path*'], } diff --git a/test/e2e/app-dir/next-after-pages/index.test.ts b/test/e2e/app-dir/next-after-pages/index.test.ts index ac7b5100b089b..960656a036abc 100644 --- a/test/e2e/app-dir/next-after-pages/index.test.ts +++ b/test/e2e/app-dir/next-after-pages/index.test.ts @@ -1,23 +1,14 @@ /* eslint-env jest */ import { nextTestSetup, isNextDev } from 'e2e-utils' import { assertHasRedbox, getRedboxSource, retry } from 'next-test-utils' -import * as fs from 'fs' -import * as path from 'path' -import * as os from 'os' import * as Log from './utils/log' // using unstable_after is a compile-time error in build mode. const _describe = isNextDev ? describe : describe.skip _describe('unstable_after() - pages', () => { - const logFileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logs-')) - const logFile = path.join(logFileDir, 'logs.jsonl') - const { next } = nextTestSetup({ files: __dirname, - env: { - PERSISTENT_LOG_FILE: logFile, - }, }) let currentCliOutputIndex = 0 diff --git a/test/lib/development-sandbox.ts b/test/lib/development-sandbox.ts index af73eeb79cae5..1462f49a39293 100644 --- a/test/lib/development-sandbox.ts +++ b/test/lib/development-sandbox.ts @@ -33,7 +33,7 @@ export function waitForHydration(browser: BrowserInterface): Promise { export async function sandbox( next: NextInstance, - initialFiles?: Map, + initialFiles?: Map string)>, initialUrl: string = '/', webDriverOptions: any = undefined ) {