From c8862e3c93d9b444b0cf9f16770284e47470947f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:13:28 +0200 Subject: [PATCH 01/10] Try to run test --- .github/workflows/build_and_test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ab1ce6a55b37f..7b172febda96d 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -705,10 +705,11 @@ jobs: afterBuild: | export NEXT_TEST_MODE=start - node run-tests.js \ + node run-tests.js --type production \ test/e2e/app-dir/app/index.test.ts \ test/e2e/app-dir/app-edge/app-edge.test.ts \ - test/e2e/app-dir/metadata-edge/index.test.ts + test/e2e/app-dir/metadata-edge/index.test.ts \ + test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts stepName: 'test-prod-windows' runs_on_labels: '["windows","self-hosted","x64"]' buildNativeTarget: 'x86_64-pc-windows-msvc' From 54013dc3e87f0d34d5ca88aac089d42500a2ba48 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:13:34 +0200 Subject: [PATCH 02/10] Fix --- crates/napi/src/next_api/project.rs | 7 ++- crates/next-api/src/project.rs | 46 +++++++++---------- packages/next/src/build/swc/types.ts | 9 ++-- .../next/src/build/turbopack-build/impl.ts | 4 +- .../webpack/loaders/css-loader/src/utils.ts | 5 +- packages/next/src/lib/normalize-path.ts | 5 ++ .../src/server/dev/hot-reloader-turbopack.ts | 13 +++--- 7 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 packages/next/src/lib/normalize-path.ts diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 2d045a831d017..b36fe0c02042a 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -392,7 +392,9 @@ pub fn project_new( let subscriber = subscriber.with(FilterLayer::try_new(&trace).unwrap()); - let internal_dir = PathBuf::from(&options.project_path).join(&options.dist_dir); + let internal_dir = PathBuf::from(&options.root_path) + .join(&options.project_path) + .join(&options.dist_dir); std::fs::create_dir_all(&internal_dir) .context("Unable to create .next directory") .unwrap(); @@ -1426,7 +1428,8 @@ pub async fn get_source_map_rope( let Some(chunk_base) = file.strip_prefix( &(format!( - "{}/{}/", + "{}/{}/{}", + container.project().await?.root_path, container.project().await?.project_path, container.project().dist_dir().await? )), diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index b1347fd2ef67d..ab10ec4df5533 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -1,4 +1,4 @@ -use std::{path::MAIN_SEPARATOR, time::Duration}; +use std::time::Duration; use anyhow::{Context, Result, bail}; use indexmap::map::Entry; @@ -36,10 +36,7 @@ use turbo_tasks::{ trace::TraceRawVcs, }; use turbo_tasks_env::{EnvMap, ProcessEnv}; -use turbo_tasks_fs::{ - DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, get_relative_path_to, - invalidation, -}; +use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, invalidation}; use turbopack::{ ModuleAssetContext, evaluate_context::node_build_environment, global_module_ids::get_global_module_id_strategy, transition::TransitionOptions, @@ -149,11 +146,13 @@ pub struct WatchOptions { )] #[serde(rename_all = "camelCase")] pub struct ProjectOptions { - /// A root path from which all files must be nested under. Trying to access - /// a file outside this root will fail. Think of this as a chroot. + /// An absolute root path from which all files must be nested under. Trying to access + /// a file outside this root will fail, so think of this as a chroot. + /// E.g. `/home/user/projects/my-repo`. pub root_path: RcStr, - /// A path inside the root_path which contains the app/pages directories. + /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// E.g. `apps/my-app` pub project_path: RcStr, /// The contents of next.config.js, serialized to JSON. @@ -538,16 +537,20 @@ impl ProjectContainer { #[turbo_tasks::value] pub struct Project { - /// A root path from which all files must be nested under. Trying to access - /// a file outside this root will fail. Think of this as a chroot. - root_path: RcStr, - - /// A path where to emit the build outputs. next.config.js's distDir. - dist_dir: RcStr, + /// An absolute root path from which all files must be nested under. Trying to access + /// a file outside this root will fail, so think of this as a chroot. + /// E.g. `/home/user/projects/my-repo`. + pub root_path: RcStr, - /// A path inside the root_path which contains the app/pages directories. + /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// E.g. `apps/my-app` pub project_path: RcStr, + /// A path where to emit the build outputs, relative to [`Project::project_path`]. + /// Corresponds to next.config.js's `distDir`. + /// E.g. `.next` + dist_dir: RcStr, + /// Filesystem watcher options. watch: WatchOptions, @@ -692,14 +695,12 @@ impl Project { #[turbo_tasks::function] pub async fn node_root(self: Vc) -> Result> { let this = self.await?; - let relative_from_root_to_project_path = - get_relative_path_to(&this.root_path, &this.project_path); Ok(self .output_fs() .root() .await? - .join(&relative_from_root_to_project_path)? - .join(&this.dist_dir.clone())? + .join(&this.project_path)? + .join(&this.dist_dir)? .cell()) } @@ -742,12 +743,7 @@ impl Project { pub async fn project_path(self: Vc) -> Result> { let this = self.await?; let root = self.project_root_path().await?; - let project_relative = this.project_path.strip_prefix(&*this.root_path).unwrap(); - let project_relative = project_relative - .strip_prefix(MAIN_SEPARATOR) - .unwrap_or(project_relative) - .replace(MAIN_SEPARATOR, "/"); - Ok(root.join(&project_relative)?.cell()) + Ok(root.join(&this.project_path)?.cell()) } #[turbo_tasks::function] diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 5c3f074d1204d..382653c0d0d96 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -346,13 +346,14 @@ export type WrittenEndpoint = export interface ProjectOptions { /** - * A root path from which all files must be nested under. Trying to access - * a file outside this root will fail. Think of this as a chroot. - */ + * An absolute root path from which all files must be nested under. Trying to access + * a file outside this root will fail, so think of this as a chroot. + * E.g. `/home/user/projects/my-repo`. */ rootPath: string /** - * A path inside the root_path which contains the app/pages directories. + * A path which contains the app/pages directories, relative to [`Project::root_path`]. + * E.g. `apps/my-app` */ projectPath: string diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index 4f046d7f01e56..391c173ea666a 100644 --- a/packages/next/src/build/turbopack-build/impl.ts +++ b/packages/next/src/build/turbopack-build/impl.ts @@ -22,6 +22,7 @@ import { setGlobal } from '../../trace' import { isCI } from '../../server/ci-info' import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events' import { getSupportedBrowsers } from '../utils' +import { normalizePath } from '../../lib/normalize-path' export async function turbopackBuild(): Promise<{ duration: number @@ -52,10 +53,11 @@ export async function turbopackBuild(): Promise<{ const supportedBrowsers = getSupportedBrowsers(dir, dev) const persistentCaching = isPersistentCachingEnabled(config) + const rootPath = config.turbopack?.root || config.outputFileTracingRoot || dir const project = await bindings.turbo.createProject( { - projectPath: dir, rootPath: config.turbopack?.root || config.outputFileTracingRoot || dir, + projectPath: normalizePath(path.relative(rootPath, dir)), distDir, nextConfig: config, jsConfig: await getTurbopackJsConfig(dir, config), diff --git a/packages/next/src/build/webpack/loaders/css-loader/src/utils.ts b/packages/next/src/build/webpack/loaders/css-loader/src/utils.ts index 5bea07bcf7a2c..aeb1487cc5e89 100644 --- a/packages/next/src/build/webpack/loaders/css-loader/src/utils.ts +++ b/packages/next/src/build/webpack/loaders/css-loader/src/utils.ts @@ -11,6 +11,7 @@ import localByDefault from 'next/dist/compiled/postcss-modules-local-by-default' import extractImports from 'next/dist/compiled/postcss-modules-extract-imports' import modulesScope from 'next/dist/compiled/postcss-modules-scope' import camelCase from './camelcase' +import { normalizePath } from '../../../../../lib/normalize-path' const whitespace = '[\\x20\\t\\r\\n\\f]' const unescapeRegExp = new RegExp( @@ -39,10 +40,6 @@ function unescape(str: string) { }) } -function normalizePath(file: string) { - return path.sep === '\\' ? file.replace(/\\/g, '/') : file -} - function fixedEncodeURIComponent(str: string) { return str.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16)}`) } diff --git a/packages/next/src/lib/normalize-path.ts b/packages/next/src/lib/normalize-path.ts new file mode 100644 index 0000000000000..4f9e91f4242f8 --- /dev/null +++ b/packages/next/src/lib/normalize-path.ts @@ -0,0 +1,5 @@ +import path from 'path' + +export function normalizePath(file: string) { + return path.sep === '\\' ? file.replace(/\\/g, '/') : file +} diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index eec77918c7144..b3c66ed74b8e3 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -1,6 +1,6 @@ import type { Socket } from 'net' import { mkdir, writeFile } from 'fs/promises' -import { join, extname } from 'path' +import { join, extname, relative } from 'path' import { pathToFileURL } from 'url' import ws from 'next/dist/compiled/ws' @@ -209,13 +209,14 @@ export async function createHotReloaderTurbopack( const supportedBrowsers = getSupportedBrowsers(projectPath, dev) const currentNodeJsVersion = process.versions.node + const rootPath = + opts.nextConfig.turbopack?.root || + opts.nextConfig.outputFileTracingRoot || + projectPath const project = await bindings.turbo.createProject( { - projectPath: projectPath, - rootPath: - opts.nextConfig.turbopack?.root || - opts.nextConfig.outputFileTracingRoot || - projectPath, + rootPath, + projectPath: relative(rootPath, projectPath), distDir, nextConfig: opts.nextConfig, jsConfig: await getTurbopackJsConfig(projectPath, nextConfig), From 3b9dbbdd0c23294ecda92cc16e76e0a4b0fa1bf9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:01:09 +0200 Subject: [PATCH 03/10] Fixup --- crates/napi/src/next_api/project.rs | 11 +++------ crates/next-api/src/project.rs | 24 +++++++++++++++---- .../next/src/build/turbopack-build/impl.ts | 2 +- .../src/server/dev/hot-reloader-turbopack.ts | 3 ++- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index b36fe0c02042a..0ff0fa94c47a6 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -1426,14 +1426,9 @@ pub async fn get_source_map_rope( Err(_) => (file_path.to_string(), None), }; - let Some(chunk_base) = file.strip_prefix( - &(format!( - "{}/{}/{}", - container.project().await?.root_path, - container.project().await?.project_path, - container.project().dist_dir().await? - )), - ) else { + let Some(chunk_base) = + file.strip_prefix(container.project().dist_dir_absolute().await?.as_str()) + else { // File doesn't exist within the dist dir return Ok(OptionStringifiedSourceMap::none()); }; diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index ab10ec4df5533..fb92aeca69210 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -36,7 +36,10 @@ use turbo_tasks::{ trace::TraceRawVcs, }; use turbo_tasks_env::{EnvMap, ProcessEnv}; -use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, invalidation}; +use turbo_tasks_fs::{ + DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, invalidation, + util::{join_path, unix_to_sys}, +}; use turbopack::{ ModuleAssetContext, evaluate_context::node_build_environment, global_module_ids::get_global_module_id_strategy, transition::TransitionOptions, @@ -540,11 +543,11 @@ pub struct Project { /// An absolute root path from which all files must be nested under. Trying to access /// a file outside this root will fail, so think of this as a chroot. /// E.g. `/home/user/projects/my-repo`. - pub root_path: RcStr, + root_path: RcStr, /// A path which contains the app/pages directories, relative to [`Project::root_path`]. /// E.g. `apps/my-app` - pub project_path: RcStr, + project_path: RcStr, /// A path where to emit the build outputs, relative to [`Project::project_path`]. /// Corresponds to next.config.js's `distDir`. @@ -688,8 +691,19 @@ impl Project { } #[turbo_tasks::function] - pub fn dist_dir(&self) -> Vc { - Vc::cell(self.dist_dir.clone()) + pub fn dist_dir_absolute(&self) -> Result> { + Ok(Vc::cell( + format!( + "{}{}{}", + self.root_path, + std::path::MAIN_SEPARATOR, + unix_to_sys( + &join_path(&self.project_path, &self.dist_dir) + .context("expected project_path to be inside of root_path")? + ) + ) + .into(), + )) } #[turbo_tasks::function] diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index 391c173ea666a..b2e86aceea4d6 100644 --- a/packages/next/src/build/turbopack-build/impl.ts +++ b/packages/next/src/build/turbopack-build/impl.ts @@ -57,7 +57,7 @@ export async function turbopackBuild(): Promise<{ const project = await bindings.turbo.createProject( { rootPath: config.turbopack?.root || config.outputFileTracingRoot || dir, - projectPath: normalizePath(path.relative(rootPath, dir)), + projectPath: normalizePath(path.relative(rootPath, dir) || '.'), distDir, nextConfig: config, jsConfig: await getTurbopackJsConfig(dir, config), diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index b3c66ed74b8e3..2bb80d8744965 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -99,6 +99,7 @@ import { getRestartDevServerMiddleware } from '../../next-devtools/server/restar import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events' import { getSupportedBrowsers } from '../../build/utils' import { receiveBrowserLogsTurbopack } from './browser-logs/receive-logs' +import { normalizePath } from '../../lib/normalize-path' const wsServer = new ws.Server({ noServer: true }) const isTestMode = !!( @@ -216,7 +217,7 @@ export async function createHotReloaderTurbopack( const project = await bindings.turbo.createProject( { rootPath, - projectPath: relative(rootPath, projectPath), + projectPath: normalizePath(relative(rootPath, projectPath) || '.'), distDir, nextConfig: opts.nextConfig, jsConfig: await getTurbopackJsConfig(projectPath, nextConfig), From ce08801e09a08d3a613760c33c827db1410ee0d9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:02:12 +0200 Subject: [PATCH 04/10] Add comment --- turbopack/crates/turbo-tasks-fs/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/turbopack/crates/turbo-tasks-fs/src/lib.rs b/turbopack/crates/turbo-tasks-fs/src/lib.rs index a6feed450ecc8..83d718a52b1b2 100644 --- a/turbopack/crates/turbo-tasks-fs/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fs/src/lib.rs @@ -997,6 +997,7 @@ impl ValueToString for DiskFileSystem { } } +/// Note: this only works for Unix-style paths (with `/` as a separator). pub fn get_relative_path_to(path: &str, other_path: &str) -> String { fn split(s: &str) -> impl Iterator { let empty = s.is_empty(); From 754e1fe8e19c47a8d8baba959c23456c0abaab51 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:10:23 +0200 Subject: [PATCH 05/10] Sync comments --- crates/napi/src/next_api/project.rs | 26 +++++++++------- .../next/src/build/swc/generated-native.d.ts | 30 ++++++++++++------- packages/next/src/build/swc/types.ts | 9 ++++-- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 0ff0fa94c47a6..b8b8c42b66be2 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -126,15 +126,18 @@ pub struct NapiWatchOptions { #[napi(object)] pub struct NapiProjectOptions { - /// A root path from which all files must be nested under. Trying to access - /// a file outside this root will fail. Think of this as a chroot. + /// An absolute root path from which all files must be nested under. Trying to access + /// a file outside this root will fail, so think of this as a chroot. + /// E.g. `/home/user/projects/my-repo`. pub root_path: RcStr, - /// A path inside the root_path which contains the app/pages directories. + /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// E.g. `apps/my-app` pub project_path: RcStr, - /// next.config's distDir. Project initialization occurs earlier than - /// deserializing next.config, so passing it as separate option. + /// A path where to emit the build outputs, relative to [`Project::project_path`]. + /// Corresponds to next.config.js's `distDir`. + /// E.g. `.next` pub dist_dir: RcStr, /// Filesystem watcher options. @@ -180,15 +183,18 @@ pub struct NapiProjectOptions { /// [NapiProjectOptions] with all fields optional. #[napi(object)] pub struct NapiPartialProjectOptions { - /// A root path from which all files must be nested under. Trying to access - /// a file outside this root will fail. Think of this as a chroot. + /// An absolute root path from which all files must be nested under. Trying to access + /// a file outside this root will fail, so think of this as a chroot. + /// E.g. `/home/user/projects/my-repo`. pub root_path: Option, - /// A path inside the root_path which contains the app/pages directories. + /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// E.g. `apps/my-app` pub project_path: Option, - /// next.config's distDir. Project initialization occurs earlier than - /// deserializing next.config, so passing it as separate option. + /// A path where to emit the build outputs, relative to [`Project::project_path`]. + /// Corresponds to next.config.js's `distDir`. + /// E.g. `.next` pub dist_dir: Option>, /// Filesystem watcher options. diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index 85bf4c77681ec..539d828eeba91 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -100,15 +100,20 @@ export interface NapiWatchOptions { } export interface NapiProjectOptions { /** - * A root path from which all files must be nested under. Trying to access - * a file outside this root will fail. Think of this as a chroot. + * An absolute root path from which all files must be nested under. Trying to access + * a file outside this root will fail, so think of this as a chroot. + * E.g. `/home/user/projects/my-repo`. */ rootPath: RcStr - /** A path inside the root_path which contains the app/pages directories. */ + /** + * A path which contains the app/pages directories, relative to [`Project::root_path`]. + * E.g. `apps/my-app` + */ projectPath: RcStr /** - * next.config's distDir. Project initialization occurs earlier than - * deserializing next.config, so passing it as separate option. + * A path where to emit the build outputs, relative to [`Project::project_path`]. + * Corresponds to next.config.js's `distDir`. + * E.g. `.next` */ distDir: RcStr /** Filesystem watcher options. */ @@ -146,15 +151,20 @@ export interface NapiProjectOptions { /** [NapiProjectOptions] with all fields optional. */ export interface NapiPartialProjectOptions { /** - * A root path from which all files must be nested under. Trying to access - * a file outside this root will fail. Think of this as a chroot. + * An absolute root path from which all files must be nested under. Trying to access + * a file outside this root will fail, so think of this as a chroot. + * E.g. `/home/user/projects/my-repo`. */ rootPath?: RcStr - /** A path inside the root_path which contains the app/pages directories. */ + /** + * A path which contains the app/pages directories, relative to [`Project::root_path`]. + * E.g. `apps/my-app` + */ projectPath?: RcStr /** - * next.config's distDir. Project initialization occurs earlier than - * deserializing next.config, so passing it as separate option. + * A path where to emit the build outputs, relative to [`Project::project_path`]. + * Corresponds to next.config.js's `distDir`. + * E.g. `.next` */ distDir?: RcStr | undefined | null /** Filesystem watcher options. */ diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 382653c0d0d96..2ccf4a16db543 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -348,17 +348,20 @@ export interface ProjectOptions { /** * An absolute root path from which all files must be nested under. Trying to access * a file outside this root will fail, so think of this as a chroot. - * E.g. `/home/user/projects/my-repo`. */ + * E.g. `/home/user/projects/my-repo`. + */ rootPath: string /** - * A path which contains the app/pages directories, relative to [`Project::root_path`]. + * A path which contains the app/pages directories, relative to `root_path`. * E.g. `apps/my-app` */ projectPath: string /** - * The path to the .next directory. + * A path where to emit the build outputs, relative to [`Project::project_path`]. + * Corresponds to next.config.js's `distDir`. + * E.g. `.next` */ distDir: string From e0894b4cc7a38851780009a59443dd4bbf8c20c3 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:50:40 +0200 Subject: [PATCH 06/10] Fix test --- crates/next-api/src/project.rs | 11 +++-------- test/development/basic/next-rs-api.test.ts | 21 ++++++++------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index fb92aeca69210..47ce6f3d96f63 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -742,15 +742,10 @@ impl Project { } #[turbo_tasks::function] - pub async fn node_root_to_root_path(self: Vc) -> Result> { - let this = self.await?; - let output_root_to_root_path = self - .project_path() - .await? - .join(&this.dist_dir.clone())? - .get_relative_path_to(&*self.project_root_path().await?) + pub async fn node_root_to_root_path(&self) -> Result> { + let output_root_to_root_path = join_path(&self.project_path, &self.dist_dir) .context("Project path need to be in root path")?; - Ok(Vc::cell(output_root_to_root_path)) + Ok(Vc::cell(output_root_to_root_path.into())) } #[turbo_tasks::function] diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index 714b8b1cf8c37..31785cd9444e4 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -187,26 +187,21 @@ describe('next.rs api', () => { let project: Project let projectUpdateSubscription: AsyncIterableIterator beforeAll(async () => { - console.log(next.testDir) const nextConfig = await loadConfig(PHASE_DEVELOPMENT_SERVER, next.testDir) const bindings = await loadBindings() - const distDir = path.join( - process.env.NEXT_SKIP_ISOLATE - ? path.resolve(__dirname, '../../..') - : next.testDir, - '.next' - ) + const rootPath = process.env.NEXT_SKIP_ISOLATE + ? path.resolve(__dirname, '../../..') + : next.testDir + const distDir = '.next' project = await bindings.turbo.createProject({ env: {}, jsConfig: { compilerOptions: {}, }, nextConfig: nextConfig, - projectPath: next.testDir, + rootPath, + projectPath: path.relative(rootPath, next.testDir) || '.', distDir, - rootPath: process.env.NEXT_SKIP_ISOLATE - ? path.resolve(__dirname, '../../..') - : next.testDir, watch: { enable: true, }, @@ -217,7 +212,7 @@ describe('next.rs api', () => { clientRouterFilters: undefined, config: nextConfig, dev: true, - distDir: distDir, + distDir: path.join(rootPath, distDir), fetchCacheKeyPrefix: undefined, hasRewrites: false, middlewareMatchers: undefined, @@ -265,7 +260,7 @@ describe('next.rs api', () => { expect(normalizeDiagnostics(entrypoints.value.diagnostics)).toMatchSnapshot( 'diagnostics' ) - entrypointsSubscription.return() + await entrypointsSubscription.return() }) const routes = [ From 4a0a8fd2d6b29de60296d27781678a480906b0ac Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:23:19 +0200 Subject: [PATCH 07/10] Fix comment --- crates/next-api/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 47ce6f3d96f63..f4f15e2070737 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -154,7 +154,7 @@ pub struct ProjectOptions { /// E.g. `/home/user/projects/my-repo`. pub root_path: RcStr, - /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// A path which contains the app/pages directories, relative to [`ProjectOptions::root_path`]. /// E.g. `apps/my-app` pub project_path: RcStr, From 97d0ad2de521c734e1ae2613395727039d4d2210 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:39:44 +0200 Subject: [PATCH 08/10] Fix node_root_to_root_path --- crates/next-api/src/project.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index f4f15e2070737..67e667e16f816 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -741,11 +741,17 @@ impl Project { .cell()) } + /// Returns the relative path from the node root to the output root. + /// E.g. from `[project]/test/e2e/app-dir/non-root-project-monorepo/apps/web/app/ + /// import-meta-url-ssr/page.tsx` to `[project]/`. #[turbo_tasks::function] - pub async fn node_root_to_root_path(&self) -> Result> { - let output_root_to_root_path = join_path(&self.project_path, &self.dist_dir) - .context("Project path need to be in root path")?; - Ok(Vc::cell(output_root_to_root_path.into())) + pub async fn node_root_to_root_path(self: Vc) -> Result> { + Ok(Vc::cell( + self.node_root() + .await? + .get_relative_path_to(&*self.output_fs().root().await?) + .context("Expected node root to be inside of output fs")?, + )) } #[turbo_tasks::function] From f9ffa446405eeee0747064eba10c5c13ba9b722e Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:04:19 +0200 Subject: [PATCH 09/10] Fixup --- packages/next/src/build/swc/index.ts | 7 +++++-- .../next/src/server/lib/router-utils/setup-dev-bundler.ts | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 910e9c9fc84f3..0299b04ad697a 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -621,7 +621,7 @@ function bindingToApi( ...options, nextConfig: await serializeNextConfig( options.nextConfig, - options.projectPath! + path.join(options.rootPath, options.projectPath) ), jsConfig: JSON.stringify(options.jsConfig), env: rustifyEnv(options.env), @@ -635,7 +635,10 @@ function bindingToApi( ...options, nextConfig: options.nextConfig && - (await serializeNextConfig(options.nextConfig, options.projectPath!)), + (await serializeNextConfig( + options.nextConfig, + path.join(options.rootPath!, options.projectPath!) + )), jsConfig: options.jsConfig && JSON.stringify(options.jsConfig), env: options.env && rustifyEnv(options.env), } diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index a9c10bc0c9ec7..9fa2953c11a36 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -83,6 +83,7 @@ import { } from '../../../shared/lib/turbopack/utils' import { getDefineEnv } from '../../../build/define-env' import { TurbopackInternalError } from '../../../shared/lib/turbopack/internal-error' +import { normalizePath } from '../../../lib/normalize-path' export type SetupOpts = { renderServer: LazyRenderServerInstance @@ -664,6 +665,10 @@ async function startWatcher( opts.fsChecker.rewrites.beforeFiles.length > 0 || opts.fsChecker.rewrites.fallback.length > 0 + const rootPath = + opts.nextConfig.turbopack?.root || + opts.nextConfig.outputFileTracingRoot || + opts.dir await hotReloader.turbopackProject.update({ defineEnv: createDefineEnv({ isTurbopack: true, @@ -679,6 +684,8 @@ async function startWatcher( projectPath: opts.dir, rewrites: opts.fsChecker.rewrites, }), + rootPath, + projectPath: normalizePath(path.relative(rootPath, dir)), }) } From 8a0712d9f71a25c8325574a628617ca1c73d5f42 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:14:08 +0200 Subject: [PATCH 10/10] Update comments --- crates/napi/src/next_api/project.rs | 23 ++++++++++++----------- crates/next-api/src/project.rs | 19 ++++++++++--------- packages/next/src/build/swc/types.ts | 10 +++++----- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index b8b8c42b66be2..1e3232b7aac71 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -126,17 +126,17 @@ pub struct NapiWatchOptions { #[napi(object)] pub struct NapiProjectOptions { - /// An absolute root path from which all files must be nested under. Trying to access - /// a file outside this root will fail, so think of this as a chroot. + /// An absolute root path (Unix or Windows path) from which all files must be nested under. + /// Trying to access a file outside this root will fail, so think of this as a chroot. /// E.g. `/home/user/projects/my-repo`. pub root_path: RcStr, - /// A path which contains the app/pages directories, relative to [`Project::root_path`]. - /// E.g. `apps/my-app` + /// A path which contains the app/pages directories, relative to [`Project::root_path`], always + /// Unix path. E.g. `apps/my-app` pub project_path: RcStr, - /// A path where to emit the build outputs, relative to [`Project::project_path`]. - /// Corresponds to next.config.js's `distDir`. + /// A path where to emit the build outputs, relative to [`Project::project_path`], always Unix + /// path. Corresponds to next.config.js's `distDir`. /// E.g. `.next` pub dist_dir: RcStr, @@ -183,17 +183,18 @@ pub struct NapiProjectOptions { /// [NapiProjectOptions] with all fields optional. #[napi(object)] pub struct NapiPartialProjectOptions { - /// An absolute root path from which all files must be nested under. Trying to access - /// a file outside this root will fail, so think of this as a chroot. + /// An absolute root path (Unix or Windows path) from which all files must be nested under. + /// Trying to access a file outside this root will fail, so think of this as a chroot. /// E.g. `/home/user/projects/my-repo`. pub root_path: Option, - /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// A path which contains the app/pages directories, relative to [`Project::root_path`], always + /// a Unix path. /// E.g. `apps/my-app` pub project_path: Option, - /// A path where to emit the build outputs, relative to [`Project::project_path`]. - /// Corresponds to next.config.js's `distDir`. + /// A path where to emit the build outputs, relative to [`Project::project_path`], always a + /// Unix path. Corresponds to next.config.js's `distDir`. /// E.g. `.next` pub dist_dir: Option>, diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 67e667e16f816..7f1f0394ab40c 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -149,13 +149,13 @@ pub struct WatchOptions { )] #[serde(rename_all = "camelCase")] pub struct ProjectOptions { - /// An absolute root path from which all files must be nested under. Trying to access - /// a file outside this root will fail, so think of this as a chroot. + /// An absolute root path (Unix or Windows path) from which all files must be nested under. + /// Trying to access a file outside this root will fail, so think of this as a chroot. /// E.g. `/home/user/projects/my-repo`. pub root_path: RcStr, - /// A path which contains the app/pages directories, relative to [`ProjectOptions::root_path`]. - /// E.g. `apps/my-app` + /// A path which contains the app/pages directories, relative to [`Project::root_path`], always + /// Unix path. E.g. `apps/my-app` pub project_path: RcStr, /// The contents of next.config.js, serialized to JSON. @@ -540,17 +540,18 @@ impl ProjectContainer { #[turbo_tasks::value] pub struct Project { - /// An absolute root path from which all files must be nested under. Trying to access - /// a file outside this root will fail, so think of this as a chroot. + /// An absolute root path (Windows or Unix path) from which all files must be nested under. + /// Trying to access a file outside this root will fail, so think of this as a chroot. /// E.g. `/home/user/projects/my-repo`. root_path: RcStr, - /// A path which contains the app/pages directories, relative to [`Project::root_path`]. + /// A path which contains the app/pages directories, relative to [`Project::root_path`], always + /// a Unix path. /// E.g. `apps/my-app` project_path: RcStr, - /// A path where to emit the build outputs, relative to [`Project::project_path`]. - /// Corresponds to next.config.js's `distDir`. + /// A path where to emit the build outputs, relative to [`Project::project_path`], always a + /// Unix path. Corresponds to next.config.js's `distDir`. /// E.g. `.next` dist_dir: RcStr, diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 2ccf4a16db543..6af182f331a39 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -346,21 +346,21 @@ export type WrittenEndpoint = export interface ProjectOptions { /** - * An absolute root path from which all files must be nested under. Trying to access - * a file outside this root will fail, so think of this as a chroot. + * An absolute root path (Unix or Windows path) from which all files must be nested under. Trying + * to access a file outside this root will fail, so think of this as a chroot. * E.g. `/home/user/projects/my-repo`. */ rootPath: string /** - * A path which contains the app/pages directories, relative to `root_path`. + * A path which contains the app/pages directories, relative to `root_path`, always a Unix path. * E.g. `apps/my-app` */ projectPath: string /** - * A path where to emit the build outputs, relative to [`Project::project_path`]. - * Corresponds to next.config.js's `distDir`. + * A path where to emit the build outputs, relative to [`Project::project_path`], always a Unix + * path. Corresponds to next.config.js's `distDir`. * E.g. `.next` */ distDir: string