Skip to content

Commit

Permalink
add different keyed app route elements (#2636)
Browse files Browse the repository at this point in the history
* add different keyed app route elements

* update for latest next.js canary version

* fix header name

* setup next.js version in benchmark
  • Loading branch information
sokra committed Nov 9, 2022
1 parent 9fd2d8d commit 92a9e8c
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 193 deletions.
2 changes: 1 addition & 1 deletion crates/next-core/js/package.json
Expand Up @@ -12,7 +12,7 @@
"@vercel/turbopack-runtime": "latest",
"anser": "2.1.1",
"css.escape": "1.5.1",
"next": "12.3.2-canary.39",
"next": "13.0.3-canary.2",
"platform": "1.3.6",
"react-dom": "^18.2.0",
"react": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion crates/next-core/js/src/dev/hot-reloader.tsx
Expand Up @@ -14,7 +14,7 @@ export default function HotReload({ assetPrefix, children }): any {
{
path,
headers: {
__rsc__: "1",
rsc: "1",
},
},
(update) => {
Expand Down
74 changes: 35 additions & 39 deletions crates/next-core/js/src/entry/app-renderer.tsx
@@ -1,9 +1,21 @@
import type { Ipc } from "@vercel/turbopack-next/internal/ipc";

// Provided by the rust generate code
type FileType =
| "layout"
| "template"
| "error"
| "loading"
| "not-found"
| "head";
declare global {
// an array of all layouts and the page
const LAYOUT_INFO: { segment: string; module: any; chunks: string[] }[];
const LAYOUT_INFO: ({
segment: string;
page?: { module: any; chunks: string[] };
} & {
[componentKey in FileType]?: { module: any; chunks: string[] };
})[];
// array of chunks for the bootstrap script
const BOOTSTRAP: string[];
const IPC: Ipc<unknown, unknown>;
Expand All @@ -21,7 +33,6 @@ import "next/dist/server/node-polyfill-web-streams";
import { RenderOpts, renderToHTMLOrFlight } from "next/dist/server/app-render";
import { PassThrough } from "stream";
import { ServerResponseShim } from "@vercel/turbopack-next/internal/http";
import { structuredError } from "@vercel/turbopack-next/internal/error";
import { ParsedUrlQuery } from "node:querystring";

globalThis.__next_require__ = (data) => {
Expand Down Expand Up @@ -77,16 +88,11 @@ type IpcOutgoingMessage = {

// TODO expose these types in next.js
type ComponentModule = () => any;
type ModuleReference = [componentModule: ComponentModule, filePath: string];
export type ComponentsType = {
readonly [componentKey in
| "layout"
| "template"
| "error"
| "loading"
| "not-found"]?: ComponentModule;
[componentKey in FileType]?: ModuleReference;
} & {
readonly layoutOrPagePath?: string;
readonly page?: ComponentModule;
page?: ModuleReference;
};
type LoaderTree = [
segment: string,
Expand All @@ -102,31 +108,24 @@ type ServerComponentsManifestModule = {
};

async function runOperation(renderData: RenderData) {
const layoutInfoChunks: Record<string, string[]> = {};
const pageItem = LAYOUT_INFO[LAYOUT_INFO.length - 1];
const pageModule = pageItem.module;
const pageModule = pageItem.page!.module;
const Page = pageModule.default;
let tree: LoaderTree = [
"",
{},
{ page: () => Page, layoutOrPagePath: "page.js" },
];
let tree: LoaderTree = ["", {}, { page: [() => Page, "page.js"] }];
layoutInfoChunks["page.js"] = pageItem.page!.chunks;
for (let i = LAYOUT_INFO.length - 2; i >= 0; i--) {
const info = LAYOUT_INFO[i];
const mod = info.module;
if (mod) {
const Layout = mod.default;
tree = [
info.segment,
{ children: tree },
{ layout: () => Layout, layoutOrPagePath: `layout${i}.js` },
];
} else {
tree = [
info.segment,
{ children: tree },
{ layoutOrPagePath: `layout${i}.js` },
];
const components: ComponentsType = {};
for (const key of Object.keys(info)) {
if (key === "segment") {
continue;
}
const k = key as FileType;
components[k] = [() => info[k]!.module.default, `${k}${i}.js`];
layoutInfoChunks[`${k}${i}.js`] = info[k]!.chunks;
}
tree = [info.segment, { children: tree }, components];
}

const proxyMethodsForModule = (
Expand Down Expand Up @@ -157,19 +156,15 @@ async function runOperation(renderData: RenderData) {
const manifest: FlightManifest = new Proxy({} as any, proxyMethods(false));
const serverCSSManifest: FlightCSSManifest = {};
serverCSSManifest.__entry_css__ = {};
for (let i = 0; i < LAYOUT_INFO.length - 1; i++) {
const { chunks } = LAYOUT_INFO[i];
const cssChunks = (chunks || []).filter((path) => path.endsWith(".css"));
serverCSSManifest[`layout${i}.js`] = cssChunks.map((chunk) =>
for (const [key, chunks] of Object.entries(layoutInfoChunks)) {
const cssChunks = chunks.filter((path) => path.endsWith(".css"));
serverCSSManifest[key] = cssChunks.map((chunk) =>
JSON.stringify([chunk, [chunk]])
);
}
serverCSSManifest.__entry_css__ = {
page: pageItem.chunks
.filter((path) => path.endsWith(".css"))
.map((chunk) => JSON.stringify([chunk, [chunk]])),
page: serverCSSManifest["page.js"],
};
serverCSSManifest["page.js"] = serverCSSManifest.__entry_css__.page;
const req: IncomingMessage = {
url: renderData.url,
method: renderData.method,
Expand All @@ -185,7 +180,8 @@ async function runOperation(renderData: RenderData) {
dev: true,
buildManifest: {
polyfillFiles: [],
rootMainFiles: LAYOUT_INFO.flatMap(({ chunks }) => chunks || [])
rootMainFiles: Object.values(layoutInfoChunks)
.flat()
.concat(BOOTSTRAP)
.filter((path) => !path.endsWith(".css")),
devFiles: [],
Expand Down
4 changes: 3 additions & 1 deletion crates/next-core/src/app_render/mod.rs
@@ -1,10 +1,12 @@
use std::collections::HashMap;

use turbo_tasks_fs::FileSystemPathVc;

pub mod next_layout_entry_transition;

#[turbo_tasks::value(shared)]
pub struct LayoutSegment {
pub file: Option<FileSystemPathVc>,
pub files: HashMap<String, FileSystemPathVc>,
pub target: FileSystemPathVc,
}

Expand Down
87 changes: 39 additions & 48 deletions crates/next-core/src/app_source.rs
@@ -1,4 +1,7 @@
use std::{collections::HashMap, io::Write};
use std::{
collections::{BTreeMap, HashMap},
io::Write,
};

use anyhow::{anyhow, Context, Result};
use turbo_tasks::{
Expand Down Expand Up @@ -289,20 +292,18 @@ async fn create_app_source_for_directory(
let mut layouts = layouts;
let mut sources = Vec::new();
let mut page = None;
let mut layout = None;
let mut files = HashMap::new();
if let DirectoryContent::Entries(entries) = &*input_dir.read_dir().await? {
for (name, entry) in entries.iter() {
if let DirectoryEntry::File(file) = entry {
if let &DirectoryEntry::File(file) = entry {
if let Some((name, _)) = name.rsplit_once('.') {
match name {
"page" => {
page = Some(file);
}
"layout" => {
layout = Some(file);
"layout" | "error" | "loading" | "template" | "not-found" | "head" => {
files.insert(name.to_string(), file);
}
"error" => { /* TODO */ }
"loading" => { /* TODO */ }
_ => {
// Any other file is ignored
}
Expand All @@ -311,8 +312,7 @@ async fn create_app_source_for_directory(
}
}

let layout_js = input_dir.join("layout.js");
let layout_tsx = input_dir.join("layout.tsx");
let layout = files.get("layout");

// TODO: Use let Some(page_file) = page in expression below when
// https://rust-lang.github.io/rfcs/2497-if-let-chains.html lands
Expand All @@ -325,23 +325,23 @@ async fn create_app_source_for_directory(
// stable does.
let is_tsx = *page_file.extension().await? == "tsx";

if is_tsx {
layout.replace(&layout_tsx);
let layout = if is_tsx {
input_dir.join("layout.tsx")
} else {
layout.replace(&layout_js);
}
input_dir.join("layout.js")
};
files.insert("layout".to_string(), layout);
let content = if is_tsx {
include_str!("assets/layout.tsx")
} else {
include_str!("assets/layout.js")
};

let layout = layout.context("required")?;
layout.write(FileContentVc::from(File::from(content)));

AppSourceIssue {
severity: IssueSeverity::Warning.into(),
path: *page_file,
path: page_file,
message: StringVc::cell(format!(
"Your page {} did not have a root layout, we created {} for you.",
page_file.await?.path,
Expand All @@ -354,15 +354,9 @@ async fn create_app_source_for_directory(
}

let mut list = layouts.await?.clone_value();
list.push(
LayoutSegment {
file: layout.copied(),
target,
}
.cell(),
);
list.push(LayoutSegment { files, target }.cell());
layouts = LayoutSegmentsVc::cell(list);
if let Some(page_path) = page.copied() {
if let Some(page_path) = page {
sources.push(create_node_rendered_source(
specificity,
server_root,
Expand Down Expand Up @@ -440,7 +434,7 @@ impl NodeEntry for AppRenderer {
#[turbo_tasks::function]
async fn entry(&self, data: Value<ContentSourceData>) -> Result<NodeRenderingEntryVc> {
let is_rsc = if let Some(headers) = data.into_value().headers {
headers.contains_key("__rsc__")
headers.contains_key("rsc")
} else {
false
};
Expand All @@ -453,14 +447,14 @@ impl NodeEntry for AppRenderer {
.copied()
.chain(std::iter::once(
LayoutSegment {
file: Some(page),
files: HashMap::from([("page".to_string(), page)]),
target: self.target,
}
.cell(),
))
.try_join()
.await?;
let segments: Vec<(String, Option<(String, String, String)>)> = layout_and_page
let segments: Vec<(String, BTreeMap<String, (String, String, String)>)> = layout_and_page
.into_iter()
.fold(
(self.server_root, Vec::new()),
Expand All @@ -470,7 +464,8 @@ impl NodeEntry for AppRenderer {
let target = &*segment.target.await?;
let segment_path =
last_path.await?.get_path_to(target).unwrap_or_default();
if let Some(file) = segment.file {
let mut imports = BTreeMap::new();
for (key, file) in segment.files.iter() {
let file_str = file.to_string().await?;
let identifier = magic_identifier::encode(&format!(
"imported namespace {}",
Expand All @@ -481,25 +476,21 @@ impl NodeEntry for AppRenderer {
file_str
));
if let Some(p) = path_value.get_relative_path_to(&*file.await?) {
Ok((
stringify_str(segment_path),
Some((p, identifier, chunks_identifier)),
))
imports.insert(
key.to_string(),
(p, identifier, chunks_identifier),
);
} else {
Err(anyhow!(
return Err(anyhow!(
"Unable to generate import as there
is no relative path to the layout module {} from context
path {}",
file_str,
path.to_string().await?
))
));
}
} else {
Ok::<(String, Option<(String, String, String)>), _>((
stringify_str(segment_path),
None,
))
}
Ok((stringify_str(segment_path), imports))
});
futures
})
Expand All @@ -513,8 +504,8 @@ impl NodeEntry for AppRenderer {
"import IPC, { Ipc } from \"@vercel/turbopack-next/internal/ipc\";\n",
);

for (_, import) in segments.iter() {
if let Some((p, identifier, chunks_identifier)) = import {
for (_, imports) in segments.iter() {
for (p, identifier, chunks_identifier) in imports.values() {
result += r#"("TURBOPACK { transition: next-layout-entry; chunking-type: parallel }");
"#;
writeln!(
Expand All @@ -537,16 +528,16 @@ import BOOTSTRAP from {};
}

result += "const LAYOUT_INFO = [";
for (segment_str_lit, import) in segments.iter() {
if let Some((_, identifier, chunks_identifier)) = import {
for (segment_str_lit, imports) in segments.iter() {
writeln!(result, " {{\n segment: {segment_str_lit},")?;
for (key, (_, identifier, chunks_identifier)) in imports {
writeln!(
result,
" {{ segment: {segment_str_lit}, module: {identifier}, chunks: \
{chunks_identifier} }},",
)?
} else {
writeln!(result, " {{ segment: {segment_str_lit} }},",)?
" {key}: {{ module: {identifier}, chunks: {chunks_identifier} }},",
key = stringify_str(key),
)?;
}
result += " },";
}
result += "];\n\n";

Expand All @@ -558,7 +549,7 @@ import BOOTSTRAP from {};
let file = File::from(result.build());
let asset = VirtualAssetVc::new(path.join("entry"), file.into());
let (context, intermediate_output_path) = if is_rsc {
(self.context, self.intermediate_output_path.join("__rsc__"))
(self.context, self.intermediate_output_path.join("rsc"))
} else {
(self.context_ssr, self.intermediate_output_path)
};
Expand Down
2 changes: 1 addition & 1 deletion crates/next-dev/benches/bundlers/turbopack.rs
Expand Up @@ -58,7 +58,7 @@ impl Bundler for Turbopack {
npm::install(
install_dir,
&[
NpmPackage::new("next", "13.0.0"),
NpmPackage::new("next", "13.0.3-canary.2"),
// Dependency on this is inserted by swc's preset_env
NpmPackage::new("@swc/helpers", "^0.4.11"),
],
Expand Down

1 comment on commit 92a9e8c

@vercel
Copy link

@vercel vercel bot commented on 92a9e8c Nov 9, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.