diff --git a/packages/next-swc/crates/napi/src/next_api/project.rs b/packages/next-swc/crates/napi/src/next_api/project.rs index 396f6ad2b6..1c70a93c7a 100644 --- a/packages/next-swc/crates/napi/src/next_api/project.rs +++ b/packages/next-swc/crates/napi/src/next_api/project.rs @@ -9,8 +9,8 @@ use napi::{ use next_api::{ entrypoints::Entrypoints, project::{ - DefineEnv, Instrumentation, Middleware, PartialProjectOptions, Project, ProjectContainer, - ProjectOptions, + DefineEnv, DraftModeOptions, Instrumentation, Middleware, PartialProjectOptions, Project, + ProjectContainer, ProjectOptions, }, route::{Endpoint, Route}, }; @@ -63,6 +63,23 @@ pub struct NapiEnvVar { pub value: String, } +#[napi(object)] +pub struct NapiDraftModeOptions { + pub preview_mode_id: String, + pub preview_mode_encryption_key: String, + pub preview_mode_signing_key: String, +} + +impl From for DraftModeOptions { + fn from(val: NapiDraftModeOptions) -> Self { + DraftModeOptions { + preview_mode_id: val.preview_mode_id, + preview_mode_encryption_key: val.preview_mode_encryption_key, + preview_mode_signing_key: val.preview_mode_signing_key, + } + } +} + #[napi(object)] pub struct NapiProjectOptions { /// A root path from which all files must be nested under. Trying to access @@ -94,6 +111,15 @@ pub struct NapiProjectOptions { /// The mode in which Next.js is running. pub dev: bool, + + /// The server actions encryption key. + pub encryption_key: String, + + /// The build id. + pub build_id: String, + + /// Options for draft mode. + pub preview_props: NapiDraftModeOptions, } /// [NapiProjectOptions] with all fields optional. @@ -128,6 +154,15 @@ pub struct NapiPartialProjectOptions { /// The mode in which Next.js is running. pub dev: Option, + + /// The server actions encryption key. + pub encryption_key: Option, + + /// The build id. + pub build_id: Option, + + /// Options for draft mode. + pub preview_props: Option, } #[napi(object)] @@ -159,6 +194,9 @@ impl From for ProjectOptions { .collect(), define_env: val.define_env.into(), dev: val.dev, + encryption_key: val.encryption_key, + build_id: val.build_id, + preview_props: val.preview_props.into(), } } } @@ -176,6 +214,9 @@ impl From for PartialProjectOptions { .map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()), define_env: val.define_env.map(|env| env.into()), dev: val.dev, + encryption_key: val.encryption_key, + build_id: val.build_id, + preview_props: val.preview_props.map(|props| props.into()), } } } diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 21763411e2..61515c5c4f 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -1100,6 +1100,7 @@ impl AppEndpoint { .clone() .map(Regions::Multiple), matchers: vec![matchers], + env: this.app_project.project().edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs index 89fbd46c50..f509b95415 100644 --- a/packages/next-swc/crates/next-api/src/middleware.rs +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -161,6 +161,7 @@ impl MiddlewareEndpoint { page: "/".to_string(), regions: None, matchers, + env: this.project.edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/pages.rs b/packages/next-swc/crates/next-api/src/pages.rs index b76e96322a..e4e7595cc2 100644 --- a/packages/next-swc/crates/next-api/src/pages.rs +++ b/packages/next-swc/crates/next-api/src/pages.rs @@ -1095,6 +1095,7 @@ impl PageEndpoint { page: original_name.to_string(), regions: None, matchers: vec![matchers], + env: this.pages_project.project().edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/project.rs b/packages/next-swc/crates/next-api/src/project.rs index ee0ee83335..9c9c9eaf05 100644 --- a/packages/next-swc/crates/next-api/src/project.rs +++ b/packages/next-swc/crates/next-api/src/project.rs @@ -1,7 +1,7 @@ use std::path::MAIN_SEPARATOR; use anyhow::Result; -use indexmap::{map::Entry, IndexMap}; +use indexmap::{indexmap, map::Entry, IndexMap}; use next_core::{ all_assets_from_entries, app_structure::find_app_dir, @@ -68,6 +68,14 @@ use crate::{ versioned_content_map::{OutputAssetsOperation, VersionedContentMap}, }; +#[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +pub struct DraftModeOptions { + pub preview_mode_id: String, + pub preview_mode_encryption_key: String, + pub preview_mode_signing_key: String, +} + #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ProjectOptions { @@ -96,6 +104,15 @@ pub struct ProjectOptions { /// The mode in which Next.js is running. pub dev: bool, + + /// The server actions encryption key. + pub encryption_key: String, + + /// The build id. + pub build_id: String, + + /// Options for draft mode. + pub preview_props: DraftModeOptions, } #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] @@ -126,6 +143,15 @@ pub struct PartialProjectOptions { /// The mode in which Next.js is running. pub dev: Option, + + /// The server actions encryption key. + pub encryption_key: Option, + + /// The build id. + pub build_id: Option, + + /// Options for draft mode. + pub preview_props: Option, } #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] @@ -166,29 +192,55 @@ impl ProjectContainer { #[turbo_tasks::function] pub fn update(&self, options: PartialProjectOptions) -> Vc<()> { + let PartialProjectOptions { + root_path, + project_path, + next_config, + js_config, + env, + define_env, + watch, + dev, + encryption_key, + build_id, + preview_props, + } = options; + let mut new_options = self.options_state.get().clone(); - if let Some(root_path) = options.root_path { + if let Some(root_path) = root_path { new_options.root_path = root_path; } - if let Some(project_path) = options.project_path { + if let Some(project_path) = project_path { new_options.project_path = project_path; } - if let Some(next_config) = options.next_config { + if let Some(next_config) = next_config { new_options.next_config = next_config; } - if let Some(js_config) = options.js_config { + if let Some(js_config) = js_config { new_options.js_config = js_config; } - if let Some(env) = options.env { + if let Some(env) = env { new_options.env = env; } - if let Some(define_env) = options.define_env { + if let Some(define_env) = define_env { new_options.define_env = define_env; } - if let Some(watch) = options.watch { + if let Some(watch) = watch { new_options.watch = watch; } + if let Some(dev) = dev { + new_options.dev = dev; + } + if let Some(encryption_key) = encryption_key { + new_options.encryption_key = encryption_key; + } + if let Some(build_id) = build_id { + new_options.build_id = build_id; + } + if let Some(preview_props) = preview_props { + new_options.preview_props = preview_props; + } // TODO: Handle mode switch, should prevent mode being switched. @@ -201,32 +253,36 @@ impl ProjectContainer { pub async fn project(self: Vc) -> Result> { let this = self.await?; - let (env, define_env, next_config, js_config, root_path, project_path, watch, dev) = { + let env_map: Vc; + let next_config; + let define_env; + let js_config; + let root_path; + let project_path; + let watch; + let dev; + let encryption_key; + let build_id; + let preview_props; + { let options = this.options_state.get(); - let env: Vc = Vc::cell(options.env.iter().cloned().collect()); - let define_env: Vc = ProjectDefineEnv { + env_map = Vc::cell(options.env.iter().cloned().collect()); + define_env = ProjectDefineEnv { client: Vc::cell(options.define_env.client.iter().cloned().collect()), edge: Vc::cell(options.define_env.edge.iter().cloned().collect()), nodejs: Vc::cell(options.define_env.nodejs.iter().cloned().collect()), } .cell(); - let next_config = NextConfig::from_string(Vc::cell(options.next_config.clone())); - let js_config = JsConfig::from_string(Vc::cell(options.js_config.clone())); - let root_path = options.root_path.clone(); - let project_path = options.project_path.clone(); - let watch = options.watch; - let dev = options.dev; - ( - env, - define_env, - next_config, - js_config, - root_path, - project_path, - watch, - dev, - ) - }; + next_config = NextConfig::from_string(Vc::cell(options.next_config.clone())); + js_config = JsConfig::from_string(Vc::cell(options.js_config.clone())); + root_path = options.root_path.clone(); + project_path = options.project_path.clone(); + watch = options.watch; + dev = options.dev; + encryption_key = options.encryption_key.clone(); + build_id = options.build_id.clone(); + preview_props = options.preview_props.clone(); + } let dist_dir = next_config .await? @@ -241,7 +297,7 @@ impl ProjectContainer { next_config, js_config, dist_dir, - env: Vc::upcast(env), + env: Vc::upcast(env_map), define_env, browserslist_query: "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \ versions, last 1 Edge versions" @@ -252,6 +308,9 @@ impl ProjectContainer { NextMode::Build.cell() }, versioned_content_map: this.versioned_content_map, + build_id, + encryption_key, + preview_props, } .cell()) } @@ -323,6 +382,12 @@ pub struct Project { mode: Vc, versioned_content_map: Vc, + + build_id: String, + + encryption_key: String, + + preview_props: DraftModeOptions, } #[turbo_tasks::value] @@ -545,6 +610,18 @@ impl Project { )) } + #[turbo_tasks::function] + pub(super) fn edge_env(&self) -> Vc { + let edge_env = indexmap! { + "__NEXT_BUILD_ID".to_string() => self.build_id.clone(), + "NEXT_SERVER_ACTIONS_ENCRYPTION_KEY".to_string() => self.encryption_key.clone(), + "__NEXT_PREVIEW_MODE_ID".to_string() => self.preview_props.preview_mode_id.clone(), + "__NEXT_PREVIEW_MODE_ENCRYPTION_KEY".to_string() => self.preview_props.preview_mode_encryption_key.clone(), + "__NEXT_PREVIEW_MODE_SIGNING_KEY".to_string() => self.preview_props.preview_mode_signing_key.clone(), + }; + Vc::cell(edge_env) + } + #[turbo_tasks::function] pub(super) async fn client_chunking_context( self: Vc, diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index fdb7dc3193..77f4a1b148 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -152,7 +152,6 @@ async fn wrap_edge_page( let next_config = &*next_config.await?; // TODO(WEB-1824): add build support - let build_id = "development"; let dev = true; // TODO(timneutkens): remove this @@ -174,7 +173,6 @@ async fn wrap_edge_page( indexmap! { "VAR_USERLAND" => INNER.to_string(), "VAR_PAGE" => page.to_string(), - "VAR_BUILD_ID" => build_id.to_string(), }, indexmap! { "sriEnabled" => serde_json::Value::Bool(sri_enabled).to_string(), diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index 632e0cf051..409078f8e0 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod client_reference_manifest; use std::collections::HashMap; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use serde::{Deserialize, Serialize}; use turbo_tasks::{trace::TraceRawVcs, TaskInput}; @@ -88,6 +88,7 @@ pub struct EdgeFunctionDefinition { pub assets: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub regions: Option, + pub env: IndexMap, } #[derive(Serialize, Default, Debug)] diff --git a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs index dbdfffaeda..4800d5b772 100644 --- a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs @@ -212,7 +212,6 @@ async fn wrap_edge_page( let next_config = &*next_config.await?; // TODO(WEB-1824): add build support - let build_id = "development"; let dev = true; let sri_enabled = !dev @@ -229,7 +228,6 @@ async fn wrap_edge_page( indexmap! { "VAR_USERLAND" => INNER.to_string(), "VAR_PAGE" => pathname.clone(), - "VAR_BUILD_ID" => build_id.to_string(), "VAR_MODULE_DOCUMENT" => INNER_DOCUMENT.to_string(), "VAR_MODULE_APP" => INNER_APP.to_string(), "VAR_MODULE_GLOBAL_ERROR" => INNER_ERROR.to_string(), diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 7ae4c637f1..9652b0a356 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -406,7 +406,6 @@ export function getEdgeServerEntry(opts: { absoluteDocumentPath: opts.pages['/_document'], absoluteErrorPath: opts.pages['/_error'], absolutePagePath: opts.absolutePagePath, - buildId: opts.buildId, dev: opts.isDev, isServerComponent: opts.isServerComponent, page: opts.page, diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 6b82ef4575..0bc4c325e3 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1397,6 +1397,9 @@ export default async function build( // TODO: Implement middlewareMatchers: undefined, }), + buildId: NextBuildContext.buildId!, + encryptionKey: NextBuildContext.encryptionKey!, + previewProps: NextBuildContext.previewProps!, }) await fs.mkdir(path.join(distDir, 'server'), { recursive: true }) @@ -2750,7 +2753,8 @@ export default async function build( }, ] - routes.forEach((route) => { + // Always sort the routes to get consistent output in manifests + getSortedRoutes(routes).forEach((route) => { if (isDynamicRoute(page) && route === page) return if (route === UNDERSCORE_NOT_FOUND_ROUTE) return diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 92a1e4845f..183605e4c1 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -19,6 +19,7 @@ import { isDeepStrictEqual } from 'util' import type { DefineEnvPluginOptions } from '../webpack/plugins/define-env-plugin' import { getDefineEnv } from '../webpack/plugins/define-env-plugin' import type { PageExtensions } from '../page-extensions-type' +import type { __ApiPreviewProps } from '../../server/api-utils' const nextVersion = process.env.__NEXT_VERSION as string @@ -387,7 +388,6 @@ function logLoadFailure(attempts: any, triedWasm = false) { process.exit(1) }) } - export interface ProjectOptions { /** * A root path from which all files must be nested under. Trying to access @@ -432,6 +432,21 @@ export interface ProjectOptions { * The mode in which Next.js is running. */ dev: boolean + + /** + * The server actions encryption key. + */ + encryptionKey: string + + /** + * The build id. + */ + buildId: string + + /** + * Options for draft mode. + */ + previewProps: __ApiPreviewProps } type RustifiedEnv = { name: string; value: string }[] diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 4d0de74d43..195f9d1726 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -78,7 +78,7 @@ const render = getRender({ serverActions: isServerComponent ? serverActions : undefined, subresourceIntegrityManifest, config: nextConfig, - buildId: 'VAR_BUILD_ID', + buildId: process.env.__NEXT_BUILD_ID!, nextFontManifest, incrementalCacheHandler, interceptionRouteRewrites, diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index d5611928ab..6a311abe7b 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -104,7 +104,7 @@ const render = getRender({ reactLoadableManifest, subresourceIntegrityManifest, config: nextConfig, - buildId: 'VAR_BUILD_ID', + buildId: process.env.__NEXT_BUILD_ID!, nextFontManifest, incrementalCacheHandler, }) diff --git a/packages/next/src/build/webpack-build/impl.ts b/packages/next/src/build/webpack-build/impl.ts index 4520602691..39ff87a40a 100644 --- a/packages/next/src/build/webpack-build/impl.ts +++ b/packages/next/src/build/webpack-build/impl.ts @@ -152,7 +152,14 @@ export async function webpackBuildImpl( middlewareMatchers: entrypoints.middlewareMatchers, compilerType: COMPILER_NAMES.edgeServer, entrypoints: entrypoints.edgeServer, - edgePreviewProps: NextBuildContext.previewProps!, + edgePreviewProps: { + __NEXT_PREVIEW_MODE_ID: + NextBuildContext.previewProps!.previewModeId, + __NEXT_PREVIEW_MODE_ENCRYPTION_KEY: + NextBuildContext.previewProps!.previewModeEncryptionKey, + __NEXT_PREVIEW_MODE_SIGNING_KEY: + NextBuildContext.previewProps!.previewModeSigningKey, + }, ...info, }), ]) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 9d839d310c..483b3361eb 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1818,7 +1818,11 @@ export default async function getBaseWebpackConfig( dev, sriEnabled: !dev && !!config.experimental.sri?.algorithm, rewrites, - edgeEnvironments: edgePreviewProps || {}, + edgeEnvironments: { + __NEXT_BUILD_ID: buildId, + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: encryptionKey, + ...edgePreviewProps, + }, }), isClient && new BuildManifestPlugin({ diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts index 8e82cf9aab..e2163b023a 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -16,7 +16,6 @@ export type EdgeSSRLoaderQuery = { absoluteDocumentPath: string absoluteErrorPath: string absolutePagePath: string - buildId: string dev: boolean isServerComponent: boolean page: string @@ -65,7 +64,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = const { dev, page, - buildId, absolutePagePath, absoluteAppPath, absoluteDocumentPath, @@ -145,7 +143,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = { VAR_USERLAND: pageModPath, VAR_PAGE: page, - VAR_BUILD_ID: buildId, }, { sriEnabled: JSON.stringify(sriEnabled), @@ -167,7 +164,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = { VAR_USERLAND: pageModPath, VAR_PAGE: page, - VAR_BUILD_ID: buildId, VAR_MODULE_DOCUMENT: documentPath, VAR_MODULE_APP: appPath, VAR_MODULE_GLOBAL_ERROR: errorPath, diff --git a/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts index bc2b5621d3..9be30ecee2 100644 --- a/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts @@ -29,6 +29,38 @@ export type ClientBuildManifest = { // generated). export const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()` +// nodejs: '/static//low-priority.js' +function buildNodejsLowPriorityPath(filename: string, buildId: string) { + return `${CLIENT_STATIC_FILES_PATH}/${buildId}/${filename}` +} + +function createEdgeRuntimeManifest(originAssetMap: BuildManifest): string { + const manifestFilenames = ['_buildManifest.js', '_ssgManifest.js'] + + const assetMap: BuildManifest = { + ...originAssetMap, + lowPriorityFiles: [], + } + + const manifestDefCode = `self.__BUILD_MANIFEST = ${JSON.stringify( + assetMap, + null, + 2 + )};\n` + // edge lowPriorityFiles item: '"/static/" + process.env.__NEXT_BUILD_ID + "/low-priority.js"'. + // Since lowPriorityFiles is not fixed and relying on `process.env.__NEXT_BUILD_ID`, we'll produce code creating it dynamically. + const lowPriorityFilesCode = + `self.__BUILD_MANIFEST.lowPriorityFiles = [\n` + + manifestFilenames + .map((filename) => { + return `"/static/" + process.env.__NEXT_BUILD_ID + "/${filename}",\n` + }) + .join(',') + + `\n];` + + return manifestDefCode + lowPriorityFilesCode +} + function normalizeRewrite(item: { source: string destination: string @@ -231,19 +263,25 @@ export default class BuildManifestPlugin { // Add the runtime build manifest file (generated later in this file) // as a dependency for the app. If the flag is false, the file won't be // downloaded by the client. - assetMap.lowPriorityFiles.push( - `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` + const buildManifestPath = buildNodejsLowPriorityPath( + '_buildManifest.js', + this.buildId ) - const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js` - - assetMap.lowPriorityFiles.push(ssgManifestPath) + const ssgManifestPath = buildNodejsLowPriorityPath( + '_ssgManifest.js', + this.buildId + ) + assetMap.lowPriorityFiles.push(buildManifestPath, ssgManifestPath) assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest) } assetMap.pages = Object.keys(assetMap.pages) .sort() - // eslint-disable-next-line - .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any) + .reduce( + // eslint-disable-next-line + (a, c) => ((a[c] = assetMap.pages[c]), a), + {} as typeof assetMap.pages + ) let buildManifestName = BUILD_MANIFEST @@ -256,7 +294,7 @@ export default class BuildManifestPlugin { ) assets[`server/${MIDDLEWARE_BUILD_MANIFEST}.js`] = new sources.RawSource( - `self.__BUILD_MANIFEST=${JSON.stringify(assetMap)}` + `${createEdgeRuntimeManifest(assetMap)}` ) if (!this.isDevFallback) { 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 cdb25cce5d..8c45bf8a99 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 @@ -1013,19 +1013,26 @@ export class FlightClientEntryPlugin { edgeServerActions[id] = action } - const json = JSON.stringify( - { - node: serverActions, - edge: edgeServerActions, - encryptionKey: this.encryptionKey, - }, + const serverManifest = { + node: serverActions, + edge: edgeServerActions, + encryptionKey: this.encryptionKey, + } + const edgeServerManifest = { + ...serverManifest, + encryptionKey: 'process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY', + } + + const json = JSON.stringify(serverManifest, null, this.dev ? 2 : undefined) + const edgeJson = JSON.stringify( + edgeServerManifest, null, this.dev ? 2 : undefined ) assets[`${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.js`] = new sources.RawSource( - `self.__RSC_SERVER_MANIFEST=${JSON.stringify(json)}` + `self.__RSC_SERVER_MANIFEST=${JSON.stringify(edgeJson)}` ) as unknown as webpack.sources.RawSource assets[`${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.json`] = new sources.RawSource(json) as unknown as webpack.sources.RawSource diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index 5c9ec94fd1..866aff2ae1 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -42,10 +42,10 @@ export interface EdgeFunctionDefinition { name: string page: string matchers: MiddlewareMatcher[] + env: Record wasm?: AssetBinding[] assets?: AssetBinding[] regions?: string[] | string - environments?: Record } export interface MiddlewareManifest { @@ -227,7 +227,7 @@ function getCreateAssets(params: { name, filePath, })), - environments: opts.edgeEnvironments, + env: opts.edgeEnvironments, ...(metadata.regions && { regions: metadata.regions }), } @@ -739,18 +739,26 @@ function getExtractMetadata(params: { } } +// These values will be replaced again in edge runtime deployment build. +// `buildId` represents BUILD_ID to be externalized in env vars. +// `encryptionKey` represents server action encryption key to be externalized in env vars. +type EdgeRuntimeEnvironments = Record & { + __NEXT_BUILD_ID: string + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: string +} + interface Options { dev: boolean sriEnabled: boolean rewrites: CustomRoutes['rewrites'] - edgeEnvironments: Record + edgeEnvironments: EdgeRuntimeEnvironments } export default class MiddlewarePlugin { private readonly dev: Options['dev'] private readonly sriEnabled: Options['sriEnabled'] private readonly rewrites: Options['rewrites'] - private readonly edgeEnvironments: Record + private readonly edgeEnvironments: EdgeRuntimeEnvironments constructor({ dev, sriEnabled, rewrites, edgeEnvironments }: Options) { this.dev = dev diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 3220403c2a..f6409ae241 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -122,6 +122,7 @@ export async function createHotReloaderTurbopack( // of the current `next dev` invocation. hotReloaderSpan.stop() + const encryptionKey = await generateEncryptionKeyBase64(true) const project = await bindings.turbo.createProject({ projectPath: dir, rootPath: opts.nextConfig.experimental.outputFileTracingRoot || dir, @@ -142,6 +143,9 @@ export async function createHotReloaderTurbopack( // TODO: Implement middlewareMatchers: undefined, }), + buildId, + encryptionKey, + previewProps: opts.fsChecker.prerenderManifest.preview, }) const entrypointsSubscription = project.entrypointsSubscribe() @@ -165,7 +169,7 @@ export async function createHotReloaderTurbopack( const manifestLoader = new TurbopackManifestLoader({ buildId, distDir, - encryptionKey: await generateEncryptionKeyBase64(), + encryptionKey, }) // Dev specific diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index ad96b43ef4..f05e37e22a 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1435,6 +1435,7 @@ export default class NextNodeServer extends BaseServer< name: string paths: string[] wasm: { filePath: string; name: string }[] + env: { [key: string]: string } assets?: { filePath: string; name: string }[] } | null { const manifest = this.getMiddlewareManifest() @@ -1476,6 +1477,7 @@ export default class NextNodeServer extends BaseServer< filePath: join(this.distDir, binding.filePath), } }), + env: pageInfo.env, } } diff --git a/packages/next/src/server/web/sandbox/context.ts b/packages/next/src/server/web/sandbox/context.ts index a872f4dd31..899758cb46 100644 --- a/packages/next/src/server/web/sandbox/context.ts +++ b/packages/next/src/server/web/sandbox/context.ts @@ -107,9 +107,14 @@ async function loadWasm( return modules } -function buildEnvironmentVariablesFrom(): Record { +function buildEnvironmentVariablesFrom( + injectedEnvironments: Record +): Record { const pairs = Object.keys(process.env).map((key) => [key, process.env[key]]) const env = Object.fromEntries(pairs) + for (const key of Object.keys(injectedEnvironments)) { + env[key] = injectedEnvironments[key] + } env.NEXT_RUNTIME = 'edge' return env } @@ -122,15 +127,16 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime`) throw error } -function createProcessPolyfill() { - const processPolyfill = { env: buildEnvironmentVariablesFrom() } - const overridenValue: Record = {} +function createProcessPolyfill(env: Record) { + const processPolyfill = { env: buildEnvironmentVariablesFrom(env) } + const overriddenValue: Record = {} + for (const key of Object.keys(process)) { if (key === 'env') continue Object.defineProperty(processPolyfill, key, { get() { - if (overridenValue[key] !== undefined) { - return overridenValue[key] + if (overriddenValue[key] !== undefined) { + return overriddenValue[key] } if (typeof (process as any)[key] === 'function') { return () => throwUnsupportedAPIError(`process.${key}`) @@ -138,7 +144,7 @@ function createProcessPolyfill() { return undefined }, set(value) { - overridenValue[key] = value + overriddenValue[key] = value }, enumerable: false, }) @@ -244,14 +250,15 @@ export const requestStore = new AsyncLocalStorage<{ async function createModuleContext(options: ModuleContextOptions) { const warnedEvals = new Set() const warnedWasmCodegens = new Set() - const wasm = await loadWasm(options.edgeFunctionEntry.wasm ?? []) + const { edgeFunctionEntry } = options + const wasm = await loadWasm(edgeFunctionEntry.wasm ?? []) const runtime = new EdgeRuntime({ codeGeneration: process.env.NODE_ENV !== 'production' ? { strings: true, wasm: true } : undefined, extend: (context) => { - context.process = createProcessPolyfill() + context.process = createProcessPolyfill(edgeFunctionEntry.env) Object.defineProperty(context, 'require', { enumerable: false, @@ -470,7 +477,7 @@ interface ModuleContextOptions { onWarning: (warn: Error) => void useCache: boolean distDir: string - edgeFunctionEntry: Pick + edgeFunctionEntry: Pick } function getModuleContextShared(options: ModuleContextOptions) { diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index ce1137a931..cde9ab6670 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -218,6 +218,13 @@ describe('next.rs api', () => { hasRewrites: false, middlewareMatchers: undefined, }), + buildId: 'development', + encryptionKey: '12345', + previewProps: { + previewModeId: 'development', + previewModeEncryptionKey: '12345', + previewModeSigningKey: '12345', + }, }) projectUpdateSubscription = filterMapAsyncIterator( project.updateInfoSubscribe(1000), diff --git a/test/e2e/middleware-general/test/index.test.ts b/test/e2e/middleware-general/test/index.test.ts index e5c23b4fe5..c8100fc959 100644 --- a/test/e2e/middleware-general/test/index.test.ts +++ b/test/e2e/middleware-general/test/index.test.ts @@ -170,9 +170,9 @@ describe('Middleware Runtime', () => { ...manifest.middleware['/'], } const envs = { - ...middlewareWithoutEnvs.environments, + ...middlewareWithoutEnvs.env, } - delete middlewareWithoutEnvs.environments + delete middlewareWithoutEnvs.env expect(middlewareWithoutEnvs).toEqual({ files: expect.arrayContaining([ 'server/edge-runtime-webpack.js', @@ -186,9 +186,11 @@ describe('Middleware Runtime', () => { regions: 'auto', }) expect(envs).toContainAllKeys([ - 'previewModeEncryptionKey', - 'previewModeId', - 'previewModeSigningKey', + 'NEXT_SERVER_ACTIONS_ENCRYPTION_KEY', + '__NEXT_BUILD_ID', + '__NEXT_PREVIEW_MODE_ENCRYPTION_KEY', + '__NEXT_PREVIEW_MODE_ID', + '__NEXT_PREVIEW_MODE_SIGNING_KEY', ]) }) diff --git a/test/e2e/middleware-trailing-slash/test/index.test.ts b/test/e2e/middleware-trailing-slash/test/index.test.ts index e6f0475493..1ee913f5a5 100644 --- a/test/e2e/middleware-trailing-slash/test/index.test.ts +++ b/test/e2e/middleware-trailing-slash/test/index.test.ts @@ -112,7 +112,7 @@ describe('Middleware Runtime trailing slash', () => { const middlewareWithoutEnvs = { ...manifest.middleware['/'], } - delete middlewareWithoutEnvs.environments + delete middlewareWithoutEnvs.env expect(middlewareWithoutEnvs).toEqual({ files: expect.arrayContaining([ 'prerender-manifest.js', diff --git a/test/production/deterministic-build/app/app-page/edge/page.js b/test/production/deterministic-build/app/app-page/edge/page.js new file mode 100644 index 0000000000..4d6fdccffa --- /dev/null +++ b/test/production/deterministic-build/app/app-page/edge/page.js @@ -0,0 +1,5 @@ +export default function Page() { + return 'app-page (edge)' +} + +export const runtime = 'edge' diff --git a/test/production/deterministic-build/app/app-page/page.js b/test/production/deterministic-build/app/app-page/page.js new file mode 100644 index 0000000000..ad16542192 --- /dev/null +++ b/test/production/deterministic-build/app/app-page/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return 'app-page (node)' +} diff --git a/test/production/deterministic-build/app/app-route/edge/route.js b/test/production/deterministic-build/app/app-route/edge/route.js new file mode 100644 index 0000000000..fc4a0205de --- /dev/null +++ b/test/production/deterministic-build/app/app-route/edge/route.js @@ -0,0 +1,5 @@ +export function GET() { + return new Response('app-route (edge)') +} + +export const runtime = 'edge' diff --git a/test/production/deterministic-build/app/app-route/route.js b/test/production/deterministic-build/app/app-route/route.js new file mode 100644 index 0000000000..b01445578f --- /dev/null +++ b/test/production/deterministic-build/app/app-route/route.js @@ -0,0 +1,5 @@ +export function GET() { + return new Response('app-route (node)') +} + +export const dynamic = 'force-dynamic' diff --git a/test/production/deterministic-build/app/layout.js b/test/production/deterministic-build/app/layout.js new file mode 100644 index 0000000000..ff05382bcc --- /dev/null +++ b/test/production/deterministic-build/app/layout.js @@ -0,0 +1,9 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} + +export const dynamic = 'force-dynamic' diff --git a/test/production/deterministic-build/index.test.ts b/test/production/deterministic-build/index.test.ts new file mode 100644 index 0000000000..c9bea71078 --- /dev/null +++ b/test/production/deterministic-build/index.test.ts @@ -0,0 +1,68 @@ +import crypto from 'crypto' +import { NextInstance, nextTestSetup } from 'e2e-utils' + +function generateMD5(text: string) { + const hash = crypto.createHash('md5') + hash.update(text) + return hash.digest('hex') +} + +const nodeFilePaths = [ + 'app/app-page/page', + 'app/app-route/route', + 'pages/api/pages-api', + 'pages/pages-page', +] + +async function getEdgeRouteFilesFromManifest(next: NextInstance) { + const manifest: any = JSON.parse( + await next.readFile('.next/server/middleware-manifest.json') + ) + const routeKeys = Object.keys(manifest.functions) + const md5Map: Record = {} + for (const route of routeKeys) { + const files: string[] = manifest.functions[route].files + const filesMd5Promises = files.map(async (filePath: string) => { + const content = await next.readFile(`.next/${filePath}`) + return generateMD5(content) + }) + const md5s = await Promise.all(filesMd5Promises) + md5Map[route] = md5s + } + return md5Map +} + +describe('deterministic build', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + // Edge - { [route]: [file md5s] } + const edgeBuildFileMd5Hashes: Record[] = [] + // Node - { [route]: page.js or route.js md5 } + const nodeBuildFileMd5Hashes: Record[] = [{}, {}] + + beforeAll(async () => { + // First build + await next.build() + edgeBuildFileMd5Hashes.push(await getEdgeRouteFilesFromManifest(next)) + for (const file of nodeFilePaths) { + const content = await next.readFile(`.next/server/${file}.js`) + nodeBuildFileMd5Hashes[0][file] = generateMD5(content) + } + + // Second build + await next.build() + edgeBuildFileMd5Hashes.push(await getEdgeRouteFilesFromManifest(next)) + for (const file of nodeFilePaths) { + const content = await next.readFile(`.next/server/${file}.js`) + nodeBuildFileMd5Hashes[1][file] = generateMD5(content) + } + }) + + it('should have same md5 file across build', async () => { + expect(edgeBuildFileMd5Hashes[0]).toEqual(edgeBuildFileMd5Hashes[1]) + expect(nodeBuildFileMd5Hashes[0]).toEqual(nodeBuildFileMd5Hashes[1]) + }) +}) diff --git a/test/production/deterministic-build/pages/api/pages-api/edge/index.js b/test/production/deterministic-build/pages/api/pages-api/edge/index.js new file mode 100644 index 0000000000..670361403e --- /dev/null +++ b/test/production/deterministic-build/pages/api/pages-api/edge/index.js @@ -0,0 +1,5 @@ +export default function handler() { + return new Response('pages-api (edge)') +} + +export const runtime = 'experimental-edge' diff --git a/test/production/deterministic-build/pages/api/pages-api/index.js b/test/production/deterministic-build/pages/api/pages-api/index.js new file mode 100644 index 0000000000..49c3323d5c --- /dev/null +++ b/test/production/deterministic-build/pages/api/pages-api/index.js @@ -0,0 +1,3 @@ +export default function handler(req, res) { + res.send('pages-api (node)') +} diff --git a/test/production/deterministic-build/pages/pages-page/edge/index.js b/test/production/deterministic-build/pages/pages-page/edge/index.js new file mode 100644 index 0000000000..a227e87964 --- /dev/null +++ b/test/production/deterministic-build/pages/pages-page/edge/index.js @@ -0,0 +1,5 @@ +export default function Page() { + return 'pages-page (edge)' +} + +export const runtime = 'experimental-edge' diff --git a/test/production/deterministic-build/pages/pages-page/index.js b/test/production/deterministic-build/pages/pages-page/index.js new file mode 100644 index 0000000000..fcfcc0a629 --- /dev/null +++ b/test/production/deterministic-build/pages/pages-page/index.js @@ -0,0 +1,8 @@ +export default function Page() { + return 'pages-page (node)' +} + +// Use gssp to opt-in dynamic rendering +export function getServerSideProps() { + return { props: {} } +} diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index 3a0a9919bd..5381a6fff0 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -14428,6 +14428,13 @@ "flakey": [], "runtimeError": false }, + "test/production/deterministic-build/index.test.ts": { + "passed": [], + "failed": ["deterministic build should have same md5 file across build"], + "pending": [], + "flakey": [], + "runtimeError": false + }, "test/production/app-dir-edge-runtime-with-wasm/index.test.ts": { "passed": ["app-dir edge runtime with wasm should have built"], "failed": [],