Re-land build(edge): extract buildId into environment (#65426)
### What * Extract `buildId` and server action encryption key into environment variables for edge to make code more deterministic * Fixed the legacy bad env names from #64108 * Always sort `routes` in prerender manifest for consistent output * Change `environments` to `env` in middleware manifest, confirmed with @javivelasco this is a fine change without need to bumping the version ### Why Dynamic variants like `buildId`, SA `encryptionKey` and preview props are different per build, which results to the non determinstic edge bundles. Once we extracted them into env vars then the bundles become deterministic which give us more space for optimization Closes NEXT-3117 Reverts vercel/next.js#65425 Co-authored-by: Jiachi Liu <inbox@huozhi.im>
This commit is contained in:
parent
715e157cba
commit
dcff078936
36 changed files with 424 additions and 83 deletions
|
@ -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<NapiDraftModeOptions> 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<bool>,
|
||||
|
||||
/// The server actions encryption key.
|
||||
pub encryption_key: Option<String>,
|
||||
|
||||
/// The build id.
|
||||
pub build_id: Option<String>,
|
||||
|
||||
/// Options for draft mode.
|
||||
pub preview_props: Option<NapiDraftModeOptions>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
|
@ -159,6 +194,9 @@ impl From<NapiProjectOptions> 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<NapiPartialProjectOptions> 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<bool>,
|
||||
|
||||
/// The server actions encryption key.
|
||||
pub encryption_key: Option<String>,
|
||||
|
||||
/// The build id.
|
||||
pub build_id: Option<String>,
|
||||
|
||||
/// Options for draft mode.
|
||||
pub preview_props: Option<DraftModeOptions>,
|
||||
}
|
||||
|
||||
#[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<Self>) -> Result<Vc<Project>> {
|
||||
let this = self.await?;
|
||||
|
||||
let (env, define_env, next_config, js_config, root_path, project_path, watch, dev) = {
|
||||
let env_map: Vc<EnvMap>;
|
||||
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<EnvMap> = Vc::cell(options.env.iter().cloned().collect());
|
||||
let define_env: Vc<ProjectDefineEnv> = 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<NextMode>,
|
||||
|
||||
versioned_content_map: Vc<VersionedContentMap>,
|
||||
|
||||
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<EnvMap> {
|
||||
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<Self>,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<AssetBinding>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regions: Option<Regions>,
|
||||
pub env: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default, Debug)]
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }[]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -104,7 +104,7 @@ const render = getRender({
|
|||
reactLoadableManifest,
|
||||
subresourceIntegrityManifest,
|
||||
config: nextConfig,
|
||||
buildId: 'VAR_BUILD_ID',
|
||||
buildId: process.env.__NEXT_BUILD_ID!,
|
||||
nextFontManifest,
|
||||
incrementalCacheHandler,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<EdgeSSRLoaderQuery> =
|
|||
const {
|
||||
dev,
|
||||
page,
|
||||
buildId,
|
||||
absolutePagePath,
|
||||
absoluteAppPath,
|
||||
absoluteDocumentPath,
|
||||
|
@ -145,7 +143,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
|
|||
{
|
||||
VAR_USERLAND: pageModPath,
|
||||
VAR_PAGE: page,
|
||||
VAR_BUILD_ID: buildId,
|
||||
},
|
||||
{
|
||||
sriEnabled: JSON.stringify(sriEnabled),
|
||||
|
@ -167,7 +164,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
|
|||
{
|
||||
VAR_USERLAND: pageModPath,
|
||||
VAR_PAGE: page,
|
||||
VAR_BUILD_ID: buildId,
|
||||
VAR_MODULE_DOCUMENT: documentPath,
|
||||
VAR_MODULE_APP: appPath,
|
||||
VAR_MODULE_GLOBAL_ERROR: errorPath,
|
||||
|
|
|
@ -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/<build id>/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()
|
||||
.reduce(
|
||||
// eslint-disable-next-line
|
||||
.reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)
|
||||
(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) {
|
||||
|
|
|
@ -1013,19 +1013,26 @@ export class FlightClientEntryPlugin {
|
|||
edgeServerActions[id] = action
|
||||
}
|
||||
|
||||
const json = JSON.stringify(
|
||||
{
|
||||
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
|
||||
|
|
|
@ -42,10 +42,10 @@ export interface EdgeFunctionDefinition {
|
|||
name: string
|
||||
page: string
|
||||
matchers: MiddlewareMatcher[]
|
||||
env: Record<string, string>
|
||||
wasm?: AssetBinding[]
|
||||
assets?: AssetBinding[]
|
||||
regions?: string[] | string
|
||||
environments?: Record<string, string>
|
||||
}
|
||||
|
||||
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<string, string> & {
|
||||
__NEXT_BUILD_ID: string
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: string
|
||||
}
|
||||
|
||||
interface Options {
|
||||
dev: boolean
|
||||
sriEnabled: boolean
|
||||
rewrites: CustomRoutes['rewrites']
|
||||
edgeEnvironments: Record<string, string>
|
||||
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<string, string>
|
||||
private readonly edgeEnvironments: EdgeRuntimeEnvironments
|
||||
|
||||
constructor({ dev, sriEnabled, rewrites, edgeEnvironments }: Options) {
|
||||
this.dev = dev
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -107,9 +107,14 @@ async function loadWasm(
|
|||
return modules
|
||||
}
|
||||
|
||||
function buildEnvironmentVariablesFrom(): Record<string, string | undefined> {
|
||||
function buildEnvironmentVariablesFrom(
|
||||
injectedEnvironments: Record<string, string>
|
||||
): Record<string, string | undefined> {
|
||||
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<string, any> = {}
|
||||
function createProcessPolyfill(env: Record<string, string>) {
|
||||
const processPolyfill = { env: buildEnvironmentVariablesFrom(env) }
|
||||
const overriddenValue: Record<string, any> = {}
|
||||
|
||||
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<string>()
|
||||
const warnedWasmCodegens = new Set<string>()
|
||||
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<EdgeFunctionDefinition, 'assets' | 'wasm'>
|
||||
edgeFunctionEntry: Pick<EdgeFunctionDefinition, 'assets' | 'wasm' | 'env'>
|
||||
}
|
||||
|
||||
function getModuleContextShared(options: ModuleContextOptions) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
])
|
||||
})
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default function Page() {
|
||||
return 'app-page (edge)'
|
||||
}
|
||||
|
||||
export const runtime = 'edge'
|
3
test/production/deterministic-build/app/app-page/page.js
Normal file
3
test/production/deterministic-build/app/app-page/page.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return 'app-page (node)'
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export function GET() {
|
||||
return new Response('app-route (edge)')
|
||||
}
|
||||
|
||||
export const runtime = 'edge'
|
|
@ -0,0 +1,5 @@
|
|||
export function GET() {
|
||||
return new Response('app-route (node)')
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
9
test/production/deterministic-build/app/layout.js
Normal file
9
test/production/deterministic-build/app/layout.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
68
test/production/deterministic-build/index.test.ts
Normal file
68
test/production/deterministic-build/index.test.ts
Normal file
|
@ -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<string, string[]> = {}
|
||||
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<string, string[]>[] = []
|
||||
// Node - { [route]: page.js or route.js md5 }
|
||||
const nodeBuildFileMd5Hashes: Record<string, string>[] = [{}, {}]
|
||||
|
||||
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])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,5 @@
|
|||
export default function handler() {
|
||||
return new Response('pages-api (edge)')
|
||||
}
|
||||
|
||||
export const runtime = 'experimental-edge'
|
|
@ -0,0 +1,3 @@
|
|||
export default function handler(req, res) {
|
||||
res.send('pages-api (node)')
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default function Page() {
|
||||
return 'pages-page (edge)'
|
||||
}
|
||||
|
||||
export const runtime = 'experimental-edge'
|
|
@ -0,0 +1,8 @@
|
|||
export default function Page() {
|
||||
return 'pages-page (node)'
|
||||
}
|
||||
|
||||
// Use gssp to opt-in dynamic rendering
|
||||
export function getServerSideProps() {
|
||||
return { props: {} }
|
||||
}
|
|
@ -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": [],
|
||||
|
|
Loading…
Reference in a new issue