Skip to content

Commit

Permalink
Fix shared action module in two layers (#51510)
Browse files Browse the repository at this point in the history
Currently an "action module" (files with `"use server"` on top) can be
imported by both the server and client layers. In that case, we can't
fork that action module into two modules (one on the server layer, one
on the action layer), but only create it once on the server layer.

This ensures that the action module instance doesn't get forked.

Closes #50801.
fix NEXT-1265
  • Loading branch information
shuding committed Jun 19, 2023
1 parent 4cc5715 commit 30bb252
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,15 @@ export class ClientReferenceEntryPlugin {
}
})

const createdActions = new Set<string>()
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,
Expand Down Expand Up @@ -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<string, string[]>()
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)
Expand Down
13 changes: 13 additions & 0 deletions test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
14 changes: 14 additions & 0 deletions test/e2e/app-dir/actions/app/shared/action.js
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/actions/app/shared/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client'

import { inc } from './action'

export function Client() {
return (
<form>
<button id="client-inc" formAction={inc}>
Inc
</button>
</form>
)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/actions/app/shared/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Server } from './server'
import { Client } from './client'

export default async function Page() {
return (
<>
<hr />
<Server />
<hr />
<Client />
</>
)
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/actions/app/shared/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { inc, get } from './action'

export async function Server() {
const x = await get()
return (
<>
<h2 id="value">Value = {x}</h2>
<form>
<button id="server-inc" formAction={inc}>
Inc
</button>
</form>
</>
)
}

0 comments on commit 30bb252

Please sign in to comment.