From 7092266076f1633f37e16f1b9a07f8192792ebba Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Wed, 3 May 2023 20:10:17 +0200 Subject: [PATCH] Add support for app global and segment 404 pages (#49085) See https://github.com/vercel/turbo/pull/4776 This adds support for: * Default and custom global app 404 pages (`app/not-found`). * Segment-level 404 pages (`app/segment/not-found`). This also updates Turbopack: * https://github.com/vercel/turbo/pull/4787 * https://github.com/vercel/turbo/pull/4789 * https://github.com/vercel/turbo/pull/4776 * https://github.com/vercel/turbo/pull/4790 ## TODO: - [ ] ~~The dev overlay shows up when `notFound()` is called, it should be hidden~~ (moved to WEB-980) - [ ] ~~Navigating to the global 404 page doesn't work~~ (this is a bug in Next.js, see NEXT-963) - [x] Tests. --------- Co-authored-by: Tobias Koppers --- Cargo.lock | 68 ++++----- Cargo.toml | 6 +- .../next-swc/crates/next-core/js/package.json | 4 +- .../next-core/js/src/entry/app-renderer.tsx | 13 +- .../crates/next-core/js/src/internal/http.ts | 28 +++- .../js/src/internal/page-server-handler.tsx | 20 +-- .../crates/next-core/src/app_source.rs | 70 ++++++++- .../crates/next-core/src/app_structure.rs | 4 +- .../next-swc/crates/next-core/src/manifest.rs | 18 +-- .../crates/next-core/src/page_loader.rs | 13 +- .../crates/next-core/src/page_source.rs | 137 +++++++++-------- .../next-swc/crates/next-core/src/util.rs | 20 +-- .../next-dev-tests/test-harness/harness.ts | 120 ++++++++++----- .../next-dev-tests/test-harness/hooks.ts | 20 +++ .../next-dev-tests/test-harness/package.json | 2 +- .../next-dev-tests/tests/integration.rs | 2 +- .../next/api/basic/input/pages/index.tsx | 9 +- .../next/app/404-custom/input/app/layout.tsx | 7 + .../input/app/link-segment/page.tsx | 14 ++ .../app/404-custom/input/app/link/page.tsx | 14 ++ .../app/404-custom/input/app/not-found.tsx | 3 + .../next/app/404-custom/input/app/page.tsx | 5 + .../input/app/segment/not-found.tsx | 3 + .../app/404-custom/input/app/segment/page.tsx | 5 + .../next/app/404-custom/input/app/test.tsx | 106 +++++++++++++ .../next/app/404-custom/input/next.config.js | 5 + .../next/app/404-default/input/app/layout.tsx | 7 + .../404-default/input/app/link/page.tsx} | 9 +- .../next/app/404-default/input/app/page.tsx | 5 + .../next/app/404-default/input/app/test.tsx | 62 ++++++++ .../next/app/404-default/input/next.config.js | 5 + .../__flakey__/metadata/input/app/test.tsx | 25 ++-- .../async-local-storage/input/app/test.tsx | 10 +- .../next/app/basic/input/app/test.tsx | 11 +- .../next/app/force-dynamic/input/app/test.tsx | 11 +- .../app/implicit-metadata/input/app/test.tsx | 139 +++++++++--------- .../app/named-client-comp/input/app/test.tsx | 9 +- .../next/app/route-WEB-869/input/app/test.tsx | 23 ++- .../next/app/route/input/app/test.tsx | 7 +- .../app/rsc-NEXT-657/input/src/app/page.jsx | 5 +- .../next/app/use-server/input/app/test.tsx | 29 ++-- ...o__use server__quo__) are not -918bb2.txt} | 2 +- .../basic/swc-helpers/input/pages/index.js | 7 +- .../next/css/deduplication/input/pages/a.tsx | 8 +- .../next/css/deduplication/input/pages/b.tsx | 8 +- .../css/deduplication/input/pages/index.tsx | 12 +- .../next/dynamic/no-ssr/input/pages/index.js | 6 +- .../next/dynamic/ssr/input/pages/index.js | 6 +- .../next/env/basic/input/pages/index.js | 7 +- .../next/error/ssr/input/pages/index.tsx | 14 +- .../externals/cjs-in-esm/input/pages/index.js | 7 +- .../at-next-font/input/pages/index.js | 7 +- .../font-google/basic/input/pages/index.js | 7 +- .../next/image/basic/input/pages/index.js | 7 +- .../image/remotepattern/input/pages/index.js | 7 +- .../transpilePackages/input/pages/index.js | 7 +- .../404-navigate}/input/pages/_error.tsx | 8 +- .../404-navigate}/input/pages/index.tsx | 12 +- .../pages/404-navigate/input/pages/link.tsx | 12 ++ .../404-navigate}/input/pages/not-found.tsx | 0 .../next/polyfill/basic/input/pages/index.tsx | 14 +- .../input/pages/[...segments].tsx | 7 +- .../dynamic-params/input/pages/[segment].tsx | 7 +- .../next/router/headers/input/pages/index.js | 7 +- .../middleware-api-fetch/input/pages/index.js | 7 +- .../middleware-env/input/pages/index.js | 7 +- .../router/middleware/input/pages/index.js | 7 +- .../next/router/redirect/input/pages/index.js | 7 +- .../next/router/rewrite/input/pages/foo.js | 7 +- .../next/tailwind/basic/input/pages/index.jsx | 7 +- .../auto-babel-loader/input/pages/index.ts | 7 +- .../basic-options/input/pages/index.js | 7 +- .../emitted-errors/input/pages/index.js | 7 +- .../no-options/input/pages/index.js | 7 +- pnpm-lock.yaml | 33 ++--- 75 files changed, 809 insertions(+), 546 deletions(-) create mode 100644 packages/next-swc/crates/next-dev-tests/test-harness/hooks.ts create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/layout.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link-segment/page.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link/page.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/not-found.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/page.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/not-found.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/page.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/test.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/next.config.js create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-default/input/app/layout.tsx rename packages/next-swc/crates/next-dev-tests/tests/integration/next/{404/navigate/input/pages/link.tsx => app/404-default/input/app/link/page.tsx} (54%) create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-default/input/app/page.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-default/input/app/test.tsx create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-default/input/next.config.js rename packages/next-swc/crates/next-dev-tests/tests/integration/next/app/use-server/issues/{Server actions (__quo__use server__quo__) are not -36fc7c.txt => Server actions (__quo__use server__quo__) are not -918bb2.txt} (93%) rename packages/next-swc/crates/next-dev-tests/tests/integration/next/{404/navigate => pages/404-navigate}/input/pages/_error.tsx (58%) rename packages/next-swc/crates/next-dev-tests/tests/integration/next/{404/navigate => pages/404-navigate}/input/pages/index.tsx (82%) create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/pages/404-navigate/input/pages/link.tsx rename packages/next-swc/crates/next-dev-tests/tests/integration/next/{404/navigate => pages/404-navigate}/input/pages/not-found.tsx (100%) diff --git a/Cargo.lock b/Cargo.lock index ac053e7a4a47f..f37195dc1abea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,7 +400,7 @@ dependencies = [ [[package]] name = "auto-hash-map" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "serde", ] @@ -3413,7 +3413,7 @@ dependencies = [ [[package]] name = "node-file-trace" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "serde", @@ -6980,7 +6980,7 @@ dependencies = [ [[package]] name = "turbo-binding" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "auto-hash-map", "mdxjs", @@ -7020,7 +7020,7 @@ dependencies = [ [[package]] name = "turbo-malloc" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "mimalloc", ] @@ -7028,7 +7028,7 @@ dependencies = [ [[package]] name = "turbo-tasks" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "auto-hash-map", @@ -7058,7 +7058,7 @@ dependencies = [ [[package]] name = "turbo-tasks-build" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "cargo-lock", @@ -7070,7 +7070,7 @@ dependencies = [ [[package]] name = "turbo-tasks-bytes" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "bytes", @@ -7085,7 +7085,7 @@ dependencies = [ [[package]] name = "turbo-tasks-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "dotenvy", @@ -7099,7 +7099,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fetch" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "indexmap", @@ -7116,7 +7116,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fs" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "auto-hash-map", @@ -7145,7 +7145,7 @@ dependencies = [ [[package]] name = "turbo-tasks-hash" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "base16", "hex", @@ -7157,7 +7157,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -7171,7 +7171,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros-shared" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "proc-macro2", "quote", @@ -7181,7 +7181,7 @@ dependencies = [ [[package]] name = "turbo-tasks-memory" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "auto-hash-map", @@ -7203,7 +7203,7 @@ dependencies = [ [[package]] name = "turbo-tasks-testing" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "auto-hash-map", @@ -7215,7 +7215,7 @@ dependencies = [ [[package]] name = "turbopack" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-recursion", @@ -7244,7 +7244,7 @@ dependencies = [ [[package]] name = "turbopack-bench" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "chromiumoxide", @@ -7274,7 +7274,7 @@ dependencies = [ [[package]] name = "turbopack-cli-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "clap 4.1.11", @@ -7291,7 +7291,7 @@ dependencies = [ [[package]] name = "turbopack-core" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-trait", @@ -7318,7 +7318,7 @@ dependencies = [ [[package]] name = "turbopack-create-test-app" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "clap 4.1.11", @@ -7331,7 +7331,7 @@ dependencies = [ [[package]] name = "turbopack-css" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-trait", @@ -7353,7 +7353,7 @@ dependencies = [ [[package]] name = "turbopack-dev" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "indexmap", @@ -7374,7 +7374,7 @@ dependencies = [ [[package]] name = "turbopack-dev-server" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-compression", @@ -7408,7 +7408,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-trait", @@ -7444,7 +7444,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-plugins" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "serde", @@ -7458,7 +7458,7 @@ dependencies = [ [[package]] name = "turbopack-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "indexmap", @@ -7474,7 +7474,7 @@ dependencies = [ [[package]] name = "turbopack-image" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "base64 0.21.0", @@ -7494,7 +7494,7 @@ dependencies = [ [[package]] name = "turbopack-json" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "serde", @@ -7509,7 +7509,7 @@ dependencies = [ [[package]] name = "turbopack-mdx" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "mdxjs", @@ -7524,7 +7524,7 @@ dependencies = [ [[package]] name = "turbopack-node" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "async-stream", @@ -7558,7 +7558,7 @@ dependencies = [ [[package]] name = "turbopack-static" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "serde", @@ -7574,7 +7574,7 @@ dependencies = [ [[package]] name = "turbopack-swc-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "swc_core", "turbo-tasks", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "turbopack-test-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230502.4#4f6069c14b5fcb33fc5574c69fcfe3b6c0194b5c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230503.2#bdf96aa38092d63e5324f04cef95a832c6b18c58" dependencies = [ "anyhow", "once_cell", @@ -7603,7 +7603,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "rand", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 0db0465601bda..5bc69dd607794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,11 +42,11 @@ swc_relay = { version = "0.2.7" } testing = { version = "0.33.6" } # Turbo crates -turbo-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230502.4" } +turbo-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230503.2" } # [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros.. -turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230502.4" } +turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230503.2" } # [TODO]: need to refactor embed_directory! macro usage in next-core -turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230502.4" } +turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230503.2" } # General Deps diff --git a/packages/next-swc/crates/next-core/js/package.json b/packages/next-swc/crates/next-core/js/package.json index f49dac474e2c4..c406e0fa87eee 100644 --- a/packages/next-swc/crates/next-core/js/package.json +++ b/packages/next-swc/crates/next-core/js/package.json @@ -10,8 +10,8 @@ "check": "tsc --noEmit" }, "dependencies": { - "@vercel/turbopack-dev": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-dev/js?turbopack-230502.4", - "@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230502.4", + "@vercel/turbopack-dev": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-dev/js?turbopack-230503.2", + "@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230503.2", "anser": "^2.1.1", "css.escape": "^1.5.1", "next": "*", diff --git a/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx b/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx index 795621d619d27..fdbea07b20562 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx +++ b/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx @@ -15,7 +15,7 @@ declare global { } import type { Ipc } from '@vercel/turbopack-node/ipc/index' -import type { IncomingMessage, ServerResponse } from 'node:http' +import type { IncomingMessage } from 'node:http' import type { ClientCSSReferenceManifest, ClientReferenceManifest, @@ -25,13 +25,13 @@ import type { RenderOpts } from 'next/dist/server/app-render/types' import { renderToHTMLOrFlight } from 'next/dist/server/app-render/app-render' import { RSC_VARY_HEADER } from 'next/dist/client/components/app-router-headers' -import { ServerResponseShim } from '../internal/http' import { headersFromEntries } from '../internal/headers' import { parse, ParsedUrlQuery } from 'node:querystring' import { PassThrough } from 'node:stream' ;('TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }') // @ts-ignore import layoutEntry from './app/layout-entry' +import { createServerResponse } from '../internal/http' globalThis.__next_require__ = (data) => { const [, , ssr_id] = JSON.parse(data) @@ -91,7 +91,7 @@ const MIME_TEXT_HTML_UTF8 = 'text/html; charset=utf-8' ipc.send({ type: 'headers', data: { - status: 200, + status: result.statusCode, headers: result.headers, }, }) @@ -124,8 +124,6 @@ type LoaderTree = [ ] async function runOperation(renderData: RenderData) { - let tree: LoaderTree = LOADER_TREE - const proxyMethodsForModule = ( id: string ): ProxyHandler => { @@ -251,7 +249,9 @@ async function runOperation(renderData: RenderData) { method: renderData.method, headers: headersFromEntries(renderData.rawHeaders), } as any - const res: ServerResponse = new ServerResponseShim(req) as any + + const res = createServerResponse(req, renderData.path) + const query = parse(renderData.rawQuery) const renderOpt: Omit< RenderOpts, @@ -304,6 +304,7 @@ async function runOperation(renderData: RenderData) { body.write(result.toUnchunkedString()) } return { + statusCode: res.statusCode, headers: [ ['Content-Type', result.contentType() ?? MIME_TEXT_HTML_UTF8], ['Vary', RSC_VARY_HEADER], diff --git a/packages/next-swc/crates/next-core/js/src/internal/http.ts b/packages/next-swc/crates/next-core/js/src/internal/http.ts index a9c96e7b1fd80..ac88b8238a673 100644 --- a/packages/next-swc/crates/next-core/js/src/internal/http.ts +++ b/packages/next-swc/crates/next-core/js/src/internal/http.ts @@ -5,7 +5,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http' * * @type {ServerResponse} */ -export class ServerResponseShim { +class ServerResponseShim { headersSent = false #headers: Map> = new Map() #statusCode: number = 200 @@ -112,3 +112,29 @@ export class ServerResponseShim { throw new Error('writeProcessing is not implemented') } } + +function getStatusCodeForPath(pathname: string): number { + if (pathname === '/404' || pathname === '/_error') { + return 404 + } + + return 200 +} + +/** + * Creates a `ServerResponse` object for a given request and pathname. + */ +export function createServerResponse( + req: IncomingMessage, + pathname: string +): ServerResponse { + const statusCode = getStatusCodeForPath(pathname) + + const res = new ServerResponseShim(req) as any + + // For pages, setting the status code on the response object is necessary for + // `Error.getInitialProps` to detect the status code. + res.statusCode = statusCode + + return res as ServerResponse +} diff --git a/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx b/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx index 9086d0ad8d431..42408905ad417 100644 --- a/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx +++ b/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx @@ -14,8 +14,8 @@ import { buildStaticPaths } from 'next/dist/build/utils' import type { BuildManifest } from 'next/dist/server/get-page-files' import type { ReactLoadableManifest } from 'next/dist/server/load-components' -import { ServerResponseShim } from './http' import { headersFromEntries } from './headers' +import { createServerResponse } from './http' import type { Ipc } from '@vercel/turbopack-node/ipc/index' import type { RenderData } from 'types/turbopack' import type { ChunkGroup } from 'types/next' @@ -220,19 +220,7 @@ export default function startHandler({ method: 'GET', headers: headersFromEntries(renderData.rawHeaders), } as any - const res: ServerResponse = new ServerResponseShim(req) as any - - // Both _error and 404 should receive a 404 status code. - const statusCode = - renderData.path === '/404' - ? 404 - : renderData.path === '/_error' - ? 404 - : 200 - - // Setting the status code on the response object is necessary for - // `Error.getInitialProps` to detect the status code. - res.statusCode = statusCode + const res: ServerResponse = createServerResponse(req, renderData.path) const parsedQuery = parse(renderData.rawQuery) const query = { ...parsedQuery, ...renderData.params } @@ -298,7 +286,7 @@ export default function startHandler({ const pageData = renderResult.metadata().pageData return { type: 'response', - statusCode, + statusCode: res.statusCode, headers: [['Content-Type', MIME_APPLICATION_JAVASCRIPT]], // Page data is only returned if the page had getXxyProps. body: JSON.stringify(pageData === undefined ? {} : pageData), @@ -316,7 +304,7 @@ export default function startHandler({ return { type: 'response', - statusCode, + statusCode: res.statusCode, headers: [ ['Content-Type', renderResult.contentType() ?? MIME_TEXT_HTML_UTF8], ], diff --git a/packages/next-swc/crates/next-core/src/app_source.rs b/packages/next-swc/crates/next-core/src/app_source.rs index 898e4d1cadf63..ee8fdd970e1f4 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -93,7 +93,7 @@ use crate::{ transition::NextEdgeTransition, }, next_image::module::{BlurPlaceholderMode, StructuredImageModuleType}, - next_route_matcher::NextParamsMatcherVc, + next_route_matcher::{NextFallbackMatcherVc, NextParamsMatcherVc}, next_server::context::{ get_server_compile_time_info, get_server_module_options_context, get_server_resolve_options_context, ServerContextType, @@ -426,8 +426,8 @@ pub async fn create_app_source( ); let render_data = render_data(next_config); - let sources = entrypoints - .await? + let entrypoints = entrypoints.await?; + let mut sources: Vec<_> = entrypoints .iter() .map(|(pathname, &loader_tree)| match loader_tree { Entrypoint::AppPage { loader_tree } => create_app_page_source_for_route( @@ -464,6 +464,27 @@ pub async fn create_app_source( ))) .collect(); + if let Some(&Entrypoint::AppPage { loader_tree }) = entrypoints.get("/") { + if loader_tree.await?.components.await?.not_found.is_some() { + // Only add a source for the app 404 page if a top-level not-found page is + // defined. Otherwise, the 404 page is handled by the pages logic. + let not_found_page_source = create_app_not_found_page_source( + loader_tree, + context_ssr, + context, + project_path, + app_dir, + env, + server_root, + server_runtime_entries, + fallback_page, + output_path, + render_data, + ); + sources.push(not_found_page_source); + } + } + Ok(CombinedContentSource { sources }.cell().into()) } @@ -555,6 +576,49 @@ async fn create_app_page_source_for_route( Ok(source.issue_context(app_dir, &format!("Next.js App Page Route {pathname}"))) } +#[allow(clippy::too_many_arguments)] +#[turbo_tasks::function] +async fn create_app_not_found_page_source( + loader_tree: LoaderTreeVc, + context_ssr: AssetContextVc, + context: AssetContextVc, + project_path: FileSystemPathVc, + app_dir: FileSystemPathVc, + env: ProcessEnvVc, + server_root: FileSystemPathVc, + runtime_entries: AssetsVc, + fallback_page: DevHtmlAssetVc, + intermediate_output_path_root: FileSystemPathVc, + render_data: JsonValueVc, +) -> Result { + let pathname_vc = StringVc::cell("/404".to_string()); + + let source = create_node_rendered_source( + project_path, + env, + SpecificityVc::not_found(), + server_root, + NextFallbackMatcherVc::new().into(), + pathname_vc, + AppRenderer { + runtime_entries, + app_dir, + context_ssr, + context, + server_root, + project_path, + intermediate_output_path: intermediate_output_path_root, + loader_tree, + } + .cell() + .into(), + fallback_page, + render_data, + ); + + Ok(source.issue_context(app_dir, "Next.js App Page Route /404")) +} + #[allow(clippy::too_many_arguments)] #[turbo_tasks::function] async fn create_app_route_source_for_route( diff --git a/packages/next-swc/crates/next-core/src/app_structure.rs b/packages/next-swc/crates/next-core/src/app_structure.rs index 4cf8fdf941563..c9524bbecabcb 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -69,7 +69,7 @@ impl Components { template: a.template.or(b.template), not_found: a.not_found.or(b.not_found), default: a.default.or(b.default), - route: a.default.or(b.route), + route: a.route.or(b.route), metadata: Metadata::merge(&a.metadata, &b.metadata), } } @@ -552,7 +552,7 @@ pub fn get_entrypoints(app_dir: FileSystemPathVc, page_extensions: StringsVc) -> } #[turbo_tasks::function] -pub fn directory_tree_to_entrypoints( +fn directory_tree_to_entrypoints( app_dir: FileSystemPathVc, directory_tree: DirectoryTreeVc, ) -> EntrypointsVc { diff --git a/packages/next-swc/crates/next-core/src/manifest.rs b/packages/next-swc/crates/next-core/src/manifest.rs index 1d0c4b7a23203..9ed0f825e0791 100644 --- a/packages/next-swc/crates/next-core/src/manifest.rs +++ b/packages/next-swc/crates/next-core/src/manifest.rs @@ -26,7 +26,7 @@ use turbo_tasks::{ use crate::{ embed_js::next_js_file, next_config::{NextConfigVc, RewritesReadRef}, - util::get_asset_path_from_route, + util::get_asset_path_from_pathname, }; /// A content source which creates the next.js `_devPagesManifest.json` and @@ -78,13 +78,7 @@ impl DevManifestContentSourceVc { let mut routes = routes .into_iter() .flatten() - .map(|s| { - if !s.starts_with('/') { - format!("/{}", s) - } else { - s.to_string() - } - }) + .map(|route| route.clone_value()) .collect::>(); routes.sort_by_cached_key(|s| s.split('/').map(PageSortKey::from).collect::>()); @@ -117,12 +111,12 @@ impl DevManifestContentSourceVc { let sorted_pages = &*self.find_pages().await?; let routes = sorted_pages .iter() - .map(|p| { + .map(|pathname| { ( - p, + pathname, vec![format!( - "_next/static/chunks/pages/{}", - get_asset_path_from_route(p, ".js") + "_next/static/chunks/pages{}", + get_asset_path_from_pathname(pathname, ".js") )], ) }) diff --git a/packages/next-swc/crates/next-core/src/page_loader.rs b/packages/next-swc/crates/next-core/src/page_loader.rs index 16ae37b197f74..b20cd1ea19600 100644 --- a/packages/next-swc/crates/next-core/src/page_loader.rs +++ b/packages/next-swc/crates/next-core/src/page_loader.rs @@ -24,7 +24,7 @@ use turbo_binding::{ }; use turbo_tasks::{primitives::StringVc, TryJoinIterExt, Value}; -use crate::{embed_js::next_js_file_path, util::get_asset_path_from_route}; +use crate::{embed_js::next_js_file_path, util::get_asset_path_from_pathname}; #[turbo_tasks::function] pub async fn create_page_loader( @@ -65,7 +65,7 @@ impl PageLoaderAssetVc { writeln!( result, "const PAGE_PATH = {};\n", - StringifyJs(&format_args!("/{}", &*this.pathname.await?)) + StringifyJs(&*this.pathname.await?) )?; let page_loader_path = next_js_file_path("entry/page-loader.ts"); @@ -126,11 +126,10 @@ fn page_loader_chunk_reference_description() -> StringVc { impl Asset for PageLoaderAsset { #[turbo_tasks::function] async fn ident(&self) -> Result { - Ok(AssetIdentVc::from_path( - self.server_root - .join("_next/static/chunks/pages") - .join(&get_asset_path_from_route(&self.pathname.await?, ".js")), - )) + Ok(AssetIdentVc::from_path(self.server_root.join(&format!( + "_next/static/chunks/pages{}", + get_asset_path_from_pathname(&self.pathname.await?, ".js") + )))) } #[turbo_tasks::function] diff --git a/packages/next-swc/crates/next-core/src/page_source.rs b/packages/next-swc/crates/next-core/src/page_source.rs index 63071a0321577..fe484b9026be1 100644 --- a/packages/next-swc/crates/next-core/src/page_source.rs +++ b/packages/next-swc/crates/next-core/src/page_source.rs @@ -22,7 +22,7 @@ use turbo_binding::{ asset_graph::AssetGraphContentSourceVc, combined::{CombinedContentSource, CombinedContentSourceVc}, specificity::SpecificityVc, - ContentSourceData, ContentSourceVc, NoContentSourceVc, + ContentSourceData, ContentSourceVc, }, }, ecmascript::{ @@ -95,10 +95,14 @@ pub async fn create_page_source( next_config: NextConfigVc, server_addr: ServerAddrVc, ) -> Result { - let Some(pages_structure) = *pages_structure.await? else { - return Ok(NoContentSourceVc::new().into()); + let (pages_dir, pages_structure) = if let Some(pages_structure) = *pages_structure.await? { + ( + pages_structure.directory().resolve().await?, + Some(pages_structure), + ) + } else { + (project_path.join("pages"), None) }; - let pages_dir = pages_structure.directory().resolve().await?; let client_ty = Value::new(ClientContextType::Pages { pages_dir }); let server_ty = Value::new(ServerContextType::Pages { pages_dir }); @@ -247,67 +251,72 @@ pub async fn create_page_source( let render_data = render_data(next_config); let page_extensions = next_config.page_extensions(); - let force_not_found_source = create_not_found_page_source( - project_path, - env, - server_context, - client_context, - pages_dir, - page_extensions, - fallback_runtime_entries, - fallback_page, - server_root, - output_path.join("force_not_found"), - SpecificityVc::exact(), - NextExactMatcherVc::new(StringVc::cell("_next/404".to_string())).into(), - render_data, + + let mut sources = vec![]; + + // Match _next/404 first to ensure rewrites work properly. + sources.push( + create_not_found_page_source( + project_path, + env, + server_context, + client_context, + pages_dir, + page_extensions, + fallback_runtime_entries, + fallback_page, + server_root, + output_path.join("force_not_found"), + SpecificityVc::exact(), + NextExactMatcherVc::new(StringVc::cell("_next/404".to_string())).into(), + render_data, + ) + .issue_context(pages_dir, "Next.js pages directory not found"), ); - let fallback_not_found_source = create_not_found_page_source( - project_path, - env, - server_context, - client_context, - pages_dir, - page_extensions, - fallback_runtime_entries, - fallback_page, - server_root, - output_path.join("fallback_not_found"), - SpecificityVc::not_found(), - NextFallbackMatcherVc::new().into(), - render_data, + + if let Some(pages_structure) = pages_structure { + sources.push(create_page_source_for_directory( + pages_structure, + project_path, + env, + server_context, + server_data_context, + client_context, + pages_dir, + server_runtime_entries, + fallback_page, + server_root, + output_path, + render_data, + )); + } + + sources.push( + AssetGraphContentSourceVc::new_eager(server_root, fallback_page.as_asset()) + .as_content_source() + .issue_context(pages_dir, "Next.js pages directory fallback"), ); - let page_source = create_page_source_for_directory( - pages_structure, - project_path, - env, - server_context, - server_data_context, - client_context, - pages_dir, - server_runtime_entries, - fallback_page, - server_root, - output_path, - render_data, + + sources.push( + create_not_found_page_source( + project_path, + env, + server_context, + client_context, + pages_dir, + page_extensions, + fallback_runtime_entries, + fallback_page, + server_root, + output_path.join("fallback_not_found"), + SpecificityVc::not_found(), + NextFallbackMatcherVc::new().into(), + render_data, + ) + .issue_context(pages_dir, "Next.js pages directory not found fallback"), ); - let fallback_source = - AssetGraphContentSourceVc::new_eager(server_root, fallback_page.as_asset()); - - let source = CombinedContentSource { - sources: vec![ - // Match _next/404 first to ensure rewrites work properly. - force_not_found_source.issue_context(pages_dir, "Next.js pages directory not found"), - page_source, - fallback_source - .as_content_source() - .issue_context(pages_dir, "Next.js pages directory fallback"), - fallback_not_found_source - .issue_context(pages_dir, "Next.js pages directory not found fallback"), - ], - } - .cell() - .into(); + + let source = CombinedContentSource { sources }.cell().into(); Ok(source) } @@ -511,14 +520,14 @@ async fn create_not_found_page_source( let (page_asset, pathname) = if let Some(not_found_page_asset) = get_not_found_page(pages_dir, page_extensions).await? { // If a 404 page is defined, the pathname should be 404. - (not_found_page_asset, StringVc::cell("404".to_string())) + (not_found_page_asset, StringVc::cell("/404".to_string())) } else { ( // The error page asset must be within the context path so it can depend on the // Next.js module. next_asset("entry/error.tsx"), // If no 404 page is defined, the pathname should be _error. - StringVc::cell("_error".to_string()), + StringVc::cell("/_error".to_string()), ) }; diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index 69d8f5cfd7aed..2ea1a15fc57be 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -55,23 +55,25 @@ pub async fn pathname_for_path( } else { path }; + // `get_path_to` always strips the leading `/` from the path, so we need to add + // it back here. let path = if path == "index" && !data { - "" + "/".to_string() } else { - path.strip_suffix("/index").unwrap_or(path) + format!("/{}", path.strip_suffix("/index").unwrap_or(path)) }; - Ok(StringVc::cell(path.to_string())) + Ok(StringVc::cell(path)) } // Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/shared/lib/router/utils/get-asset-path-from-route.ts -pub fn get_asset_path_from_route(route: &str, ext: &str) -> String { - if route.is_empty() { - format!("index{}", ext) - } else if route == "index" || route.starts_with("index/") { - format!("index/{}{}", route, ext) +pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String { + if pathname == "/" { + format!("/index{}", ext) + } else if pathname == "/index" || pathname.starts_with("/index/") { + format!("/index{}{}", pathname, ext) } else { - format!("{}{}", route, ext) + format!("{}{}", pathname, ext) } } diff --git a/packages/next-swc/crates/next-dev-tests/test-harness/harness.ts b/packages/next-swc/crates/next-dev-tests/test-harness/harness.ts index 15db1eae3859f..0d3bd2ef2ce11 100644 --- a/packages/next-swc/crates/next-dev-tests/test-harness/harness.ts +++ b/packages/next-swc/crates/next-dev-tests/test-harness/harness.ts @@ -1,16 +1,12 @@ import * as jest from 'jest-circus-browser/dist/umd/jest-circus' import expectMod from 'expect/build-es5/index' -type CallSignature unknown> = ( - ...a: Parameters -) => ReturnType - declare global { var __jest__: typeof jest var expect: typeof expectMod // We need to extract only the call signature as `autoReady(jest.describe)` drops all the other properties - var describe: CallSignature - var it: CallSignature + var describe: AutoReady + var it: AutoReady var READY: (arg: string) => void var nsObj: (obj: any) => any @@ -20,36 +16,10 @@ declare global { } } -let isReady = false -function autoReady unknown>( - fn: (...args: Parameters) => ReturnType -): (...args: Parameters) => ReturnType { - return (...args) => { - if (!isReady) { - isReady = true - requestIdleCallback( - () => { - if (typeof READY === 'function') { - READY('') - } else { - console.info( - '%cTurbopack tests:', - 'font-weight: bold;', - 'Entering debug mode. Run `await __jest__.run()` in the browser console to run tests.' - ) - } - }, - { timeout: 20000 } - ) - } - return fn(...args) - } -} - globalThis.__jest__ = jest globalThis.expect = expectMod -globalThis.describe = autoReady(jest.describe) -globalThis.it = autoReady(jest.it) +globalThis.describe = autoReady(jest.describe, markReady) +globalThis.it = autoReady(jest.it, markReady) // From https://github.com/webpack/webpack/blob/9fcaa243573005d6fdece9a3f8d89a0e8b399613/test/TestCases.template.js#L422 globalThis.nsObj = function nsObj(obj) { @@ -59,6 +29,54 @@ globalThis.nsObj = function nsObj(obj) { return obj } +type AnyFunction = (...args: any[]) => any + +type AutoReady = T & { + [K in keyof T]: T[K] extends AnyFunction ? AutoReady : T[K] +} + +function autoReady void>( + fn: T, + callback: F +): AutoReady { + const wrappedFn = ((...args: Parameters): ReturnType => { + callback() + + return fn(...args) + }) as AutoReady + + for (const key in fn) { + if (typeof fn[key] === 'function') { + ;(wrappedFn as any)[key] = autoReady(fn[key] as AnyFunction, callback) + } else { + ;(wrappedFn as any)[key] = fn[key] + } + } + + return wrappedFn +} + +let isReady = false +function markReady() { + if (!isReady) { + isReady = true + requestIdleCallback( + () => { + if (typeof READY === 'function') { + READY('') + } else { + console.info( + '%cTurbopack tests:', + 'font-weight: bold;', + 'Entering debug mode. Run `await __jest__.run()` in the browser console to run tests.' + ) + } + }, + { timeout: 20000 } + ) + } +} + export function wait(ms: number): Promise { return new Promise((resolve) => { setTimeout(resolve, ms) @@ -75,6 +93,28 @@ async function waitForPath(contentWindow: Window, path: string): Promise { } } +/** + * Loads a new page in an iframe and waits for it to load. + */ +export function load(iframe: HTMLIFrameElement, path: string): Promise { + iframe.src = path + + return new Promise((resolve) => { + let eventListener = () => { + iframe.removeEventListener('load', eventListener) + resolve() + } + iframe.addEventListener('load', eventListener) + }) +} + +/** + * Waits for the currently loading page in an iframe to finish loading. + * + * If the iframe is already loaded, this function will return immediately. + * + * Note: if you've just changed the iframe's `src` attribute, you should use `load` instead. + */ export function waitForLoaded(iframe: HTMLIFrameElement): Promise { return new Promise((resolve) => { if ( @@ -83,9 +123,11 @@ export function waitForLoaded(iframe: HTMLIFrameElement): Promise { ) { resolve() } else { - iframe.addEventListener('load', () => { + let eventListener = () => { + iframe.removeEventListener('load', eventListener) resolve() - }) + } + iframe.addEventListener('load', eventListener) } }) } @@ -136,9 +178,11 @@ export function waitForHydration( ) { waitForHydrationAndResolve(iframe.contentWindow!, path).then(resolve) } else { - iframe.addEventListener('load', () => { + const eventListener = () => { waitForHydrationAndResolve(iframe.contentWindow!, path).then(resolve) - }) + iframe.removeEventListener('load', eventListener) + } + iframe.addEventListener('load', eventListener) } }) } diff --git a/packages/next-swc/crates/next-dev-tests/test-harness/hooks.ts b/packages/next-swc/crates/next-dev-tests/test-harness/hooks.ts new file mode 100644 index 0000000000000..235e92d0f2ca7 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/test-harness/hooks.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react' + +export type Harness = typeof import('./harness') + +let ranOnce = false +/** + * Run a callback once the test harness is loaded. + */ +export async function useTestHarness void>( + callback: T +) { + useEffect(() => { + if (ranOnce) { + return + } + + ranOnce = true + import('./harness').then(callback) + }) +} diff --git a/packages/next-swc/crates/next-dev-tests/test-harness/package.json b/packages/next-swc/crates/next-dev-tests/test-harness/package.json index dd6004ec1036c..c42a95bf6bcb7 100644 --- a/packages/next-swc/crates/next-dev-tests/test-harness/package.json +++ b/packages/next-swc/crates/next-dev-tests/test-harness/package.json @@ -2,7 +2,7 @@ "name": "@turbo/pack-test-harness", "private": true, "version": "0.0.1", - "main": "./harness.ts", + "main": "./hooks.ts", "dependencies": { "expect": "24.5.0", "jest-circus": "27.5.1", diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration.rs b/packages/next-swc/crates/next-dev-tests/tests/integration.rs index c422731e5485f..b0dc049cfdebb 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration.rs +++ b/packages/next-swc/crates/next-dev-tests/tests/integration.rs @@ -211,7 +211,7 @@ async fn run_test(resource: PathBuf) -> JestRunResult { ) .entry_request(EntryRequest::Module( "@turbo/pack-test-harness".to_string(), - "".to_string(), + "/harness".to_string(), )) .entry_request(EntryRequest::Relative("index.js".to_owned())) .eager_compile(false) diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/api/basic/input/pages/index.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/api/basic/input/pages/index.tsx index 88c9a63664bcb..69739902ff905 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/api/basic/input/pages/index.tsx +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/api/basic/input/pages/index.tsx @@ -1,16 +1,11 @@ -import { useEffect } from 'react' +import { useTestHarness, Harness } from '@turbo/pack-test-harness' export default function Page() { - useEffect(() => { - // Only run on client - import('@turbo/pack-test-harness').then((mod) => runTests(mod)) - }) + useTestHarness(runTests) return

Ready

} -type Harness = typeof import('@turbo/pack-test-harness') - let once = true function runTests(harness: Harness) { if (!once) return diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/layout.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/layout.tsx new file mode 100644 index 0000000000000..12c84680889be --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/layout.tsx @@ -0,0 +1,7 @@ +export default function RootLayout({ children }: { children: any }) { + return ( + + {children} + + ) +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link-segment/page.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link-segment/page.tsx new file mode 100644 index 0000000000000..e70deb9e207fc --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link-segment/page.tsx @@ -0,0 +1,14 @@ +'use client' + +import Link from 'next/link' +import { useTestHarness } from '@turbo/pack-test-harness' + +export default function Page() { + useTestHarness((mod) => mod.markAsHydrated()) + + return ( + + -> Segment not found + + ) +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link/page.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link/page.tsx new file mode 100644 index 0000000000000..7e831f7838735 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/link/page.tsx @@ -0,0 +1,14 @@ +'use client' + +import Link from 'next/link' +import { useTestHarness } from '@turbo/pack-test-harness' + +export default function Page() { + useTestHarness((mod) => mod.markAsHydrated()) + + return ( + + -> Not found + + ) +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/not-found.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/not-found.tsx new file mode 100644 index 0000000000000..ba6deda1a014f --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/not-found.tsx @@ -0,0 +1,3 @@ +export default function NotFound() { + return
Custom not found
+} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/page.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/page.tsx new file mode 100644 index 0000000000000..373bf2633967c --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/page.tsx @@ -0,0 +1,5 @@ +import Test from './test' + +export default function Page() { + return +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/not-found.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/not-found.tsx new file mode 100644 index 0000000000000..464c918dc5307 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/not-found.tsx @@ -0,0 +1,3 @@ +export default function SegmentNotFound() { + return
Segment Not Found
+} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/page.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/page.tsx new file mode 100644 index 0000000000000..0c66f3714c95b --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/segment/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from 'next/navigation' + +export default function SegmentPage() { + notFound() +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/test.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/test.tsx new file mode 100644 index 0000000000000..2179fe73aa1a2 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/404-custom/input/app/test.tsx @@ -0,0 +1,106 @@ +'use client' + +import { useRef } from 'react' +import { useTestHarness, Harness } from '@turbo/pack-test-harness' + +export default function Test() { + const iframeRef = useRef(null) + + useTestHarness((harness) => runTests(harness, iframeRef.current!)) + + return