From e4e1bae15a6da81e649752ab257342471e4caf17 Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Tue, 2 May 2023 13:55:02 +0200 Subject: [PATCH 01/16] Add support for app global and segment 404 pages --- .../next-core/js/src/entry/app-renderer.tsx | 9 ++- .../crates/next-core/js/src/internal/http.ts | 28 +++++++- .../js/src/internal/page-server-handler.tsx | 20 ++---- .../crates/next-core/src/app_source.rs | 66 ++++++++++++++++++- .../crates/next-core/src/app_structure.rs | 4 +- .../next-swc/crates/next-core/src/manifest.rs | 8 +-- .../next-swc/crates/next-core/src/util.rs | 6 +- 7 files changed, 107 insertions(+), 34 deletions(-) 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..17626de1faf7e 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 @@ -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, }, }) @@ -251,7 +251,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 +306,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..d8709321224ca 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,23 @@ pub async fn create_app_source( ))) .collect(); + if let Some(&Entrypoint::AppPage { loader_tree }) = entrypoints.get("/") { + 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 +572,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, &format!("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..96259e44c7fa7 100644 --- a/packages/next-swc/crates/next-core/src/manifest.rs +++ b/packages/next-swc/crates/next-core/src/manifest.rs @@ -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::>()); diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index 69d8f5cfd7aed..2074cec5d7588 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -55,10 +55,12 @@ 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())) From 6d99a5f7665269ee363deac4f3fb24340e698d5e Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Tue, 2 May 2023 15:27:30 +0200 Subject: [PATCH 02/16] Misc fixes --- .../next-swc/crates/next-core/js/src/entry/app-renderer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 17626de1faf7e..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, @@ -124,8 +124,6 @@ type LoaderTree = [ ] async function runOperation(renderData: RenderData) { - let tree: LoaderTree = LOADER_TREE - const proxyMethodsForModule = ( id: string ): ProxyHandler => { From 53c461a0243d7d71790780279af9a44692766727 Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Tue, 2 May 2023 15:28:11 +0200 Subject: [PATCH 03/16] Only add app not found source when the not found page exists --- .../crates/next-core/src/app_source.rs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) 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 d8709321224ca..3d2c250e49b3e 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -465,20 +465,24 @@ pub async fn create_app_source( .collect(); if let Some(&Entrypoint::AppPage { loader_tree }) = entrypoints.get("/") { - 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); + if let Some(_) = loader_tree.await?.components.await?.not_found { + // 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()) From d29b58e606f44d9aac8fe81b1e07c0cf95622cdc Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Tue, 2 May 2023 15:28:29 +0200 Subject: [PATCH 04/16] Always add a page source --- .../crates/next-core/src/page_source.rs | 133 ++++++++++-------- 1 file changed, 71 insertions(+), 62 deletions(-) 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..ffea7e980ada5 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) } From 265460a2c63d32c8a7384d1874905eddae4bc10c Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Tue, 2 May 2023 16:39:01 +0200 Subject: [PATCH 05/16] Fix not found pages --- packages/next-swc/crates/next-core/src/page_source.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ffea7e980ada5..fe484b9026be1 100644 --- a/packages/next-swc/crates/next-core/src/page_source.rs +++ b/packages/next-swc/crates/next-core/src/page_source.rs @@ -520,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()), ) }; From 1d0b10e13a401b6d8ef61c7f755a195ee68d9893 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 3 May 2023 07:02:05 +0200 Subject: [PATCH 06/16] Update packages/next-swc/crates/next-core/src/util.rs --- packages/next-swc/crates/next-core/src/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index 2074cec5d7588..1bead545433c2 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -63,7 +63,7 @@ pub async fn pathname_for_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 From 120e8445200d52f3e3bdfda821519b1c174b3172 Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Wed, 3 May 2023 11:40:30 +0200 Subject: [PATCH 07/16] Add tests for 404s and improve the test harness API --- .../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/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 +++++++++--------- .../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 ++-- .../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/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 +- 58 files changed, 550 insertions(+), 351 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/{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/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/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