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

Revert "add edge rendering for app dir for Turbopack" #51617

Merged
merged 1 commit into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

This file was deleted.

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

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 @@ -14,39 +25,211 @@ 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 { chunking-type: isolatedParallel }')
import entry from 'APP_ENTRY'
import BOOTSTRAP from 'APP_BOOTSTRAP'
;('TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }')
// @ts-ignore
import layoutEntry from './app/layout-entry'
import { createServerResponse } from '../internal/http'
import { createManifests, installRequireAndChunkLoad } from './app/manifest'

installRequireAndChunkLoad()
globalThis.__next_require__ = (data) => {
const [, , ssr_id] = JSON.parse(data)
return __turbopack_require__(ssr_id)
}
globalThis.__next_chunk_load__ = () => Promise.resolve()

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'

startOperationStreamHandler(async (renderData: RenderData, respond) => {
const result = await runOperation(renderData)
;(async () => {
while (true) {
const msg = await ipc.recv()

if (result == null) {
throw new Error('no html returned')
}
let renderData: RenderData
switch (msg.type) {
case 'headers': {
renderData = msg.data
break
}
default: {
console.error('unexpected message type', msg.type)
process.exit(1)
}
}

const channel = respond({
status: result.statusCode,
headers: result.headers,
})
const result = await runOperation(renderData)

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

channel.end()
ipc.send({ type: 'bodyEnd' })
}
})().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
]

async function runOperation(renderData: RenderData) {
const { clientReferenceManifest } = createManifests()
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 req: IncomingMessage = {
url: renderData.originalUrl,
Expand Down Expand Up @@ -83,14 +266,12 @@ async function runOperation(renderData: RenderData) {
ampFirstPages: [],
},
ComponentMod: {
...entry,
__next_app__: {
require: __next_require__,
loadChunk: __next_chunk_load__,
},
...layoutEntry,
default: undefined,
tree: LOADER_TREE,
pages: ['page.js'],
},
clientReferenceManifest,
clientReferenceManifest: manifest,
runtime: 'nodejs',
serverComponents: true,
assetPrefix: '',
Expand Down Expand Up @@ -124,3 +305,23 @@ 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]
)
}

This file was deleted.