diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index 9a8a45e8dea5c..005bc657faa3b 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -66,6 +66,16 @@ const getCacheKey = () => { return pathname + search } +async function sha1(message: string) { + const arrayBuffer = await crypto.subtle.digest( + 'SHA-1', + new TextEncoder().encode(message) + ) + const data = Array.from(new Uint8Array(arrayBuffer)) + const hex = data.map((b) => b.toString(16).padStart(2, '0')).join('') + return hex +} + const encoder = new TextEncoder() let initialServerDataBuffer: string[] | undefined = undefined @@ -150,7 +160,32 @@ function useInitialServerResponse(cacheKey: string): Promise { }, }) - const newResponse = createFromReadableStream(readable) + const newResponse = createFromReadableStream(readable, { + async callServer( + metadata: { + id: string + name: string + }, + args: any[] + ) { + const actionId = await sha1(metadata.id + ':' + metadata.name) + + // Fetching the current url with the action header. + // TODO: Refactor this to look up from a manifest. + const res = await fetch('', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Next-Action': actionId, + }, + body: JSON.stringify({ + bound: args, + }), + }) + + return res.json() + }, + }) rscCache.set(cacheKey, newResponse) return newResponse diff --git a/packages/next/src/client/components/app-router-headers.ts b/packages/next/src/client/components/app-router-headers.ts index 26886848a0ab8..8fc8a64c3b6f0 100644 --- a/packages/next/src/client/components/app-router-headers.ts +++ b/packages/next/src/client/components/app-router-headers.ts @@ -1,5 +1,5 @@ export const RSC = 'RSC' as const -export const ACTION = 'Action' as const +export const ACTION = 'Next-Action' as const export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx index b619ccbe428d2..ba8930abe4db5 100644 --- a/packages/next/src/server/app-render.tsx +++ b/packages/next/src/server/app-render.tsx @@ -1703,20 +1703,33 @@ export async function renderToHTMLOrFlight( } // For action requests, we handle them differently with a sepcial render result. - if (isAction && process.env.NEXT_RUNTIME !== 'edge') { - const workerName = 'app' + renderOpts.pathname - const actionModId = serverActionsManifest[actionId].workers[workerName] + if (isAction) { + if (process.env.NEXT_RUNTIME !== 'edge') { + const workerName = 'app' + renderOpts.pathname + const actionModId = serverActionsManifest[actionId].workers[workerName] - const { parseBody } = - require('./api-utils/node') as typeof import('./api-utils/node') - const actionData = (await parseBody(req, '1mb')) || {} + const { parseBody } = + require('./api-utils/node') as typeof import('./api-utils/node') + const actionData = (await parseBody(req, '1mb')) || {} - const actionHandler = - ComponentMod.__next_app_webpack_require__(actionModId).default + const actionHandler = + ComponentMod.__next_app_webpack_require__(actionModId).default - return new ActionRenderResult( - JSON.stringify(await actionHandler(actionId, actionData.bound || [])) - ) + try { + return new ActionRenderResult( + JSON.stringify( + await actionHandler(actionId, actionData.bound || []) + ) + ) + } catch (err) { + if (isRedirectError(err)) { + throw new Error('Invariant: not implemented.') + } + throw err + } + } else { + throw new Error('Not implemented in Edge Runtime.') + } } // Below this line is handling for rendering to HTML. diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts new file mode 100644 index 0000000000000..a4a7e173e5bae --- /dev/null +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -0,0 +1,38 @@ +import { createNextDescribe } from 'e2e-utils' +import { check } from 'next-test-utils' + +createNextDescribe( + 'app-dir action handling', + { + files: __dirname, + skipDeployment: true, + }, + ({ next, isNextDev }) => { + if (!isNextDev) { + it('should create the server reference manifest', async () => { + const content = await next.readFile( + '.next/server/server-reference-manifest.json' + ) + // Make sure it's valid JSON + JSON.parse(content) + expect(content.length > 0).toBeTrue() + }) + } + + // TODO: Ensure this works in production. + if (isNextDev) { + it('should handle basic actions correctly', async () => { + const browser = await next.browser('/server') + + const cnt = await browser.elementByCss('h1').text() + expect(cnt).toBe('0') + + await browser.elementByCss('#inc').click() + await check(() => browser.elementByCss('h1').text(), '1') + + await browser.elementByCss('#dec').click() + await check(() => browser.elementByCss('h1').text(), '0') + }) + } + } +) diff --git a/test/e2e/app-dir/actions/app/layout.js b/test/e2e/app-dir/actions/app/layout.js new file mode 100644 index 0000000000000..6d7e1ed585862 --- /dev/null +++ b/test/e2e/app-dir/actions/app/layout.js @@ -0,0 +1,8 @@ +export default function RootLayout({ children }) { + return ( + + + {children} + + ) +} diff --git a/test/e2e/app-dir/actions/app/server/actions.js b/test/e2e/app-dir/actions/app/server/actions.js new file mode 100644 index 0000000000000..fdd9f22f3a658 --- /dev/null +++ b/test/e2e/app-dir/actions/app/server/actions.js @@ -0,0 +1,9 @@ +'use server' + +export async function inc(value) { + return value + 1 +} + +export async function dec(value) { + return value - 1 +} diff --git a/test/e2e/app-dir/actions/app/server/counter.js b/test/e2e/app-dir/actions/app/server/counter.js new file mode 100644 index 0000000000000..b685677ac72ab --- /dev/null +++ b/test/e2e/app-dir/actions/app/server/counter.js @@ -0,0 +1,31 @@ +'use client' + +import { useState } from 'react' + +export default function Counter({ inc, dec }) { + const [count, setCount] = useState(0) + + return ( +
+

{count}

+ + +
+ ) +} diff --git a/test/e2e/app-dir/actions/app/server/page.js b/test/e2e/app-dir/actions/app/server/page.js new file mode 100644 index 0000000000000..7e6f7f73233d9 --- /dev/null +++ b/test/e2e/app-dir/actions/app/server/page.js @@ -0,0 +1,7 @@ +import Counter from './counter' + +import { inc, dec } from './actions' + +export default function Page() { + return +} diff --git a/test/e2e/app-dir/actions/next.config.js b/test/e2e/app-dir/actions/next.config.js new file mode 100644 index 0000000000000..cfa3ac3d7aa94 --- /dev/null +++ b/test/e2e/app-dir/actions/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + appDir: true, + }, +}