add edge rendering for app dir for Turbopack (#51666)

relanding #50830 and #51631 

### Turbopack Changes

* https://github.com/vercel/turbo/pull/5254 <!-- OJ Kwon - ci(workflow):
upload daily next.js test trace -->
* https://github.com/vercel/turbo/pull/5363 <!-- Tobias Koppers -
disable default features for turbopack-ecmascript-plugins -->
This commit is contained in:
Tobias Koppers 2023-06-23 07:08:17 +02:00 committed by GitHub
parent f1624b65b4
commit 5abeb99b49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1430 additions and 782 deletions

72
Cargo.lock generated
View file

@ -400,7 +400,7 @@ dependencies = [
[[package]]
name = "auto-hash-map"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"serde",
]
@ -3463,7 +3463,7 @@ dependencies = [
[[package]]
name = "node-file-trace"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"serde",
@ -6412,6 +6412,7 @@ dependencies = [
"swc_common",
"swc_ecma_ast",
"swc_plugin_proxy",
"tokio",
"tracing",
"wasmer",
"wasmer-cache",
@ -7103,7 +7104,7 @@ dependencies = [
[[package]]
name = "turbo-tasks"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"auto-hash-map",
@ -7134,7 +7135,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-build"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"cargo-lock",
@ -7146,7 +7147,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-bytes"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"bytes",
@ -7161,7 +7162,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-env"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"dotenvy",
@ -7175,7 +7176,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-fetch"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"indexmap",
@ -7192,7 +7193,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-fs"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"auto-hash-map",
@ -7222,7 +7223,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-hash"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"base16",
"hex",
@ -7234,7 +7235,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-macros"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@ -7248,7 +7249,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-macros-shared"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"proc-macro2",
"quote",
@ -7258,7 +7259,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-malloc"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"mimalloc",
]
@ -7266,7 +7267,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-memory"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"auto-hash-map",
@ -7289,7 +7290,7 @@ dependencies = [
[[package]]
name = "turbo-tasks-testing"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"auto-hash-map",
@ -7301,7 +7302,7 @@ dependencies = [
[[package]]
name = "turbopack"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-recursion",
@ -7331,7 +7332,7 @@ dependencies = [
[[package]]
name = "turbopack-bench"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"chromiumoxide",
@ -7361,7 +7362,7 @@ dependencies = [
[[package]]
name = "turbopack-binding"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"auto-hash-map",
"mdxjs",
@ -7403,7 +7404,7 @@ dependencies = [
[[package]]
name = "turbopack-build"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230621.2#ee6683e43e31a470e13f0bc16dbaa375face74b5"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"indexmap",
@ -7423,7 +7424,7 @@ dependencies = [
[[package]]
name = "turbopack-cli-utils"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"clap 4.1.11",
@ -7447,7 +7448,7 @@ dependencies = [
[[package]]
name = "turbopack-core"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-trait",
@ -7475,7 +7476,7 @@ dependencies = [
[[package]]
name = "turbopack-create-test-app"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"clap 4.1.11",
@ -7488,7 +7489,7 @@ dependencies = [
[[package]]
name = "turbopack-css"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-trait",
@ -7510,7 +7511,7 @@ dependencies = [
[[package]]
name = "turbopack-dev"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"indexmap",
@ -7534,7 +7535,7 @@ dependencies = [
[[package]]
name = "turbopack-dev-server"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-compression",
@ -7569,7 +7570,7 @@ dependencies = [
[[package]]
name = "turbopack-ecmascript"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-trait",
@ -7602,7 +7603,7 @@ dependencies = [
[[package]]
name = "turbopack-ecmascript-plugins"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-trait",
@ -7625,7 +7626,7 @@ dependencies = [
[[package]]
name = "turbopack-ecmascript-runtime"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"indoc",
@ -7642,7 +7643,7 @@ dependencies = [
[[package]]
name = "turbopack-env"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"indexmap",
@ -7658,7 +7659,7 @@ dependencies = [
[[package]]
name = "turbopack-image"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"base64 0.21.0",
@ -7678,7 +7679,7 @@ dependencies = [
[[package]]
name = "turbopack-json"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"serde",
@ -7693,7 +7694,7 @@ dependencies = [
[[package]]
name = "turbopack-mdx"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"mdxjs",
@ -7708,7 +7709,7 @@ dependencies = [
[[package]]
name = "turbopack-node"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"async-stream",
@ -7743,7 +7744,7 @@ dependencies = [
[[package]]
name = "turbopack-static"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"serde",
@ -7759,7 +7760,7 @@ dependencies = [
[[package]]
name = "turbopack-swc-utils"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"swc_core",
"turbo-tasks",
@ -7770,7 +7771,7 @@ dependencies = [
[[package]]
name = "turbopack-test-utils"
version = "0.1.0"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8"
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230622.2#08aba99a2796b37324cabf18b6ea9eff886d93b4"
dependencies = [
"anyhow",
"once_cell",
@ -8450,6 +8451,7 @@ dependencies = [
"pin-project",
"rand 0.8.5",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"serde_yaml 0.8.26",

View file

@ -42,11 +42,11 @@ swc_core = { version = "0.76.46" }
testing = { version = "0.33.13" }
# Turbo crates
turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230615.1" }
turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230622.2" }
# [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros..
turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230615.1" }
turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230622.2" }
# [TODO]: need to refactor embed_directory! macro usage in next-core
turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230615.1" }
turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230622.2" }
# General Deps

View file

@ -10,8 +10,8 @@
"check": "tsc --noEmit"
},
"dependencies": {
"@vercel/turbopack-ecmascript-runtime": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1",
"@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230615.1",
"@vercel/turbopack-ecmascript-runtime": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2",
"@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230622.2",
"anser": "^2.1.1",
"css.escape": "^1.5.1",
"next": "*",

View file

@ -0,0 +1,82 @@
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import startOperationStreamHandler from '../internal/operation-stream'
import { join } from 'path'
import { parse as parseUrl } from 'node:url'
import { runEdgeFunction } from '../internal/edge'
import { headersFromEntries, initProxiedHeaders } from '../internal/headers'
import { NodeNextRequest } from 'next/dist/server/base-http/node'
import type { IncomingMessage } from 'node:http'
import type { RenderData } from 'types/turbopack'
import chunkGroup from 'INNER_EDGE_CHUNK_GROUP'
import { attachRequestMeta } from '../internal/next-request-helpers'
import { Readable } from 'stream'
startOperationStreamHandler(async (renderData: RenderData, respond) => {
const { response } = await runOperation(renderData)
if (response == null) {
throw new Error('no html returned')
}
const channel = respond({
status: response.status,
// @ts-expect-error Headers is iterable since node.js 18
headers: [...response.headers],
})
if (response.body) {
const reader = response.body.getReader()
for (;;) {
let { done, value } = await reader.read()
if (done) {
break
}
channel.chunk(Buffer.from(value!))
}
}
channel.end()
})
async function runOperation(renderData: RenderData) {
const edgeInfo = {
name: 'edge',
paths: chunkGroup
.filter((chunk) => chunk.endsWith('.js'))
.map((chunk: string) => join(process.cwd(), '.next/server/app', chunk)),
wasm: [],
env: Object.keys(process.env),
assets: [],
}
const parsedUrl = parseUrl(renderData.originalUrl, true)
const incoming = new Readable() as IncomingMessage
incoming.push(null)
incoming.url = renderData.originalUrl
incoming.method = renderData.method
incoming.headers = initProxiedHeaders(
headersFromEntries(renderData.rawHeaders),
renderData.data?.serverInfo
)
const req = new NodeNextRequest(incoming)
attachRequestMeta(req, parsedUrl, req.headers.host!)
const res = await runEdgeFunction({
edgeInfo,
outputDir: 'edge-pages',
req,
query: renderData.rawQuery,
params: renderData.params,
path: renderData.path,
onWarning(warning) {
console.warn(warning)
},
})
return res as { response: Response }
}

View file

@ -1,22 +1,11 @@
// Provided by the rust generate code
type FileType =
| 'layout'
| 'template'
| 'error'
| 'loading'
| 'not-found'
| 'head'
declare global {
// an tree of all layouts and the page
const LOADER_TREE: LoaderTree
// array of chunks for the bootstrap script
const BOOTSTRAP: string[]
const IPC: Ipc<unknown, unknown>
}
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import startOperationStreamHandler from '../internal/operation-stream'
import '../polyfill/app-polyfills.ts'
import type { Ipc } from '@vercel/turbopack-node/ipc/index'
import type { IncomingMessage } from 'node:http'
import type { ClientReferenceManifest } from 'next/dist/build/webpack/plugins/flight-manifest-plugin'
import type { RenderData } from 'types/turbopack'
import type { RenderOpts } from 'next/dist/server/app-render/types'
@ -25,211 +14,39 @@ import { RSC_VARY_HEADER } from 'next/dist/client/components/app-router-headers'
import { headersFromEntries, initProxiedHeaders } from '../internal/headers'
import { parse, ParsedUrlQuery } from 'node:querystring'
import { PassThrough } from 'node:stream'
;('TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }')
// @ts-ignore
import layoutEntry from './app/layout-entry'
;('TURBOPACK { chunking-type: isolatedParallel }')
import entry from 'APP_ENTRY'
import BOOTSTRAP from 'APP_BOOTSTRAP'
import { createServerResponse } from '../internal/http'
import { createManifests, installRequireAndChunkLoad } from './app/manifest'
globalThis.__next_require__ = (data) => {
const [, , ssr_id] = JSON.parse(data)
return __turbopack_require__(ssr_id)
}
globalThis.__next_chunk_load__ = () => Promise.resolve()
installRequireAndChunkLoad()
process.env.__NEXT_NEW_LINK_BEHAVIOR = 'true'
const ipc = IPC as Ipc<IpcIncomingMessage, IpcOutgoingMessage>
type IpcIncomingMessage = {
type: 'headers'
data: RenderData
}
type IpcOutgoingMessage =
| {
type: 'headers'
data: {
status: number
headers: [string, string][]
}
}
| {
type: 'bodyChunk'
data: number[]
}
| {
type: 'bodyEnd'
}
const MIME_TEXT_HTML_UTF8 = 'text/html; charset=utf-8'
;(async () => {
while (true) {
const msg = await ipc.recv()
startOperationStreamHandler(async (renderData: RenderData, respond) => {
const result = await runOperation(renderData)
let renderData: RenderData
switch (msg.type) {
case 'headers': {
renderData = msg.data
break
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
const result = await runOperation(renderData)
if (result == null) {
throw new Error('no html returned')
}
ipc.send({
type: 'headers',
data: {
status: result.statusCode,
headers: result.headers,
},
})
for await (const chunk of result.body) {
ipc.send({
type: 'bodyChunk',
data: (chunk as Buffer).toJSON().data,
})
}
ipc.send({ type: 'bodyEnd' })
if (result == null) {
throw new Error('no html returned')
}
})().catch((err) => {
ipc.sendError(err)
const channel = respond({
status: result.statusCode,
headers: result.headers,
})
for await (const chunk of result.body) {
channel.chunk(chunk as Buffer)
}
channel.end()
})
// TODO expose these types in next.js
type ComponentModule = () => any
type ModuleReference = [componentModule: ComponentModule, filePath: string]
export type ComponentsType = {
[componentKey in FileType]?: ModuleReference
} & {
page?: ModuleReference
}
type LoaderTree = [
segment: string,
parallelRoutes: { [parallelRouterKey: string]: LoaderTree },
components: ComponentsType
]
async function runOperation(renderData: RenderData) {
const proxyMethodsForModule = (
id: string
): ProxyHandler<ClientReferenceManifest['ssrModuleMapping']> => {
return {
get(_target, prop: string) {
return {
id,
chunks: JSON.parse(id)[1],
name: prop,
}
},
}
}
const proxyMethodsNested = (
type: 'ssrModuleMapping' | 'clientModules' | 'entryCSSFiles'
): ProxyHandler<
| ClientReferenceManifest['ssrModuleMapping']
| ClientReferenceManifest['clientModules']
| ClientReferenceManifest['entryCSSFiles']
> => {
return {
get(_target, key: string) {
if (type === 'ssrModuleMapping') {
return new Proxy({}, proxyMethodsForModule(key as string))
}
if (type === 'clientModules') {
// The key is a `${file}#${name}`, but `file` can contain `#` itself.
// There are 2 possibilities:
// "file#" => id = "file", name = ""
// "file#foo" => id = "file", name = "foo"
const pos = key.lastIndexOf('#')
let id = key
let name = ''
if (pos !== -1) {
id = key.slice(0, pos)
name = key.slice(pos + 1)
} else {
throw new Error('keys need to be formatted as {file}#{name}')
}
return {
id,
name,
chunks: JSON.parse(id)[1],
}
}
if (type === 'entryCSSFiles') {
const cssChunks = JSON.parse(key)
// TODO(WEB-856) subscribe to changes
return {
modules: [],
files: cssChunks.filter(filterAvailable).map(toPath),
}
}
},
}
}
const proxyMethods = (): ProxyHandler<ClientReferenceManifest> => {
const clientModulesProxy = new Proxy(
{},
proxyMethodsNested('clientModules')
)
const ssrModuleMappingProxy = new Proxy(
{},
proxyMethodsNested('ssrModuleMapping')
)
const entryCSSFilesProxy = new Proxy(
{},
proxyMethodsNested('entryCSSFiles')
)
return {
get(_target: any, prop: string) {
if (prop === 'ssrModuleMapping') {
return ssrModuleMappingProxy
}
if (prop === 'clientModules') {
return clientModulesProxy
}
if (prop === 'entryCSSFiles') {
return entryCSSFilesProxy
}
},
}
}
const availableModules = new Set()
const toPath = (chunk: ChunkData) =>
typeof chunk === 'string' ? chunk : chunk.path
/// determines if a chunk is needed based on the current available modules
const filterAvailable = (chunk: ChunkData) => {
if (typeof chunk === 'string') {
return true
} else {
let includedList = chunk.included || []
if (includedList.length === 0) {
return true
}
let needed = false
for (const item of includedList) {
if (!availableModules.has(item)) {
availableModules.add(item)
needed = true
}
}
return needed
}
}
const manifest: ClientReferenceManifest = new Proxy({} as any, proxyMethods())
const { clientReferenceManifest } = createManifests()
const req: IncomingMessage = {
url: renderData.originalUrl,
@ -266,12 +83,14 @@ async function runOperation(renderData: RenderData) {
ampFirstPages: [],
},
ComponentMod: {
...layoutEntry,
default: undefined,
tree: LOADER_TREE,
...entry,
__next_app__: {
require: __next_require__,
loadChunk: __next_chunk_load__,
},
pages: ['page.js'],
},
clientReferenceManifest: manifest,
clientReferenceManifest,
runtime: 'nodejs',
serverComponents: true,
assetPrefix: '',
@ -304,23 +123,3 @@ async function runOperation(renderData: RenderData) {
body,
}
}
// This utility is based on https://github.com/zertosh/htmlescape
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
const ESCAPE_LOOKUP = {
'&': '\\u0026',
'>': '\\u003e',
'<': '\\u003c',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
}
const ESCAPE_REGEX = /[&><\u2028\u2029]/g
export function htmlEscapeJsonString(str: string) {
return str.replace(
ESCAPE_REGEX,
(match) => ESCAPE_LOOKUP[match as keyof typeof ESCAPE_LOOKUP]
)
}

View file

@ -0,0 +1 @@
// This file is generated by app_source.rs

View file

@ -0,0 +1,106 @@
import { adapter } from 'next/dist/server/web/adapter'
import { RSC_VARY_HEADER } from 'next/dist/client/components/app-router-headers'
import { IncrementalCache } from 'next/dist/server/lib/incremental-cache'
import { renderToHTMLOrFlight } from 'next/dist/server/app-render/app-render'
;('TURBOPACK { chunking-type: isolatedParallel }')
import entry from 'APP_ENTRY'
import BOOTSTRAP from 'APP_BOOTSTRAP'
import { createManifests, installRequireAndChunkLoad } from './manifest'
import type { NextRequest, NextFetchEvent } from 'next/server'
import type { RenderOpts } from 'next/dist/server/app-render/types'
import type { ParsedUrlQuery } from 'querystring'
installRequireAndChunkLoad()
// avoid limiting stack traces to 10 lines
Error.stackTraceLimit = 100
const { clientReferenceManifest } = createManifests()
const MIME_TEXT_HTML_UTF8 = 'text/html; charset=utf-8'
async function render(request: NextRequest, event: NextFetchEvent) {
const renderOpt: Omit<
RenderOpts,
'App' | 'Document' | 'Component' | 'pathname'
> & { params: ParsedUrlQuery } = {
// TODO(WEB-1195) params
params: {},
supportsDynamicHTML: true,
dev: true,
buildId: 'development',
buildManifest: {
polyfillFiles: [],
rootMainFiles: BOOTSTRAP.filter((path) => path.endsWith('.js')),
devFiles: [],
ampDevFiles: [],
lowPriorityFiles: [],
pages: {
'/_app': [],
},
ampFirstPages: [],
},
ComponentMod: {
...entry,
__next_app__: {
require: __next_require__,
loadChunk: __next_chunk_load__,
},
pages: ['page.js'],
},
clientReferenceManifest,
runtime: 'nodejs',
serverComponents: true,
assetPrefix: '',
pageConfig: {},
reactLoadableManifest: {},
// TODO nextConfigOutput
nextConfigOutput: undefined,
}
const tranform = new TransformStream()
const response = new Response(tranform.readable)
let { pathname, search: query } = new URL(request.url, 'next://')
const result = await renderToHTMLOrFlight(
// @ts-expect-error - TODO renderToHTMLOrFlight types should accept web platform types
request,
response,
pathname,
// TODO(WEB-1195) query
{},
renderOpt as any as RenderOpts
)
response.headers.append(
'Content-Type',
result.contentType || MIME_TEXT_HTML_UTF8
)
response.headers.append('Vary', RSC_VARY_HEADER)
const writer = tranform.writable.getWriter()
result.pipe({
write: (chunk: Uint8Array) => writer.write(chunk),
end: () => writer.close(),
destroy: (reason?: Error) => writer.abort(reason),
})
return response
}
// adapter uses this to detect edge rendering
self.__BUILD_MANIFEST = {}
// @ts-expect-error - exposed for edge support
globalThis._ENTRIES = {
middleware_edge: {
default: function (opts: any) {
return adapter({
...opts,
IncrementalCache,
handler: render,
})
},
},
}

View file

@ -12,7 +12,7 @@ import {
NodeNextResponse,
} from 'next/dist/server/base-http/node'
import { runEdgeFunction } from '../../internal/edge'
import { runEdgeFunction, updateResponse } from '../../internal/edge'
import { attachRequestMeta } from '../../internal/next-request-helpers'
import chunkGroup from 'ROUTE_CHUNK_GROUP'
@ -34,11 +34,10 @@ startHandler(async ({ request, response, query, params, path }) => {
assets: [],
}
await runEdgeFunction({
const result = await runEdgeFunction({
edgeInfo,
outputDir: 'app',
req,
res,
query,
params,
path,
@ -46,4 +45,6 @@ startHandler(async ({ request, response, query, params, path }) => {
console.warn(warning)
},
})
updateResponse(res, result)
})

View file

@ -1,25 +0,0 @@
export { default as AppRouter } from 'next/dist/client/components/app-router'
export { default as LayoutRouter } from 'next/dist/client/components/layout-router'
export { default as RenderFromTemplateContext } from 'next/dist/client/components/render-from-template-context'
export { default as GlobalError } from 'next/dist/client/components/error-boundary'
export { staticGenerationAsyncStorage } from 'next/dist/client/components/static-generation-async-storage'
export { requestAsyncStorage } from 'next/dist/client/components/request-async-storage'
export { actionAsyncStorage } from 'next/dist/client/components/action-async-storage'
export { staticGenerationBailout } from 'next/dist/client/components/static-generation-bailout'
export { default as StaticGenerationSearchParamsBailoutProvider } from 'next/dist/client/components/static-generation-searchparams-bailout-provider'
export { createSearchParamsBailoutProxy } from 'next/dist/client/components/searchparams-bailout-proxy'
import * as serverHooks from 'next/dist/client/components/hooks-server-context'
export { serverHooks }
export {
renderToReadableStream,
decodeReply,
} from 'next/dist/compiled/react-server-dom-webpack/server.edge'
export {
preloadStyle,
preloadFont,
preconnect,
} from 'next/dist/server/app-render/rsc/preloads'

View file

@ -0,0 +1,127 @@
import type { ClientReferenceManifest } from 'next/dist/build/webpack/plugins/flight-manifest-plugin'
export function createManifests() {
const proxyMethodsForModule = (
id: string
): ProxyHandler<ClientReferenceManifest['ssrModuleMapping']> => {
return {
get(_target, prop: string) {
return {
id,
chunks: JSON.parse(id)[1],
name: prop,
}
},
}
}
const availableModules = new Set()
const toPath = (chunk: ChunkData) =>
typeof chunk === 'string' ? chunk : chunk.path
/// determines if a chunk is needed based on the current available modules
const filterAvailable = (chunk: ChunkData) => {
if (typeof chunk === 'string') {
return true
} else {
let includedList = chunk.included || []
if (includedList.length === 0) {
return true
}
let needed = false
for (const item of includedList) {
if (!availableModules.has(item)) {
availableModules.add(item)
needed = true
}
}
return needed
}
}
const proxyMethodsNested = (
type: 'ssrModuleMapping' | 'clientModules' | 'entryCSSFiles'
): ProxyHandler<
| ClientReferenceManifest['ssrModuleMapping']
| ClientReferenceManifest['clientModules']
| ClientReferenceManifest['entryCSSFiles']
> => {
return {
get(_target, key: string) {
if (type === 'ssrModuleMapping') {
return new Proxy({}, proxyMethodsForModule(key as string))
}
if (type === 'clientModules') {
// The key is a `${file}#${name}`, but `file` can contain `#` itself.
// There are 2 possibilities:
// "file#" => id = "file", name = ""
// "file#foo" => id = "file", name = "foo"
const pos = key.lastIndexOf('#')
let id = key
let name = ''
if (pos !== -1) {
id = key.slice(0, pos)
name = key.slice(pos + 1)
} else {
throw new Error('keys need to be formatted as {file}#{name}')
}
return {
id,
name,
chunks: JSON.parse(id)[1],
}
}
if (type === 'entryCSSFiles') {
const cssChunks = JSON.parse(key)
// TODO(WEB-856) subscribe to changes
return {
modules: [],
files: cssChunks.filter(filterAvailable).map(toPath),
}
}
},
}
}
const proxyMethods = (): ProxyHandler<ClientReferenceManifest> => {
const clientModulesProxy = new Proxy(
{},
proxyMethodsNested('clientModules')
)
const ssrModuleMappingProxy = new Proxy(
{},
proxyMethodsNested('ssrModuleMapping')
)
const entryCSSFilesProxy = new Proxy(
{},
proxyMethodsNested('entryCSSFiles')
)
return {
get(_target: any, prop: string) {
if (prop === 'ssrModuleMapping') {
return ssrModuleMappingProxy
}
if (prop === 'clientModules') {
return clientModulesProxy
}
if (prop === 'entryCSSFiles') {
return entryCSSFilesProxy
}
},
}
}
const clientReferenceManifest: ClientReferenceManifest = new Proxy(
{} as any,
proxyMethods()
)
return { clientReferenceManifest }
}
export function installRequireAndChunkLoad() {
globalThis.__next_require__ = (data) => {
const [, , ssr_id] = JSON.parse(data)
return __turbopack_require__(ssr_id)
}
globalThis.__next_chunk_load__ = () => Promise.resolve()
}

View file

@ -1,20 +1,6 @@
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import startHandler from '../../internal/api-server-handler'
import '../../polyfill/app-polyfills'
import { parse as parseUrl } from 'node:url'
import {
NodeNextRequest,
NodeNextResponse,
} from 'next/dist/server/base-http/node'
import { sendResponse } from 'next/dist/server/send-response'
import { NextRequestAdapter } from 'next/dist/server/web/spec-extension/adapters/next-request'
import { RouteHandlerManagerContext } from 'next/dist/server/future/route-handler-managers/route-handler-manager'
import { attachRequestMeta } from '../../internal/next-request-helpers'
import startHandler from '../../internal/nodejs-proxy-handler'
import RouteModule from 'ROUTE_MODULE'
import * as userland from 'ENTRY'
@ -34,33 +20,4 @@ const routeModule = new RouteModule({
nextConfigOutput: undefined,
})
startHandler(async ({ request, response, query, params, path }) => {
const req = new NodeNextRequest(request)
const res = new NodeNextResponse(response)
const parsedUrl = parseUrl(req.url!, true)
attachRequestMeta(req, parsedUrl, request.headers.host!)
const context: RouteHandlerManagerContext = {
params,
prerenderManifest: {
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
dynamicRoutes: {},
notFoundRoutes: [],
preview: {
previewModeId: 'development-id',
} as any,
},
staticGenerationContext: {
supportsDynamicHTML: true,
},
}
const routeResponse = await routeModule.handle(
NextRequestAdapter.fromNodeNextRequest(req),
context
)
await sendResponse(req, res, routeResponse)
})
startHandler(routeModule)

View file

@ -13,7 +13,7 @@ import {
} from 'next/dist/server/base-http/node'
import { attachRequestMeta } from '../internal/next-request-helpers'
import { runEdgeFunction } from '../internal/edge'
import { runEdgeFunction, updateResponse } from '../internal/edge'
import chunkGroup from 'INNER_EDGE_CHUNK_GROUP'
@ -34,11 +34,10 @@ startHandler(async ({ request, response, query, params, path }) => {
assets: [],
}
await runEdgeFunction({
const result = await runEdgeFunction({
edgeInfo,
outputDir: 'pages',
req,
res,
query,
params,
path,
@ -46,4 +45,6 @@ startHandler(async ({ request, response, query, params, path }) => {
console.warn(warning)
},
})
await updateResponse(res, result)
})

View file

@ -1,48 +1,15 @@
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import { IPC } from '@vercel/turbopack-node/ipc/index'
import startOperationStreamHandler from './operation-stream'
import type { ClientRequest, IncomingMessage, Server } from 'node:http'
import type { ServerResponse } from 'node:http'
import { Buffer } from 'node:buffer'
import type { Ipc } from '@vercel/turbopack-node/ipc/index'
import type { RenderData } from 'types/turbopack'
import { createServer, makeRequest } from '../internal/server'
import { toPairs } from '../internal/headers'
const ipc = IPC as Ipc<IpcIncomingMessage, IpcOutgoingMessage>
type IpcIncomingMessage =
| {
type: 'headers'
data: RenderData
}
| {
type: 'bodyChunk'
data: number[]
}
| { type: 'bodyEnd' }
type IpcOutgoingMessage =
| {
type: 'headers'
data: ResponseHeaders
}
| {
type: 'bodyChunk'
data: number[]
}
| {
type: 'bodyEnd'
}
type ResponseHeaders = {
status: number
headers: [string, string][]
}
type Handler = (data: {
request: IncomingMessage
response: ServerResponse<IncomingMessage>
@ -58,134 +25,96 @@ type Operation = {
server: Server
}
export default function startHandler(handler: Handler): void {
;(async () => {
while (true) {
let operationPromise: Promise<Operation> | null = null
const msg = await ipc.recv()
switch (msg.type) {
case 'headers': {
operationPromise = createOperation(msg.data)
break
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
let body = Buffer.alloc(0)
let operation: Operation
loop: while (true) {
const msg = await ipc.recv()
switch (msg.type) {
case 'bodyChunk': {
body = Buffer.concat([body, Buffer.from(msg.data)])
break
}
case 'bodyEnd': {
operation = await operationPromise
break loop
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
}
await Promise.all([
endOperation(operation, body),
operation.clientResponsePromise.then((clientResponse) =>
handleClientResponse(operation.server, clientResponse)
),
])
}
})().catch((err) => {
ipc.sendError(err)
})
async function createOperation(renderData: RenderData): Promise<Operation> {
const server = await createServer()
const {
clientRequest,
clientResponsePromise,
serverRequest,
serverResponse,
} = await makeRequest(
server,
renderData.method,
renderData.path,
renderData.rawQuery,
renderData.rawHeaders,
renderData.data?.serverInfo
)
return {
clientRequest,
server,
clientResponsePromise,
apiOperation: handler({
request: serverRequest,
response: serverResponse,
query: renderData.rawQuery,
params: renderData.params,
path: renderData.path,
}),
}
}
function handleClientResponse(
server: Server,
clientResponse: IncomingMessage
) {
const responseHeaders: ResponseHeaders = {
status: clientResponse.statusCode!,
headers: toPairs(clientResponse.rawHeaders),
}
ipc.send({
type: 'headers',
data: responseHeaders,
})
clientResponse.on('data', (chunk) => {
ipc.send({
type: 'bodyChunk',
data: chunk.toJSON().data,
})
})
clientResponse.once('end', () => {
ipc.send({ type: 'bodyEnd' })
server.close()
})
clientResponse.once('error', (err) => {
ipc.sendError(err)
})
}
/**
* Ends an operation by writing the response body to the client and waiting for the Next.js API resolver to finish.
*/
async function endOperation(operation: Operation, body: Buffer) {
operation.clientRequest.end(body)
try {
await operation.apiOperation
} catch (error) {
if (error instanceof Error) {
await ipc.sendError(error)
} else {
await ipc.sendError(new Error('an unknown error occurred'))
}
return
}
}
type ResponseHeaders = {
status: number
headers: [string, string][]
}
export default function startHandler(handler: Handler): void {
startOperationStreamHandler(
async (renderData: RenderData, respond, reportError) => {
const operationPromise = (async function createOperation() {
const server = await createServer()
const {
clientRequest,
clientResponsePromise,
serverRequest,
serverResponse,
} = await makeRequest(
server,
renderData.method,
renderData.path,
renderData.rawQuery,
renderData.rawHeaders,
renderData.data?.serverInfo
)
return {
clientRequest,
server,
clientResponsePromise,
apiOperation: handler({
request: serverRequest,
response: serverResponse,
query: renderData.rawQuery,
params: renderData.params,
path: renderData.path,
}),
}
})()
function handleClientResponse(
server: Server,
clientResponse: IncomingMessage
) {
const responseHeaders: ResponseHeaders = {
status: clientResponse.statusCode!,
headers: toPairs(clientResponse.rawHeaders),
}
const channel = respond(responseHeaders)
clientResponse.on('data', (chunk) => {
channel.chunk(chunk)
})
clientResponse.once('end', () => {
channel.end()
server.close()
})
clientResponse.once('error', (err) => {
reportError(err)
})
}
/**
* Ends an operation by writing the response body to the client and waiting for the Next.js API resolver to finish.
*/
async function endOperation(operation: Operation, body: Buffer) {
operation.clientRequest.end(body)
try {
await operation.apiOperation
} catch (error) {
await reportError(error)
return
}
}
return {
streamRequest: false,
onBody: async (body) => {
const operation = await operationPromise
await Promise.all([
endOperation(operation, body),
operation.clientResponsePromise.then((clientResponse) =>
handleClientResponse(operation.server, clientResponse)
),
])
},
}
}
)
}

View file

@ -10,7 +10,7 @@ import type {
} from 'next/dist/server/base-http/node'
import { parse, ParsedUrlQuery } from 'querystring'
import type { Params } from 'next/dist/shared/lib/router/utils/route-matcher'
import { FetchEventResult } from 'next/dist/server/web/types'
import type { FetchEventResult } from 'next/dist/server/web/types'
import { getCloneableBody } from 'next/dist/server/body-streams'
// This is an adapted version of a similar function in next-dev-server.
@ -19,7 +19,6 @@ export async function runEdgeFunction({
edgeInfo,
outputDir,
req,
res,
query,
params,
path,
@ -34,12 +33,11 @@ export async function runEdgeFunction({
}
outputDir: string
req: BaseNextRequest | NodeNextRequest
res: BaseNextResponse | NodeNextResponse
query: string
path: string
params: Params | undefined
onWarning?: (warning: Error) => void
}): Promise<FetchEventResult | null> {
}): Promise<FetchEventResult> {
// For edge to "fetch" we must always provide an absolute URL
const initialUrl = new URL(path, 'http://n')
const parsedQuery = parse(query)
@ -83,21 +81,28 @@ export async function runEdgeFunction({
onWarning,
})) as FetchEventResult
res.statusCode = result.response.status
res.statusMessage = result.response.statusText
return result
}
export async function updateResponse(
response: BaseNextResponse<any> | NodeNextResponse,
result: FetchEventResult
) {
response.statusCode = result.response.status
response.statusMessage = result.response.statusText
result.response.headers.forEach((value: string, key) => {
// the append handling is special cased for `set-cookie`
if (key.toLowerCase() === 'set-cookie') {
res.setHeader(key, value)
response.setHeader(key, value)
} else {
res.appendHeader(key, value)
response.appendHeader(key, value)
}
})
if (result.response.body) {
// TODO(gal): not sure that we always need to stream
const nodeResStream = (res as NodeNextResponse).originalResponse
const nodeResStream = (response as NodeNextResponse).originalResponse
const {
consumeUint8ArrayReadableStream,
} = require('next/dist/compiled/edge-runtime')
@ -111,10 +116,8 @@ export async function runEdgeFunction({
nodeResStream.end()
}
} else {
;(res as NodeNextResponse).originalResponse.end()
;(response as NodeNextResponse).originalResponse.end()
}
return result
}
function stringifyUrlQueryParam(param: unknown): string {

View file

@ -0,0 +1,52 @@
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import startHandler from './api-server-handler'
import '../polyfill/app-polyfills'
import { parse as parseUrl } from 'node:url'
import {
NodeNextRequest,
NodeNextResponse,
} from 'next/dist/server/base-http/node'
import { sendResponse } from 'next/dist/server/send-response'
import { NextRequestAdapter } from 'next/dist/server/web/spec-extension/adapters/next-request'
import { RouteHandlerManagerContext } from 'next/dist/server/future/route-handler-managers/route-handler-manager'
import { attachRequestMeta } from './next-request-helpers'
import type { RouteModule } from 'next/dist/server/future/route-modules/route-module'
export default (routeModule: RouteModule) => {
startHandler(async ({ request, response, params }) => {
const req = new NodeNextRequest(request)
const res = new NodeNextResponse(response)
const parsedUrl = parseUrl(req.url!, true)
attachRequestMeta(req, parsedUrl, request.headers.host!)
const context: RouteHandlerManagerContext = {
params,
prerenderManifest: {
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
dynamicRoutes: {},
notFoundRoutes: [],
preview: {
previewModeId: 'development-id',
} as any,
},
staticGenerationContext: {
supportsDynamicHTML: true,
},
}
const routeResponse = await routeModule.handle(
NextRequestAdapter.fromNodeNextRequest(req),
context
)
await sendResponse(req, res, routeResponse)
})
}

View file

@ -0,0 +1,155 @@
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
import { IPC } from '@vercel/turbopack-node/ipc/index'
import { Buffer } from 'node:buffer'
import type { Ipc } from '@vercel/turbopack-node/ipc/index'
type Operation =
| {
streamRequest: true
onChunk?: (chunk: Buffer) => Promise<void>
onEnd?: () => Promise<void>
}
| {
streamRequest?: false
onBody?: (body: Buffer) => Promise<void>
}
type IpcIncomingMessage<T> =
| {
type: 'headers'
data: T
}
| {
type: 'bodyChunk'
data: number[]
}
| {
type: 'bodyText'
data: number[]
}
| { type: 'bodyEnd' }
type IpcOutgoingMessage<R> =
| {
type: 'headers'
data: R
}
| {
type: 'bodyChunk'
data: number[]
}
| {
type: 'bodyEnd'
}
interface Response {
chunk: (data: Buffer) => Promise<void>
end: () => Promise<void>
}
export default function startHandler<T, R>(
createOperation: (
data: T,
respond: (data: R) => Response,
reportError: (error: unknown) => Promise<never>
) => Promise<Operation | void>
) {
const ipc = IPC as Ipc<IpcIncomingMessage<T>, IpcOutgoingMessage<R>>
;(async () => {
while (true) {
let operation: Operation | void
{
const msg = await ipc.recv()
switch (msg.type) {
case 'headers': {
operation = await createOperation(
msg.data,
(data) => {
ipc.send({
type: 'headers',
data,
})
return {
chunk: (buf: Buffer) => {
return ipc.send({
type: 'bodyChunk',
data: buf.toJSON().data,
})
},
end: () => {
return ipc.send({ type: 'bodyEnd' })
},
}
},
(error) => {
return ipc.sendError(
error instanceof Error
? error
: new Error(`an unknown error occurred: ${error}`)
)
}
)
break
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
}
if (operation) {
if (operation.streamRequest) {
loop: while (true) {
const msg = await ipc.recv()
switch (msg.type) {
case 'bodyChunk':
case 'bodyText': {
await operation.onChunk?.(Buffer.from(msg.data))
break
}
case 'bodyEnd': {
await operation.onEnd?.()
break loop
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
}
} else {
let body = Buffer.alloc(0)
loop: while (true) {
const msg = await ipc.recv()
switch (msg.type) {
case 'bodyChunk':
case 'bodyText': {
body = Buffer.concat([body, Buffer.from(msg.data)])
break
}
case 'bodyEnd': {
await operation.onBody?.(body)
break loop
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
}
}
}
}
})().catch((err) => {
ipc.sendError(err)
})
}

View file

@ -27,8 +27,7 @@
"@vercel/turbopack-next/pages/_app": ["node_modules/next/app"],
"@vercel/turbopack-next/pages/_document": ["node_modules/next/document"],
"@vercel/turbopack-next/pages/_error": ["node_modules/next/error"],
"@vercel/turbopack-next/internal/_error": ["node_modules/next/error"],
"next/*": ["node_modules/next/*"]
"@vercel/turbopack-next/internal/_error": ["node_modules/next/error"]
},
"resolveJsonModule": true,
"types": ["react/next"],

View file

@ -1,2 +1 @@
declare module 'next/dist/compiled/react-server-dom-webpack/client'
declare module 'next/dist/client/app-call-server'

View file

@ -80,6 +80,17 @@ declare module 'BOOTSTRAP_CONFIG' {
export const KIND: RouteKind
}
declare module 'APP_BOOTSTRAP' {
const chunks: Array<string>
export default chunks
}
declare module 'APP_ENTRY' {
export const tree: any
export const pathname: string
// and more...
}
declare module 'CLIENT_MODULE' {
export const __turbopack_module_id__: string
}

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use turbopack_binding::turbo::tasks_fs::FileSystemPathVc;
pub mod next_layout_entry_transition;
pub mod next_server_component_transition;
#[turbo_tasks::value(shared)]
pub struct LayoutSegment {

View file

@ -13,7 +13,10 @@ use turbopack_binding::{
},
};
use crate::next_client_component::with_client_chunks::WithClientChunksAsset;
use crate::next_client_component::{
with_chunking_context_scope_asset::WithChunkingContextScopeAsset,
with_client_chunks::WithClientChunksAsset,
};
#[turbo_tasks::value(shared)]
pub struct NextServerComponentTransition {
@ -59,10 +62,15 @@ impl Transition for NextServerComponentTransition {
bail!("Not an ecmascript module");
};
Ok(WithClientChunksAsset {
asset,
// next.js code already adds _next prefix
server_root: self.server_root.join("_next"),
Ok(WithChunkingContextScopeAsset {
asset: WithClientChunksAsset {
asset,
// next.js code already adds _next prefix
server_root: self.server_root.join("_next"),
}
.cell()
.into(),
layer: "rsc".to_string(),
}
.cell()
.into())

View file

@ -1,21 +1,24 @@
use std::ops::Deref;
use anyhow::Result;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use swc_core::{
common::{source_map::Pos, Span, Spanned},
ecma::ast::{Expr, Ident, Program},
};
use turbo_tasks::{primitives::StringVc, trace::TraceRawVcs};
use turbo_tasks::{primitives::StringVc, trace::TraceRawVcs, TryJoinIterExt};
use turbo_tasks_fs::FileSystemPathVc;
use turbopack_binding::turbopack::{
core::{
asset::{Asset, AssetVc},
context::{AssetContext, AssetContextVc},
ident::AssetIdentVc,
issue::{
Issue, IssueSeverity, IssueSeverityVc, IssueSourceVc, IssueVc, OptionIssueSourceVc,
},
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
source_asset::SourceAssetVc,
},
ecmascript::{
analyzer::{graph::EvalContext, JsValue},
@ -24,7 +27,7 @@ use turbopack_binding::turbopack::{
},
};
use crate::util::NextRuntime;
use crate::{app_structure::LoaderTreeVc, util::NextRuntime};
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, TraceRawVcs, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
@ -50,15 +53,14 @@ pub enum NextSegmentFetchCache {
}
#[turbo_tasks::value]
#[derive(Debug)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Default)]
pub struct NextSegmentConfig {
pub dynamic: NextSegmentDynamic,
pub dynamic_params: bool,
pub revalidate: bool,
pub fetch_cache: NextSegmentFetchCache,
pub runtime: NextRuntime,
pub referred_region: String,
pub dynamic: Option<NextSegmentDynamic>,
pub dynamic_params: Option<bool>,
pub revalidate: Option<bool>,
pub fetch_cache: Option<NextSegmentFetchCache>,
pub runtime: Option<NextRuntime>,
pub preferred_region: Option<String>,
}
#[turbo_tasks::value_impl]
@ -69,16 +71,73 @@ impl NextSegmentConfigVc {
}
}
impl Default for NextSegmentConfig {
fn default() -> Self {
NextSegmentConfig {
dynamic: Default::default(),
dynamic_params: true,
revalidate: false,
fetch_cache: Default::default(),
runtime: Default::default(),
referred_region: "auto".to_string(),
impl NextSegmentConfig {
/// Applies the parent config to this config, setting any unset values to
/// the parent's values.
pub fn apply_parent_config(&mut self, parent: &Self) {
let NextSegmentConfig {
dynamic,
dynamic_params,
revalidate,
fetch_cache,
runtime,
preferred_region,
} = self;
*dynamic = dynamic.or(parent.dynamic);
*dynamic_params = dynamic_params.or(parent.dynamic_params);
*revalidate = revalidate.or(parent.revalidate);
*fetch_cache = fetch_cache.or(parent.fetch_cache);
*runtime = runtime.or(parent.runtime);
*preferred_region = preferred_region.take().or(parent.preferred_region.clone());
}
/// Applies a config from a paralllel route to this config, returning an
/// error if there are conflicting values.
pub fn apply_parallel_config(&mut self, parallel_config: &Self) -> Result<()> {
fn merge_parallel<T: PartialEq + Clone>(
a: &mut Option<T>,
b: &Option<T>,
name: &str,
) -> Result<()> {
match (a.as_ref(), b) {
(Some(a), Some(b)) => {
if *a != *b {
bail!(
"Sibling segment configs have conflicting values for {}",
name
)
}
}
(None, Some(b)) => {
*a = Some(b.clone());
}
_ => {}
}
Ok(())
}
let Self {
dynamic,
dynamic_params,
revalidate,
fetch_cache,
runtime,
preferred_region,
} = self;
merge_parallel(dynamic, &parallel_config.dynamic, "dynamic")?;
merge_parallel(
dynamic_params,
&parallel_config.dynamic_params,
"dynamicParams",
)?;
merge_parallel(revalidate, &parallel_config.revalidate, "revalidate")?;
merge_parallel(fetch_cache, &parallel_config.fetch_cache, "fetchCache")?;
merge_parallel(runtime, &parallel_config.runtime, "runtime")?;
merge_parallel(
preferred_region,
&parallel_config.preferred_region,
"referredRegion",
)?;
Ok(())
}
}
@ -216,7 +275,7 @@ fn parse_config_value(
};
config.dynamic = match serde_json::from_value(Value::String(val.to_string())) {
Ok(dynamic) => dynamic,
Ok(dynamic) => Some(dynamic),
Err(err) => {
return invalid_config(
&format!("`dynamic` has an invalid value: {}", err),
@ -231,7 +290,7 @@ fn parse_config_value(
return invalid_config("`dynamicParams` needs to be a static boolean", &value);
};
config.dynamic_params = val;
config.dynamic_params = Some(val);
}
"revalidate" => {
let value = eval_context.eval(init);
@ -239,7 +298,7 @@ fn parse_config_value(
return invalid_config("`revalidate` needs to be a static boolean", &value);
};
config.revalidate = val;
config.revalidate = Some(val);
}
"fetchCache" => {
let value = eval_context.eval(init);
@ -248,7 +307,7 @@ fn parse_config_value(
};
config.fetch_cache = match serde_json::from_value(Value::String(val.to_string())) {
Ok(fetch_cache) => fetch_cache,
Ok(fetch_cache) => Some(fetch_cache),
Err(err) => {
return invalid_config(
&format!("`fetchCache` has an invalid value: {}", err),
@ -264,7 +323,7 @@ fn parse_config_value(
};
config.runtime = match serde_json::from_value(Value::String(val.to_string())) {
Ok(runtime) => runtime,
Ok(runtime) => Some(runtime),
Err(err) => {
return invalid_config(
&format!("`runtime` has an invalid value: {}", err),
@ -279,8 +338,43 @@ fn parse_config_value(
return invalid_config("`preferredRegion` needs to be a static string", &value);
};
config.referred_region = val.to_string();
config.preferred_region = Some(val.to_string());
}
_ => {}
}
}
#[turbo_tasks::function]
pub async fn parse_segment_config_from_loader_tree(
loader_tree: LoaderTreeVc,
context: AssetContextVc,
) -> Result<NextSegmentConfigVc> {
let loader_tree = loader_tree.await?;
let components = loader_tree.components.await?;
let mut config = NextSegmentConfig::default();
let parallel_configs = loader_tree
.parallel_routes
.values()
.copied()
.map(|tree| parse_segment_config_from_loader_tree(tree, context))
.try_join()
.await?;
for tree in parallel_configs {
config.apply_parallel_config(&tree)?;
}
for component in [components.page, components.default, components.layout]
.into_iter()
.flatten()
{
config.apply_parent_config(
&*parse_segment_config_from_source(context.process(
SourceAssetVc::new(component).into(),
turbo_tasks::Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
))
.await?,
);
}
Ok(config.cell())
}

View file

@ -1,14 +1,15 @@
use std::{collections::HashMap, io::Write, iter::once};
use std::{collections::HashMap, io::Write as _, iter::once};
use anyhow::{bail, Result};
use async_recursion::async_recursion;
use indexmap::{indexmap, IndexMap};
use indoc::indoc;
use turbo_tasks::{primitives::JsonValueVc, TryJoinIterExt, ValueToString};
use turbopack_binding::{
turbo::{
tasks::{primitives::StringVc, Value},
tasks_env::{CustomProcessEnvVc, EnvMapVc, ProcessEnvVc},
tasks_fs::{rope::RopeBuilder, File, FileContent, FileSystemPathVc},
tasks_fs::{rope::RopeBuilder, File, FileSystemPathVc},
},
turbopack::{
core::{
@ -59,14 +60,14 @@ use turbopack_binding::{
};
use crate::{
app_render::next_layout_entry_transition::NextServerComponentTransition,
app_segment_config::parse_segment_config_from_source,
app_render::next_server_component_transition::NextServerComponentTransition,
app_segment_config::{parse_segment_config_from_loader_tree, parse_segment_config_from_source},
app_structure::{
get_entrypoints, get_global_metadata, Components, Entrypoint, GlobalMetadataVc, LoaderTree,
LoaderTreeVc, Metadata, MetadataItem, MetadataWithAltItem, OptionAppDirVc,
},
bootstrap::{route_bootstrap, BootstrapConfigVc},
embed_js::{next_asset, next_js_file, next_js_file_path},
embed_js::{next_asset, next_js_file_path},
env::env_for_js,
fallback::get_fallback_page,
mode::NextMode,
@ -86,7 +87,8 @@ use crate::{
next_config::NextConfigVc,
next_edge::{
context::{get_edge_compile_time_info, get_edge_resolve_options_context},
transition::NextEdgeTransition,
page_transition::NextEdgePageTransition,
route_transition::NextEdgeRouteTransition,
},
next_image::module::{BlurPlaceholderMode, StructuredImageModuleType},
next_route_matcher::{NextFallbackMatcherVc, NextParamsMatcherVc},
@ -200,7 +202,7 @@ fn next_ssr_client_module_transition(
}
#[turbo_tasks::function]
fn next_layout_entry_transition(
fn next_server_component_transition(
project_path: FileSystemPathVc,
execution_context: ExecutionContextVc,
app_dir: FileSystemPathVc,
@ -227,6 +229,37 @@ fn next_layout_entry_transition(
.into()
}
#[turbo_tasks::function]
fn next_edge_server_component_transition(
project_path: FileSystemPathVc,
execution_context: ExecutionContextVc,
app_dir: FileSystemPathVc,
server_root: FileSystemPathVc,
next_config: NextConfigVc,
server_addr: ServerAddrVc,
) -> TransitionVc {
let ty = Value::new(ServerContextType::AppRSC { app_dir });
let mode = NextMode::Development;
let rsc_compile_time_info = get_edge_compile_time_info(
project_path,
server_addr,
Value::new(EnvironmentIntention::ServerRendering),
);
let rsc_resolve_options_context =
get_edge_resolve_options_context(project_path, ty, next_config, execution_context);
let rsc_module_options_context =
get_server_module_options_context(project_path, execution_context, ty, mode, next_config);
NextServerComponentTransition {
rsc_compile_time_info,
rsc_module_options_context,
rsc_resolve_options_context,
server_root,
}
.cell()
.into()
}
#[turbo_tasks::function]
fn next_edge_route_transition(
project_path: FileSystemPathVc,
@ -252,12 +285,12 @@ fn next_edge_route_transition(
get_client_assets_path(server_root, Value::new(ClientContextType::App { app_dir })),
edge_compile_time_info.environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("app_source"))
.build();
let edge_resolve_options_context =
get_edge_resolve_options_context(project_path, server_ty, next_config, execution_context);
NextEdgeTransition {
NextEdgeRouteTransition {
edge_compile_time_info,
edge_chunking_context,
edge_module_options_context: None,
@ -271,6 +304,49 @@ fn next_edge_route_transition(
.into()
}
#[turbo_tasks::function]
fn next_edge_page_transition(
project_path: FileSystemPathVc,
app_dir: FileSystemPathVc,
server_root: FileSystemPathVc,
next_config: NextConfigVc,
server_addr: ServerAddrVc,
output_path: FileSystemPathVc,
execution_context: ExecutionContextVc,
) -> TransitionVc {
let server_ty = Value::new(ServerContextType::AppRoute { app_dir });
let edge_compile_time_info = get_edge_compile_time_info(
project_path,
server_addr,
Value::new(EnvironmentIntention::ServerRendering),
);
let edge_chunking_context = DevChunkingContextVc::builder(
project_path,
output_path.join("edge-pages"),
output_path.join("edge-pages/chunks"),
get_client_assets_path(server_root, Value::new(ClientContextType::App { app_dir })),
edge_compile_time_info.environment(),
)
.layer("ssr")
.reference_chunk_source_maps(should_debug("app_source"))
.build();
let edge_resolve_options_context =
get_edge_resolve_options_context(project_path, server_ty, next_config, execution_context);
NextEdgePageTransition {
edge_compile_time_info,
edge_chunking_context,
edge_module_options_context: None,
edge_resolve_options_context,
output_path,
bootstrap_asset: next_asset("entry/app/edge-page-bootstrap.ts"),
}
.cell()
.into()
}
#[allow(clippy::too_many_arguments)]
#[turbo_tasks::function]
fn app_context(
@ -302,8 +378,20 @@ fn app_context(
),
);
transitions.insert(
"next-layout-entry".to_string(),
next_layout_entry_transition(
"next-edge-page".to_string(),
next_edge_page_transition(
project_path,
app_dir,
server_root,
next_config,
server_addr,
output_path,
execution_context,
),
);
transitions.insert(
"next-server-component".to_string(),
next_server_component_transition(
project_path,
execution_context,
app_dir,
@ -313,6 +401,17 @@ fn app_context(
server_addr,
),
);
transitions.insert(
"next-edge-server-component".to_string(),
next_edge_server_component_transition(
project_path,
execution_context,
app_dir,
server_root,
next_config,
server_addr,
),
);
transitions.insert(
"server-to-client".to_string(),
next_server_to_client_transition,
@ -716,6 +815,14 @@ impl AppRendererVc {
(context_ssr, intermediate_output_path)
};
let config = parse_segment_config_from_loader_tree(loader_tree, context);
let runtime = config.await?.runtime;
let rsc_transition = match runtime {
Some(NextRuntime::NodeJs) | None => "next-server-component",
Some(NextRuntime::Edge) => "next-edge-server-component",
};
struct State {
inner_assets: IndexMap<String, AssetVc>,
counter: usize,
@ -723,6 +830,7 @@ impl AppRendererVc {
loader_tree_code: String,
context: AssetContextVc,
unsupported_metadata: Vec<FileSystemPathVc>,
rsc_transition: &'static str,
}
impl State {
@ -740,6 +848,7 @@ impl AppRendererVc {
loader_tree_code: String::new(),
context,
unsupported_metadata: Vec::new(),
rsc_transition,
};
fn write_component(
@ -767,7 +876,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}";
state.inner_assets.insert(
format!("COMPONENT_{i}"),
state.context.with_transition("next-layout-entry").process(
state.context.with_transition(state.rsc_transition).process(
SourceAssetVc::new(component).into(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
@ -958,7 +1067,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}";
walk_tree(&mut state, loader_tree).await?;
let State {
mut inner_assets,
inner_assets,
imports,
loader_tree_code,
unsupported_metadata,
@ -975,41 +1084,29 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}";
.emit();
}
// IPC need to be the first import to allow it to catch errors happening during
// the other imports
let mut result =
RopeBuilder::from("import { IPC } from \"@vercel/turbopack-node/ipc/index\";\n");
let import_path = next_js_file_path("entry")
.await?
.get_relative_path_to(&*next_js_file_path("polyfill/app-polyfills.ts").await?)
.unwrap();
writeln!(result, "import \"{import_path}\";\n",)?;
let mut result = RopeBuilder::from(indoc! {"
\"TURBOPACK { chunking-type: isolatedParallel; transition: next-edge-server-component }\";
import GlobalErrorMod from \"next/dist/client/components/error-boundary\"
const { GlobalError } = GlobalErrorMod;
\"TURBOPACK { chunking-type: isolatedParallel; transition: next-edge-server-component }\";
import base from \"next/dist/server/app-render/entry-base\"\n
"});
for import in imports {
writeln!(result, "{import}")?;
}
writeln!(result, "const LOADER_TREE = {loader_tree_code};\n")?;
writeln!(result, "import BOOTSTRAP from \"BOOTSTRAP\";\n")?;
inner_assets.insert(
"BOOTSTRAP".to_string(),
context.with_transition("next-client").process(
SourceAssetVc::new(next_js_file_path("entry/app/hydrate.tsx")).into(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
),
);
let base_code = next_js_file("entry/app-renderer.tsx");
if let FileContent::Content(base_file) = &*base_code.await? {
result += base_file.content()
}
writeln!(result, "const tree = {loader_tree_code};\n")?;
writeln!(result, "const pathname = '';\n")?;
writeln!(
result,
// Need this hack because "export *" from CommonJS will trigger a warning
// otherwise
"__turbopack_export_value__({{ tree, GlobalError, pathname, ...base }});\n"
)?;
let file = File::from(result.build());
let asset = VirtualAssetVc::new(next_js_file_path("entry/app-renderer.tsx"), file.into());
let asset = VirtualAssetVc::new(next_js_file_path("entry/app-entry.tsx"), file.into());
let chunking_context = DevChunkingContextVc::builder(
project_path,
@ -1019,15 +1116,38 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}";
context.compile_time_info().environment(),
)
.layer("ssr")
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("app_source"))
.build();
let module = context.process(
asset.into(),
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(inner_assets))),
);
let renderer_module = match runtime {
Some(NextRuntime::NodeJs) | None => context.process(
SourceAssetVc::new(next_js_file_path("entry/app-renderer.tsx")).into(),
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(indexmap! {
"APP_ENTRY".to_string() => context.with_transition(rsc_transition).process(
asset.into(),
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(inner_assets))),
),
"APP_BOOTSTRAP".to_string() => context.with_transition("next-client").process(
SourceAssetVc::new(next_js_file_path("entry/app/hydrate.tsx")).into(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
),
}))),
),
Some(NextRuntime::Edge) =>
context.process(
SourceAssetVc::new(next_js_file_path("entry/app-edge-renderer.tsx")).into(),
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(indexmap! {
"INNER_EDGE_CHUNK_GROUP".to_string() => context.with_transition("next-edge-page").process(
asset.into(),
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(inner_assets))),
),
}))),
)
};
let Some(module) = EvaluatableAssetVc::resolve_from(module).await? else {
let Some(module) = EvaluatableAssetVc::resolve_from(renderer_module).await? else {
bail!("internal module must be evaluatable");
};
@ -1096,7 +1216,7 @@ impl AppRouteVc {
this.context.compile_time_info().environment(),
)
.layer("ssr")
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("app_source"))
.build();
let entry_source_asset = SourceAssetVc::new(this.entry_path);
@ -1107,7 +1227,7 @@ impl AppRouteVc {
let config = parse_segment_config_from_source(entry_asset);
let module = match config.await?.runtime {
NextRuntime::NodeJs => {
Some(NextRuntime::NodeJs) | None => {
let bootstrap_asset = next_asset("entry/app/route.ts");
route_bootstrap(
@ -1118,7 +1238,7 @@ impl AppRouteVc {
BootstrapConfigVc::empty(),
)
}
NextRuntime::Edge => {
Some(NextRuntime::Edge) => {
let internal_asset = next_asset("entry/app/edge-route.ts");
let entry = this.context.with_transition("next-edge-route").process(

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use indexmap::{indexmap, IndexMap};
use turbo_tasks::Value;
use turbo_tasks::{Value, ValueToString};
use turbo_tasks_fs::{File, FileSystemPathVc};
use turbopack_binding::turbopack::{
core::{
@ -80,10 +80,13 @@ pub async fn bootstrap(
config: BootstrapConfigVc,
) -> Result<EvaluatableAssetVc> {
let path = asset.ident().path().await?;
let path = base_path
.await?
.get_path_to(&path)
.context("asset is not in base_path")?;
let Some(path) = base_path.await?.get_path_to(&path) else {
bail!(
"asset {} is not in base path {}",
asset.ident().to_string().await?,
base_path.to_string().await?
);
};
let path = if let Some((name, ext)) = path.rsplit_once('.') {
if !ext.contains('/') {
name

View file

@ -1,2 +1,3 @@
pub mod context;
pub mod transition;
pub mod page_transition;
pub mod route_transition;

View file

@ -0,0 +1,100 @@
use anyhow::{bail, Result};
use indexmap::indexmap;
use turbo_tasks::Value;
use turbopack_binding::{
turbo::tasks_fs::FileSystemPathVc,
turbopack::{
core::{
asset::AssetVc,
chunk::{ChunkableAssetVc, ChunkingContextVc},
compile_time_info::CompileTimeInfoVc,
context::AssetContext,
reference_type::{EcmaScriptModulesReferenceSubType, InnerAssetsVc, ReferenceType},
source_asset::SourceAssetVc,
},
ecmascript::chunk_group_files_asset::ChunkGroupFilesAsset,
turbopack::{
module_options::ModuleOptionsContextVc,
resolve_options_context::ResolveOptionsContextVc,
transition::{Transition, TransitionVc},
ModuleAssetContextVc,
},
},
};
use crate::embed_js::next_js_file_path;
/// Transition into edge environment to render an app directory page.
///
/// It changes the environment to the provided edge environment, and wraps the
/// process asset with the provided bootstrap_asset returning the chunks of all
/// that for running them in the edge sandbox.
#[turbo_tasks::value(shared)]
pub struct NextEdgePageTransition {
pub edge_compile_time_info: CompileTimeInfoVc,
pub edge_chunking_context: ChunkingContextVc,
pub edge_module_options_context: Option<ModuleOptionsContextVc>,
pub edge_resolve_options_context: ResolveOptionsContextVc,
pub output_path: FileSystemPathVc,
pub bootstrap_asset: AssetVc,
}
#[turbo_tasks::value_impl]
impl Transition for NextEdgePageTransition {
#[turbo_tasks::function]
fn process_compile_time_info(
&self,
_compile_time_info: CompileTimeInfoVc,
) -> CompileTimeInfoVc {
self.edge_compile_time_info
}
#[turbo_tasks::function]
fn process_module_options_context(
&self,
context: ModuleOptionsContextVc,
) -> ModuleOptionsContextVc {
self.edge_module_options_context.unwrap_or(context)
}
#[turbo_tasks::function]
fn process_resolve_options_context(
&self,
_context: ResolveOptionsContextVc,
) -> ResolveOptionsContextVc {
self.edge_resolve_options_context
}
#[turbo_tasks::function]
async fn process_module(
&self,
asset: AssetVc,
context: ModuleAssetContextVc,
) -> Result<AssetVc> {
let asset = context.process(
self.bootstrap_asset,
Value::new(ReferenceType::Internal(InnerAssetsVc::cell(indexmap! {
"APP_ENTRY".to_string() => asset,
"APP_BOOTSTRAP".to_string() => context.with_transition("next-client").process(
SourceAssetVc::new(next_js_file_path("entry/app/hydrate.tsx")).into(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
),
}))),
);
let Some(asset) = ChunkableAssetVc::resolve_from(asset).await? else {
bail!("Internal module is not evaluatable");
};
let asset = ChunkGroupFilesAsset {
asset,
client_root: self.output_path,
chunking_context: self.edge_chunking_context,
runtime_entries: None,
};
Ok(asset.cell().into())
}
}

View file

@ -17,7 +17,7 @@ use turbopack_binding::{
use crate::bootstrap::{route_bootstrap, BootstrapConfigVc};
#[turbo_tasks::value(shared)]
pub struct NextEdgeTransition {
pub struct NextEdgeRouteTransition {
pub edge_compile_time_info: CompileTimeInfoVc,
pub edge_chunking_context: ChunkingContextVc,
pub edge_module_options_context: Option<ModuleOptionsContextVc>,
@ -29,7 +29,7 @@ pub struct NextEdgeTransition {
}
#[turbo_tasks::value_impl]
impl Transition for NextEdgeTransition {
impl Transition for NextEdgeRouteTransition {
#[turbo_tasks::function]
fn process_compile_time_info(
&self,

View file

@ -209,12 +209,6 @@ pub async fn get_next_server_import_map(
)
.await?;
import_map.insert_exact_alias(
"@opentelemetry/api",
// TODO(WEB-625) this actually need to prefer the local version of @opentelemetry/api
ImportMapping::External(Some("next/dist/compiled/@opentelemetry/api".to_string())).into(),
);
let ty = ty.into_value();
insert_next_server_special_aliases(&mut import_map, ty).await?;
@ -357,6 +351,12 @@ pub async fn insert_next_server_special_aliases(
) -> Result<()> {
match ty {
ServerContextType::Pages { pages_dir } => {
import_map.insert_exact_alias(
"@opentelemetry/api",
// TODO(WEB-625) this actually need to prefer the local version of
// @opentelemetry/api
external_request_to_import_mapping("next/dist/compiled/@opentelemetry/api"),
);
insert_alias_to_alternatives(
import_map,
format!("{VIRTUAL_PACKAGE_NAME}/pages/_app"),
@ -386,6 +386,12 @@ pub async fn insert_next_server_special_aliases(
ServerContextType::AppSSR { app_dir }
| ServerContextType::AppRSC { app_dir }
| ServerContextType::AppRoute { app_dir } => {
import_map.insert_exact_alias(
"@opentelemetry/api",
// TODO(WEB-625) this actually need to prefer the local version of
// @opentelemetry/api
request_to_import_mapping(app_dir, "next/dist/compiled/@opentelemetry/api"),
);
import_map.insert_exact_alias(
"react",
request_to_import_mapping(app_dir, "next/dist/compiled/react"),

View file

@ -62,7 +62,7 @@ use crate::{
next_config::NextConfigVc,
next_edge::{
context::{get_edge_compile_time_info, get_edge_resolve_options_context},
transition::NextEdgeTransition,
route_transition::NextEdgeRouteTransition,
},
next_route_matcher::{
NextExactMatcherVc, NextFallbackMatcherVc, NextParamsMatcherVc,
@ -168,12 +168,12 @@ pub async fn create_page_source(
),
edge_compile_time_info.environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("page_source"))
.build();
let edge_resolve_options_context =
get_edge_resolve_options_context(project_root, server_ty, next_config, execution_context);
let next_edge_transition = NextEdgeTransition {
let next_edge_transition = NextEdgeRouteTransition {
edge_compile_time_info,
edge_chunking_context,
edge_module_options_context: None,
@ -374,7 +374,7 @@ async fn create_page_source_for_file(
),
server_context.compile_time_info().environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("page_source"))
.build();
let data_node_path = node_path.join("data");
@ -389,7 +389,7 @@ async fn create_page_source_for_file(
),
server_context.compile_time_info().environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("page_source"))
.build();
let client_chunking_context = get_client_chunking_context(
@ -534,7 +534,7 @@ async fn create_not_found_page_source(
),
server_context.compile_time_info().environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("page_source"))
.build();
let client_chunking_context = get_client_chunking_context(

View file

@ -47,7 +47,7 @@ use crate::{
next_config::NextConfigVc,
next_edge::{
context::{get_edge_compile_time_info, get_edge_resolve_options_context},
transition::NextEdgeTransition,
route_transition::NextEdgeRouteTransition,
},
next_import_map::get_next_build_import_map,
next_server::context::{get_server_module_options_context, ServerContextType},
@ -250,7 +250,7 @@ fn edge_transition_map(
output_path.join("edge/assets"),
edge_compile_time_info.environment(),
)
.reference_chunk_source_maps(false)
.reference_chunk_source_maps(should_debug("router"))
.build();
let edge_resolve_options_context = get_edge_resolve_options_context(
@ -268,7 +268,7 @@ fn edge_transition_map(
next_config,
);
let next_edge_transition = NextEdgeTransition {
let next_edge_transition = NextEdgeRouteTransition {
edge_compile_time_info,
edge_chunking_context,
edge_module_options_context: Some(server_module_options_context),

View file

@ -92,6 +92,8 @@ lazy_static! {
static ref DEBUG_BROWSER: bool = env::var("TURBOPACK_DEBUG_BROWSER").is_ok();
/// Only starts the dev server on port 3000, but doesn't spawn a browser or run any tests.
static ref DEBUG_START: bool = env::var("TURBOPACK_DEBUG_START").is_ok();
/// When using TURBOPACK_DEBUG_START, this will open the browser to the dev server.
static ref DEBUG_OPEN: bool = env::var("TURBOPACK_DEBUG_OPEN").is_ok();
}
fn run_async_test<'a, T>(future: impl Future<Output = T> + Send + 'a) -> T {
@ -137,19 +139,12 @@ fn test(resource: PathBuf) {
run_result,
} = run_async_test(run_test(resource));
if !uncaught_exceptions.is_empty() {
panic!(
"Uncaught exception(s) in test:\n{}",
uncaught_exceptions.join("\n")
)
let mut messages = vec![];
if run_result.test_results.is_empty() {
messages.push("No tests were run.".to_string());
}
assert!(
!run_result.test_results.is_empty(),
"Expected one or more tests to run."
);
let mut messages = vec![];
for test_result in run_result.test_results {
// It's possible to fail multiple tests across these tests,
// so collect them and fail the respective test in Rust with
@ -163,6 +158,10 @@ fn test(resource: PathBuf) {
}
}
for uncaught_exception in uncaught_exceptions {
messages.push(format!("Uncaught exception: {}", uncaught_exception));
}
if !messages.is_empty() {
panic!(
"Failed with error(s) in the following test(s):\n\n{}",
@ -257,7 +256,11 @@ async fn run_test(resource: PathBuf) -> JsResult {
let test_dir = resource_temp.to_path_buf();
let workspace_root = cargo_workspace_root.parent().unwrap().parent().unwrap();
let project_dir = test_dir.join("input");
let requested_addr = get_free_local_addr().unwrap();
let requested_addr = if *DEBUG_START {
"127.0.0.1:3000".parse().unwrap()
} else {
get_free_local_addr().unwrap()
};
let mock_dir = resource_temp.join("__httpmock__");
let mock_server_future = get_mock_server_future(&mock_dir);
@ -265,89 +268,95 @@ async fn run_test(resource: PathBuf) -> JsResult {
let (issue_tx, mut issue_rx) = unbounded_channel();
let issue_tx = TransientInstance::new(issue_tx);
let tt = TurboTasks::new(MemoryBackend::default());
let server = NextDevServerBuilder::new(
tt.clone(),
project_dir.to_string_lossy().to_string(),
workspace_root.to_string_lossy().to_string(),
)
.entry_request(EntryRequest::Module(
"@turbo/pack-test-harness".to_string(),
"/harness".to_string(),
))
.entry_request(EntryRequest::Relative("index.js".to_owned()))
.eager_compile(false)
.hostname(requested_addr.ip())
.port(requested_addr.port())
.log_level(turbopack_binding::turbopack::core::issue::IssueSeverity::Warning)
.log_detail(true)
.issue_reporter(Box::new(move || {
TestIssueReporterVc::new(issue_tx.clone()).into()
}))
.show_all(true)
.build()
.await
.unwrap();
let result;
let local_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), server.addr.port());
println!(
"{event_type} - server started at http://{address}",
event_type = "ready".green(),
address = server.addr
);
if *DEBUG_START {
webbrowser::open(&local_addr.to_string()).unwrap();
tokio::select! {
_ = mock_server_future => {},
_ = pending() => {},
_ = server.future => {},
};
panic!("Never resolves")
}
let result = tokio::select! {
// Poll the mock_server first to add the env var
_ = mock_server_future => panic!("Never resolves"),
r = run_browser(local_addr, &project_dir) => r.expect("error while running browser"),
_ = server.future => panic!("Never resolves"),
};
env::remove_var("TURBOPACK_TEST_ONLY_MOCK_SERVER");
let task = tt.spawn_once_task(async move {
let issues_fs = DiskFileSystemVc::new(
"issues".to_string(),
resource.join("issues").to_string_lossy().to_string(),
{
let tt = TurboTasks::new(MemoryBackend::default());
let server = NextDevServerBuilder::new(
tt.clone(),
project_dir.to_string_lossy().to_string(),
workspace_root.to_string_lossy().to_string(),
)
.as_file_system();
.entry_request(EntryRequest::Module(
"@turbo/pack-test-harness".to_string(),
"/harness".to_string(),
))
.entry_request(EntryRequest::Relative("index.js".to_owned()))
.eager_compile(false)
.hostname(requested_addr.ip())
.port(requested_addr.port())
.log_level(turbopack_binding::turbopack::core::issue::IssueSeverity::Warning)
.log_detail(true)
.issue_reporter(Box::new(move || {
TestIssueReporterVc::new(issue_tx.clone()).into()
}))
.show_all(true)
.build()
.await
.unwrap();
let mut issues = vec![];
while let Ok(issue) = issue_rx.try_recv() {
issues.push(issue);
let local_addr =
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), server.addr.port());
println!(
"{event_type} - server started at http://{address}",
event_type = "ready".green(),
address = server.addr
);
if *DEBUG_START {
if *DEBUG_OPEN {
webbrowser::open(&format!("http://{}", local_addr)).unwrap();
}
tokio::select! {
_ = mock_server_future => {},
_ = pending() => {},
_ = server.future => {},
};
panic!("Never resolves")
}
snapshot_issues(
issues.iter().cloned(),
issues_fs.root(),
&cargo_workspace_root.to_string_lossy(),
)
.await?;
result = tokio::select! {
// Poll the mock_server first to add the env var
_ = mock_server_future => panic!("Never resolves"),
r = run_browser(local_addr, &project_dir) => r.expect("error while running browser"),
_ = server.future => panic!("Never resolves"),
};
Ok(NothingVc::new().into())
});
tt.wait_task_completion(task, true).await.unwrap();
env::remove_var("TURBOPACK_TEST_ONLY_MOCK_SERVER");
// This sometimes fails for the following test:
// test_tests__integration__next__webpack_loaders__no_options__input
retry(
let task = tt.spawn_once_task(async move {
let issues_fs = DiskFileSystemVc::new(
"issues".to_string(),
resource.join("issues").to_string_lossy().to_string(),
)
.as_file_system();
let mut issues = vec![];
while let Ok(issue) = issue_rx.try_recv() {
issues.push(issue);
}
snapshot_issues(
issues.iter().cloned(),
issues_fs.root(),
&cargo_workspace_root.to_string_lossy(),
)
.await?;
Ok(NothingVc::new().into())
});
tt.wait_task_completion(task, true).await.unwrap();
}
if let Err(err) = retry(
(),
|()| std::fs::remove_dir_all(&resource_temp),
3,
Duration::from_millis(100),
)
.expect("failed to remove temporary directory");
) {
eprintln!("Failed to remove temporary directory: {}", err);
}
result
}

View file

@ -10,7 +10,11 @@ async function getJson(url) {
const res = await fetch(url)
const text = await res.text()
const jsonText = /(\{[^}]*\})/.exec(text)
return JSON.parse(jsonText[0].replace(/&quot;/g, '"'))
try {
return JSON.parse(jsonText[0].replace(/&quot;/g, '"'))
} catch (err) {
throw new Error(`Expected JSON but got:\n${text}`)
}
}
function runTests() {
@ -63,17 +67,10 @@ function runTests() {
it('app with edge runtime should import edge conditions', async () => {
const json = await getJson('/app-edge')
// TODO We don't currently support edge config in app rendering.
// When we do, this needs to be updated.
expect(json).not.toMatchObject({
expect(json).toMatchObject({
edgeThenNode: 'edge',
nodeThenEdge: 'edge',
})
// TODO: delete this.
expect(json).toMatchObject({
edgeThenNode: 'node',
nodeThenEdge: 'node',
})
})
it('app route with nodejs runtime should import node conditions', async () => {

View file

@ -0,0 +1,35 @@
PlainIssue {
severity: Error,
context: "[project]/packages/next/dist/compiled/nanoid/index.cjs",
category: "resolve",
title: "Error resolving commonjs request",
description: "unable to resolve module \"crypto\"",
detail: "It was not possible to find the requested file.\nParsed request as written in source code: module \"crypto\"\nPath where resolving has started: [project]/packages/next/dist/compiled/nanoid/index.cjs\nType of request: commonjs request\nImport map: No import map entry\n",
documentation_link: "",
source: Some(
PlainIssueSource {
asset: PlainAsset {
ident: "[project]/packages/next/dist/compiled/nanoid/index.cjs",
},
start: SourcePos {
line: 0,
column: 45,
},
end: SourcePos {
line: 0,
column: 62,
},
},
),
sub_issues: [],
processing_path: Some(
[
PlainIssueProcessingPathItem {
context: Some(
"[project]/packages/next-swc/crates/next-dev-tests/tests/temp/next/import/conditions/input/app",
),
description: "Next.js App Page Route /app-edge",
},
],
),
}

View file

@ -0,0 +1,41 @@
PlainIssue {
severity: Error,
context: "[project]/packages/next/dist/compiled/nanoid/index.cjs",
category: "resolve",
title: "Error resolving commonjs request",
description: "unable to resolve module \"crypto\"",
detail: "It was not possible to find the requested file.\nParsed request as written in source code: module \"crypto\"\nPath where resolving has started: [project]/packages/next/dist/compiled/nanoid/index.cjs\nType of request: commonjs request\nImport map: No import map entry\n",
documentation_link: "",
source: Some(
PlainIssueSource {
asset: PlainAsset {
ident: "[project]/packages/next/dist/compiled/nanoid/index.cjs",
},
start: SourcePos {
line: 0,
column: 45,
},
end: SourcePos {
line: 0,
column: 62,
},
},
),
sub_issues: [],
processing_path: Some(
[
PlainIssueProcessingPathItem {
context: Some(
"[project]/packages/next-swc/crates/next-dev-tests/tests/temp/next/import/conditions/input/app",
),
description: "Next.js App Page Route /app-edge",
},
PlainIssueProcessingPathItem {
context: Some(
"[next]/entry/app-edge-renderer.tsx",
),
description: "server-side rendering /app-edge",
},
],
),
}

View file

@ -623,33 +623,22 @@ const nextAppLoader: AppLoader = async function nextAppLoader() {
}
}
// Prefer to modify next/src/server/app-render/entry-base.ts since this is shared with Turbopack.
// Any changes to this code should be reflected in Turbopack's app_source.rs and/or app-renderer.tsx as well.
const result = `
export ${treeCodeResult.treeCode}
export ${treeCodeResult.pages}
export { default as AppRouter } from 'next/dist/client/components/app-router'
export { default as LayoutRouter } from 'next/dist/client/components/layout-router'
export { default as RenderFromTemplateContext } from 'next/dist/client/components/render-from-template-context'
export { default as GlobalError } from ${JSON.stringify(
treeCodeResult.globalError || 'next/dist/client/components/error-boundary'
)}
export const originalPathname = ${JSON.stringify(page)}
export const __next_app__ = {
require: __webpack_require__,
// all modules are in the entry chunk, so we never actually need to load chunks in webpack
loadChunk: () => Promise.resolve()
}
export { staticGenerationAsyncStorage } from 'next/dist/client/components/static-generation-async-storage'
export { requestAsyncStorage } from 'next/dist/client/components/request-async-storage'
export { actionAsyncStorage } from 'next/dist/client/components/action-async-storage'
export { staticGenerationBailout } from 'next/dist/client/components/static-generation-bailout'
export { default as StaticGenerationSearchParamsBailoutProvider } from 'next/dist/client/components/static-generation-searchparams-bailout-provider'
export { createSearchParamsBailoutProxy } from 'next/dist/client/components/searchparams-bailout-proxy'
export * as serverHooks from 'next/dist/client/components/hooks-server-context'
export { renderToReadableStream, decodeReply, decodeAction } from 'react-server-dom-webpack/server.edge'
export const __next_app_webpack_require__ = __webpack_require__
export { preloadStyle, preloadFont, preconnect } from 'next/dist/server/app-render/rsc/preloads'
export const originalPathname = "${page}"
export * from 'next/dist/server/app-render/entry-base'
`
return result

View file

@ -98,7 +98,7 @@ export function getRender({
getServerSideProps: pageMod.getServerSideProps,
getStaticPaths: pageMod.getStaticPaths,
ComponentMod: pageMod,
isAppPath: !!pageMod.__next_app_webpack_require__,
isAppPath: !!pageMod.__next_app__,
pathname,
}
}

View file

@ -400,7 +400,7 @@ export async function handleAction({
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
][actionId].workers[workerName]
const actionHandler =
ComponentMod.__next_app_webpack_require__(actionModId)[actionId]
ComponentMod.__next_app__.require(actionModId)[actionId]
const returnVal = await actionHandler.apply(null, bound)

View file

@ -13,7 +13,10 @@ export function createServerComponentRenderer<Props>(
ComponentToRender: (props: Props) => any,
ComponentMod: {
renderToReadableStream: any
__next_app_webpack_require__?: any
__next_app__?: {
require: any
loadChunk: any
}
},
{
transformStream,
@ -31,14 +34,14 @@ export function createServerComponentRenderer<Props>(
serverComponentsErrorHandler: ReturnType<typeof createErrorHandler>,
nonce?: string
): (props: Props) => JSX.Element {
// We need to expose the `__webpack_require__` API globally for
// We need to expose the bundled `require` API globally for
// react-server-dom-webpack. This is a hack until we find a better way.
if (ComponentMod.__next_app_webpack_require__) {
if (ComponentMod.__next_app__) {
// @ts-ignore
globalThis.__next_require__ = ComponentMod.__next_app_webpack_require__
globalThis.__next_require__ = ComponentMod.__next_app__.require
// @ts-ignore
globalThis.__next_chunk_load__ = () => Promise.resolve()
globalThis.__next_chunk_load__ = ComponentMod.__next_app__.loadChunk
}
let RSCStream: ReadableStream<Uint8Array>

View file

@ -0,0 +1,52 @@
const { default: AppRouter } =
require('next/dist/client/components/app-router') as typeof import('../../client/components/app-router')
const { default: LayoutRouter } =
require('next/dist/client/components/layout-router') as typeof import('../../client/components/layout-router')
const { default: RenderFromTemplateContext } =
require('next/dist/client/components/render-from-template-context') as typeof import('../../client/components/render-from-template-context')
const { staticGenerationAsyncStorage } =
require('next/dist/client/components/static-generation-async-storage') as typeof import('../../client/components/static-generation-async-storage')
const { requestAsyncStorage } =
require('next/dist/client/components/request-async-storage') as typeof import('../../client/components/request-async-storage')
const { actionAsyncStorage } =
require('next/dist/client/components/action-async-storage') as typeof import('../../client/components/action-async-storage')
const { staticGenerationBailout } =
require('next/dist/client/components/static-generation-bailout') as typeof import('../../client/components/static-generation-bailout')
const { default: StaticGenerationSearchParamsBailoutProvider } =
require('next/dist/client/components/static-generation-searchparams-bailout-provider') as typeof import('../../client/components/static-generation-searchparams-bailout-provider')
const { createSearchParamsBailoutProxy } =
require('next/dist/client/components/searchparams-bailout-proxy') as typeof import('../../client/components/searchparams-bailout-proxy')
const serverHooks =
require('next/dist/client/components/hooks-server-context') as typeof import('../../client/components/hooks-server-context')
const {
renderToReadableStream,
decodeReply,
decodeAction,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('react-server-dom-webpack/server.edge')
const { preloadStyle, preloadFont, preconnect } =
require('next/dist/server/app-render/rsc/preloads') as typeof import('../../server/app-render/rsc/preloads')
export {
AppRouter,
LayoutRouter,
RenderFromTemplateContext,
staticGenerationAsyncStorage,
requestAsyncStorage,
actionAsyncStorage,
staticGenerationBailout,
createSearchParamsBailoutProxy,
serverHooks,
renderToReadableStream,
decodeReply,
decodeAction,
preloadStyle,
preloadFont,
preconnect,
StaticGenerationSearchParamsBailoutProvider,
}

View file

@ -1,6 +1,3 @@
import type { ServerResponse } from 'http'
import { Writable } from 'stream'
type ContentTypeOption = string | undefined
export type RenderResultMetadata = {
@ -14,6 +11,13 @@ export type RenderResultMetadata = {
type RenderResultResponse = string | ReadableStream<Uint8Array> | null
export interface PipeTarget {
write: (chunk: Uint8Array) => unknown
end: () => unknown
flush?: () => unknown
destroy: (err?: Error) => unknown
}
export default class RenderResult {
/**
* The detected content type for the response. This is used to set the
@ -91,7 +95,7 @@ export default class RenderResult {
return this.response
}
public async pipe(res: ServerResponse | Writable): Promise<void> {
public async pipe(res: PipeTarget): Promise<void> {
if (this.response === null) {
throw new Error('Invariant: response is null. This is a bug in Next.js')
}

View file

@ -984,8 +984,8 @@ importers:
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
'@vercel/ncc': ^0.36.0
'@vercel/turbopack-ecmascript-runtime': https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1
'@vercel/turbopack-node': https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230615.1
'@vercel/turbopack-ecmascript-runtime': https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2
'@vercel/turbopack-node': https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230622.2
anser: ^2.1.1
css.escape: ^1.5.1
find-up: ^6.3.0
@ -997,8 +997,8 @@ importers:
stacktrace-parser: ^0.1.10
strip-ansi: ^7.0.1
dependencies:
'@vercel/turbopack-ecmascript-runtime': '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1_react-refresh@0.12.0'
'@vercel/turbopack-node': '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230615.1'
'@vercel/turbopack-ecmascript-runtime': '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2_react-refresh@0.12.0'
'@vercel/turbopack-node': '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230622.2'
anser: 2.1.1
css.escape: 1.5.1
next: link:../../../../next
@ -6116,7 +6116,7 @@ packages:
dependencies:
'@mdx-js/mdx': 2.2.1
source-map: 0.7.3
webpack: 5.86.0
webpack: 5.86.0_@swc+core@1.3.55
transitivePeerDependencies:
- supports-color
@ -6790,7 +6790,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@swc/core-darwin-x64/1.3.55:
@ -6799,7 +6798,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm-gnueabihf/1.3.55:
@ -6808,7 +6806,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm64-gnu/1.3.55:
@ -6817,7 +6814,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-arm64-musl/1.3.55:
@ -6826,7 +6822,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-x64-gnu/1.3.55:
@ -6835,7 +6830,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-linux-x64-musl/1.3.55:
@ -6844,7 +6838,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-arm64-msvc/1.3.55:
@ -6853,7 +6846,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-ia32-msvc/1.3.55:
@ -6862,7 +6854,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core-win32-x64-msvc/1.3.55:
@ -6871,7 +6862,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@swc/core/1.3.55_@swc+helpers@0.5.1:
@ -6896,7 +6886,6 @@ packages:
'@swc/core-win32-arm64-msvc': 1.3.55
'@swc/core-win32-ia32-msvc': 1.3.55
'@swc/core-win32-x64-msvc': 1.3.55
dev: true
/@swc/helpers/0.4.14:
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
@ -23799,7 +23788,6 @@ packages:
serialize-javascript: 6.0.1
terser: 5.17.7
webpack: 5.86.0_@swc+core@1.3.55
dev: true
/terser/5.10.0:
resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==}
@ -25157,7 +25145,6 @@ packages:
- '@swc/core'
- esbuild
- uglify-js
dev: true
/websocket-driver/0.7.3:
resolution: {integrity: sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==}
@ -25566,9 +25553,9 @@ packages:
/zwitch/2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
'@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1_react-refresh@0.12.0':
resolution: {tarball: https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1}
id: '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230615.1'
'@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2_react-refresh@0.12.0':
resolution: {tarball: https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2}
id: '@gitpkg.vercel.app/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230622.2'
name: '@vercel/turbopack-ecmascript-runtime'
version: 0.0.0
dependencies:
@ -25579,8 +25566,8 @@ packages:
- webpack
dev: false
'@gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230615.1':
resolution: {tarball: https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230615.1}
'@gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230622.2':
resolution: {tarball: https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230622.2}
name: '@vercel/turbopack-node'
version: 0.0.0
dependencies: