New app routes resolving logic for turbopack (#47737)

### What?

Refactors the resolving logic for the `app` loader tree. This PR ensures
it's used to create entrypoints in turbopack. Next up is integrating it
into the webpack build too.

These changes also ensure that parallel routes resolving is applied,
which previously wasn't supported in turbopack.

### Why?

Part of the effort to deduplicate critical logic between
turbopack/webpack in Next.js, this will help land features in
turbopack/webpack at the same time.

### How?

Quite a few changes. @sokra helped a ton on this PR. `app_structure.rs`
was changed to the new resolving logic so most of the logic is there.

Additionally we added support for calling the same function in two ways
from Node.js: `turbo.entrypoints.get` and `turbo.entrypoints.getStream`.
`get` can be used by `next build` to get the full list of
entrypoints/loadertrees once. `getStream` has watching built-in and
calls a callback function with the new list anytime a file is added that
would change the loadertree.



<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation or adding/fixing Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md



## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
This commit is contained in:
Tim Neutkens 2023-04-03 13:07:28 +02:00 committed by GitHub
parent 217fbbfd39
commit 06d60ac140
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1342 additions and 690 deletions

View file

@ -68,5 +68,11 @@
"CARGO_TARGET_DIR": "target/rust-analyzer",
"RUST_BACKTRACE": "0"
},
"cSpell.words": ["opentelemetry", "zipkin"]
"cSpell.words": [
"Entrypoints",
"napi",
"opentelemetry",
"Threadsafe",
"zipkin"
]
}

View file

@ -3127,6 +3127,7 @@ version = "0.1.0"
dependencies = [
"allsorts",
"anyhow",
"async-recursion",
"futures",
"indexmap",
"indoc",
@ -3240,6 +3241,7 @@ dependencies = [
"napi-build",
"napi-derive",
"next-build",
"next-core",
"next-dev",
"next-swc",
"once_cell",
@ -3251,6 +3253,7 @@ dependencies = [
"tracing-futures",
"tracing-subscriber",
"turbo-binding",
"turbo-tasks",
]
[[package]]

View file

@ -1,5 +1,5 @@
[package]
edition = "2018"
edition = "2021"
name = "next-swc-napi"
version = "0.0.0"
publish = false
@ -48,6 +48,8 @@ napi-derive = "2"
next-swc = { version = "0.0.0", path = "../core" }
next-dev = { workspace = true }
next-build = { workspace = true }
next-core = { workspace = true }
turbo-tasks = { workspace = true }
once_cell = { workspace = true }
serde = "1"
serde_json = "1"
@ -59,6 +61,7 @@ turbo-binding = { workspace = true, features = [
"__swc_core_binding_napi",
"__feature_node_file_trace",
"__feature_mdx_rs",
"__turbo",
"__turbo_tasks",
"__turbo_tasks_memory",
"__turbopack"
@ -84,3 +87,6 @@ sentry = { version = "0.27.0", default-features = false, features = [
napi-build = "2"
serde = "1"
serde_json = "1"
turbo-binding = { workspace = true, features = [
"__turbo_tasks_build"
]}

View file

@ -5,6 +5,8 @@ use std::{
path::Path,
};
use turbo_binding::turbo::tasks_build::generate_register;
extern crate napi_build;
fn main() {
@ -39,4 +41,6 @@ fn main() {
.expect("Failed to write target triple text");
napi_build::setup();
generate_register();
}

View file

@ -0,0 +1,305 @@
use std::{collections::HashMap, path::MAIN_SEPARATOR, sync::Arc};
use anyhow::{anyhow, Result};
use napi::{
bindgen_prelude::External,
threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode},
JsFunction,
};
use next_core::app_structure::{
find_app_dir, get_entrypoints as get_entrypoints_impl, Components, ComponentsVc, Entrypoint,
EntrypointsVc, LoaderTree, LoaderTreeVc,
};
use serde::{Deserialize, Serialize};
use turbo_binding::{
turbo::{
tasks,
tasks::{
debug::ValueDebugFormat, primitives::StringsVc, trace::TraceRawVcs, NothingVc,
TryJoinIterExt, TurboTasks, ValueToString,
},
tasks_fs::{DiskFileSystemVc, FileSystem, FileSystemPathVc, FileSystemVc},
tasks_memory::MemoryBackend,
},
turbopack::core::PROJECT_FILESYSTEM_NAME,
};
use crate::register;
#[tasks::function]
async fn project_fs(project_dir: &str, watching: bool) -> Result<FileSystemVc> {
let disk_fs =
DiskFileSystemVc::new(PROJECT_FILESYSTEM_NAME.to_string(), project_dir.to_string());
if watching {
disk_fs.await?.start_watching_with_invalidation_reason()?;
}
Ok(disk_fs.into())
}
#[tasks::value]
#[serde(rename_all = "camelCase")]
struct LoaderTreeForJs {
segment: String,
parallel_routes: HashMap<String, LoaderTreeForJsReadRef>,
components: serde_json::Value,
}
#[derive(PartialEq, Eq, Serialize, Deserialize, ValueDebugFormat, TraceRawVcs)]
#[serde(rename_all = "camelCase")]
enum EntrypointForJs {
AppPage { loader_tree: LoaderTreeForJsReadRef },
AppRoute { path: String },
}
#[tasks::value(transparent)]
#[serde(rename_all = "camelCase")]
struct EntrypointsForJs(HashMap<String, EntrypointForJs>);
#[tasks::value(transparent)]
struct OptionEntrypointsForJs(Option<EntrypointsForJsVc>);
async fn fs_path_to_path(project_path: FileSystemPathVc, path: FileSystemPathVc) -> Result<String> {
match project_path.await?.get_path_to(&*path.await?) {
None => Err(anyhow!(
"Path {} is not inside of the project path {}",
path.to_string().await?,
project_path.to_string().await?
)),
Some(p) => Ok(p.to_string()),
}
}
async fn prepare_components_for_js(
project_path: FileSystemPathVc,
components: ComponentsVc,
) -> Result<serde_json::Value> {
let Components {
page,
layout,
error,
loading,
template,
default,
route,
metadata,
} = &*components.await?;
let mut map = serde_json::value::Map::new();
async fn add(
map: &mut serde_json::value::Map<String, serde_json::Value>,
project_path: FileSystemPathVc,
key: &str,
value: &Option<FileSystemPathVc>,
) -> Result<()> {
if let Some(value) = value {
map.insert(
key.to_string(),
fs_path_to_path(project_path, *value).await?.into(),
);
}
Ok::<_, anyhow::Error>(())
}
add(&mut map, project_path, "page", page).await?;
add(&mut map, project_path, "layout", layout).await?;
add(&mut map, project_path, "error", error).await?;
add(&mut map, project_path, "loading", loading).await?;
add(&mut map, project_path, "template", template).await?;
add(&mut map, project_path, "default", default).await?;
add(&mut map, project_path, "route", route).await?;
let mut meta = serde_json::value::Map::new();
async fn add_meta(
meta: &mut serde_json::value::Map<String, serde_json::Value>,
project_path: FileSystemPathVc,
key: &str,
value: &Vec<FileSystemPathVc>,
) -> Result<()> {
if !value.is_empty() {
meta.insert(
key.to_string(),
value
.iter()
.map(|value| async move {
Ok(serde_json::Value::from(
fs_path_to_path(project_path, *value).await?,
))
})
.try_join()
.await?
.into(),
);
}
Ok::<_, anyhow::Error>(())
}
add_meta(&mut meta, project_path, "icon", &metadata.icon).await?;
add_meta(&mut meta, project_path, "apple", &metadata.apple).await?;
add_meta(&mut meta, project_path, "twitter", &metadata.twitter).await?;
add_meta(&mut meta, project_path, "openGraph", &metadata.open_graph).await?;
add_meta(&mut meta, project_path, "favicon", &metadata.favicon).await?;
map.insert("metadata".to_string(), meta.into());
Ok(map.into())
}
#[tasks::function]
async fn prepare_loader_tree_for_js(
project_path: FileSystemPathVc,
loader_tree: LoaderTreeVc,
) -> Result<LoaderTreeForJsVc> {
let LoaderTree {
segment,
parallel_routes,
components,
} = &*loader_tree.await?;
let parallel_routes = parallel_routes
.iter()
.map(|(key, &value)| async move {
Ok((
key.clone(),
prepare_loader_tree_for_js(project_path, value).await?,
))
})
.try_join()
.await?
.into_iter()
.collect();
let components = prepare_components_for_js(project_path, *components).await?;
Ok(LoaderTreeForJs {
segment: segment.clone(),
parallel_routes,
components,
}
.cell())
}
#[tasks::function]
async fn prepare_entrypoints_for_js(
project_path: FileSystemPathVc,
entrypoints: EntrypointsVc,
) -> Result<EntrypointsForJsVc> {
let entrypoints = entrypoints
.await?
.iter()
.map(|(key, &value)| {
let key = key.to_string();
async move {
let value = match value {
Entrypoint::AppPage { loader_tree } => EntrypointForJs::AppPage {
loader_tree: prepare_loader_tree_for_js(project_path, loader_tree).await?,
},
Entrypoint::AppRoute { path } => EntrypointForJs::AppRoute {
path: fs_path_to_path(project_path, path).await?,
},
};
Ok((key, value))
}
})
.try_join()
.await?
.into_iter()
.collect();
Ok(EntrypointsForJsVc::cell(entrypoints))
}
#[tasks::function]
async fn get_value(
root_dir: &str,
project_dir: &str,
page_extensions: Vec<String>,
watching: bool,
) -> Result<OptionEntrypointsForJsVc> {
let page_extensions = StringsVc::cell(page_extensions);
let fs = project_fs(root_dir, watching);
let project_relative = project_dir.strip_prefix(root_dir).unwrap();
let project_relative = project_relative
.strip_prefix(MAIN_SEPARATOR)
.unwrap_or(project_relative)
.replace(MAIN_SEPARATOR, "/");
let project_path = fs.root().join(&project_relative);
let app_dir = find_app_dir(project_path);
let result = if let Some(app_dir) = *app_dir.await? {
let entrypoints = get_entrypoints_impl(app_dir, page_extensions);
let entrypoints_for_js = prepare_entrypoints_for_js(project_path, entrypoints);
Some(entrypoints_for_js)
} else {
None
};
Ok(OptionEntrypointsForJsVc::cell(result))
}
#[napi]
pub fn stream_entrypoints(
turbo_tasks: External<Arc<TurboTasks<MemoryBackend>>>,
root_dir: String,
project_dir: String,
page_extensions: Vec<String>,
func: JsFunction,
) -> napi::Result<()> {
register();
let func: ThreadsafeFunction<Option<EntrypointsForJsReadRef>, ErrorStrategy::CalleeHandled> =
func.create_threadsafe_function(0, |ctx| {
let value = ctx.value;
let value = serde_json::to_value(value)?;
Ok(vec![value])
})?;
let root_dir = Arc::new(root_dir);
let project_dir = Arc::new(project_dir);
let page_extensions = Arc::new(page_extensions);
turbo_tasks.spawn_root_task(move || {
let func = func.clone();
let project_dir = project_dir.clone();
let root_dir = root_dir.clone();
let page_extensions = page_extensions.clone();
Box::pin(async move {
if let Some(entrypoints) = &*get_value(
&root_dir,
&project_dir,
page_extensions.iter().map(|s| s.to_string()).collect(),
true,
)
.await?
{
func.call(
Ok(Some(entrypoints.await?)),
ThreadsafeFunctionCallMode::NonBlocking,
);
} else {
func.call(Ok(None), ThreadsafeFunctionCallMode::NonBlocking);
}
Ok(NothingVc::new().into())
})
});
Ok(())
}
#[napi]
pub async fn get_entrypoints(
turbo_tasks: External<Arc<TurboTasks<MemoryBackend>>>,
root_dir: String,
project_dir: String,
page_extensions: Vec<String>,
) -> napi::Result<serde_json::Value> {
register();
let result = turbo_tasks
.run_once(async move {
let value = if let Some(entrypoints) = &*get_value(
&root_dir,
&project_dir,
page_extensions.iter().map(|s| s.to_string()).collect(),
false,
)
.await?
{
Some(entrypoints.await?)
} else {
None
};
let value = serde_json::to_value(value)?;
Ok(value)
})
.await?;
Ok(result)
}

View file

@ -32,7 +32,11 @@ DEALINGS IN THE SOFTWARE.
#[macro_use]
extern crate napi_derive;
use std::{env, panic::set_hook, sync::Arc};
use std::{
env,
panic::set_hook,
sync::{Arc, Once},
};
use backtrace::Backtrace;
use fxhash::FxHashSet;
@ -42,6 +46,7 @@ use turbo_binding::swc::core::{
common::{sync::Lazy, FilePathMapping, SourceMap},
};
pub mod app_structure;
pub mod mdx;
pub mod minify;
pub mod parse;
@ -106,6 +111,15 @@ pub fn complete_output(
pub type ArcCompiler = Arc<Compiler>;
static REGISTER_ONCE: Once = Once::new();
fn register() {
REGISTER_ONCE.call_once(|| {
next_core::register();
include!(concat!(env!("OUT_DIR"), "/register.rs"));
});
}
#[cfg(all(feature = "native-tls", feature = "rustls-tls"))]
compile_error!("You can't enable both `native-tls` and `rustls-tls`");

View file

@ -9,17 +9,18 @@ edition = "2021"
bench = false
[dependencies]
allsorts = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
indoc = { workspace = true }
mime = { workspace = true }
async-recursion = "1.0.2"
once_cell = { workspace = true }
qstring = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
mime = { workspace = true }
indoc = { workspace = true }
allsorts = { workspace = true }
futures = { workspace = true }
turbo-binding = { workspace = true, features = [
"__feature_auto_hash_map",
"__turbo_tasks",
@ -60,3 +61,4 @@ dynamic_embed_contents = [
"turbo-binding/__turbo_tasks_fs_dynamic_embed_contents",
"turbo-binding/__turbopack_dev_dynamic_embed_contents",
]

View file

@ -7,13 +7,8 @@ type FileType =
| "not-found"
| "head";
declare global {
// an array of all layouts and the page
const LAYOUT_INFO: ({
segment: string;
page?: { module: any; chunks: string[] };
} & {
[componentKey in FileType]?: { module: any; chunks: string[] };
})[];
// 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>;
@ -37,6 +32,11 @@ import { ServerResponseShim } from "@vercel/turbopack-next/internal/http";
import { headersFromEntries } from "@vercel/turbopack-next/internal/headers";
import { parse, ParsedUrlQuery } from "node:querystring";
("TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }");
// @ts-ignore
import layoutEntry, { chunks as layoutEntryClientChunks } from "@vercel/turbopack-next/entry/app/layout-entry";
globalThis.__next_require__ = (data) => {
const [, , , ssr_id] = JSON.parse(data);
return __turbopack_require__(ssr_id);
@ -129,27 +129,7 @@ type LoaderTree = [
async function runOperation(renderData: RenderData) {
const layoutInfoChunks: Record<string, string[]> = {};
const pageItem = LAYOUT_INFO[LAYOUT_INFO.length - 1];
const pageModule = pageItem.page!.module;
let tree: LoaderTree = [
"",
{},
{ page: [() => pageModule.module, "page.js"] },
];
layoutInfoChunks["page"] = pageItem.page!.chunks;
for (let i = LAYOUT_INFO.length - 2; i >= 0; i--) {
const info = LAYOUT_INFO[i];
const components: ComponentsType = {};
for (const key of Object.keys(info)) {
if (key === "segment") {
continue;
}
const k = key as FileType;
components[k] = [() => info[k]!.module.module, `${k}${i}.js`];
layoutInfoChunks[`${k}${i}`] = info[k]!.chunks;
}
tree = [info.segment, { children: tree }, components];
}
let tree: LoaderTree = LOADER_TREE;
const proxyMethodsForModule = (
id: string
@ -219,29 +199,33 @@ async function runOperation(renderData: RenderData) {
return clientModulesProxy;
}
if (prop === "cssFiles") {
return cssFiles;
return new Proxy({} as any, cssFilesProxyMethods);
}
},
};
};
const manifest: ClientReferenceManifest = new Proxy(
{} as any,
proxyMethods()
);
const serverCSSManifest: ClientCSSReferenceManifest = {
cssImports: {},
cssModules: {},
const cssFilesProxyMethods = {
get(_target: any, prop: string) {
const chunks = JSON.parse(prop);
const cssChunks = chunks.filter((path: string) => path.endsWith(".css"));
return cssChunks;
}
};
const cssFiles: ClientReferenceManifest["cssFiles"] = {};
for (const [key, chunks] of Object.entries(layoutInfoChunks)) {
const cssChunks = chunks.filter((path) => path.endsWith(".css"));
serverCSSManifest.cssImports[`${key}.js`] = cssChunks.map((chunk) =>
JSON.stringify([chunk, [chunk]])
);
cssFiles[key] = cssChunks;
const cssImportProxyMethods = {
get(_target: any, prop: string) {
const chunks = JSON.parse(prop.replace(/\.js$/, ""));
const cssChunks = chunks.filter((path: string) => path.endsWith(".css"));
return cssChunks.map((chunk: string) =>
JSON.stringify([chunk, [chunk]])
)
}
}
serverCSSManifest.cssModules = {
page: serverCSSManifest.cssImports["page.js"],
const manifest: ClientReferenceManifest = new Proxy({} as any, proxyMethods());
const serverCSSManifest: ClientCSSReferenceManifest = {
cssImports: new Proxy({} as any, cssImportProxyMethods),
cssModules: {},
};
const req: IncomingMessage = {
url: renderData.url,
@ -273,9 +257,9 @@ async function runOperation(renderData: RenderData) {
ampFirstPages: [],
},
ComponentMod: {
...pageModule,
...layoutEntry,
default: undefined,
tree,
tree: LOADER_TREE,
pages: ["page.js"],
},
clientReferenceManifest: manifest,
@ -283,7 +267,7 @@ async function runOperation(renderData: RenderData) {
runtime: "nodejs",
serverComponents: true,
assetPrefix: "",
pageConfig: pageModule.config,
pageConfig: {},
reactLoadableManifest: {},
};
const result = await renderToHTMLOrFlight(

View file

@ -8,5 +8,3 @@ import * as serverHooks from "next/dist/client/components/hooks-server-context.j
export { serverHooks };
export { renderToReadableStream } from "next/dist/compiled/react-server-dom-webpack/server.edge";
import * as module from "PAGE";
export { module };

View file

@ -1,13 +1,9 @@
use anyhow::Result;
use indexmap::indexmap;
use anyhow::{bail, Result};
use turbo_binding::{
turbo::tasks_fs::FileSystemPathVc,
turbopack::{
core::{asset::AssetVc, compile_time_info::CompileTimeInfoVc, context::AssetContext},
ecmascript::{
EcmascriptInputTransform, EcmascriptInputTransformsVc, EcmascriptModuleAssetType,
EcmascriptModuleAssetVc, InnerAssetsVc,
},
core::{asset::AssetVc, compile_time_info::CompileTimeInfoVc},
ecmascript::chunk::EcmascriptChunkPlaceableVc,
turbopack::{
module_options::ModuleOptionsContextVc,
resolve_options_context::ResolveOptionsContextVc,
@ -16,14 +12,11 @@ use turbo_binding::{
},
},
};
use turbo_tasks::{primitives::OptionStringVc, Value};
use crate::{
embed_js::next_asset, next_client_component::with_client_chunks::WithClientChunksAsset,
};
use crate::next_client_component::with_client_chunks::WithClientChunksAsset;
#[turbo_tasks::value(shared)]
pub struct NextLayoutEntryTransition {
pub struct NextServerComponentTransition {
pub rsc_compile_time_info: CompileTimeInfoVc,
pub rsc_module_options_context: ModuleOptionsContextVc,
pub rsc_resolve_options_context: ResolveOptionsContextVc,
@ -31,7 +24,7 @@ pub struct NextLayoutEntryTransition {
}
#[turbo_tasks::value_impl]
impl Transition for NextLayoutEntryTransition {
impl Transition for NextServerComponentTransition {
#[turbo_tasks::function]
fn process_compile_time_info(
&self,
@ -60,33 +53,14 @@ impl Transition for NextLayoutEntryTransition {
async fn process_module(
&self,
asset: AssetVc,
context: ModuleAssetContextVc,
_context: ModuleAssetContextVc,
) -> Result<AssetVc> {
let internal_asset = next_asset("entry/app/layout-entry.tsx");
let asset = EcmascriptModuleAssetVc::new_with_inner_assets(
internal_asset,
context.into(),
Value::new(EcmascriptModuleAssetType::Typescript),
EcmascriptInputTransformsVc::cell(vec![
EcmascriptInputTransform::TypeScript {
use_define_for_class_fields: false,
},
EcmascriptInputTransform::React {
refresh: false,
import_source: OptionStringVc::cell(None),
runtime: OptionStringVc::cell(None),
},
]),
Default::default(),
context.compile_time_info(),
InnerAssetsVc::cell(indexmap! {
"PAGE".to_string() => asset
}),
);
let Some(asset) = EcmascriptChunkPlaceableVc::resolve_from(asset).await? else {
bail!("Not an ecmascript module");
};
Ok(WithClientChunksAsset {
asset: asset.into(),
asset,
// next.js code already adds _next prefix
server_root: self.server_root.join("_next"),
}

View file

@ -1,24 +1,23 @@
use std::{
collections::{BTreeMap, HashMap},
io::Write,
iter::once,
};
use std::{collections::HashMap, io::Write};
use anyhow::{anyhow, Result};
use indexmap::indexmap;
use anyhow::Result;
use async_recursion::async_recursion;
use indexmap::{indexmap, IndexMap};
use turbo_binding::{
turbo::{
tasks::{primitives::OptionStringVc, TryJoinIterExt, Value, ValueToString},
tasks::{primitives::OptionStringVc, Value},
tasks_env::{CustomProcessEnvVc, EnvMapVc, ProcessEnvVc},
tasks_fs::{rope::RopeBuilder, File, FileContent, FileSystemPathVc},
},
turbopack::{
core::{
asset::AssetsVc,
asset::{AssetVc, AssetsVc},
compile_time_info::CompileTimeInfoVc,
context::{AssetContext, AssetContextVc},
environment::{EnvironmentIntention, ServerAddrVc},
reference_type::{EntryReferenceSubType, ReferenceType},
reference_type::{
EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType,
},
source_asset::SourceAssetVc,
virtual_asset::VirtualAssetVc,
},
@ -26,8 +25,9 @@ use turbo_binding::{
dev_server::{
html::DevHtmlAssetVc,
source::{
combined::CombinedContentSource, ContentSourceData, ContentSourceVc,
NoContentSourceVc,
combined::CombinedContentSource,
specificity::{Specificity, SpecificityElementType, SpecificityVc},
ContentSourceData, ContentSourceVc, NoContentSourceVc,
},
},
ecmascript::{
@ -50,13 +50,14 @@ use turbo_binding::{
},
},
};
use turbo_tasks::primitives::StringVc;
use crate::{
app_render::{
next_layout_entry_transition::NextLayoutEntryTransition, LayoutSegment, LayoutSegmentsVc,
app_render::next_layout_entry_transition::NextServerComponentTransition,
app_structure::{
get_entrypoints, Components, Entrypoint, LoaderTree, LoaderTreeVc, OptionAppDirVc,
},
app_structure::{AppStructure, AppStructureItem, AppStructureVc, OptionAppStructureVc},
embed_js::next_js_file,
embed_js::{next_js_file, next_js_file_path},
env::env_for_js,
fallback::get_fallback_page,
next_client::{
@ -82,9 +83,35 @@ use crate::{
get_server_compile_time_info, get_server_module_options_context,
get_server_resolve_options_context, ServerContextType,
},
util::pathname_for_path,
};
#[turbo_tasks::function]
fn pathname_to_specificity(pathname: &str) -> SpecificityVc {
let mut current = Specificity::new();
let mut position = 0;
for segment in pathname.split('/') {
if segment.starts_with('(') && segment.ends_with(')') || segment.starts_with('@') {
// ignore
} else if segment.starts_with("[[...") && segment.ends_with("]]")
|| segment.starts_with("[...") && segment.ends_with(']')
{
// optional catch all segment
current.add(position - 1, SpecificityElementType::CatchAll);
position += 1;
} else if segment.starts_with("[[") || segment.ends_with("]]") {
// optional segment
position += 1;
} else if segment.starts_with('[') || segment.ends_with(']') {
current.add(position - 1, SpecificityElementType::DynamicSegment);
position += 1;
} else {
// normal segment
position += 1;
}
}
SpecificityVc::cell(current)
}
#[turbo_tasks::function]
async fn next_client_transition(
project_path: FileSystemPathVc,
@ -172,7 +199,7 @@ fn next_layout_entry_transition(
let rsc_module_options_context =
get_server_module_options_context(project_path, execution_context, ty, next_config);
NextLayoutEntryTransition {
NextServerComponentTransition {
rsc_compile_time_info,
rsc_module_options_context,
rsc_resolve_options_context,
@ -322,7 +349,7 @@ fn app_context(
/// Next.js app folder.
#[turbo_tasks::function]
pub async fn create_app_source(
app_structure: OptionAppStructureVc,
app_dir: OptionAppDirVc,
project_path: FileSystemPathVc,
execution_context: ExecutionContextVc,
output_path: FileSystemPathVc,
@ -332,10 +359,10 @@ pub async fn create_app_source(
next_config: NextConfigVc,
server_addr: ServerAddrVc,
) -> Result<ContentSourceVc> {
let Some(app_structure) = *app_structure.await? else {
let Some(app_dir) = *app_dir.await? else {
return Ok(NoContentSourceVc::new().into());
};
let app_dir = app_structure.directory();
let entrypoints = get_entrypoints(app_dir, next_config.page_extensions());
let client_compile_time_info = get_client_compile_time_info(browserslist_query);
@ -378,140 +405,123 @@ pub async fn create_app_source(
next_config,
);
let source = create_app_source_for_directory(
app_structure,
context_ssr,
context,
project_path,
env,
server_root,
AssetsVc::cell(server_runtime_entries),
fallback_page,
output_path,
);
Ok(source)
let server_runtime_entries = AssetsVc::cell(server_runtime_entries);
let sources = entrypoints
.await?
.iter()
.map(|(pathname, &loader_tree)| match loader_tree {
Entrypoint::AppPage { loader_tree } => create_app_page_source_for_route(
pathname,
loader_tree,
context_ssr,
context,
project_path,
app_dir,
env,
server_root,
server_runtime_entries,
fallback_page,
output_path,
),
Entrypoint::AppRoute { path } => create_app_route_source_for_route(
pathname,
path,
context_ssr,
project_path,
app_dir,
env,
server_root,
server_runtime_entries,
output_path,
),
})
.collect();
Ok(CombinedContentSource { sources }.cell().into())
}
#[allow(clippy::too_many_arguments)]
#[turbo_tasks::function]
async fn create_app_source_for_directory(
app_structure: AppStructureVc,
async fn create_app_page_source_for_route(
pathname: &str,
loader_tree: LoaderTreeVc,
context_ssr: AssetContextVc,
context: AssetContextVc,
project_path: FileSystemPathVc,
app_dir: FileSystemPathVc,
env: ProcessEnvVc,
server_root: FileSystemPathVc,
runtime_entries: AssetsVc,
fallback_page: DevHtmlAssetVc,
intermediate_output_path_root: FileSystemPathVc,
) -> Result<ContentSourceVc> {
let AppStructure {
item,
ref children,
directory,
} = *app_structure.await?;
let mut sources = Vec::new();
let pathname_vc = StringVc::cell(pathname.to_string());
if let Some(item) = item {
match *item.await? {
AppStructureItem::Page {
segment,
url,
specificity,
page,
segments: layouts,
} => {
let LayoutSegment { target, .. } = *segment.await?;
let pathname = pathname_for_path(server_root, url, false, false);
let params_matcher = NextParamsMatcherVc::new(pathname);
let params_matcher = NextParamsMatcherVc::new(pathname_vc);
sources.push(create_node_rendered_source(
project_path,
env,
specificity,
server_root,
params_matcher.into(),
pathname,
AppRenderer {
context_ssr,
context,
server_root,
layout_path: layouts,
page_path: page,
target,
project_path,
intermediate_output_path: intermediate_output_path_root,
}
.cell()
.into(),
runtime_entries,
fallback_page,
));
}
AppStructureItem::Route {
url,
specificity,
route,
..
} => {
let pathname = pathname_for_path(server_root, url, false, false);
let params_matcher = NextParamsMatcherVc::new(pathname);
sources.push(create_node_api_source(
project_path,
env,
specificity,
server_root,
params_matcher.into(),
pathname,
AppRoute {
context: context_ssr,
server_root,
entry_path: route,
project_path,
intermediate_output_path: intermediate_output_path_root,
output_root: intermediate_output_path_root,
}
.cell()
.into(),
runtime_entries,
));
}
let source = create_node_rendered_source(
project_path,
env,
pathname_to_specificity(pathname),
server_root,
params_matcher.into(),
pathname_vc,
AppRenderer {
context_ssr,
context,
server_root,
project_path,
intermediate_output_path: intermediate_output_path_root,
loader_tree,
}
}
if children.is_empty() {
if let Some(source) = sources.into_iter().next() {
return Ok(source);
} else {
return Ok(NoContentSourceVc::new().into());
}
}
let source = CombinedContentSource { sources }
.cell()
.as_content_source()
.issue_context(directory, "Next.js App Router");
.into(),
runtime_entries,
fallback_page,
);
Ok(CombinedContentSource {
sources: once(source)
.chain(children.iter().map(|child| {
create_app_source_for_directory(
*child,
context_ssr,
context,
project_path,
env,
server_root,
runtime_entries,
fallback_page,
intermediate_output_path_root,
)
}))
.collect(),
}
.cell()
.into())
Ok(source.issue_context(app_dir, &format!("Next.js App Page Route {pathname}")))
}
#[allow(clippy::too_many_arguments)]
#[turbo_tasks::function]
async fn create_app_route_source_for_route(
pathname: &str,
entry_path: FileSystemPathVc,
context_ssr: AssetContextVc,
project_path: FileSystemPathVc,
app_dir: FileSystemPathVc,
env: ProcessEnvVc,
server_root: FileSystemPathVc,
runtime_entries: AssetsVc,
intermediate_output_path_root: FileSystemPathVc,
) -> Result<ContentSourceVc> {
let pathname_vc = StringVc::cell(pathname.to_string());
let params_matcher = NextParamsMatcherVc::new(pathname_vc);
let source = create_node_api_source(
project_path,
env,
pathname_to_specificity(pathname),
server_root,
params_matcher.into(),
pathname_vc,
AppRoute {
context: context_ssr,
server_root,
entry_path,
project_path,
intermediate_output_path: intermediate_output_path_root,
output_root: intermediate_output_path_root,
}
.cell()
.into(),
runtime_entries,
);
Ok(source.issue_context(app_dir, &format!("Next.js App Route {pathname}")))
}
/// The renderer for pages in app directory
@ -519,125 +529,158 @@ async fn create_app_source_for_directory(
struct AppRenderer {
context_ssr: AssetContextVc,
context: AssetContextVc,
server_root: FileSystemPathVc,
layout_path: LayoutSegmentsVc,
page_path: FileSystemPathVc,
target: FileSystemPathVc,
project_path: FileSystemPathVc,
server_root: FileSystemPathVc,
intermediate_output_path: FileSystemPathVc,
loader_tree: LoaderTreeVc,
}
#[turbo_tasks::value_impl]
impl AppRendererVc {
#[turbo_tasks::function]
async fn entry(self, is_rsc: bool) -> Result<NodeRenderingEntryVc> {
let this = self.await?;
let layout_path = this.layout_path.await?;
let page = this.page_path;
let path = page.parent();
let path_value = &*path.await?;
let AppRenderer {
context_ssr,
context,
project_path,
server_root,
intermediate_output_path,
loader_tree,
} = *self.await?;
let layout_and_page = layout_path
.iter()
.copied()
.chain(std::iter::once(
LayoutSegment {
files: HashMap::from([("page".to_string(), page)]),
target: this.target,
}
.cell(),
))
.try_join()
.await?;
let (context, intermediate_output_path) = if is_rsc {
(context, intermediate_output_path.join("rsc"))
} else {
(context_ssr, intermediate_output_path)
};
let segments: Vec<_> = layout_and_page
.into_iter()
.fold(
(this.server_root, Vec::new()),
|(last_path, mut futures), segment| {
(segment.target, {
futures.push(async move {
let target = &*segment.target.await?;
let segment_path =
last_path.await?.get_path_to(target).unwrap_or_default();
let mut imports = BTreeMap::new();
for (key, file) in segment.files.iter() {
let file_str = file.to_string().await?;
let identifier = magic_identifier::mangle(&format!(
"imported namespace {}",
file_str
));
let chunks_identifier = magic_identifier::mangle(&format!(
"client chunks for {}",
file_str
));
if let Some(p) = path_value.get_relative_path_to(&*file.await?) {
imports.insert(
key.to_string(),
(p, identifier, chunks_identifier),
);
} else {
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?
));
}
}
Ok((StringifyJs(segment_path).to_string(), imports))
});
futures
})
},
)
.1
.into_iter()
.try_join()
.await?;
struct State {
inner_assets: IndexMap<String, AssetVc>,
counter: usize,
imports: Vec<String>,
loader_tree_code: String,
context: AssetContextVc,
}
let mut state = State {
inner_assets: IndexMap::new(),
counter: 0,
imports: Vec::new(),
loader_tree_code: String::new(),
context,
};
fn write_component(
state: &mut State,
name: &str,
component: Option<FileSystemPathVc>,
) -> Result<()> {
use std::fmt::Write;
if let Some(component) = component {
let i = state.counter;
state.counter += 1;
let identifier = magic_identifier::mangle(&format!("{name} #{i}"));
let chunks_identifier = magic_identifier::mangle(&format!("chunks of {name} #{i}"));
write!(
state.loader_tree_code,
"{name}: [() => {identifier}, JSON.stringify({chunks_identifier}) + '.js']",
name = StringifyJs(name)
)?;
state.imports.push(format!(
r#"("TURBOPACK {{ chunking-type: isolatedParallel }}");
import {}, {{ chunks as {} }} from "COMPONENT_{}";
"#,
identifier, chunks_identifier, i
));
state.inner_assets.insert(
format!("COMPONENT_{i}"),
state.context.with_transition("next-layout-entry").process(
SourceAssetVc::new(component).into(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
),
);
}
Ok(())
}
#[async_recursion]
async fn walk_tree(state: &mut State, loader_tree: LoaderTreeVc) -> Result<()> {
use std::fmt::Write;
let LoaderTree {
segment,
parallel_routes,
components,
} = &*loader_tree.await?;
write!(
state.loader_tree_code,
"[{segment}, {{",
segment = StringifyJs(segment)
)?;
// add parallel_routers
for (key, &parallel_route) in parallel_routes.iter() {
write!(state.loader_tree_code, "{key}: ", key = StringifyJs(key))?;
walk_tree(state, parallel_route).await?;
writeln!(state.loader_tree_code, ",")?;
}
write!(state.loader_tree_code, "}}, {{")?;
// add components
let Components {
page,
default,
error,
layout,
loading,
template,
metadata: _,
route: _,
} = &*components.await?;
write_component(state, "page", *page)?;
write_component(state, "default", *default)?;
write_component(state, "error", *error)?;
write_component(state, "layout", *layout)?;
write_component(state, "loading", *loading)?;
write_component(state, "template", *template)?;
// TODO something for metadata
write!(state.loader_tree_code, "}}]")?;
Ok(())
}
walk_tree(&mut state, loader_tree).await?;
let State {
mut inner_assets,
imports,
loader_tree_code,
..
} = state;
// 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-next/ipc/index\";\n");
for (_, imports) in segments.iter() {
for (p, identifier, chunks_identifier) in imports.values() {
result += r#"("TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }");
"#;
writeln!(
result,
"import {}, {{ chunks as {} }} from {};\n",
identifier,
chunks_identifier,
StringifyJs(p)
)?
}
}
if let Some(page) = path_value.get_relative_path_to(&*page.await?) {
writeln!(
result,
r#"("TURBOPACK {{ transition: next-client }}");
import BOOTSTRAP from {};
"#,
StringifyJs(&page)
)?;
for import in imports {
writeln!(result, "{import}")?;
}
result += "const LAYOUT_INFO = [";
for (segment_str_lit, imports) in segments.iter() {
writeln!(result, " {{\n segment: {segment_str_lit},")?;
for (key, (_, identifier, chunks_identifier)) in imports {
writeln!(
result,
" {key}: {{ module: {identifier}, chunks: {chunks_identifier} }},",
key = StringifyJs(key),
)?;
}
result += " },";
}
result += "];\n\n";
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? {
@ -645,28 +688,23 @@ 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 {
(this.context, this.intermediate_output_path.join("rsc"))
} else {
(this.context_ssr, this.intermediate_output_path)
};
let asset = VirtualAssetVc::new(next_js_file_path("entry/app-renderer.tsx"), file.into());
let chunking_context = DevChunkingContextVc::builder(
this.project_path,
project_path,
intermediate_output_path,
intermediate_output_path.join("chunks"),
this.server_root.join("_next/static/assets"),
server_root.join("_next/static/assets"),
context.compile_time_info().environment(),
)
.layer("ssr")
.css_chunk_root_path(this.server_root.join("_next/static/chunks"))
.css_chunk_root_path(server_root.join("_next/static/chunks"))
.reference_chunk_source_maps(false)
.build();
Ok(NodeRenderingEntry {
context,
module: EcmascriptModuleAssetVc::new(
module: EcmascriptModuleAssetVc::new_with_inner_assets(
asset.into(),
context,
Value::new(EcmascriptModuleAssetType::Typescript),
@ -682,11 +720,12 @@ import BOOTSTRAP from {};
]),
Default::default(),
context.compile_time_info(),
InnerAssetsVc::cell(inner_assets),
),
chunking_context,
intermediate_output_path,
output_root: intermediate_output_path.root(),
project_dir: this.project_path,
project_dir: project_path,
}
.cell())
}

View file

@ -1,114 +1,180 @@
use std::collections::HashMap;
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use anyhow::{bail, Result};
use indexmap::{indexmap, map::Entry, IndexMap};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use turbo_binding::{
turbo::tasks_fs::{
DirectoryContent, DirectoryEntry, File, FileContentVc, FileSystemEntryType,
FileSystemPathVc,
},
turbopack::{
core::issue::{Issue, IssueSeverity, IssueSeverityVc, IssueVc},
dev_server::source::specificity::SpecificityVc,
},
turbo::tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPathVc},
turbopack::core::issue::{Issue, IssueSeverity, IssueSeverityVc, IssueVc},
};
use turbo_tasks::{
debug::ValueDebugFormat,
primitives::{StringVc, StringsVc},
CompletionVc, ValueToString,
trace::TraceRawVcs,
CompletionVc, CompletionsVc,
};
use crate::{
app_render::{LayoutSegment, LayoutSegmentVc, LayoutSegmentsVc},
next_config::NextConfigVc,
};
use crate::next_config::NextConfigVc;
/// A final route in the app directory.
#[turbo_tasks::value]
pub enum AppStructureItem {
Page {
segment: LayoutSegmentVc,
segments: LayoutSegmentsVc,
url: FileSystemPathVc,
specificity: SpecificityVc,
page: FileSystemPathVc,
},
Route {
segment: LayoutSegmentVc,
url: FileSystemPathVc,
specificity: SpecificityVc,
route: FileSystemPathVc,
},
#[derive(Default, Debug, Clone)]
pub struct Components {
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub loading: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route: Option<FileSystemPathVc>,
#[serde(skip_serializing_if = "Metadata::is_empty")]
pub metadata: Metadata,
}
impl Components {
fn without_leafs(&self) -> Self {
Self {
page: None,
layout: self.layout,
error: self.error,
loading: self.loading,
template: self.template,
default: None,
route: None,
metadata: self.metadata.clone(),
}
}
fn merge(a: &Self, b: &Self) -> Self {
Self {
page: a.page.or(b.page),
layout: a.layout.or(b.layout),
error: a.error.or(b.error),
loading: a.loading.or(b.loading),
template: a.template.or(b.template),
default: a.default.or(b.default),
route: a.default.or(b.route),
metadata: Metadata::merge(&a.metadata, &b.metadata),
}
}
}
#[turbo_tasks::value_impl]
impl AppStructureItemVc {
impl ComponentsVc {
/// Returns a completion that changes when any route in the components
/// changes.
#[turbo_tasks::function]
pub async fn routes_changed(self) -> Result<CompletionVc> {
match *self.await? {
AppStructureItem::Page { url, .. } => url.await?,
AppStructureItem::Route { url, .. } => url.await?,
};
self.await?;
Ok(CompletionVc::new())
}
}
/// A (sub)directory in the app directory with all analyzed routes and folders.
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs)]
pub struct Metadata {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub icon: Vec<FileSystemPathVc>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub apple: Vec<FileSystemPathVc>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub twitter: Vec<FileSystemPathVc>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub open_graph: Vec<FileSystemPathVc>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub favicon: Vec<FileSystemPathVc>,
}
impl Metadata {
pub fn is_empty(&self) -> bool {
let Metadata {
icon,
apple,
twitter,
open_graph,
favicon,
} = self;
icon.is_empty()
&& apple.is_empty()
&& twitter.is_empty()
&& open_graph.is_empty()
&& favicon.is_empty()
}
fn merge(a: &Self, b: &Self) -> Self {
Self {
icon: a.icon.iter().chain(b.icon.iter()).copied().collect(),
apple: a.apple.iter().chain(b.apple.iter()).copied().collect(),
twitter: a.twitter.iter().chain(b.twitter.iter()).copied().collect(),
open_graph: a
.open_graph
.iter()
.chain(b.open_graph.iter())
.copied()
.collect(),
favicon: a.favicon.iter().chain(b.favicon.iter()).copied().collect(),
}
}
}
#[turbo_tasks::value]
pub struct AppStructure {
pub directory: FileSystemPathVc,
pub item: Option<AppStructureItemVc>,
pub children: Vec<AppStructureVc>,
#[derive(Debug)]
pub struct DirectoryTree {
/// key is e.g. "dashboard", "(dashboard)", "@slot"
pub subdirectories: BTreeMap<String, DirectoryTreeVc>,
pub components: ComponentsVc,
}
#[turbo_tasks::value_impl]
impl AppStructureVc {
/// Returns the directory of this structure.
#[turbo_tasks::function]
pub async fn directory(self) -> Result<FileSystemPathVc> {
Ok(self.await?.directory)
}
impl DirectoryTreeVc {
/// Returns a completion that changes when any route in the whole tree
/// changes.
#[turbo_tasks::function]
pub async fn routes_changed(self) -> Result<CompletionVc> {
if let Some(item) = self.await?.item {
item.routes_changed().await?;
let DirectoryTree {
subdirectories,
components,
} = &*self.await?;
let mut children = Vec::new();
children.push(components.routes_changed());
for child in subdirectories.values() {
children.push(child.routes_changed());
}
for child in self.await?.children.iter() {
child.routes_changed().await?;
}
Ok(CompletionVc::new())
Ok(CompletionsVc::cell(children).completed())
}
}
#[turbo_tasks::value(transparent)]
pub struct OptionAppStructure(Option<AppStructureVc>);
pub struct OptionAppDir(Option<FileSystemPathVc>);
#[turbo_tasks::value_impl]
impl OptionAppStructureVc {
impl OptionAppDirVc {
/// Returns a completion that changes when any route in the whole tree
/// changes.
#[turbo_tasks::function]
pub async fn routes_changed(self) -> Result<CompletionVc> {
if let Some(app_structure) = *self.await? {
app_structure.routes_changed().await?;
pub async fn routes_changed(self, next_config: NextConfigVc) -> Result<CompletionVc> {
if let Some(app_dir) = *self.await? {
let directory_tree = get_directory_tree(app_dir, next_config.page_extensions());
directory_tree.routes_changed().await?;
}
Ok(CompletionVc::new())
}
}
/// Finds and returns the [AppStructure] of the app directory if enabled and
/// existing.
/// Finds and returns the [DirectoryTree] of the app directory if existing.
#[turbo_tasks::function]
pub async fn find_app_structure(
project_path: FileSystemPathVc,
server_root: FileSystemPathVc,
next_config: NextConfigVc,
) -> Result<OptionAppStructureVc> {
if !*next_config.app_dir().await? {
return Ok(OptionAppStructureVc::cell(None));
}
pub async fn find_app_dir(project_path: FileSystemPathVc) -> Result<OptionAppDirVc> {
let app = project_path.join("app");
let src_app = project_path.join("src/app");
let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
@ -116,229 +182,409 @@ pub async fn find_app_structure(
} else if *src_app.get_type().await? == FileSystemEntryType::Directory {
src_app
} else {
return Ok(OptionAppStructureVc::cell(None));
return Ok(OptionAppDirVc::cell(None));
}
.resolve()
.await?;
Ok(OptionAppStructureVc::cell(Some(get_app_structure(
app_dir,
server_root,
next_config.page_extensions(),
))))
Ok(OptionAppDirVc::cell(Some(app_dir)))
}
/// Parses a directory as app directory and returns the [AppStructure].
/// Finds and returns the [DirectoryTree] of the app directory if enabled and
/// existing.
#[turbo_tasks::function]
pub fn get_app_structure(
pub async fn find_app_dir_if_enabled(
project_path: FileSystemPathVc,
next_config: NextConfigVc,
) -> Result<OptionAppDirVc> {
if !*next_config.app_dir().await? {
return Ok(OptionAppDirVc::cell(None));
}
Ok(find_app_dir(project_path))
}
static STATIC_METADATA_IMAGES: Lazy<HashMap<&'static str, &'static [&'static str]>> =
Lazy::new(|| {
HashMap::from([
(
"icon",
&["ico", "jpg", "jpeg", "png", "svg"] as &'static [&'static str],
),
("apple-icon", &["jpg", "jpeg", "png"]),
("favicon", &["ico"]),
("opengraph-image", &["jpg", "jpeg", "png", "gif"]),
("twitter-image", &["jpg", "jpeg", "png", "gif"]),
])
});
fn match_metadata_file(basename: &str) -> Option<&'static str> {
let (stem, ext) = basename.split_once('.')?;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\d*$").unwrap());
let stem = REGEX.replace(stem, "");
let (key, exts) = STATIC_METADATA_IMAGES.get_key_value(stem.as_ref())?;
exts.contains(&ext).then_some(key)
}
#[turbo_tasks::function]
async fn get_directory_tree(
app_dir: FileSystemPathVc,
server_root: FileSystemPathVc,
page_extensions: StringsVc,
) -> AppStructureVc {
get_app_structure_for_directory(
app_dir,
true,
SpecificityVc::exact(),
0,
server_root,
server_root,
LayoutSegmentsVc::cell(Vec::new()),
page_extensions,
)
}
#[allow(clippy::too_many_arguments)]
#[turbo_tasks::function]
async fn get_app_structure_for_directory(
input_dir: FileSystemPathVc,
root: bool,
specificity: SpecificityVc,
position: u32,
target: FileSystemPathVc,
url: FileSystemPathVc,
layouts: LayoutSegmentsVc,
page_extensions: StringsVc,
) -> Result<AppStructureVc> {
let mut layouts = layouts;
let mut page = None;
let mut route = None;
let mut files = HashMap::new();
let DirectoryContent::Entries(entries) = &*input_dir.read_dir().await? else {
bail!("{} is not a directory", input_dir.to_string().await?)
) -> Result<DirectoryTreeVc> {
let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
bail!("app_dir must be a directory")
};
let mut subdirectories = BTreeMap::new();
let mut components = Components::default();
let allowed_extensions = &*page_extensions.await?;
for (name, entry) in entries.iter() {
if let &DirectoryEntry::File(file) = entry {
if let Some((name, ext)) = name.rsplit_once('.') {
if !allowed_extensions.iter().any(|allowed| allowed == ext) {
continue;
for (basename, entry) in entries {
match *entry {
DirectoryEntry::File(file) => {
if let Some((stem, ext)) = basename.split_once('.') {
if page_extensions.await?.iter().any(|e| e == ext) {
match stem {
"page" => components.page = Some(file),
"layout" => components.layout = Some(file),
"error" => components.error = Some(file),
"loading" => components.loading = Some(file),
"template" => components.template = Some(file),
"default" => components.default = Some(file),
"route" => components.route = Some(file),
_ => {}
}
}
}
if let Some(metadata_type) = match_metadata_file(basename.as_str()) {
let metadata = &mut components.metadata;
match name {
"page" => {
page = Some(file);
}
"route" => {
route = Some(file);
}
"layout" | "error" | "loading" | "template" | "not-found" | "head" => {
files.insert(name.to_string(), file);
}
_ => {
// Any other file is ignored
let entry = match metadata_type {
"icon" => Some(&mut metadata.icon),
"apple-icon" => Some(&mut metadata.apple),
"twitter-image" => Some(&mut metadata.twitter),
"opengraph-image" => Some(&mut metadata.open_graph),
"favicon" => Some(&mut metadata.favicon),
_ => None,
};
if let Some(entry) = entry {
entry.push(file)
}
}
}
}
}
let layout = files.get("layout");
if let (Some(_), Some(route_path)) = (page, route) {
AppStructureIssue {
severity: IssueSeverity::Error.into(),
path: route_path,
message: StringVc::cell(
"It's not possible to have a page and a route in the same directory. The route \
will be ignored in favor of the page."
.to_string(),
),
}
.cell()
.as_issue()
.emit();
route = None;
}
// If a page exists but no layout exists, create a basic root layout
// in `app/layout.js` or `app/layout.tsx`.
//
// TODO: Use let Some(page_file) = page in expression below when
// https://rust-lang.github.io/rfcs/2497-if-let-chains.html lands
if let (Some(page_file), None, true) = (page, layout, root) {
// Use the extension to determine if the page file is TypeScript.
// TODO: Use the presence of a tsconfig.json instead, like Next.js
// stable does.
let is_tsx = *page_file.extension().await? == "tsx";
let layout = if is_tsx {
input_dir.join("layout.tsx")
} else {
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")
};
layout.write(FileContentVc::from(File::from(content)));
AppStructureIssue {
severity: IssueSeverity::Warning.into(),
path: page_file,
message: StringVc::cell(format!(
"Your page {} did not have a root layout, we created {} for you.",
page_file.await?.path,
layout.await?.path,
)),
}
.cell()
.as_issue()
.emit();
}
let mut list = layouts.await?.clone_value();
let segment = LayoutSegment { files, target }.cell();
list.push(segment);
layouts = LayoutSegmentsVc::cell(list);
let mut children = Vec::new();
for (name, entry) in entries.iter() {
let DirectoryEntry::Directory(dir) = entry else {
continue;
};
let specificity = if name.starts_with("[[") || name.starts_with("[...") {
specificity.with_catch_all(position)
} else if name.starts_with('[') {
specificity.with_dynamic_segment(position)
} else {
specificity
};
let new_target = target.join(name);
let (new_root, new_url, position) = if name.starts_with('(') && name.ends_with(')') {
// This doesn't affect the url
(root, url, position)
} else {
// This adds to the url
(false, url.join(name), position + 1)
};
children.push((
name,
get_app_structure_for_directory(
*dir,
new_root,
specificity,
position,
new_target,
new_url,
layouts,
page_extensions,
),
));
}
let item = page
.map(|page| {
AppStructureItem::Page {
page,
segment,
segments: layouts,
url,
specificity,
DirectoryEntry::Directory(dir) => {
let result = get_directory_tree(dir, page_extensions);
subdirectories.insert(basename.to_string(), result);
}
.cell()
})
.or_else(|| {
route.map(|route| {
AppStructureItem::Route {
route,
segment,
url,
specificity,
}
.cell()
})
});
// TODO handle symlinks in app dir
_ => {}
}
}
// Ensure deterministic order since read_dir is not deterministic
children.sort_by_key(|(k, _)| *k);
Ok(AppStructure {
item,
directory: input_dir,
children: children.into_iter().map(|(_, v)| v).collect(),
Ok(DirectoryTree {
subdirectories,
components: components.cell(),
}
.cell())
}
#[turbo_tasks::value]
#[derive(Debug, Clone)]
pub struct LoaderTree {
pub segment: String,
pub parallel_routes: IndexMap<String, LoaderTreeVc>,
pub components: ComponentsVc,
}
#[turbo_tasks::function]
async fn merge_loader_trees(
app_dir: FileSystemPathVc,
tree1: LoaderTreeVc,
tree2: LoaderTreeVc,
) -> Result<LoaderTreeVc> {
let tree1 = tree1.await?;
let tree2 = tree2.await?;
let segment = if !tree1.segment.is_empty() {
tree1.segment.to_string()
} else {
tree2.segment.to_string()
};
let mut parallel_routes = tree1.parallel_routes.clone();
for (key, &tree2_route) in tree2.parallel_routes.iter() {
add_parallel_route(app_dir, &mut parallel_routes, key.clone(), tree2_route).await?
}
let components = Components::merge(&*tree1.components.await?, &*tree2.components.await?).cell();
Ok(LoaderTree {
segment,
parallel_routes,
components,
}
.cell())
}
#[derive(
Copy, Clone, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, ValueDebugFormat, Debug,
)]
pub enum Entrypoint {
AppPage { loader_tree: LoaderTreeVc },
AppRoute { path: FileSystemPathVc },
}
#[turbo_tasks::value(transparent)]
pub struct Entrypoints(IndexMap<String, Entrypoint>);
fn is_parallel_route(name: &str) -> bool {
name.starts_with('@')
}
fn match_parallel_route(name: &str) -> Option<&str> {
name.strip_prefix('@')
}
fn is_optional_segment(name: &str) -> bool {
name.starts_with('(') && name.ends_with(')')
}
async fn add_parallel_route(
app_dir: FileSystemPathVc,
result: &mut IndexMap<String, LoaderTreeVc>,
key: String,
loader_tree: LoaderTreeVc,
) -> Result<()> {
match result.entry(key) {
Entry::Occupied(mut e) => {
let value = e.get_mut();
*value = merge_loader_trees(app_dir, *value, loader_tree)
.resolve()
.await?;
}
Entry::Vacant(e) => {
e.insert(loader_tree);
}
}
Ok(())
}
async fn add_app_page(
app_dir: FileSystemPathVc,
result: &mut IndexMap<String, Entrypoint>,
key: String,
loader_tree: LoaderTreeVc,
) -> Result<()> {
match result.entry(key) {
Entry::Occupied(mut e) => {
let value = e.get_mut();
let Entrypoint::AppPage { loader_tree: value } = value else {
DirectoryTreeIssue {
app_dir,
message: StringVc::cell(format!("Conflicting route at {}", e.key())),
severity: IssueSeverity::Error.cell(),
}.cell().as_issue().emit();
return Ok(());
};
*value = merge_loader_trees(app_dir, *value, loader_tree)
.resolve()
.await?;
}
Entry::Vacant(e) => {
e.insert(Entrypoint::AppPage { loader_tree });
}
}
Ok(())
}
async fn add_app_route(
app_dir: FileSystemPathVc,
result: &mut IndexMap<String, Entrypoint>,
key: String,
path: FileSystemPathVc,
) -> Result<()> {
match result.entry(key) {
Entry::Occupied(mut e) => {
DirectoryTreeIssue {
app_dir,
message: StringVc::cell(format!("Conflicting route at {}", e.key())),
severity: IssueSeverity::Error.cell(),
}
.cell()
.as_issue()
.emit();
*e.get_mut() = Entrypoint::AppRoute { path };
}
Entry::Vacant(e) => {
e.insert(Entrypoint::AppRoute { path });
}
}
Ok(())
}
#[turbo_tasks::function]
pub fn get_entrypoints(app_dir: FileSystemPathVc, page_extensions: StringsVc) -> EntrypointsVc {
directory_tree_to_entrypoints(app_dir, get_directory_tree(app_dir, page_extensions))
}
#[turbo_tasks::function]
pub fn directory_tree_to_entrypoints(
app_dir: FileSystemPathVc,
directory_tree: DirectoryTreeVc,
) -> EntrypointsVc {
directory_tree_to_entrypoints_internal(app_dir, "", directory_tree, "/")
}
#[turbo_tasks::function]
async fn directory_tree_to_entrypoints_internal(
app_dir: FileSystemPathVc,
directory_name: &str,
directory_tree: DirectoryTreeVc,
path_prefix: &str,
) -> Result<EntrypointsVc> {
let mut result = IndexMap::new();
let directory_tree = &*directory_tree.await?;
let subdirectories = &directory_tree.subdirectories;
let components = directory_tree.components.await?;
let current_level_is_parallel_route = is_parallel_route(directory_name);
if let Some(page) = components.page {
add_app_page(
app_dir,
&mut result,
path_prefix.to_string(),
if current_level_is_parallel_route {
LoaderTree {
segment: "__PAGE__".to_string(),
parallel_routes: IndexMap::new(),
components: Components {
page: Some(page),
..Default::default()
}
.cell(),
}
.cell()
} else {
LoaderTree {
segment: directory_name.to_string(),
parallel_routes: indexmap! {
"children".to_string() => LoaderTree {
segment: "__PAGE__".to_string(),
parallel_routes: IndexMap::new(),
components: Components {
page: Some(page),
..Default::default()
}
.cell(),
}
.cell(),
},
components: components.without_leafs().cell(),
}
.cell()
},
)
.await?;
}
if let Some(default) = components.default {
add_app_page(
app_dir,
&mut result,
path_prefix.to_string(),
if current_level_is_parallel_route {
LoaderTree {
segment: "__DEFAULT__".to_string(),
parallel_routes: IndexMap::new(),
components: Components {
default: Some(default),
..Default::default()
}
.cell(),
}
.cell()
} else {
LoaderTree {
segment: directory_name.to_string(),
parallel_routes: indexmap! {
"children".to_string() => LoaderTree {
segment: "__DEFAULT__".to_string(),
parallel_routes: IndexMap::new(),
components: Components {
default: Some(default),
..Default::default()
}
.cell(),
}
.cell(),
},
components: components.without_leafs().cell(),
}
.cell()
},
)
.await?;
}
if let Some(route) = components.route {
add_app_route(app_dir, &mut result, path_prefix.to_string(), route).await?;
}
for (subdir_name, &subdirectory) in subdirectories.iter() {
let parallel_route_key = match_parallel_route(subdir_name);
let optional_segment = is_optional_segment(subdir_name);
let map = directory_tree_to_entrypoints_internal(
app_dir,
subdir_name,
subdirectory,
if parallel_route_key.is_some() || optional_segment {
Cow::Borrowed(path_prefix)
} else if path_prefix == "/" {
format!("/{subdir_name}").into()
} else {
format!("{path_prefix}/{subdir_name}").into()
}
.as_ref(),
)
.await?;
for (full_path, &entrypoint) in map.iter() {
match entrypoint {
Entrypoint::AppPage { loader_tree } => {
if current_level_is_parallel_route {
add_app_page(app_dir, &mut result, full_path.clone(), loader_tree).await?;
} else {
let key = parallel_route_key.unwrap_or("children").to_string();
let child_loader_tree = LoaderTree {
segment: directory_name.to_string(),
parallel_routes: indexmap! {
key => loader_tree,
},
components: components.without_leafs().cell(),
}
.cell();
add_app_page(app_dir, &mut result, full_path.clone(), child_loader_tree)
.await?;
}
}
Entrypoint::AppRoute { path } => {
add_app_route(app_dir, &mut result, full_path.clone(), path).await?;
}
}
}
}
Ok(EntrypointsVc::cell(result))
}
#[turbo_tasks::value(shared)]
struct AppStructureIssue {
struct DirectoryTreeIssue {
pub severity: IssueSeverityVc,
pub path: FileSystemPathVc,
pub app_dir: FileSystemPathVc,
pub message: StringVc,
}
#[turbo_tasks::value_impl]
impl Issue for AppStructureIssue {
impl Issue for DirectoryTreeIssue {
#[turbo_tasks::function]
fn severity(&self) -> IssueSeverityVc {
self.severity
@ -358,7 +604,7 @@ impl Issue for AppStructureIssue {
#[turbo_tasks::function]
fn context(&self) -> FileSystemPathVc {
self.path
self.app_dir
}
#[turbo_tasks::function]

View file

@ -3,7 +3,7 @@ use indexmap::IndexMap;
use mime::{APPLICATION_JAVASCRIPT_UTF_8, APPLICATION_JSON};
use serde::Serialize;
use turbo_binding::{
turbo::tasks_fs::File,
turbo::{tasks::TryJoinIterExt, tasks_fs::File},
turbopack::{
core::asset::AssetContentVc,
dev_server::source::{
@ -18,7 +18,6 @@ use turbo_binding::{
use turbo_tasks::{
graph::{GraphTraversal, NonDeterministic},
primitives::{StringVc, StringsVc},
TryJoinIterExt,
};
use crate::{

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Result};
use turbo_binding::{
turbo::tasks_fs::FileSystemPathVc,
turbo::{tasks::ValueToString, tasks_fs::FileSystemPathVc},
turbopack::{
core::{
asset::{Asset, AssetVc, AssetsVc},
@ -14,7 +14,6 @@ use turbo_binding::{
},
},
};
use turbo_tasks::ValueToString;
#[turbo_tasks::value(shared)]
pub enum RuntimeEntry {

View file

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{bail, Result};
use indexmap::indexmap;
use turbo_binding::turbopack::{
core::{
@ -8,8 +8,8 @@ use turbo_binding::turbopack::{
context::AssetContext,
},
ecmascript::{
EcmascriptInputTransform, EcmascriptInputTransformsVc, EcmascriptModuleAssetType,
EcmascriptModuleAssetVc, InnerAssetsVc,
chunk::EcmascriptChunkPlaceableVc, EcmascriptInputTransform, EcmascriptInputTransformsVc,
EcmascriptModuleAssetType, EcmascriptModuleAssetVc, InnerAssetsVc,
},
turbopack::{
ecmascript::chunk_group_files_asset::ChunkGroupFilesAsset,
@ -71,32 +71,36 @@ impl Transition for NextClientTransition {
asset: AssetVc,
context: ModuleAssetContextVc,
) -> Result<AssetVc> {
let internal_asset = if self.is_app {
next_asset("entry/app/hydrate.tsx")
} else {
next_asset("entry/next-hydrate.tsx")
};
let asset = if !self.is_app {
let internal_asset = next_asset("entry/next-hydrate.tsx");
let asset = EcmascriptModuleAssetVc::new_with_inner_assets(
internal_asset,
context.into(),
Value::new(EcmascriptModuleAssetType::Typescript),
EcmascriptInputTransformsVc::cell(vec![
EcmascriptInputTransform::TypeScript {
use_define_for_class_fields: false,
},
EcmascriptInputTransform::React {
refresh: false,
import_source: OptionStringVc::cell(None),
runtime: OptionStringVc::cell(None),
},
]),
Default::default(),
context.compile_time_info(),
InnerAssetsVc::cell(indexmap! {
"PAGE".to_string() => asset
}),
);
EcmascriptModuleAssetVc::new_with_inner_assets(
internal_asset,
context.into(),
Value::new(EcmascriptModuleAssetType::Typescript),
EcmascriptInputTransformsVc::cell(vec![
EcmascriptInputTransform::TypeScript {
use_define_for_class_fields: false,
},
EcmascriptInputTransform::React {
refresh: false,
import_source: OptionStringVc::cell(None),
runtime: OptionStringVc::cell(None),
},
]),
Default::default(),
context.compile_time_info(),
InnerAssetsVc::cell(indexmap! {
"PAGE".to_string() => asset
}),
)
.into()
} else {
let Some(asset) = EcmascriptChunkPlaceableVc::resolve_from(asset).await? else {
bail!("Not an ecmascript module");
};
asset
};
let runtime_entries = self.runtime_entries.resolve_entries(context.into());

View file

@ -1,7 +1,7 @@
use anyhow::{bail, Result};
use indoc::formatdoc;
use turbo_binding::{
turbo::tasks_fs::FileSystemPathVc,
turbo::{tasks::TryJoinIterExt, tasks_fs::FileSystemPathVc},
turbopack::{
core::{
asset::{Asset, AssetContentVc, AssetVc},
@ -24,7 +24,7 @@ use turbo_binding::{
},
},
};
use turbo_tasks::{primitives::StringVc, TryJoinIterExt, Value};
use turbo_tasks::{primitives::StringVc, Value};
#[turbo_tasks::function]
fn modifier() -> StringVc {
StringVc::cell("chunks".to_string())

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use turbo_tasks::{
use turbo_binding::turbo::tasks::{
primitives::{StringVc, StringsVc},
trace::TraceRawVcs,
};

View file

@ -1,7 +1,7 @@
use anyhow::{anyhow, Context, Result};
use indexmap::{indexset, IndexMap, IndexSet};
use serde::{Deserialize, Serialize};
use turbo_tasks::{primitives::StringVc, trace::TraceRawVcs, Value};
use turbo_binding::turbo::tasks::{primitives::StringVc, trace::TraceRawVcs, Value};
use super::request::{NextFontRequest, OneOrManyStrings};

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use turbo_tasks::primitives::{OptionStringVc, StringVc};
use turbo_binding::turbo::tasks::primitives::{OptionStringVc, StringVc};
use super::FontCssPropertiesVc;
use crate::next_font::{

View file

@ -3,8 +3,10 @@ use allsorts::{
Font,
};
use anyhow::{bail, Context, Result};
use turbo_tasks::primitives::{StringVc, StringsVc, U32Vc};
use turbo_tasks_fs::{FileContent, FileSystemPathVc};
use turbo_binding::turbo::{
tasks::primitives::{StringVc, StringsVc, U32Vc},
tasks_fs::{FileContent, FileSystemPathVc},
};
use super::{
options::{FontDescriptor, FontDescriptors, FontWeight, NextFontLocalOptionsVc},

View file

@ -1,22 +1,26 @@
use anyhow::{bail, Context, Result};
use indoc::formatdoc;
use turbo_binding::turbopack::core::{
resolve::{
options::{
ImportMapResult, ImportMapResultVc, ImportMapping, ImportMappingReplacement,
ImportMappingReplacementVc, ImportMappingVc,
use turbo_binding::{
turbo::{
tasks::{
primitives::{OptionStringVc, U32Vc},
Value,
},
parse::{Request, RequestVc},
pattern::QueryMapVc,
ResolveResult,
tasks_fs::{json::parse_json_with_source_context, FileContent, FileSystemPathVc},
},
turbopack::core::{
resolve::{
options::{
ImportMapResult, ImportMapResultVc, ImportMapping, ImportMappingReplacement,
ImportMappingReplacementVc, ImportMappingVc,
},
parse::{Request, RequestVc},
pattern::QueryMapVc,
ResolveResult,
},
virtual_asset::VirtualAssetVc,
},
virtual_asset::VirtualAssetVc,
};
use turbo_tasks::{
primitives::{OptionStringVc, U32Vc},
Value,
};
use turbo_tasks_fs::{json::parse_json_with_source_context, FileContent, FileSystemPathVc};
use self::{
font_fallback::get_font_fallbacks,

View file

@ -2,7 +2,7 @@ use std::{fmt::Display, str::FromStr};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use turbo_tasks::{primitives::StringVc, trace::TraceRawVcs, Value};
use turbo_binding::turbo::tasks::{primitives::StringVc, trace::TraceRawVcs, Value};
use super::request::{
AdjustFontFallback, NextFontLocalRequest, NextFontLocalRequestArguments, SrcDescriptor,
@ -173,7 +173,7 @@ pub(super) fn options_from_request(request: &NextFontLocalRequest) -> Result<Nex
#[cfg(test)]
mod tests {
use anyhow::Result;
use turbo_tasks_fs::json::parse_json_with_source_context;
use turbo_binding::turbo::tasks_fs::json::parse_json_with_source_context;
use super::{options_from_request, NextFontLocalOptions};
use crate::next_font::local::{

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use turbo_tasks::trace::TraceRawVcs;
use turbo_binding::turbo::tasks::trace::TraceRawVcs;
/// The top-most structure encoded into the query param in requests to
/// `next/font/local` generated by the next/font swc transform. e.g.

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Result};
use indoc::formatdoc;
use turbo_tasks::primitives::{StringVc, U32Vc};
use turbo_binding::turbo::tasks::primitives::{StringVc, U32Vc};
use super::options::{FontDescriptors, NextFontLocalOptionsVc};
use crate::next_font::{

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use turbo_tasks::primitives::{StringVc, U32Vc};
use turbo_binding::turbo::tasks::primitives::{StringVc, U32Vc};
use super::options::NextFontLocalOptionsVc;
use crate::next_font::{

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use indoc::formatdoc;
use turbo_tasks::primitives::StringVc;
use turbo_binding::turbo::tasks::primitives::StringVc;
use super::{
font_fallback::{FontFallback, FontFallbacksVc},

View file

@ -1,6 +1,8 @@
use anyhow::{bail, Result};
use turbo_binding::turbopack::node::route_matcher::{ParamsVc, RouteMatcher, RouteMatcherVc};
use turbo_tasks::primitives::{BoolVc, StringVc};
use turbo_binding::{
turbo::tasks::primitives::{BoolVc, StringVc},
turbopack::node::route_matcher::{ParamsVc, RouteMatcher, RouteMatcherVc},
};
use self::{
all::AllMatch,

View file

@ -1,7 +1,9 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use turbo_binding::turbopack::node::route_matcher::{Param, ParamsVc, RouteMatcher};
use turbo_tasks::primitives::{BoolVc, Regex};
use turbo_binding::{
turbo::tasks::primitives::{BoolVc, Regex},
turbopack::node::route_matcher::{Param, ParamsVc, RouteMatcher},
};
/// A regular expression that matches a path, with named capture groups for the
/// dynamic parts of the path.

View file

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize};
use turbo_binding::turbopack::node::route_matcher::{ParamsVc, RouteMatcher};
use turbo_tasks::primitives::BoolVc;
use turbo_binding::{
turbo::tasks::primitives::BoolVc,
turbopack::node::route_matcher::{ParamsVc, RouteMatcher},
};
/// A composite route matcher that matches a path if it has a given prefix and
/// suffix.

View file

@ -15,7 +15,7 @@ use turbo_binding::turbopack::{
use turbo_tasks::{primitives::StringVc, CompletionVc, CompletionsVc, Value};
use crate::{
app_structure::OptionAppStructureVc,
app_structure::OptionAppDirVc,
next_config::NextConfigVc,
pages_structure::OptionPagesStructureVc,
router::{route, RouterRequest, RouterResult},
@ -28,7 +28,7 @@ pub struct NextRouterContentSource {
execution_context: ExecutionContextVc,
next_config: NextConfigVc,
server_addr: ServerAddrVc,
app_structure: OptionAppStructureVc,
app_dir: OptionAppDirVc,
pages_structure: OptionPagesStructureVc,
}
@ -40,7 +40,7 @@ impl NextRouterContentSourceVc {
execution_context: ExecutionContextVc,
next_config: NextConfigVc,
server_addr: ServerAddrVc,
app_structure: OptionAppStructureVc,
app_dir: OptionAppDirVc,
pages_structure: OptionPagesStructureVc,
) -> NextRouterContentSourceVc {
NextRouterContentSource {
@ -48,7 +48,7 @@ impl NextRouterContentSourceVc {
execution_context,
next_config,
server_addr,
app_structure,
app_dir,
pages_structure,
}
.cell()
@ -74,11 +74,12 @@ fn need_data(source: ContentSourceVc, path: &str) -> ContentSourceResultVc {
#[turbo_tasks::function]
fn routes_changed(
app_structure: OptionAppStructureVc,
app_dir: OptionAppDirVc,
pages_structure: OptionPagesStructureVc,
next_config: NextConfigVc,
) -> CompletionVc {
CompletionsVc::all(vec![
app_structure.routes_changed(),
app_dir.routes_changed(next_config),
pages_structure.routes_changed(),
])
}
@ -96,7 +97,7 @@ impl ContentSource for NextRouterContentSource {
// The next-dev server can currently run against projects as simple as
// `index.js`. If this isn't a Next.js project, don't try to use the Next.js
// router.
if this.app_structure.await?.is_none() && this.pages_structure.await?.is_none() {
if this.app_dir.await?.is_none() && this.pages_structure.await?.is_none() {
return Ok(this
.inner
.get(path, Value::new(ContentSourceData::default())));
@ -124,7 +125,7 @@ impl ContentSource for NextRouterContentSource {
request,
this.next_config,
this.server_addr,
routes_changed(this.app_structure, this.pages_structure),
routes_changed(this.app_dir, this.pages_structure, this.next_config),
);
let res = res

View file

@ -21,7 +21,7 @@ use tokio::{
runtime::Runtime,
time::{sleep, timeout},
};
use turbo_tasks::util::FormatDuration;
use turbo_binding::turbo::tasks::util::FormatDuration;
use util::{
build_test, create_browser,
env::{read_env, read_env_bool, read_env_list},

View file

@ -19,7 +19,7 @@ use anyhow::{Context, Result};
use devserver_options::DevServerOptions;
use dunce::canonicalize;
use next_core::{
app_structure::find_app_structure, create_app_source, create_page_source,
app_structure::find_app_dir_if_enabled, create_app_source, create_page_source,
create_web_entry_source, env::load_env, manifest::DevManifestContentSource,
next_config::load_next_config, next_image::NextImageContentSourceVc,
pages_structure::find_pages_structure, router_source::NextRouterContentSourceVc,
@ -332,9 +332,9 @@ async fn source(
next_config,
server_addr,
);
let app_structure = find_app_structure(project_path, dev_server_root, next_config);
let app_dir = find_app_dir_if_enabled(project_path, next_config);
let app_source = create_app_source(
app_structure,
app_dir,
project_path,
execution_context,
output_root.join("app"),
@ -381,7 +381,7 @@ async fn source(
execution_context,
next_config,
server_addr,
app_structure,
app_dir,
pages_structure,
)
.into();

View file

@ -1070,7 +1070,7 @@ export default async function build(
})
)
let turboTasks: unknown
let turboTasksForTrace: unknown
async function runTurbotrace(staticPages: Set<string>) {
if (!turbotraceContext) {
@ -1082,7 +1082,7 @@ export default async function build(
) {
let turbotraceOutputPath: string | undefined
let turbotraceFiles: string[] | undefined
turboTasks = binding.turbo.createTurboTasks(
turboTasksForTrace = binding.turbo.createTurboTasks(
(config.experimental.turbotrace?.memoryLimit ??
TURBO_TRACE_DEFAULT_MEMORY_LIMIT) *
1024 *
@ -1100,7 +1100,7 @@ export default async function build(
} = entriesTrace
const depModSet = new Set(depModArray)
const filesTracedInEntries: string[] =
await binding.turbo.startTrace(action, turboTasks)
await binding.turbo.startTrace(action, turboTasksForTrace)
const { contextDirectory, input: entriesToTrace } = action
@ -1145,7 +1145,7 @@ export default async function build(
)
)
})
await binding.turbo.startTrace(action, turboTasks)
await binding.turbo.startTrace(action, turboTasksForTrace)
if (turbotraceOutputPath && turbotraceFiles) {
const existedNftFile = await promises
.readFile(turbotraceOutputPath, 'utf8')
@ -2033,7 +2033,7 @@ export default async function build(
logDetail: config.experimental.turbotrace.logDetail,
showAll: config.experimental.turbotrace.logAll,
},
turboTasks
turboTasksForTrace
)
for (const file of files) {
if (!ignoreFn(path.join(traceContext, file))) {

View file

@ -293,6 +293,34 @@ async function loadWasm(importPath = '', isCustomTurbopack: boolean) {
startTrace: () => {
Log.error('Wasm binding does not support trace yet')
},
entrypoints: {
stream: (
turboTasks: any,
rootDir: string,
applicationDir: string,
pageExtensions: string[]
) => {
return bindings.streamEntrypoints(
turboTasks,
rootDir,
applicationDir,
pageExtensions
)
},
get: (
turboTasks: any,
rootDir: string,
applicationDir: string,
pageExtensions: string[]
) => {
return bindings.getEntrypoints(
turboTasks,
rootDir,
applicationDir,
pageExtensions
)
},
},
},
mdx: {
compile: (src: string, options: any) =>
@ -524,6 +552,34 @@ function loadNative(isCustomTurbopack = false) {
},
createTurboTasks: (memoryLimit?: number): unknown =>
bindings.createTurboTasks(memoryLimit),
entrypoints: {
stream: (
turboTasks: any,
rootDir: string,
applicationDir: string,
pageExtensions: string[]
) => {
return bindings.streamEntrypoints(
turboTasks,
rootDir,
applicationDir,
pageExtensions
)
},
get: (
turboTasks: any,
rootDir: string,
applicationDir: string,
pageExtensions: string[]
) => {
return bindings.getEntrypoints(
turboTasks,
rootDir,
applicationDir,
pageExtensions
)
},
},
},
mdx: {
compile: (src: string, options: any) =>