diff --git a/packages/next/src/build/webpack/loaders/next-flight-action-entry-loader.ts b/packages/next/src/build/webpack/loaders/next-flight-action-entry-loader.ts index b18f17e5d03e0..43715a887bd80 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-action-entry-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-action-entry-loader.ts @@ -20,29 +20,12 @@ function nextFlightActionEntryLoader(this: any) { .flat() return ` -const actions = { ${individualActions .map(([id, path, name]) => { - return `'${id}': () => import(/* webpackMode: "eager" */ ${JSON.stringify( - path - )}).then(mod => mod[${JSON.stringify(name)}]),` + // Re-export the same functions from the original module path as action IDs. + return `export { ${name} as "${id}" } from ${JSON.stringify(path)}` }) .join('\n')} -} - -async function endpoint(id, ...args) { - const action = await actions[id]() - return action.apply(null, args) -} - -// Using CJS to avoid this to be tree-shaken away due to unused exports. -module.exports = { -${individualActions - .map(([id]) => { - return ` '${id}': endpoint.bind(null, '${id}'),` - }) - .join('\n')} -} ` } 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 95525bc8636d1..89ff129159c40 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 @@ -55,7 +55,11 @@ const PLUGIN_NAME = 'FlightClientEntryPlugin' type Actions = { [actionId: string]: { workers: { - [name: string]: string | number + [name: string]: + | { moduleId: string | number; async: boolean } + // TODO: This is legacy for Turbopack, and needs to be changed to the + // object above. + | string } // Record which layer the action is in (rsc or sc_action), in the specific entry. layer: { @@ -79,15 +83,15 @@ const pluginState = getProxiedPluginState({ actionModServerId: {} as Record< string, { - server?: string | number - client?: string | number + server?: { moduleId: string | number; async: boolean } + client?: { moduleId: string | number; async: boolean } } >, actionModEdgeServerId: {} as Record< string, { - server?: string | number - client?: string | number + server?: { moduleId: string | number; async: boolean } + client?: { moduleId: string | number; async: boolean } } >, @@ -879,7 +883,11 @@ export class FlightClientEntryPlugin { layer: {}, } } - currentCompilerServerActions[id].workers[bundlePath] = '' + currentCompilerServerActions[id].workers[bundlePath] = { + moduleId: '', // TODO: What's the meaning of this? + async: false, + } + currentCompilerServerActions[id].layer[bundlePath] = fromClient ? WEBPACK_LAYERS.actionBrowser : WEBPACK_LAYERS.reactServerComponents @@ -928,6 +936,13 @@ export class FlightClientEntryPlugin { } compilation.hooks.succeedEntry.call(dependency, options, module) + + compilation.moduleGraph + .getExportsInfo(module) + .setUsedInUnknownWay( + this.isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK + ) + return resolve(module) } ) @@ -958,7 +973,10 @@ export class FlightClientEntryPlugin { if (!mapping[chunkGroup.name]) { mapping[chunkGroup.name] = {} } - mapping[chunkGroup.name][fromClient ? 'client' : 'server'] = modId + mapping[chunkGroup.name][fromClient ? 'client' : 'server'] = { + moduleId: modId, + async: compilation.moduleGraph.isAsync(mod), + } } }) diff --git a/packages/next/src/server/app-render/action-utils.ts b/packages/next/src/server/app-render/action-utils.ts index ccc695a2f7de9..f5ebaa3c698bd 100644 --- a/packages/next/src/server/app-render/action-utils.ts +++ b/packages/next/src/server/app-render/action-utils.ts @@ -18,13 +18,18 @@ export function createServerModuleMap({ {}, { get: (_, id: string) => { - return { - id: serverActionsManifest[ + const workerEntry = + serverActionsManifest[ process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node' - ][id].workers[normalizeWorkerPageName(pageName)], - name: id, - chunks: [], + ][id].workers[normalizeWorkerPageName(pageName)] + + if (typeof workerEntry === 'string') { + return { id: workerEntry, name: id, chunks: [] } } + + const { moduleId, async } = workerEntry + + return { id: moduleId, name: id, chunks: [], async } }, } ) diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 4e2bccbaf1bf3..21968dcc4b9aa 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -828,6 +828,24 @@ describe('app-dir action handling', () => { ).toBe(true) }) + // we don't have access to runtime logs on deploy + if (!isNextDeploy) { + it('should keep action instances identical', async () => { + const logs: string[] = [] + next.on('stdout', (log) => { + logs.push(log) + }) + + const browser = await next.browser('/identity') + + await browser.elementByCss('button').click() + + await retry(() => { + expect(logs.join('')).toContain('result: true') + }) + }) + } + it.each(['node', 'edge'])( 'should forward action request to a worker that contains the action handler (%s)', async (runtime) => { diff --git a/test/e2e/app-dir/actions/app/identity/client.js b/test/e2e/app-dir/actions/app/identity/client.js new file mode 100644 index 0000000000000..054cd2329609b --- /dev/null +++ b/test/e2e/app-dir/actions/app/identity/client.js @@ -0,0 +1,13 @@ +'use client' + +export function Client({ foo, b }) { + return ( + + ) +} diff --git a/test/e2e/app-dir/actions/app/identity/page.js b/test/e2e/app-dir/actions/app/identity/page.js new file mode 100644 index 0000000000000..32f5c43734fa6 --- /dev/null +++ b/test/e2e/app-dir/actions/app/identity/page.js @@ -0,0 +1,14 @@ +import { Client } from './client' + +export default async function Page() { + async function b() { + 'use server' + } + + async function foo(a) { + 'use server' + console.log('result:', a === b) + } + + return +}