Skip to content

Commit 475bdb1

Browse files
unstubbableshuding
andauthored
Avoid server action endpoint function indirection (#71572)
This PR removes the extra wrapper functions and `.bind()` calls from the action entrypoint loader. Instead, the loader module now only re-exports the actions from the original module using the IDs as export names. This ensures that the same action instance will always remain identical. It also unblocks the combined usage of `"use cache"` and `"use server"` functions. --------- Co-authored-by: Shu Ding <[email protected]>
1 parent 45a328a commit 475bdb1

File tree

6 files changed

+82
-31
lines changed

6 files changed

+82
-31
lines changed

packages/next/src/build/webpack/loaders/next-flight-action-entry-loader.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,12 @@ function nextFlightActionEntryLoader(this: any) {
2020
.flat()
2121

2222
return `
23-
const actions = {
2423
${individualActions
2524
.map(([id, path, name]) => {
26-
return `'${id}': () => import(/* webpackMode: "eager" */ ${JSON.stringify(
27-
path
28-
)}).then(mod => mod[${JSON.stringify(name)}]),`
25+
// Re-export the same functions from the original module path as action IDs.
26+
return `export { ${name} as "${id}" } from ${JSON.stringify(path)}`
2927
})
3028
.join('\n')}
31-
}
32-
33-
async function endpoint(id, ...args) {
34-
const action = await actions[id]()
35-
return action.apply(null, args)
36-
}
37-
38-
// Using CJS to avoid this to be tree-shaken away due to unused exports.
39-
module.exports = {
40-
${individualActions
41-
.map(([id]) => {
42-
return ` '${id}': endpoint.bind(null, '${id}'),`
43-
})
44-
.join('\n')}
45-
}
4629
`
4730
}
4831

packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ const PLUGIN_NAME = 'FlightClientEntryPlugin'
5555
type Actions = {
5656
[actionId: string]: {
5757
workers: {
58-
[name: string]: string | number
58+
[name: string]:
59+
| { moduleId: string | number; async: boolean }
60+
// TODO: This is legacy for Turbopack, and needs to be changed to the
61+
// object above.
62+
| string
5963
}
6064
// Record which layer the action is in (rsc or sc_action), in the specific entry.
6165
layer: {
@@ -79,15 +83,15 @@ const pluginState = getProxiedPluginState({
7983
actionModServerId: {} as Record<
8084
string,
8185
{
82-
server?: string | number
83-
client?: string | number
86+
server?: { moduleId: string | number; async: boolean }
87+
client?: { moduleId: string | number; async: boolean }
8488
}
8589
>,
8690
actionModEdgeServerId: {} as Record<
8791
string,
8892
{
89-
server?: string | number
90-
client?: string | number
93+
server?: { moduleId: string | number; async: boolean }
94+
client?: { moduleId: string | number; async: boolean }
9195
}
9296
>,
9397

@@ -879,7 +883,11 @@ export class FlightClientEntryPlugin {
879883
layer: {},
880884
}
881885
}
882-
currentCompilerServerActions[id].workers[bundlePath] = ''
886+
currentCompilerServerActions[id].workers[bundlePath] = {
887+
moduleId: '', // TODO: What's the meaning of this?
888+
async: false,
889+
}
890+
883891
currentCompilerServerActions[id].layer[bundlePath] = fromClient
884892
? WEBPACK_LAYERS.actionBrowser
885893
: WEBPACK_LAYERS.reactServerComponents
@@ -928,6 +936,13 @@ export class FlightClientEntryPlugin {
928936
}
929937

930938
compilation.hooks.succeedEntry.call(dependency, options, module)
939+
940+
compilation.moduleGraph
941+
.getExportsInfo(module)
942+
.setUsedInUnknownWay(
943+
this.isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK
944+
)
945+
931946
return resolve(module)
932947
}
933948
)
@@ -958,7 +973,10 @@ export class FlightClientEntryPlugin {
958973
if (!mapping[chunkGroup.name]) {
959974
mapping[chunkGroup.name] = {}
960975
}
961-
mapping[chunkGroup.name][fromClient ? 'client' : 'server'] = modId
976+
mapping[chunkGroup.name][fromClient ? 'client' : 'server'] = {
977+
moduleId: modId,
978+
async: compilation.moduleGraph.isAsync(mod),
979+
}
962980
}
963981
})
964982

packages/next/src/server/app-render/action-utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ export function createServerModuleMap({
1818
{},
1919
{
2020
get: (_, id: string) => {
21-
return {
22-
id: serverActionsManifest[
21+
const workerEntry =
22+
serverActionsManifest[
2323
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
24-
][id].workers[normalizeWorkerPageName(pageName)],
25-
name: id,
26-
chunks: [],
24+
][id].workers[normalizeWorkerPageName(pageName)]
25+
26+
if (typeof workerEntry === 'string') {
27+
return { id: workerEntry, name: id, chunks: [] }
2728
}
29+
30+
const { moduleId, async } = workerEntry
31+
32+
return { id: moduleId, name: id, chunks: [], async }
2833
},
2934
}
3035
)

test/e2e/app-dir/actions/app-action.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,24 @@ describe('app-dir action handling', () => {
828828
).toBe(true)
829829
})
830830

831+
// we don't have access to runtime logs on deploy
832+
if (!isNextDeploy) {
833+
it('should keep action instances identical', async () => {
834+
const logs: string[] = []
835+
next.on('stdout', (log) => {
836+
logs.push(log)
837+
})
838+
839+
const browser = await next.browser('/identity')
840+
841+
await browser.elementByCss('button').click()
842+
843+
await retry(() => {
844+
expect(logs.join('')).toContain('result: true')
845+
})
846+
})
847+
}
848+
831849
it.each(['node', 'edge'])(
832850
'should forward action request to a worker that contains the action handler (%s)',
833851
async (runtime) => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
export function Client({ foo, b }) {
4+
return (
5+
<button
6+
onClick={() => {
7+
foo(b)
8+
}}
9+
>
10+
Trigger
11+
</button>
12+
)
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Client } from './client'
2+
3+
export default async function Page() {
4+
async function b() {
5+
'use server'
6+
}
7+
8+
async function foo(a) {
9+
'use server'
10+
console.log('result:', a === b)
11+
}
12+
13+
return <Client foo={foo} b={b} />
14+
}

0 commit comments

Comments
 (0)