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:
JJ Kasper 2024-05-08 01:40:15 -07:00 committed by GitHub
parent 715e157cba
commit dcff078936
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 424 additions and 83 deletions

View file

@ -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()),
}
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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>,

View file

@ -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(),

View file

@ -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)]

View file

@ -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(),

View file

@ -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,

View file

@ -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

View file

@ -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 }[]

View file

@ -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,

View file

@ -104,7 +104,7 @@ const render = getRender({
reactLoadableManifest,
subresourceIntegrityManifest,
config: nextConfig,
buildId: 'VAR_BUILD_ID',
buildId: process.env.__NEXT_BUILD_ID!,
nextFontManifest,
incrementalCacheHandler,
})

View file

@ -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,
}),
])

View file

@ -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({

View file

@ -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,

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
}
}

View file

@ -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) {

View file

@ -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),

View file

@ -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',
])
})

View file

@ -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',

View file

@ -0,0 +1,5 @@
export default function Page() {
return 'app-page (edge)'
}
export const runtime = 'edge'

View file

@ -0,0 +1,3 @@
export default function Page() {
return 'app-page (node)'
}

View file

@ -0,0 +1,5 @@
export function GET() {
return new Response('app-route (edge)')
}
export const runtime = 'edge'

View file

@ -0,0 +1,5 @@
export function GET() {
return new Response('app-route (node)')
}
export const dynamic = 'force-dynamic'

View file

@ -0,0 +1,9 @@
export default function Layout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
export const dynamic = 'force-dynamic'

View 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])
})
})

View file

@ -0,0 +1,5 @@
export default function handler() {
return new Response('pages-api (edge)')
}
export const runtime = 'experimental-edge'

View file

@ -0,0 +1,3 @@
export default function handler(req, res) {
res.send('pages-api (node)')
}

View file

@ -0,0 +1,5 @@
export default function Page() {
return 'pages-page (edge)'
}
export const runtime = 'experimental-edge'

View file

@ -0,0 +1,8 @@
export default function Page() {
return 'pages-page (node)'
}
// Use gssp to opt-in dynamic rendering
export function getServerSideProps() {
return { props: {} }
}

View file

@ -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": [],