diff --git a/crates/next-core/src/next_client/transforms.rs b/crates/next-core/src/next_client/transforms.rs index 8800e2332d0d29..d4535342344670 100644 --- a/crates/next-core/src/next_client/transforms.rs +++ b/crates/next-core/src/next_client/transforms.rs @@ -48,6 +48,7 @@ pub async fn get_next_client_transforms_rules( } let dynamic_io_enabled = *next_config.enable_dynamic_io().await?; + let cache_kinds = next_config.cache_kinds(); let mut is_app_dir = false; match context_ty { @@ -74,6 +75,7 @@ pub async fn get_next_client_transforms_rules( ActionsTransform::Client, enable_mdx_rs, dynamic_io_enabled, + cache_kinds, )); } ClientContextType::Fallback | ClientContextType::Other => {} diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 95363a6b7b52dc..ad32a711c63dda 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use anyhow::{bail, Context, Result}; +use rustc_hash::FxHashSet; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; use turbo_tasks::{trace::TraceRawVcs, FxIndexMap, RcStr, ResolvedVc, TaskInput, Vc}; @@ -39,6 +40,9 @@ struct CustomRoutes { #[turbo_tasks::value(transparent)] pub struct ModularizeImports(FxIndexMap); +#[turbo_tasks::value(transparent)] +pub struct CacheKinds(FxHashSet); + #[turbo_tasks::value(serialization = "custom", eq = "manual")] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -528,6 +532,7 @@ pub struct ExperimentalConfig { after: Option, amp: Option, app_document_preloading: Option, + cache_handlers: Option>, cache_life: Option>, case_sensitive_routes: Option, cpus: Option, @@ -1158,6 +1163,19 @@ impl NextConfig { Vc::cell(self.experimental.dynamic_io.unwrap_or(false)) } + #[turbo_tasks::function] + pub fn cache_kinds(&self) -> Vc { + Vc::cell( + self.experimental + .cache_handlers + .clone() + .unwrap_or_default() + .keys() + .cloned() + .collect(), + ) + } + #[turbo_tasks::function] pub fn optimize_package_imports(&self) -> Vc> { Vc::cell( diff --git a/crates/next-core/src/next_server/transforms.rs b/crates/next-core/src/next_server/transforms.rs index b42d720c83ccaa..f12aa9352d39e1 100644 --- a/crates/next-core/src/next_server/transforms.rs +++ b/crates/next-core/src/next_server/transforms.rs @@ -55,6 +55,7 @@ pub async fn get_next_server_transforms_rules( } let dynamic_io_enabled = *next_config.enable_dynamic_io().await?; + let cache_kinds = next_config.cache_kinds(); let mut is_app_dir = false; let is_server_components = match context_ty { @@ -91,6 +92,7 @@ pub async fn get_next_server_transforms_rules( ActionsTransform::Client, mdx_rs, dynamic_io_enabled, + cache_kinds, )); is_app_dir = true; @@ -102,6 +104,7 @@ pub async fn get_next_server_transforms_rules( ActionsTransform::Server, mdx_rs, dynamic_io_enabled, + cache_kinds, )); is_app_dir = true; @@ -113,6 +116,7 @@ pub async fn get_next_server_transforms_rules( ActionsTransform::Server, mdx_rs, dynamic_io_enabled, + cache_kinds, )); is_app_dir = true; diff --git a/crates/next-core/src/next_shared/transforms/server_actions.rs b/crates/next-core/src/next_shared/transforms/server_actions.rs index fce9dfe18c6582..c8a454239d3ddb 100644 --- a/crates/next-core/src/next_shared/transforms/server_actions.rs +++ b/crates/next-core/src/next_shared/transforms/server_actions.rs @@ -7,6 +7,7 @@ use turbopack::module_options::{ModuleRule, ModuleRuleEffect}; use turbopack_ecmascript::{CustomTransformer, EcmascriptInputTransform, TransformContext}; use super::module_rule_match_js_no_url; +use crate::next_config::CacheKinds; #[derive(Debug)] pub enum ActionsTransform { @@ -19,10 +20,12 @@ pub fn get_server_actions_transform_rule( transform: ActionsTransform, enable_mdx_rs: bool, dynamic_io_enabled: bool, + cache_kinds: Vc, ) -> ModuleRule { let transformer = EcmascriptInputTransform::Plugin(Vc::cell(Box::new(NextServerActions { transform, dynamic_io_enabled, + cache_kinds, }) as _)); ModuleRule::new( module_rule_match_js_no_url(enable_mdx_rs), @@ -37,6 +40,7 @@ pub fn get_server_actions_transform_rule( struct NextServerActions { transform: ActionsTransform, dynamic_io_enabled: bool, + cache_kinds: Vc, } #[async_trait] @@ -49,6 +53,7 @@ impl CustomTransformer for NextServerActions { is_react_server_layer: matches!(self.transform, ActionsTransform::Server), dynamic_io_enabled: self.dynamic_io_enabled, hash_salt: "".into(), + cache_kinds: self.cache_kinds.await?.clone_value(), }, ctx.comments.clone(), ); diff --git a/crates/next-custom-transforms/src/transforms/server_actions.rs b/crates/next-custom-transforms/src/transforms/server_actions.rs index c3916da3c1040c..8d82dd3abcf5e9 100644 --- a/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -6,6 +6,7 @@ use std::{ use hex::encode as hex_encode; use indoc::formatdoc; +use rustc_hash::FxHashSet; use serde::Deserialize; use sha1::{Digest, Sha1}; use swc_core::{ @@ -29,6 +30,7 @@ pub struct Config { pub is_react_server_layer: bool, pub dynamic_io_enabled: bool, pub hash_salt: String, + pub cache_kinds: FxHashSet, } enum DirectiveLocation { @@ -65,6 +67,10 @@ enum ServerActionsErrorKind { directive: String, expected_directive: String, }, + UnknownCacheKind { + span: Span, + cache_kind: String, + }, UseCacheWithoutDynamicIO { span: Span, directive: String, @@ -311,7 +317,7 @@ impl ServerActions { &mut is_action_fn, &mut cache_kind, &mut span, - self.config.dynamic_io_enabled, + &self.config, ); if !self.config.is_react_server_layer { @@ -1307,7 +1313,7 @@ impl VisitMut for ServerActions { &mut self.file_cache_kind, &mut self.has_action, &mut self.has_cache, - self.config.dynamic_io_enabled, + &self.config, ); // If we're in a "use cache" file, collect all original IDs from export @@ -2349,7 +2355,7 @@ fn remove_server_directive_index_in_module( file_cache_kind: &mut Option, has_action: &mut bool, has_cache: &mut bool, - dynamic_io_enabled: bool, + config: &Config, ) { let mut is_directive = true; @@ -2375,19 +2381,30 @@ fn remove_server_directive_index_in_module( // `use cache` or `use cache: foo` if value == "use cache" || value.starts_with("use cache: ") { if is_directive { - if !dynamic_io_enabled { + if !config.dynamic_io_enabled { emit_error(ServerActionsErrorKind::UseCacheWithoutDynamicIO { span: *span, directive: value.to_string(), }); } - *file_cache_kind = Some(if value == "use cache" { - "default".into() + if value == "use cache" { + *file_cache_kind = Some("default".into()); } else { // Slice the value after "use cache: " - value.split_at("use cache: ".len()).1.into() - }); + let cache_kind_str: String = + value.split_at("use cache: ".len()).1.into(); + + if !config.cache_kinds.contains(&cache_kind_str) { + emit_error(ServerActionsErrorKind::UnknownCacheKind { + span: *span, + cache_kind: cache_kind_str.clone(), + }); + } + + *file_cache_kind = Some(cache_kind_str) + } + *has_cache = true; return false; } else { @@ -2488,7 +2505,7 @@ fn remove_server_directive_index_in_fn( is_action_fn: &mut bool, cache_kind: &mut Option, action_span: &mut Option, - dynamic_io_enabled: bool, + config: &Config, ) { let mut is_directive = true; @@ -2520,19 +2537,28 @@ fn remove_server_directive_index_in_fn( }); } else if value == "use cache" || value.starts_with("use cache: ") { if is_directive { - if !dynamic_io_enabled { + if !config.dynamic_io_enabled { emit_error(ServerActionsErrorKind::UseCacheWithoutDynamicIO { span: *span, directive: value.to_string(), }); } - *cache_kind = Some(if value == "use cache" { - "default".into() + if value == "use cache" { + *cache_kind = Some("default".into()); } else { // Slice the value after "use cache: " - value.split_at("use cache: ".len()).1.into() - }); + let cache_kind_str: String = value.split_at("use cache: ".len()).1.into(); + + if !config.cache_kinds.contains(&cache_kind_str) { + emit_error(ServerActionsErrorKind::UnknownCacheKind { + span: *span, + cache_kind: cache_kind_str.clone(), + }); + } + + *cache_kind = Some(cache_kind_str); + }; return false; } else { emit_error(ServerActionsErrorKind::MisplacedDirective { @@ -2891,6 +2917,14 @@ fn emit_error(error_kind: ServerActionsErrorKind) { "# }, ), + ServerActionsErrorKind::UnknownCacheKind { span, cache_kind } => ( + span, + formatdoc! { + r#" + Unknown cache kind "{cache_kind}". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + "# + }, + ), ServerActionsErrorKind::UseCacheWithoutDynamicIO { span, directive } => ( span, formatdoc! { diff --git a/crates/next-custom-transforms/tests/errors.rs b/crates/next-custom-transforms/tests/errors.rs index eaf15623992bae..7bee850a1c62eb 100644 --- a/crates/next-custom-transforms/tests/errors.rs +++ b/crates/next-custom-transforms/tests/errors.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{iter::FromIterator, path::PathBuf}; use next_custom_transforms::transforms::{ disallow_re_export_all_in_page::disallow_re_export_all_in_page, @@ -11,6 +11,7 @@ use next_custom_transforms::transforms::{ }, strip_page_exports::{next_transform_strip_page_exports, ExportFilter}, }; +use rustc_hash::FxHashSet; use swc_core::{ common::{FileName, Mark}, ecma::{ @@ -187,6 +188,7 @@ fn react_server_actions_server_errors(input: PathBuf) { is_react_server_layer: true, dynamic_io_enabled: true, hash_salt: "".into(), + cache_kinds: FxHashSet::default(), }, tr.comments.as_ref().clone(), ), @@ -226,6 +228,7 @@ fn react_server_actions_client_errors(input: PathBuf) { is_react_server_layer: false, dynamic_io_enabled: true, hash_salt: "".into(), + cache_kinds: FxHashSet::default(), }, tr.comments.as_ref().clone(), ), @@ -283,6 +286,7 @@ fn use_cache_not_allowed(input: PathBuf) { is_react_server_layer: true, dynamic_io_enabled: false, hash_salt: "".into(), + cache_kinds: FxHashSet::from_iter(["x".into()]), }, tr.comments.as_ref().clone(), ), diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/input.js b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/input.js new file mode 100644 index 00000000000000..be374144ada15d --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/input.js @@ -0,0 +1,5 @@ +'use cache: x' + +export async function foo() { + return 'data' +} diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.js b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.js new file mode 100644 index 00000000000000..fd8981f7b00a23 --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.js @@ -0,0 +1,11 @@ +/* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; +import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; +import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; +export var $$RSC_SERVER_CACHE_0 = $$cache__("x", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() { + return 'data'; +}); +Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { + "value": "foo", + "writable": false +}); +export var foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.stderr b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.stderr new file mode 100644 index 00000000000000..9bfb78fcee2da1 --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/16/output.stderr @@ -0,0 +1,6 @@ + x Unknown cache kind "x". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + | + ,-[input.js:1:1] + 1 | 'use cache: x' + : ^^^^^^^^^^^^^^ + `---- diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/input.js b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/input.js new file mode 100644 index 00000000000000..583e0663a9a77c --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/input.js @@ -0,0 +1,5 @@ +export async function foo() { + 'use cache: x' + + return 'data' +} diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.js b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.js new file mode 100644 index 00000000000000..fd8981f7b00a23 --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.js @@ -0,0 +1,11 @@ +/* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; +import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; +import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; +export var $$RSC_SERVER_CACHE_0 = $$cache__("x", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() { + return 'data'; +}); +Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { + "value": "foo", + "writable": false +}); +export var foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); diff --git a/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.stderr b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.stderr new file mode 100644 index 00000000000000..fa0d10e3839946 --- /dev/null +++ b/crates/next-custom-transforms/tests/errors/server-actions/server-graph/17/output.stderr @@ -0,0 +1,7 @@ + x Unknown cache kind "x". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + | + ,-[input.js:2:1] + 1 | export async function foo() { + 2 | 'use cache: x' + : ^^^^^^^^^^^^^^ + `---- diff --git a/crates/next-custom-transforms/tests/fixture.rs b/crates/next-custom-transforms/tests/fixture.rs index 4275d58e3825f0..27e0203fdb7a2e 100644 --- a/crates/next-custom-transforms/tests/fixture.rs +++ b/crates/next-custom-transforms/tests/fixture.rs @@ -1,5 +1,6 @@ use std::{ env::current_dir, + iter::FromIterator, path::{Path, PathBuf}, }; @@ -21,6 +22,7 @@ use next_custom_transforms::transforms::{ strip_page_exports::{next_transform_strip_page_exports, ExportFilter}, warn_for_edge_runtime::warn_for_edge_runtime, }; +use rustc_hash::FxHashSet; use serde::de::DeserializeOwned; use swc_core::{ common::{comments::SingleThreadedComments, FileName, Mark, SyntaxContext}, @@ -416,6 +418,7 @@ fn server_actions_server_fixture(input: PathBuf) { is_react_server_layer: true, dynamic_io_enabled: true, hash_salt: "".into(), + cache_kinds: FxHashSet::from_iter(["x".into()]), }, _tr.comments.as_ref().clone(), ), @@ -448,6 +451,7 @@ fn next_font_with_directive_fixture(input: PathBuf) { is_react_server_layer: true, dynamic_io_enabled: true, hash_salt: "".into(), + cache_kinds: FxHashSet::default(), }, _tr.comments.as_ref().clone(), ), @@ -473,6 +477,7 @@ fn server_actions_client_fixture(input: PathBuf) { is_react_server_layer: false, dynamic_io_enabled: true, hash_salt: "".into(), + cache_kinds: FxHashSet::default(), }, _tr.comments.as_ref().clone(), ), diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 4b5d3717657fae..1e90db96857ddf 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -65,6 +65,7 @@ function getBaseSWCOptions({ serverReferenceHashSalt, bundleLayer, isDynamicIo, + cacheHandlers, }: { filename: string jest?: boolean @@ -82,6 +83,7 @@ function getBaseSWCOptions({ serverReferenceHashSalt: string bundleLayer?: WebpackLayerName isDynamicIo?: boolean + cacheHandlers?: ExperimentalConfig['cacheHandlers'] }) { const isReactServerLayer = isWebpackServerOnlyLayer(bundleLayer) const isAppRouterPagesLayer = isWebpackAppPagesLayer(bundleLayer) @@ -211,6 +213,7 @@ function getBaseSWCOptions({ isReactServerLayer, dynamicIoEnabled: isDynamicIo, hashSalt: serverReferenceHashSalt, + cacheKinds: cacheHandlers ? Object.keys(cacheHandlers) : [], } : undefined, // For app router we prefer to bundle ESM, @@ -355,6 +358,7 @@ export function getLoaderSWCOptions({ serverReferenceHashSalt, bundleLayer, esm, + cacheHandlers, }: { filename: string development: boolean @@ -379,6 +383,7 @@ export function getLoaderSWCOptions({ serverComponents?: boolean serverReferenceHashSalt: string bundleLayer?: WebpackLayerName + cacheHandlers: ExperimentalConfig['cacheHandlers'] }) { let baseOptions: any = getBaseSWCOptions({ filename, @@ -396,6 +401,7 @@ export function getLoaderSWCOptions({ serverReferenceHashSalt, esm: !!esm, isDynamicIo, + cacheHandlers, }) baseOptions.fontLoaders = { fontLoaders: ['next/font/local', 'next/font/google'], diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index 8b89aba6420478..bce034cfeb4108 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -142,6 +142,7 @@ async function loaderTransform( serverReferenceHashSalt, bundleLayer, esm, + cacheHandlers: nextConfig.experimental?.cacheHandlers, }) const programmaticOptions = { diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/app/layout.tsx b/test/e2e/app-dir/use-cache-unknown-cache-kind/app/layout.tsx new file mode 100644 index 00000000000000..888614deda3ba5 --- /dev/null +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/app/page.tsx b/test/e2e/app-dir/use-cache-unknown-cache-kind/app/page.tsx new file mode 100644 index 00000000000000..7fa0f0ec81336a --- /dev/null +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/app/page.tsx @@ -0,0 +1,5 @@ +'use cache: custom' + +export default async function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/next.config.js b/test/e2e/app-dir/use-cache-unknown-cache-kind/next.config.js new file mode 100644 index 00000000000000..ac4afcf4321968 --- /dev/null +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + dynamicIO: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts b/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts new file mode 100644 index 00000000000000..499d03436220ac --- /dev/null +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts @@ -0,0 +1,155 @@ +import { nextTestSetup } from 'e2e-utils' +import { NextConfig } from 'next' +import { + assertHasRedbox, + assertNoRedbox, + getRedboxDescription, + getRedboxSource, + retry, +} from 'next-test-utils' +import stripAnsi from 'strip-ansi' + +const nextConfigWithCacheHandler: NextConfig = { + experimental: { + dynamicIO: true, + cacheHandlers: { + custom: require.resolve('next/dist/server/lib/cache-handlers/default'), + }, + }, +} + +describe('use-cache-unknown-cache-kind', () => { + const { next, isNextStart, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: process.env.NEXT_TEST_MODE !== 'dev', + skipDeployment: true, + }) + + if (skipped) { + return + } + + if (isNextStart) { + it('should fail the build with an error', async () => { + const { cliOutput } = await next.build() + const buildOutput = getBuildOutput(cliOutput) + + if (isTurbopack) { + expect(buildOutput).toMatchInlineSnapshot(` + "Error: Turbopack build failed with 1 errors: + Page: {"type":"app","side":"server","page":"/page"} + ./app/page.tsx:1:1 + Ecmascript file had an error + > 1 | 'use cache: custom' + | ^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default async function Page() { + 4 | return

hello world

+ + Unknown cache kind "custom". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + + + + at (./app/page.tsx:1:1)" + `) + } else { + expect(buildOutput).toMatchInlineSnapshot(` + " + ./app/page.tsx + Error: x Unknown cache kind "custom". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + | + ,-[1:1] + 1 | 'use cache: custom' + : ^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default async function Page() { + 4 | return

hello world

+ \`---- + + Import trace for requested module: + ./app/page.tsx + + + > Build failed because of webpack errors + " + `) + } + }) + } else { + it('should show a build error', async () => { + const browser = await next.browser('/') + + await assertHasRedbox(browser) + + const errorDescription = await getRedboxDescription(browser) + const errorSource = await getRedboxSource(browser) + + expect(errorDescription).toBe('Failed to compile') + + if (isTurbopack) { + expect(errorSource).toMatchInlineSnapshot(` + "./app/page.tsx:1:1 + Ecmascript file had an error + > 1 | 'use cache: custom' + | ^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default async function Page() { + 4 | return

hello world

+ + Unknown cache kind "custom". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config." + `) + } else { + expect(errorSource).toMatchInlineSnapshot(` + "./app/page.tsx + Error: x Unknown cache kind "custom". Please configure a cache handler for this kind in the experimental "cacheHandlers" object in your Next.js config. + | + ,-[1:1] + 1 | 'use cache: custom' + : ^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default async function Page() { + 4 | return

hello world

+ \`----" + `) + } + }) + + it('should recover from the build error if the cache handler is defined', async () => { + const browser = await next.browser('/') + + await assertHasRedbox(browser) + + await next.patchFile( + 'next.config.js', + `module.exports = ${JSON.stringify(nextConfigWithCacheHandler)}`, + () => + retry(async () => { + expect(await browser.elementByCss('p').text()).toBe('hello world') + await assertNoRedbox(browser) + }) + ) + }) + } +}) + +function getBuildOutput(cliOutput: string): string { + const lines: string[] = [] + let skipLines = true + + for (const line of cliOutput.split('\n')) { + if (!skipLines) { + if (line.includes('at turbopackBuild')) { + break + } + + lines.push(stripAnsi(line)) + } else if ( + line.includes('Build error occurred') || + line.includes('Failed to compile') + ) { + skipLines = false + } + } + + return lines.join('\n') +}