diff --git a/crates/next-core/src/next_server/transforms.rs b/crates/next-core/src/next_server/transforms.rs index 808c715b020220..4721aa982c60a5 100644 --- a/crates/next-core/src/next_server/transforms.rs +++ b/crates/next-core/src/next_server/transforms.rs @@ -12,10 +12,11 @@ use crate::{ next_config::NextConfig, next_server::context::ServerContextType, next_shared::transforms::{ - get_next_dynamic_transform_rule, get_next_font_transform_rule, get_next_image_rule, - get_next_lint_transform_rule, get_next_modularize_imports_rule, - get_next_pages_transforms_rule, get_next_track_dynamic_imports_transform_rule, - get_server_actions_transform_rule, next_cjs_optimizer::get_next_cjs_optimizer_rule, + get_next_debug_instant_stack_rule, get_next_dynamic_transform_rule, + get_next_font_transform_rule, get_next_image_rule, get_next_lint_transform_rule, + get_next_modularize_imports_rule, get_next_pages_transforms_rule, + get_next_track_dynamic_imports_transform_rule, get_server_actions_transform_rule, + next_cjs_optimizer::get_next_cjs_optimizer_rule, next_disallow_re_export_all_in_page::get_next_disallow_export_all_in_page_rule, next_edge_node_api_assert::next_edge_node_api_assert, next_middleware_dynamic_assert::get_middleware_dynamic_assert_rule, @@ -147,6 +148,10 @@ pub async fn get_next_server_transforms_rules( ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => false, }; + if is_app_dir { + rules.push(get_next_debug_instant_stack_rule(mdx_rs)); + } + if is_app_dir && // `cacheComponents` is not supported in the edge runtime. // (also, the code generated by the dynamic imports transform relies on `CacheSignal`, which uses nodejs-specific APIs) diff --git a/crates/next-core/src/next_shared/transforms/mod.rs b/crates/next-core/src/next_shared/transforms/mod.rs index da31772f4614c7..b20fe491c17baa 100644 --- a/crates/next-core/src/next_shared/transforms/mod.rs +++ b/crates/next-core/src/next_shared/transforms/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod debug_fn_name; pub(crate) mod emotion; pub(crate) mod modularize_imports; pub(crate) mod next_cjs_optimizer; +pub(crate) mod next_debug_instant_stack; pub(crate) mod next_disallow_re_export_all_in_page; pub(crate) mod next_dynamic; pub(crate) mod next_edge_node_api_assert; @@ -23,6 +24,7 @@ pub(crate) mod swc_ecma_transform_plugins; use anyhow::Result; pub use modularize_imports::{ModularizeImportPackageConfig, get_next_modularize_imports_rule}; +pub use next_debug_instant_stack::get_next_debug_instant_stack_rule; pub use next_dynamic::get_next_dynamic_transform_rule; pub use next_font::get_next_font_transform_rule; pub use next_lint::get_next_lint_transform_rule; diff --git a/crates/next-core/src/next_shared/transforms/next_debug_instant_stack.rs b/crates/next-core/src/next_shared/transforms/next_debug_instant_stack.rs new file mode 100644 index 00000000000000..7c6cbcff3c1795 --- /dev/null +++ b/crates/next-core/src/next_shared/transforms/next_debug_instant_stack.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use async_trait::async_trait; +use next_custom_transforms::transforms::debug_instant_stack::debug_instant_stack; +use swc_core::ecma::ast::Program; +use turbo_tasks::ResolvedVc; +use turbopack::module_options::{ModuleRule, ModuleRuleEffect}; +use turbopack_ecmascript::{CustomTransformer, EcmascriptInputTransform, TransformContext}; + +use super::module_rule_match_js_no_url; + +pub fn get_next_debug_instant_stack_rule(enable_mdx_rs: bool) -> ModuleRule { + let transform = + EcmascriptInputTransform::Plugin(ResolvedVc::cell(Box::new(NextDebugInstantStack {}) as _)); + + ModuleRule::new( + module_rule_match_js_no_url(enable_mdx_rs), + vec![ModuleRuleEffect::ExtendEcmascriptTransforms { + preprocess: ResolvedVc::cell(vec![]), + main: ResolvedVc::cell(vec![]), + postprocess: ResolvedVc::cell(vec![transform]), + }], + ) +} + +#[derive(Debug)] +struct NextDebugInstantStack {} + +#[async_trait] +impl CustomTransformer for NextDebugInstantStack { + #[tracing::instrument(level = tracing::Level::TRACE, name = "debug_instant_stack", skip_all)] + async fn transform(&self, program: &mut Program, _ctx: &TransformContext<'_>) -> Result<()> { + program.mutate(debug_instant_stack()); + Ok(()) + } +} diff --git a/crates/next-custom-transforms/src/chain_transforms.rs b/crates/next-custom-transforms/src/chain_transforms.rs index 9be8bd718f0a9f..691fa38a5e10b8 100644 --- a/crates/next-custom-transforms/src/chain_transforms.rs +++ b/crates/next-custom-transforms/src/chain_transforms.rs @@ -347,6 +347,7 @@ where crate::transforms::debug_fn_name::debug_fn_name(), opts.debug_function_name, ), + crate::transforms::debug_instant_stack::debug_instant_stack(), visit_mut_pass(crate::transforms::pure::pure_magic(comments.clone())), Optional::new( linter(lint_codemod_comments(comments)), diff --git a/crates/next-custom-transforms/src/transforms/debug_instant_stack.rs b/crates/next-custom-transforms/src/transforms/debug_instant_stack.rs new file mode 100644 index 00000000000000..4b4b007c5a87c4 --- /dev/null +++ b/crates/next-custom-transforms/src/transforms/debug_instant_stack.rs @@ -0,0 +1,77 @@ +use swc_core::{ + common::{Span, Spanned}, + ecma::{ + ast::*, + visit::{fold_pass, Fold}, + }, + quote, +}; + +pub fn debug_instant_stack() -> impl Pass { + fold_pass(DebugInstantStack { + instant_export_span: None, + }) +} + +struct DebugInstantStack { + instant_export_span: Option, +} + +impl Fold for DebugInstantStack { + fn fold_module_items(&mut self, items: Vec) -> Vec { + // Scan for `export const unstable_instant = ...` + for item in &items { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = item { + if let Decl::Var(var_decl) = &export_decl.decl { + for decl in &var_decl.decls { + if let Pat::Ident(ident) = &decl.name { + if ident.id.sym == "unstable_instant" { + if let Some(init) = &decl.init { + self.instant_export_span = Some(init.span()); + } + } + } + } + } + } + } + + if let Some(source_span) = self.instant_export_span { + let mut new_items = items; + + // TODO: Change React to deserialize errors with an empty message + // instead of using a fallback message ("no message was provided"). + let mut new_error = quote!("new Error('\u{200B}')" as Expr); + if let Expr::New(new_expr) = &mut new_error { + new_expr.span = source_span; + } + + let mut cons = quote!( + "function unstable_instant() { + const error = $new_error + error.name = 'Instant Validation' + return error + }" as Expr, + new_error: Expr = new_error, + ); + + // Patch source_span onto the Function + // for sourcemap mapping back to the unstable_instant config value + if let Expr::Fn(f) = &mut cons { + f.function.span = source_span; + } + + let export = quote!( + "export const __debugCreateInstantConfigStack = + process.env.NODE_ENV !== 'production' ? $cons : null" + as ModuleItem, + cons: Expr = cons, + ); + + new_items.push(export); + new_items + } else { + items + } + } +} diff --git a/crates/next-custom-transforms/src/transforms/mod.rs b/crates/next-custom-transforms/src/transforms/mod.rs index 287bfec0a24728..3cfff9c47f6cee 100644 --- a/crates/next-custom-transforms/src/transforms/mod.rs +++ b/crates/next-custom-transforms/src/transforms/mod.rs @@ -1,6 +1,7 @@ pub mod cjs_finder; pub mod cjs_optimizer; pub mod debug_fn_name; +pub mod debug_instant_stack; pub mod disallow_re_export_all_in_page; pub mod dynamic; pub mod fonts; diff --git a/crates/next-custom-transforms/tests/fixture.rs b/crates/next-custom-transforms/tests/fixture.rs index a8be20c637278f..dd3e13752004f5 100644 --- a/crates/next-custom-transforms/tests/fixture.rs +++ b/crates/next-custom-transforms/tests/fixture.rs @@ -8,6 +8,7 @@ use bytes_str::BytesStr; use next_custom_transforms::transforms::{ cjs_optimizer::cjs_optimizer, debug_fn_name::debug_fn_name, + debug_instant_stack::debug_instant_stack, dynamic::{next_dynamic, NextDynamicMode}, fonts::{next_font_loaders, Config as FontLoaderConfig}, named_import_transform::named_import_transform, @@ -869,6 +870,22 @@ fn test_debug_name(input: PathBuf) { ); } +#[fixture("tests/fixture/debug-instant-stack/**/input.js")] +fn test_debug_instant_stack(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + + test_fixture( + syntax(), + &|_| debug_instant_stack(), + &input, + &output, + FixtureTestConfig { + sourcemap: true, + ..Default::default() + }, + ); +} + #[fixture("tests/fixture/edge-assert/**/input.js")] fn test_edge_assert(input: PathBuf) { let output = input.parent().unwrap().join("output.js"); diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/input.js b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/input.js new file mode 100644 index 00000000000000..3d789924d3c293 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/input.js @@ -0,0 +1,5 @@ +export const unstable_instant = { prefetch: 'static' } + +export default function Page() { + return
Hello
+} diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.js b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.js new file mode 100644 index 00000000000000..85d4f23a5f4d6a --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.js @@ -0,0 +1,11 @@ +export const unstable_instant = { + prefetch: 'static' +}; +export default function Page() { + return
Hello
; +} +export const __debugCreateInstantConfigStack = process.env.NODE_ENV !== 'production' ? function unstable_instant() { + const error = new Error('​'); + error.name = 'Instant Validation'; + return error; +} : null; diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.map b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.map new file mode 100644 index 00000000000000..da79b18a669fcd --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/with-instant/output.map @@ -0,0 +1 @@ +{"version":3,"sources":["input.js"],"sourcesContent":["export const unstable_instant = { prefetch: 'static' }\n\nexport default function Page() {\n return
Hello
\n}\n"],"names":[],"mappings":"AAAA,OAAO,MAAM,mBAAmB;IAAE,UAAU;AAAS,EAAC;AAEtD,eAAe,SAAS;IACtB,QAAQ,IAAI,KAAK,EAAE;AACrB;uFAJgC;kBAAA"} diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/input.js b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/input.js new file mode 100644 index 00000000000000..62390ed7d97f71 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/input.js @@ -0,0 +1,5 @@ +export const revalidate = 60 + +export default function Page() { + return
Hello
+} diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.js b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.js new file mode 100644 index 00000000000000..c28954e6b3dfd1 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.js @@ -0,0 +1,4 @@ +export const revalidate = 60; +export default function Page() { + return
Hello
; +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.map b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.map new file mode 100644 index 00000000000000..f842e4de890531 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-instant-stack/without-instant/output.map @@ -0,0 +1 @@ +{"version":3,"sources":["input.js"],"sourcesContent":["export const revalidate = 60\n\nexport default function Page() {\n return
Hello
\n}\n"],"names":[],"mappings":"AAAA,OAAO,MAAM,aAAa,GAAE;AAE5B,eAAe,SAAS;IACtB,QAAQ,IAAI,KAAK,EAAE;AACrB"} diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index b758ad5df1a604..8a20885618e4a4 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -4131,6 +4131,16 @@ async function validateInstantConfigs( ? instantConfig.prefetch === 'runtime' : false }) + + // Collect the first stack factory from segments with instant configs so we can + // pass it into validation state for creating errors with the config's stack trace. + const createInstantStack = + segmentsWithInstantConfigs + .map( + (segmentPath) => treeNodes.get(segmentPath)?.module?.createInstantStack + ) + .find((factory): factory is () => Error => factory != null) ?? null + const clientReferenceManifest = getClientReferenceManifest() const { @@ -4170,7 +4180,8 @@ async function validateInstantConfigs( hmrRefreshHash, validationRouteTree, navigationParent, - false // use static stage for static segments + false, // use static stage for static segments + createInstantStack ) if (initialResults.errors.length === 0) { debug?.(` ✅ Validation successful`) @@ -4189,7 +4200,8 @@ async function validateInstantConfigs( hmrRefreshHash, validationRouteTree, navigationParent, - true // use runtime stage for static segments instead + true, // use runtime stage for static segments instead + createInstantStack ) if (runtimeResults.errors.length > 0) { // The errors remained in the runtime stage, so they were caused by a dynamic access. @@ -4221,7 +4233,8 @@ async function validateInstantConfigNavigation( hmrRefreshHash: string | undefined, routeTree: InstantValidation.RouteTree, navigationParent: InstantValidation.SegmentPath, - useRuntimeStageForPartialSegments: boolean + useRuntimeStageForPartialSegments: boolean, + createInstantStack: (() => Error) | null ): Promise<{ dynamicHoleKind: DynamicHoleKind; errors: Array }> { const { implicitTags, nonce, workStore } = ctx const isDebugChannelEnabled = !!ctx.renderOpts.setReactDebugChannel @@ -4237,7 +4250,7 @@ async function validateInstantConfigNavigation( const preinitScripts = () => {} const { ServerInsertedHTMLProvider } = createServerInsertedHTML() - const dynamicValidation = createInstantValidationState() + const dynamicValidation = createInstantValidationState(createInstantStack) const boundaryState = createValidationBoundaryTracking() const finalClientPrerenderStore: PrerenderStore = { diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 043f09a1ffb898..3c97df63e2512e 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -769,7 +769,11 @@ export function trackAllowedDynamicAccess( '. This delays the entire page from rendering, resulting in a ' + 'slow user experience. Learn more: ' + 'https://nextjs.org/docs/messages/blocking-route' - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicErrors.push(error) return } @@ -792,9 +796,12 @@ export type InstantValidationState = { dynamicErrors: Array validationPreventingErrors: Array thrownErrorsOutsideBoundary: Array + createInstantStack: (() => Error) | null } -export function createInstantValidationState(): InstantValidationState { +export function createInstantValidationState( + createInstantStack: (() => Error) | null +): InstantValidationState { return { hasDynamicMetadata: false, hasAllowedClientDynamicAboveBoundary: false, @@ -804,6 +811,7 @@ export function createInstantValidationState(): InstantValidationState { dynamicErrors: [], validationPreventingErrors: [], thrownErrorsOutsideBoundary: [], + createInstantStack, } } @@ -825,7 +833,11 @@ export function trackDynamicHoleInNavigation( ? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateMetadata\` or you have file-based metadata such as icons that depend on dynamic params segments.` : `Uncached data or \`connection()\` was accessed inside \`generateMetadata\`.` const message = `Route "${workStore.route}": ${usageDescription} Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + dynamicValidation.createInstantStack + ) dynamicValidation.dynamicMetadata = error return } @@ -835,7 +847,11 @@ export function trackDynamicHoleInNavigation( ? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateViewport\`.` : `Uncached data or \`connection()\` was accessed inside \`generateViewport\`.` const message = `Route "${workStore.route}": ${usageDescription} This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + dynamicValidation.createInstantStack + ) dynamicValidation.dynamicErrors.push(error) return } @@ -863,7 +879,8 @@ export function trackDynamicHoleInNavigation( const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because a Client Component in a parent segment prevented the page from rendering.` const error = createErrorWithComponentOrOwnerStack( message, - componentStack + componentStack, + dynamicValidation.createInstantStack ) dynamicValidation.validationPreventingErrors.push(error) return @@ -902,9 +919,14 @@ export function trackDynamicHoleInNavigation( if (clientDynamic.syncDynamicErrorWithStack) { // This task was the task that called the sync error. - dynamicValidation.dynamicErrors.push( - clientDynamic.syncDynamicErrorWithStack - ) + const syncError = clientDynamic.syncDynamicErrorWithStack + if ( + dynamicValidation.createInstantStack !== null && + syncError.cause === undefined + ) { + syncError.cause = dynamicValidation.createInstantStack() + } + dynamicValidation.dynamicErrors.push(syncError) return } @@ -913,7 +935,11 @@ export function trackDynamicHoleInNavigation( ? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed outside of \`\`.` : `Uncached data or \`connection()\` was accessed outside of \`\`.` const message = `Route "${workStore.route}": ${usageDescription} This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + dynamicValidation.createInstantStack + ) dynamicValidation.dynamicErrors.push(error) return } @@ -942,12 +968,20 @@ export function trackDynamicHoleInRuntimeShell( return } else if (hasMetadataRegex.test(componentStack)) { const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateMetadata\`. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicMetadata = error return } else if (hasViewportRegex.test(componentStack)) { const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicErrors.push(error) return } else if ( @@ -975,7 +1009,11 @@ export function trackDynamicHoleInRuntimeShell( } const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed outside of \`\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicErrors.push(error) return } @@ -991,12 +1029,20 @@ export function trackDynamicHoleInStaticShell( return } else if (hasMetadataRegex.test(componentStack)) { const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateMetadata\` or you have file-based metadata such as icons that depend on dynamic params segments. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicMetadata = error return } else if (hasViewportRegex.test(componentStack)) { const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicErrors.push(error) return } else if ( @@ -1023,7 +1069,11 @@ export function trackDynamicHoleInStaticShell( return } else { const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed outside of \`\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route` - const error = createErrorWithComponentOrOwnerStack(message, componentStack) + const error = createErrorWithComponentOrOwnerStack( + message, + componentStack, + null + ) dynamicValidation.dynamicErrors.push(error) return } @@ -1035,14 +1085,16 @@ export function trackDynamicHoleInStaticShell( */ function createErrorWithComponentOrOwnerStack( message: string, - componentStack: string + componentStack: string, + createInstantStack: (() => Error) | null ) { const ownerStack = process.env.NODE_ENV !== 'production' && React.captureOwnerStack ? React.captureOwnerStack() : null - const error = new Error(message) + const cause = createInstantStack !== null ? createInstantStack() : null + const error = new Error(message, cause !== null ? { cause } : undefined) // TODO go back to owner stack here if available. This is temporarily using componentStack to get the right // error.stack = error.name + ': ' + message + (ownerStack || componentStack) @@ -1197,27 +1249,29 @@ export function getNavigationDisallowedDynamicReasons( } if (boundaryState.renderedIds.size < boundaryState.expectedIds.size) { - const { thrownErrorsOutsideBoundary } = dynamicValidation + const { thrownErrorsOutsideBoundary, createInstantStack } = + dynamicValidation if (thrownErrorsOutsideBoundary.length === 0) { - return [ - new Error( - `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.` - ), - ] + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.` + const error = + createInstantStack !== null ? createInstantStack() : new Error() + error.name = 'Error' + error.message = message + return [error] } else if (thrownErrorsOutsideBoundary.length === 1) { - return [ - new Error( - `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.` - ), - thrownErrorsOutsideBoundary[0] as Error, - ] + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.` + const error = + createInstantStack !== null ? createInstantStack() : new Error() + error.name = 'Error' + error.message = message + return [error, thrownErrorsOutsideBoundary[0] as Error] } else { - return [ - new Error( - `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.` - ), - ...(thrownErrorsOutsideBoundary as Error[]), - ] + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.` + const error = + createInstantStack !== null ? createInstantStack() : new Error() + error.name = 'Error' + error.message = message + return [error, ...(thrownErrorsOutsideBoundary as Error[])] } } diff --git a/packages/next/src/server/app-render/instant-validation/instant-validation.tsx b/packages/next/src/server/app-render/instant-validation/instant-validation.tsx index 50abee6be06cc2..de7590ccc42686 100644 --- a/packages/next/src/server/app-render/instant-validation/instant-validation.tsx +++ b/packages/next/src/server/app-render/instant-validation/instant-validation.tsx @@ -86,6 +86,7 @@ export type RouteTree = { // TODO(instant-validation): We should know if a layout segment is shared instantConfig: InstantConfig | null conventionPath: string + createInstantStack: (() => Error) | null } slots: { [parallelRouteKey: string]: RouteTree } | null @@ -143,10 +144,15 @@ export async function findNavigationsToValidate( // TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules? const instantConfig = (layoutOrPageMod as AppSegmentConfig).unstable_instant ?? null + const rawFactory: unknown = (layoutOrPageMod as any) + .__debugCreateInstantConfigStack + const createInstantStack: (() => Error) | null = + typeof rawFactory === 'function' ? (rawFactory as () => Error) : null moduleInfo = { type: modType!, instantConfig, conventionPath: conventionPath!, + createInstantStack, } if (isInsideParallelSlot) { @@ -171,9 +177,13 @@ export async function findNavigationsToValidate( } else { const isRootLayout = parentLayoutPath === null if (isRootLayout && instantConfig.prefetch === 'runtime') { - throw new Error( - `${conventionPath}: \`unstable_instant\` with mode 'runtime' is not supported in root layouts.` - ) + const message = `${conventionPath}: \`unstable_instant\` with mode 'runtime' is not supported in root layouts.` + const error = + createInstantStack !== null + ? createInstantStack() + : new Error() + error.message = message + throw error } const task: ValidationTask = { diff --git a/test/e2e/app-dir/instant-validation/instant-validation.test.ts b/test/e2e/app-dir/instant-validation/instant-validation.test.ts index e6208c300a8cd4..1239cfc1200b78 100644 --- a/test/e2e/app-dir/instant-validation/instant-validation.test.ts +++ b/test/e2e/app-dir/instant-validation/instant-validation.test.ts @@ -87,6 +87,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-around-runtime/page.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-around-runtime/page.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -119,6 +132,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-around-dynamic/page.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-around-dynamic/page.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -149,6 +175,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/runtime/missing-suspense-around-dynamic/page.tsx (4:33) @ unstable_instant + > 4 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/runtime/missing-suspense-around-dynamic/page.tsx (4:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -181,6 +220,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-around-dynamic-layout/layout.tsx (4:33) @ unstable_instant + > 4 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-around-dynamic-layout/layout.tsx (4:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -213,6 +265,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/runtime/missing-suspense-around-dynamic-layout/layout.tsx (4:33) @ unstable_instant + > 4 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/runtime/missing-suspense-around-dynamic-layout/layout.tsx (4:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -244,6 +309,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-around-params/[param]/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-around-params/[param]/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -285,6 +363,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-around-search-params/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-around-search-params/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -339,6 +430,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/suspense-too-high/page.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/suspense-too-high/page.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -371,6 +475,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/runtime/suspense-too-high/page.tsx (4:33) @ unstable_instant + > 4 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/runtime/suspense-too-high/page.tsx (4:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -488,6 +605,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/invalid-only-loading-around-dynamic/page.tsx (4:33) @ unstable_instant + > 4 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/invalid-only-loading-around-dynamic/page.tsx (4:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -527,6 +657,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/blocking-layout/missing-suspense-around-dynamic/page.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/blocking-layout/missing-suspense-around-dynamic/page.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -573,6 +716,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/invalid-blocking-inside-static/layout.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/invalid-blocking-inside-static/layout.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -605,6 +761,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/runtime/invalid-blocking-inside-runtime/layout.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/runtime/invalid-blocking-inside-runtime/layout.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Data that blocks navigation was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. Uncached data such as fetch(...), cached data with a low expire time, or connection() are all examples of data that only resolve on navigation. @@ -638,6 +807,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-in-parallel-route/page.tsx (3:33) @ unstable_instant + > 3 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-in-parallel-route/page.tsx (3:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -671,6 +853,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-in-parallel-route/foo/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-in-parallel-route/foo/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -704,6 +899,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/missing-suspense-in-parallel-route/bar/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { prefetch: 'static' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/missing-suspense-in-parallel-route/bar/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Runtime data was accessed outside of This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation. cookies(), headers(), and searchParams, are examples of Runtime data that can only come from a user request. @@ -739,6 +947,19 @@ describe('instant validation', () => { ) await expect(browser).toDisplayCollapsedRedbox(` { + "cause": [ + { + "label": "Caused by: Instant Validation", + "message": "​", + "source": "app/suspense-in-root/static/invalid-client-data-blocks-validation/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/invalid-client-data-blocks-validation/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], "description": "Route "/suspense-in-root/static/invalid-client-data-blocks-validation": Could not validate \`unstable_instant\` because a Client Component in a parent segment prevented the page from rendering.", "environmentLabel": "Server", "label": "Console Error", @@ -835,8 +1056,12 @@ describe('instant validation', () => { "description": "Route "/suspense-in-root/static/invalid-client-error-in-parent-blocks-children": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.", "environmentLabel": "Server", "label": "Console Error", - "source": null, - "stack": [], + "source": "app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/page.tsx (1:33)", + ], }, { "description": "No SSR please", @@ -888,8 +1113,12 @@ describe('instant validation', () => { "description": "Route "/suspense-in-root/static/invalid-client-error-in-parent-sibling": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.", "environmentLabel": "Server", "label": "Console Error", - "source": null, - "stack": [], + "source": "app/suspense-in-root/static/invalid-client-error-in-parent-sibling/page.tsx (1:33) @ unstable_instant + > 1 | export const unstable_instant = { + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/invalid-client-error-in-parent-sibling/page.tsx (1:33)", + ], }, { "description": "No SSR please",