Skip to content

Commit

Permalink
partially support next metadata (vercel#3464)
Browse files Browse the repository at this point in the history
This adds support for metadata in pages and layouts.

Full metadata support also needs support for implicit metadata with
files named `icon.ico` etc.

This PR also improves the test suite and adds a basic test case for app
dir support
  • Loading branch information
sokra authored Jan 25, 2023
1 parent 2226336 commit 8e2ab73
Show file tree
Hide file tree
Showing 28 changed files with 359 additions and 149 deletions.
2 changes: 1 addition & 1 deletion crates/next-core/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@vercel/turbopack-runtime": "latest",
"anser": "^2.1.1",
"css.escape": "^1.5.1",
"next": "^13.0.8-canary.2",
"next": "13.1.6-canary.1",
"platform": "1.3.6",
"react-dom": "^18.2.0",
"react": "^18.2.0",
Expand Down
21 changes: 21 additions & 0 deletions crates/next-core/js/src/entry/app-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ async function runOperation(renderData: RenderData) {
const pageItem = LAYOUT_INFO[LAYOUT_INFO.length - 1];
const pageModule = pageItem.page!.module;
const Page = pageModule.default;
const metadata = [];
for (let i = 0; i < LAYOUT_INFO.length; i++) {
const info = LAYOUT_INFO[i];
if (info.layout) {
metadata.push({
type: "layout",
layer: i,
mod: () => info.layout!.module,
path: `layout${i}.js`,
});
}
if (info.page) {
metadata.push({
type: "page",
layer: i - 1,
mod: () => info.page!.module,
path: "page.js",
});
}
}
let tree: LoaderTree = ["", {}, { page: [() => Page, "page.js"] }];
layoutInfoChunks["page"] = pageItem.page!.chunks;
for (let i = LAYOUT_INFO.length - 2; i >= 0; i--) {
Expand Down Expand Up @@ -198,6 +218,7 @@ async function runOperation(renderData: RenderData) {
default: undefined,
tree,
pages: ["page.js"],
metadata,
},
serverComponentManifest: manifest,
serverCSSManifest,
Expand Down
1 change: 1 addition & 0 deletions crates/next-core/js/src/entry/app/layout-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { serverHooks };
export { renderToReadableStream } from "next/dist/compiled/react-server-dom-webpack/server.browser";

export { default } from ".";
export * from ".";
169 changes: 140 additions & 29 deletions crates/next-dev-tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extern crate test_generator;

use std::{
env,
fmt::Write,
net::SocketAddr,
path::{Path, PathBuf},
time::Duration,
Expand All @@ -11,6 +12,13 @@ use std::{
use anyhow::{anyhow, Context, Result};
use chromiumoxide::{
browser::{Browser, BrowserConfig},
cdp::{
browser_protocol::network::EventResponseReceived,
js_protocol::runtime::{
AddBindingParams, EventBindingCalled, EventConsoleApiCalled, EventExceptionThrown,
PropertyPreview, RemoteObject,
},
},
error::CdpError::Ws,
};
use futures::StreamExt;
Expand Down Expand Up @@ -207,49 +215,152 @@ async fn create_browser(is_debugging: bool) -> Result<(Browser, JoinHandle<()>)>
}

async fn run_browser(addr: SocketAddr) -> Result<JestRunResult> {
if *DEBUG_BROWSER {
run_debug_browser(addr).await?;
}

run_test_browser(addr).await
run_test_browser(addr, *DEBUG_BROWSER).await
}

async fn run_debug_browser(addr: SocketAddr) -> Result<()> {
let (browser, handle) = create_browser(true).await?;
let page = browser.new_page(format!("http://{}", addr)).await?;

let run_tests_msg =
"Entering debug mode. Run `await __jest__.run()` in the browser console to run tests.";
println!("\n\n{}", run_tests_msg);
page.evaluate(format!(
r#"console.info("%cTurbopack tests:", "font-weight: bold;", "{}");"#,
run_tests_msg
))
.await?;

// Wait for the user to close the browser
handle.await?;

Ok(())
}

async fn run_test_browser(addr: SocketAddr) -> Result<JestRunResult> {
let (browser, _) = create_browser(false).await?;
async fn run_test_browser(addr: SocketAddr, is_debugging: bool) -> Result<JestRunResult> {
let (browser, mut handle) = create_browser(is_debugging).await?;

// `browser.new_page()` opens a tab, navigates to the destination, and waits for
// the page to load. chromiumoxide/Chrome DevTools Protocol has been flakey,
// returning `ChannelSendError`s (WEB-259). Retry if necessary.
let page = retry_async(
(),
|_| browser.new_page(format!("http://{}", addr)),
|_| browser.new_page("about:blank"),
5,
Duration::from_millis(100),
)
.await
.context("Failed to create new browser page")?;

let value = page.evaluate("globalThis.waitForTests?.() ?? __jest__.run()");
Ok(value.await?.into_value()?)
page.execute(AddBindingParams::new("READY")).await?;

let mut errors = page
.event_listener::<EventExceptionThrown>()
.await
.context("Unable to listen to exception events")?;
let mut binding_events = page
.event_listener::<EventBindingCalled>()
.await
.context("Unable to listen to binding events")?;
let mut console_events = page
.event_listener::<EventConsoleApiCalled>()
.await
.context("Unable to listen to console events")?;
let mut network_response_events = page
.event_listener::<EventResponseReceived>()
.await
.context("Unable to listen to response received events")?;

page.evaluate_expression(format!("window.location='http://{addr}'"))
.await
.context("Unable to evaluate javascript to naviagate to target page")?;

// Wait for the next network response event
// This is the HTML page that we're testing
network_response_events.next().await.context(
"Network events channel ended unexpectedly while waiting on the network response",
)?;

if is_debugging {
let _ = page.evaluate(
r#"console.info("%cTurbopack tests:", "font-weight: bold;", "Waiting for READY to be signaled by page...");"#,
)
.await;
}

let mut errors_next = errors.next();
let mut bindings_next = binding_events.next();
let mut console_next = console_events.next();
let mut network_next = network_response_events.next();

loop {
tokio::select! {
event = &mut console_next => {
if let Some(event) = event {
println!(
"console {:?}: {}",
event.r#type,
event
.args
.iter()
.filter_map(|a| a.value.as_ref().map(|v| format!("{:?}", v)))
.collect::<Vec<_>>()
.join(", ")
);
} else {
return Err(anyhow!("Console events channel ended unexpectedly"));
}
console_next = console_events.next();
}
event = &mut errors_next => {
if let Some(event) = event {
let mut message = String::new();
let d = &event.exception_details;
writeln!(message, "{}", d.text)?;
if let Some(RemoteObject { preview: Some(ref exception), .. }) = d.exception {
if let Some(PropertyPreview{ value: Some(ref exception_message), .. }) = exception.properties.iter().find(|p| p.name == "message") {
writeln!(message, "{}", exception_message)?;
}
}
if let Some(stack_trace) = &d.stack_trace {
for frame in &stack_trace.call_frames {
writeln!(message, " at {} ({}:{}:{})", frame.function_name, frame.url, frame.line_number, frame.column_number)?;
}
}
let message = message.trim_end();
if !is_debugging {
return Err(anyhow!(
"Exception throw in page: {}",
message
))
} else {
println!("Exception throw in page (this would fail the test case without TURBOPACK_DEBUG_BROWSER):\n{}", message);
}
} else {
return Err(anyhow!("Error events channel ended unexpectedly"));
}
errors_next = errors.next();
}
event = &mut bindings_next => {
if event.is_some() {
if is_debugging {
let run_tests_msg =
"Entering debug mode. Run `await __jest__.run()` in the browser console to run tests.";
println!("\n\n{}", run_tests_msg);
page.evaluate(format!(
r#"console.info("%cTurbopack tests:", "font-weight: bold;", "{}");"#,
run_tests_msg
))
.await?;
} else {
let value = page.evaluate("__jest__.run()").await?.into_value()?;
return Ok(value);
}
} else {
return Err(anyhow!("Binding events channel ended unexpectedly"));
}
bindings_next = binding_events.next();
}
event = &mut network_next => {
if let Some(event) = event {
println!("network {} [{}]", event.response.url, event.response.status);
} else {
return Err(anyhow!("Network events channel ended unexpectedly"));
}
network_next = network_response_events.next();
}
result = &mut handle => {
result?;
return Err(anyhow!("Browser closed"));
}
() = tokio::time::sleep(Duration::from_secs(60)) => {
if !is_debugging {
return Err(anyhow!("Test timeout while waiting for READY"));
}
}
};
}
}

fn get_free_local_addr() -> Result<SocketAddr, std::io::Error> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function RootLayout({ children }: { children: any }) {
return (
<html>
<body>{children}</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Test from "./test";

export default function Page() {
return (
<div>
<Test />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useEffect } from "react";

export default function Test() {
useEffect(() => {
import("@turbo/pack-test-harness").then(() => {
it("should run", () => {});
});
return () => {};
}, []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function RootLayout({ children }: { children: any }) {
return (
<html>
<body>{children}</body>
</html>
);
}

export const metadata = {
icons: {
icon: new URL("./triangle-black.png", import.meta.url).pathname,
},
title: {
absolute: "RootLayout absolute",
template: "%s - RootLayout",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type React from "react";
import Test from "./test";

export default function Page(): React.ReactElement {
return (
<div>
<Test />
</div>
);
}

export async function generateMetadata({ params }) {
return {
title: "Page",
openGraph: {
images: new URL("./triangle-black.png", import.meta.url).pathname,
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import type React from "react";
import { useEffect } from "react";

export default function Test(): React.ReactElement | null {
useEffect(() => {
import("@turbo/pack-test-harness").then(() => {
it("should have the correct title set", () => {
expect(document.title).toBe("Page - RootLayout");
let iconMeta = document.querySelector("link[rel=icon]");
expect(iconMeta).toHaveProperty("href");
expect(iconMeta.href).toMatch(/\/_next\/static\/assets/);
let ogImageMeta = document.querySelector("meta[property='og:image']");
expect(ogImageMeta).toHaveProperty("content");
expect(ogImageMeta.content).toMatch(/\/_next\/static\/assets/);
});
});
return () => {};
}, []);
return null;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Loading

0 comments on commit 8e2ab73

Please sign in to comment.