diff --git a/crates/next-custom-transforms/src/transforms/server_actions.rs b/crates/next-custom-transforms/src/transforms/server_actions.rs index 54914606e2ea8..85ced02aa9235 100644 --- a/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -170,12 +170,13 @@ impl ServerActions { } } - if self.in_export_decl { - if self.in_action_file { - // All export functions in a server file are actions - is_action_fn = true; - } else if let Some(cache_file_type) = &self.in_cache_file { - // All export functions in a cache file are cache functions + if self.in_export_decl && self.in_action_file { + // All export functions in a server file are actions + is_action_fn = true; + } + + if self.in_module_level { + if let Some(cache_file_type) = &self.in_cache_file { cache_type = Some(cache_file_type.clone()); } } @@ -471,7 +472,10 @@ impl ServerActions { self.has_cache = true; self.has_action = true; - self.export_actions.push(export_name.to_string()); + + if self.config.is_react_server_layer { + self.export_actions.push(export_name.to_string()); + } let reference_id = generate_action_id(&self.config.hash_salt, &self.file_name, &export_name); @@ -668,7 +672,10 @@ impl ServerActions { self.has_cache = true; self.has_action = true; - self.export_actions.push(cache_name.to_string()); + + if self.config.is_react_server_layer { + self.export_actions.push(cache_name.to_string()); + } let reference_id = generate_action_id(&self.config.hash_salt, &self.file_name, &cache_name); @@ -1659,7 +1666,9 @@ impl VisitMut for ServerActions { let mut actions = self.export_actions.clone(); // All exported values are considered as actions if the file is an action file. - if self.in_action_file { + if self.in_action_file + || self.in_cache_file.is_some() && !self.config.is_react_server_layer + { actions.extend(self.exported_idents.iter().map(|e| e.1.clone())); }; diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/client/6/input.js b/crates/next-custom-transforms/tests/fixture/server-actions/client/6/input.js index 6c81b7aea14b5..50b9988d8867b 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/client/6/input.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/client/6/input.js @@ -1,3 +1,5 @@ 'use cache' export async function foo() {} +const bar = async () => {} +export { bar } diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/client/6/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/client/6/output.js index 9ba040f715f1b..c10b22c39eb79 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/client/6/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/client/6/output.js @@ -1,2 +1,3 @@ -/* __next_internal_action_entry_do_not_use__ {"3128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { createServerReference, callServer, findSourceMapURL } from "private-next-rsc-action-client-wrapper"; +/* __next_internal_action_entry_do_not_use__ {"ab21efdafbe611287bc25c0462b1e0510d13e48b":"foo","ac840dcaf5e8197cb02b7f3a43c119b7a770b272":"bar"} */ import { createServerReference, callServer, findSourceMapURL } from "private-next-rsc-action-client-wrapper"; export var foo = /*#__PURE__*/ createServerReference("ab21efdafbe611287bc25c0462b1e0510d13e48b", callServer, void 0, findSourceMapURL, "foo"); +export var bar = /*#__PURE__*/ createServerReference("ac840dcaf5e8197cb02b7f3a43c119b7a770b272", callServer, void 0, findSourceMapURL, "bar"); diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/input.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/input.js index 8b850a4e11b81..50b9988d8867b 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/input.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/input.js @@ -1,5 +1,5 @@ 'use cache' -export async function foo() { - return 'data' -} +export async function foo() {} +const bar = async () => {} +export { bar } diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js index dd2c15cd89a78..5ac2d6476edd8 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js @@ -1,7 +1,8 @@ -/* __next_internal_action_entry_do_not_use__ {"3128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; +/* __next_internal_action_entry_do_not_use__ {"3128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0","951c375b4a6a6e89d67b743ec5808127cfde405d":"$$RSC_SERVER_CACHE_1"} */ 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__("default", "3128060c414d59f8552e4788b846c0d2b7f74743", async function foo() { - return 'data'; -}); +export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "3128060c414d59f8552e4788b846c0d2b7f74743", async function foo() {}); export var foo = registerServerReference($$RSC_SERVER_CACHE_0, "3128060c414d59f8552e4788b846c0d2b7f74743", null); +export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "951c375b4a6a6e89d67b743ec5808127cfde405d", async function() {}); +const bar = registerServerReference($$RSC_SERVER_CACHE_1, "951c375b4a6a6e89d67b743ec5808127cfde405d", null); +export { bar }; diff --git a/test/e2e/app-dir/use-cache/app/imported-from-client/cached.ts b/test/e2e/app-dir/use-cache/app/imported-from-client/cached.ts new file mode 100644 index 0000000000000..873f0c8a62ab2 --- /dev/null +++ b/test/e2e/app-dir/use-cache/app/imported-from-client/cached.ts @@ -0,0 +1,15 @@ +'use cache' + +export async function bar() { + const v = Math.random() + console.log(v) + return v +} + +const baz = async () => { + const v = Math.random() + console.log(v) + return v +} + +export { baz } diff --git a/test/e2e/app-dir/use-cache/app/imported-from-client/page.tsx b/test/e2e/app-dir/use-cache/app/imported-from-client/page.tsx new file mode 100644 index 0000000000000..57cc34947221a --- /dev/null +++ b/test/e2e/app-dir/use-cache/app/imported-from-client/page.tsx @@ -0,0 +1,32 @@ +'use client' + +import { useActionState } from 'react' +import { bar, baz } from './cached' + +export default function Page() { + const [result, dispatch] = useActionState< + [number, number], + 'submit' | 'reset' + >( + async (_state, event) => { + if (event === 'reset') { + return [0, 0] + } + + return [await bar(), await baz()] + }, + [0, 0] + ) + + return ( +
dispatch('submit')}> + +

+ {result[0]} {result[1]} +

+ +
+ ) +} diff --git a/test/e2e/app-dir/use-cache/use-cache.test.ts b/test/e2e/app-dir/use-cache/use-cache.test.ts index 5a38570f6523b..7ee969e72501d 100644 --- a/test/e2e/app-dir/use-cache/use-cache.test.ts +++ b/test/e2e/app-dir/use-cache/use-cache.test.ts @@ -1,5 +1,6 @@ /* eslint-disable jest/no-standalone-expect */ import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' const GENERIC_RSC_ERROR = 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.' @@ -108,6 +109,28 @@ describe('use-cache', () => { expect(rand1).toEqual(rand2) }) + it('should cache results for cached funtions imported from client components', async () => { + const browser = await next.browser('/imported-from-client') + expect(await browser.elementByCss('p').text()).toBe('0 0') + await browser.elementById('submit-button').click() + + let twoRandomValues: string + + await retry(async () => { + twoRandomValues = await browser.elementByCss('p').text() + expect(twoRandomValues).toMatch(/\d\.\d+ \d\.\d+/) + }) + + await browser.elementById('reset-button').click() + expect(await browser.elementByCss('p').text()).toBe('0 0') + + await browser.elementById('submit-button').click() + + await retry(async () => { + expect(await browser.elementByCss('p').text()).toBe(twoRandomValues) + }) + }) + if (isNextStart) { it('should match the expected revalidate config on the prerender manifest', async () => { const prerenderManifest = JSON.parse(