Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add edge rendering for app dir for Turbopack #50830

Merged
merged 64 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
03886a8
deduplicate part of the next-app-loader
sokra May 25, 2023
944c3aa
parse segment config from loader tree
sokra May 25, 2023
4e0176d
extra manifest and next_require code into separate file
sokra May 25, 2023
c094ea3
fixup
sokra May 25, 2023
87b629d
fixup
sokra May 25, 2023
cc1cae2
prettier
sokra May 26, 2023
1a0d55d
pull out common code into separate files
sokra May 26, 2023
57309fd
WIP
sokra May 29, 2023
ae92832
WIP
sokra May 30, 2023
e3bbb6c
WIP
sokra May 30, 2023
7d0ccaf
WIP
sokra Jun 3, 2023
d569b0b
WIP: working
sokra Jun 6, 2023
3218b5a
fix types
sokra Jun 6, 2023
3647a48
allow to enable source maps for node.js debugging
sokra Jun 7, 2023
299ab1f
types fixes
sokra Jun 7, 2023
f425b82
always start on port 3000 with TURBOPACK_DEBUG_START
sokra Jun 7, 2023
e064377
fix open call
sokra Jun 7, 2023
2855a6c
add TURBOPACK_DEBUG_OPEN
sokra Jun 7, 2023
6a9a913
fix layer
sokra Jun 7, 2023
2931e79
remove console.log
sokra Jun 7, 2023
90e081e
update test case
sokra Jun 7, 2023
addf581
improve test result reporting, remove cleanup
sokra Jun 7, 2023
cb765b8
fmt
sokra Jun 7, 2023
8cb01cd
prettier
sokra Jun 7, 2023
2d18e8c
remove debug script
sokra Jun 7, 2023
82dca72
use ESM reexports
sokra Jun 7, 2023
afffe05
fix types
sokra Jun 7, 2023
f736099
clippy
sokra Jun 9, 2023
e710ce1
warnings
sokra Jun 9, 2023
d72b179
fix enhanceGlobals
sokra Jun 9, 2023
27143a1
use cjs version
sokra Jun 9, 2023
40419ba
remove disk paths
sokra Jun 12, 2023
09b930e
entry-base need to be ESM
sokra Jun 12, 2023
acf76b1
improve error message
sokra Jun 12, 2023
cdbcc27
avoid double declare globals
sokra Jun 12, 2023
7bf5531
magic hacks by wyattjoh
sokra Jun 14, 2023
0f10b0d
add buildId
sokra Jun 14, 2023
804e6ab
updates for rebase
sokra Jun 15, 2023
64095fd
no need to use esm version
sokra Jun 15, 2023
f19cf28
fix incorrect import
sokra Jun 15, 2023
9d8310a
prettier
sokra Jun 15, 2023
ae8ce04
Merge branch 'canary' into sokra/app-edge
sokra Jun 15, 2023
331428f
Merge branch 'canary' into sokra/app-edge
sokra Jun 16, 2023
f442982
move update response logic into separate function
sokra Jun 16, 2023
9f245e3
review
sokra Jun 16, 2023
687aa6f
referred_region -> preferred_region
sokra Jun 16, 2023
54aaeb5
use destructuring
sokra Jun 16, 2023
bc1a5b8
rename sibling -> parallel
sokra Jun 16, 2023
8ffb4bf
remove NODE_JS_SOURCE_MAPS const in favor of env var
sokra Jun 16, 2023
68adc09
add comment
sokra Jun 16, 2023
bc621b6
dedupe alias
sokra Jun 16, 2023
291d6f6
cleanup after test case
sokra Jun 16, 2023
b3f8a41
Revert "dedupe alias"
sokra Jun 16, 2023
114199c
expose `__next_app__` with require and loadChunk combined
sokra Jun 16, 2023
fd9814c
add comments
sokra Jun 16, 2023
c6fe47f
improve url parsing
sokra Jun 16, 2023
bd2800b
link issue
sokra Jun 16, 2023
3334d1c
Merge remote-tracking branch 'origin/canary' into sokra/app-edge
sokra Jun 16, 2023
e1ee57b
fix typo
sokra Jun 19, 2023
249265c
Merge branch 'canary' into sokra/app-edge
sokra Jun 19, 2023
8dedf4d
use destructuring
sokra Jun 19, 2023
a739e5b
link issue
sokra Jun 19, 2023
1af0d49
Merge branch 'canary' into sokra/app-edge
sokra Jun 19, 2023
d615de4
Merge branch 'canary' into sokra/app-edge
sokra Jun 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.map((chunk: string) =>
join(process.cwd(), '.next/server/app', chunk)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come these chunks aren't already under .next/server/app?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they relative to the immediate_output_directory, so this need to be added here.

),
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 }
}
263 changes: 31 additions & 232 deletions packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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()

let renderData: RenderData
switch (msg.type) {
case 'headers': {
renderData = msg.data
break
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}
startOperationStreamHandler(async (renderData: RenderData, respond) => {
const result = await runOperation(renderData)

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

// 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
]
const channel = respond({
status: result.statusCode,
headers: result.headers,
})

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,
}
},
}
for await (const chunk of result.body) {
channel.chunk(chunk as Buffer)
}

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),
}
}
},
}
}
channel.end()
})

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())
async function runOperation(renderData: RenderData) {
const { clientReferenceManifest } = createManifests()

const req: IncomingMessage = {
url: renderData.originalUrl,
Expand Down Expand Up @@ -266,12 +83,14 @@ async function runOperation(renderData: RenderData) {
ampFirstPages: [],
},
ComponentMod: {
...layoutEntry,
default: undefined,
tree: LOADER_TREE,
...entry,
__next_app_: {
require: __next_require__,
sokra marked this conversation as resolved.
Show resolved Hide resolved
loadChunk: __next_chunk_load__,
},
pages: ['page.js'],
},
clientReferenceManifest: manifest,
clientReferenceManifest,
runtime: 'nodejs',
serverComponents: true,
assetPrefix: '',
Expand Down Expand Up @@ -305,23 +124,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]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This file is generated by app_source.rs