diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 8b7fe317537d5..bdc066b9e9271 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -11,6 +11,8 @@ import type { BuildManifest } from '../../server/get-page-files' import type { RequestData } from '../../server/web/types' import type { NextConfigComplete } from '../../server/config-shared' import { PAGE_TYPES } from '../../lib/page-types' +import { setReferenceManifestsSingleton } from '../../server/app-render/action-encryption-utils' +import { createServerModuleMap } from '../../server/app-render/action-utils' declare const incrementalCacheHandler: any // OPTIONAL_IMPORT:incrementalCacheHandler @@ -44,6 +46,17 @@ const subresourceIntegrityManifest = sriEnabled : undefined const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST) +if (rscManifest && rscServerManifest) { + setReferenceManifestsSingleton({ + clientReferenceManifest: rscManifest, + serverActionsManifest: rscServerManifest, + serverModuleMap: createServerModuleMap({ + serverActionsManifest: rscServerManifest, + pageName: 'VAR_PAGE', + }), + }) +} + const render = getRender({ pagesType: PAGE_TYPES.APP, dev, diff --git a/packages/next/src/server/app-render/action-utils.ts b/packages/next/src/server/app-render/action-utils.ts new file mode 100644 index 0000000000000..001b85b9ff1ff --- /dev/null +++ b/packages/next/src/server/app-render/action-utils.ts @@ -0,0 +1,28 @@ +import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin' + +// This function creates a Flight-acceptable server module map proxy from our +// Server Reference Manifest similar to our client module map. +// This is because our manifest contains a lot of internal Next.js data that +// are relevant to the runtime, workers, etc. that React doesn't need to know. +export function createServerModuleMap({ + serverActionsManifest, + pageName, +}: { + serverActionsManifest: ActionManifest + pageName: string +}) { + return new Proxy( + {}, + { + get: (_, id: string) => { + return { + id: serverActionsManifest[ + process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node' + ][id].workers['app' + pageName], + name: id, + chunks: [], + } + }, + } + ) +} diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index a88c869987db7..5f83c8d675120 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -80,6 +80,7 @@ import { DetachedPromise } from '../../lib/detached-promise' import { isDynamicServerError } from '../../client/components/hooks-server-context' import { useFlightResponse } from './use-flight-response' import { isStaticGenBailoutError } from '../../client/components/static-generation-bailout' +import { createServerModuleMap } from './action-utils' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -586,27 +587,10 @@ async function renderToHTMLOrFlightImpl( // TODO: fix this typescript const clientReferenceManifest = renderOpts.clientReferenceManifest! - const workerName = 'app' + renderOpts.page - const serverModuleMap: { - [id: string]: { - id: string - chunks: string[] - name: string - } - } = new Proxy( - {}, - { - get: (_, id: string) => { - return { - id: serverActionsManifest[ - process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node' - ][id].workers[workerName], - name: id, - chunks: [], - } - }, - } - ) + const serverModuleMap = createServerModuleMap({ + serverActionsManifest, + pageName: renderOpts.page, + }) setReferenceManifestsSingleton({ clientReferenceManifest, diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index ad5ef1c7c2832..75b004b75f0e3 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -12,6 +12,7 @@ import type { } from 'next/types' import type { RouteModule } from './future/route-modules/route-module' import type { BuildManifest } from './get-page-files' +import type { ActionManifest } from '../build/webpack/plugins/flight-client-entry-plugin' import { BUILD_MANIFEST, @@ -26,6 +27,9 @@ import { getTracer } from './lib/trace/tracer' import { LoadComponentsSpan } from './lib/trace/constants' import { evalManifest, loadManifest } from './load-manifest' import { wait } from '../lib/wait' +import { setReferenceManifestsSingleton } from './app-render/action-encryption-utils' +import { createServerModuleMap } from './app-render/action-utils' + export type ManifestItem = { id: number | string files: string[] @@ -132,15 +136,13 @@ async function loadComponentsImpl({ Promise.resolve().then(() => requirePage('/_app', distDir, false)), ]) } - const ComponentMod = await Promise.resolve().then(() => - requirePage(page, distDir, isAppPath) - ) // Make sure to avoid loading the manifest for Route Handlers const hasClientManifest = isAppPath && (page.endsWith('/page') || page === '/not-found' || page === '/_not-found') + // Load the manifest files first const [ buildManifest, reactLoadableManifest, @@ -165,12 +167,30 @@ async function loadComponentsImpl({ ) : undefined, isAppPath - ? loadManifestWithRetries( + ? (loadManifestWithRetries( join(distDir, 'server', SERVER_REFERENCE_MANIFEST + '.json') - ).catch(() => null) + ).catch(() => null) as Promise) : null, ]) + // Before requring the actual page module, we have to set the reference manifests + // to our global store so Server Action's encryption util can access to them + // at the top level of the page module. + if (serverActionsManifest && clientReferenceManifest) { + setReferenceManifestsSingleton({ + clientReferenceManifest, + serverActionsManifest, + serverModuleMap: createServerModuleMap({ + serverActionsManifest, + pageName: page, + }), + }) + } + + const ComponentMod = await Promise.resolve().then(() => + requirePage(page, distDir, isAppPath) + ) + const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) const App = interopDefault(AppMod) diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b832e407b866e..c89c3710518d5 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -981,6 +981,7 @@ createNextDescribe( const res = await next.fetch('/encryption') const html = await res.text() expect(html).not.toContain('qwerty123') + expect(html).not.toContain('some-module-level-encryption-value') }) }) diff --git a/test/e2e/app-dir/actions/app/encryption/page.js b/test/e2e/app-dir/actions/app/encryption/page.js index 77b8c56111e53..822d5a9fa1288 100644 --- a/test/e2e/app-dir/actions/app/encryption/page.js +++ b/test/e2e/app-dir/actions/app/encryption/page.js @@ -1,3 +1,14 @@ +// Test top-level encryption (happens during the module load phase) +function wrapAction(value) { + return async function () { + 'use server' + console.log(value) + } +} + +const action = wrapAction('some-module-level-encryption-value') + +// Test runtime encryption (happens during the rendering phase) export default function Page() { const secret = 'my password is qwerty123' @@ -6,6 +17,7 @@ export default function Page() { action={async () => { 'use server' console.log(secret) + await action() return 'success' }} >