From a7360212e8cd5cd80d8d87c5fda87a923fd9af1f Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 19 Jun 2023 16:12:14 +0200 Subject: [PATCH] fix shared action module in two layers --- .../plugins/flight-client-entry-plugin.ts | 48 +++++++++++++++---- test/e2e/app-dir/actions/app-action.test.ts | 13 +++++ test/e2e/app-dir/actions/app/shared/action.js | 14 ++++++ test/e2e/app-dir/actions/app/shared/client.js | 13 +++++ test/e2e/app-dir/actions/app/shared/page.js | 13 +++++ test/e2e/app-dir/actions/app/shared/server.js | 15 ++++++ 6 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 test/e2e/app-dir/actions/app/shared/action.js create mode 100644 test/e2e/app-dir/actions/app/shared/client.js create mode 100644 test/e2e/app-dir/actions/app/shared/page.js create mode 100644 test/e2e/app-dir/actions/app/shared/server.js diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index cf6f0d097c43a..04d85470434bd 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -353,9 +353,15 @@ export class ClientReferenceEntryPlugin { } }) + const createdActions = new Set() for (const [name, actionEntryImports] of Object.entries( actionMapsPerEntry )) { + for (const [dep, actionNames] of actionEntryImports) { + for (const actionName of actionNames) { + createdActions.add(name + '@' + dep + '@' + actionName) + } + } addActionEntryList.push( this.injectActionEntry({ compiler, @@ -453,16 +459,38 @@ export class ClientReferenceEntryPlugin { for (const [name, actionEntryImports] of Object.entries( actionMapsPerClientEntry )) { - addedClientActionEntryList.push( - this.injectActionEntry({ - compiler, - compilation, - actions: actionEntryImports, - entryName: name, - bundlePath: name, - fromClient: true, - }) - ) + // If an action method is already created in the server layer, we don't + // need to create it again in the action layer. + // This is to avoid duplicate action instances and make sure the module + // state is shared. + let remainingClientImportedActions = false + const remainingActionEntryImports = new Map() + for (const [dep, actionNames] of actionEntryImports) { + const remainingActionNames = [] + for (const actionName of actionNames) { + const id = name + '@' + dep + '@' + actionName + if (!createdActions.has(id)) { + remainingActionNames.push(actionName) + } + } + if (remainingActionNames.length > 0) { + remainingActionEntryImports.set(dep, remainingActionNames) + remainingClientImportedActions = true + } + } + + if (remainingClientImportedActions) { + addedClientActionEntryList.push( + this.injectActionEntry({ + compiler, + compilation, + actions: remainingActionEntryImports, + entryName: name, + bundlePath: name, + fromClient: true, + }) + ) + } } return Promise.all(addedClientActionEntryList) diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 62b857c5a38ea..5fd93b69a5851 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -237,6 +237,19 @@ createNextDescribe( await check(() => browser.elementByCss('h1').text(), '3') }) + it('should support importing the same action module instance in both server and action layers', async () => { + const browser = await next.browser('/shared') + + const v = await browser.elementByCss('#value').text() + expect(v).toBe('Value = 0') + + await browser.elementByCss('#server-inc').click() + await check(() => browser.elementByCss('#value').text(), 'Value = 1') + + await browser.elementByCss('#client-inc').click() + await check(() => browser.elementByCss('#value').text(), 'Value = 2') + }) + if (isNextStart) { it('should not expose action content in sourcemaps', async () => { const sourcemap = ( diff --git a/test/e2e/app-dir/actions/app/shared/action.js b/test/e2e/app-dir/actions/app/shared/action.js new file mode 100644 index 0000000000000..f3dc0d5e73fc3 --- /dev/null +++ b/test/e2e/app-dir/actions/app/shared/action.js @@ -0,0 +1,14 @@ +'use server' + +import { revalidatePath } from 'next/cache' + +let x = 0 + +export async function inc() { + ++x + revalidatePath('/shared') +} + +export async function get() { + return x +} diff --git a/test/e2e/app-dir/actions/app/shared/client.js b/test/e2e/app-dir/actions/app/shared/client.js new file mode 100644 index 0000000000000..bb8f722f228d8 --- /dev/null +++ b/test/e2e/app-dir/actions/app/shared/client.js @@ -0,0 +1,13 @@ +'use client' + +import { inc } from './action' + +export function Client() { + return ( +
+ +
+ ) +} diff --git a/test/e2e/app-dir/actions/app/shared/page.js b/test/e2e/app-dir/actions/app/shared/page.js new file mode 100644 index 0000000000000..007c4990c9307 --- /dev/null +++ b/test/e2e/app-dir/actions/app/shared/page.js @@ -0,0 +1,13 @@ +import { Server } from './server' +import { Client } from './client' + +export default async function Page() { + return ( + <> +
+ +
+ + + ) +} diff --git a/test/e2e/app-dir/actions/app/shared/server.js b/test/e2e/app-dir/actions/app/shared/server.js new file mode 100644 index 0000000000000..57651e53f23a1 --- /dev/null +++ b/test/e2e/app-dir/actions/app/shared/server.js @@ -0,0 +1,15 @@ +import { inc, get } from './action' + +export async function Server() { + const x = await get() + return ( + <> +

Value = {x}

+
+ +
+ + ) +}