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' diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 2d045a831d017..1e3232b7aac71 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 (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 inside the root_path which contains the app/pages directories. + /// 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, - /// 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`], always Unix + /// path. Corresponds to next.config.js's `distDir`. + /// E.g. `.next` pub dist_dir: RcStr, /// Filesystem watcher options. @@ -180,15 +183,19 @@ 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 (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 inside the root_path which contains the app/pages directories. + /// 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, - /// 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`], always a + /// Unix path. Corresponds to next.config.js's `distDir`. + /// E.g. `.next` pub dist_dir: Option>, /// Filesystem watcher options. @@ -392,7 +399,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(); @@ -1424,13 +1433,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?.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 b1347fd2ef67d..7f1f0394ab40c 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; @@ -37,8 +37,8 @@ use turbo_tasks::{ }; use turbo_tasks_env::{EnvMap, ProcessEnv}; use turbo_tasks_fs::{ - DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, get_relative_path_to, - invalidation, + DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, invalidation, + util::{join_path, unix_to_sys}, }; use turbopack::{ ModuleAssetContext, evaluate_context::node_build_environment, @@ -149,11 +149,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 (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 inside the root_path which contains the app/pages directories. + /// 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. @@ -538,15 +540,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. + /// 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 where to emit the build outputs. next.config.js's distDir. - dist_dir: RcStr, + /// 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 inside the root_path which contains the app/pages directories. - pub project_path: RcStr, + /// 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, /// Filesystem watcher options. watch: WatchOptions, @@ -685,21 +692,30 @@ 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] 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()) } @@ -726,28 +742,24 @@ 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: 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?) - .context("Project path need to be in root path")?; - Ok(Vc::cell(output_root_to_root_path)) + 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] 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/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/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/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 5c3f074d1204d..6af182f331a39 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -346,18 +346,22 @@ 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 (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 inside the root_path which contains the app/pages directories. + * A path which contains the app/pages directories, relative to `root_path`, always a Unix 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`], always a Unix + * path. Corresponds to next.config.js's `distDir`. + * E.g. `.next` */ distDir: string diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index 4f046d7f01e56..b2e86aceea4d6 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..2bb80d8744965 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' @@ -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 = !!( @@ -209,13 +210,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: normalizePath(relative(rootPath, projectPath) || '.'), distDir, nextConfig: opts.nextConfig, jsConfig: await getTurbopackJsConfig(projectPath, nextConfig), 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)), }) } 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 = [ 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();