Skip to content

Commit 1a1fcad

Browse files
mischnicalii
authored andcommitted
Turbopack: fix dist dir on Windows (vercel#81758)
Regression from vercel#80683 Closes PACK-5071 Closes vercel#81628
1 parent 39bb596 commit 1a1fcad

File tree

13 files changed

+136
-92
lines changed

13 files changed

+136
-92
lines changed

.github/workflows/build_and_test.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,10 +705,11 @@ jobs:
705705
afterBuild: |
706706
export NEXT_TEST_MODE=start
707707
708-
node run-tests.js \
708+
node run-tests.js --type production \
709709
test/e2e/app-dir/app/index.test.ts \
710710
test/e2e/app-dir/app-edge/app-edge.test.ts \
711-
test/e2e/app-dir/metadata-edge/index.test.ts
711+
test/e2e/app-dir/metadata-edge/index.test.ts \
712+
test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts
712713
stepName: 'test-prod-windows'
713714
runs_on_labels: '["windows","self-hosted","x64"]'
714715
buildNativeTarget: 'x86_64-pc-windows-msvc'

crates/napi/src/next_api/project.rs

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,18 @@ pub struct NapiWatchOptions {
126126

127127
#[napi(object)]
128128
pub struct NapiProjectOptions {
129-
/// A root path from which all files must be nested under. Trying to access
130-
/// a file outside this root will fail. Think of this as a chroot.
129+
/// An absolute root path (Unix or Windows path) from which all files must be nested under.
130+
/// Trying to access a file outside this root will fail, so think of this as a chroot.
131+
/// E.g. `/home/user/projects/my-repo`.
131132
pub root_path: RcStr,
132133

133-
/// A path inside the root_path which contains the app/pages directories.
134+
/// A path which contains the app/pages directories, relative to [`Project::root_path`], always
135+
/// Unix path. E.g. `apps/my-app`
134136
pub project_path: RcStr,
135137

136-
/// next.config's distDir. Project initialization occurs earlier than
137-
/// deserializing next.config, so passing it as separate option.
138+
/// A path where to emit the build outputs, relative to [`Project::project_path`], always Unix
139+
/// path. Corresponds to next.config.js's `distDir`.
140+
/// E.g. `.next`
138141
pub dist_dir: RcStr,
139142

140143
/// Filesystem watcher options.
@@ -180,15 +183,19 @@ pub struct NapiProjectOptions {
180183
/// [NapiProjectOptions] with all fields optional.
181184
#[napi(object)]
182185
pub struct NapiPartialProjectOptions {
183-
/// A root path from which all files must be nested under. Trying to access
184-
/// a file outside this root will fail. Think of this as a chroot.
186+
/// An absolute root path (Unix or Windows path) from which all files must be nested under.
187+
/// Trying to access a file outside this root will fail, so think of this as a chroot.
188+
/// E.g. `/home/user/projects/my-repo`.
185189
pub root_path: Option<RcStr>,
186190

187-
/// A path inside the root_path which contains the app/pages directories.
191+
/// A path which contains the app/pages directories, relative to [`Project::root_path`], always
192+
/// a Unix path.
193+
/// E.g. `apps/my-app`
188194
pub project_path: Option<RcStr>,
189195

190-
/// next.config's distDir. Project initialization occurs earlier than
191-
/// deserializing next.config, so passing it as separate option.
196+
/// A path where to emit the build outputs, relative to [`Project::project_path`], always a
197+
/// Unix path. Corresponds to next.config.js's `distDir`.
198+
/// E.g. `.next`
192199
pub dist_dir: Option<Option<RcStr>>,
193200

194201
/// Filesystem watcher options.
@@ -392,7 +399,9 @@ pub fn project_new(
392399

393400
let subscriber = subscriber.with(FilterLayer::try_new(&trace).unwrap());
394401

395-
let internal_dir = PathBuf::from(&options.project_path).join(&options.dist_dir);
402+
let internal_dir = PathBuf::from(&options.root_path)
403+
.join(&options.project_path)
404+
.join(&options.dist_dir);
396405
std::fs::create_dir_all(&internal_dir)
397406
.context("Unable to create .next directory")
398407
.unwrap();
@@ -1424,13 +1433,9 @@ pub async fn get_source_map_rope(
14241433
Err(_) => (file_path.to_string(), None),
14251434
};
14261435

1427-
let Some(chunk_base) = file.strip_prefix(
1428-
&(format!(
1429-
"{}/{}/",
1430-
container.project().await?.project_path,
1431-
container.project().dist_dir().await?
1432-
)),
1433-
) else {
1436+
let Some(chunk_base) =
1437+
file.strip_prefix(container.project().dist_dir_absolute().await?.as_str())
1438+
else {
14341439
// File doesn't exist within the dist dir
14351440
return Ok(OptionStringifiedSourceMap::none());
14361441
};

crates/next-api/src/project.rs

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{path::MAIN_SEPARATOR, time::Duration};
1+
use std::time::Duration;
22

33
use anyhow::{Context, Result, bail};
44
use indexmap::map::Entry;
@@ -37,8 +37,8 @@ use turbo_tasks::{
3737
};
3838
use turbo_tasks_env::{EnvMap, ProcessEnv};
3939
use turbo_tasks_fs::{
40-
DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, get_relative_path_to,
41-
invalidation,
40+
DiskFileSystem, FileSystem, FileSystemPath, VirtualFileSystem, invalidation,
41+
util::{join_path, unix_to_sys},
4242
};
4343
use turbopack::{
4444
ModuleAssetContext, evaluate_context::node_build_environment,
@@ -149,11 +149,13 @@ pub struct WatchOptions {
149149
)]
150150
#[serde(rename_all = "camelCase")]
151151
pub struct ProjectOptions {
152-
/// A root path from which all files must be nested under. Trying to access
153-
/// a file outside this root will fail. Think of this as a chroot.
152+
/// An absolute root path (Unix or Windows path) from which all files must be nested under.
153+
/// Trying to access a file outside this root will fail, so think of this as a chroot.
154+
/// E.g. `/home/user/projects/my-repo`.
154155
pub root_path: RcStr,
155156

156-
/// A path inside the root_path which contains the app/pages directories.
157+
/// A path which contains the app/pages directories, relative to [`Project::root_path`], always
158+
/// Unix path. E.g. `apps/my-app`
157159
pub project_path: RcStr,
158160

159161
/// The contents of next.config.js, serialized to JSON.
@@ -538,15 +540,20 @@ impl ProjectContainer {
538540

539541
#[turbo_tasks::value]
540542
pub struct Project {
541-
/// A root path from which all files must be nested under. Trying to access
542-
/// a file outside this root will fail. Think of this as a chroot.
543+
/// An absolute root path (Windows or Unix path) from which all files must be nested under.
544+
/// Trying to access a file outside this root will fail, so think of this as a chroot.
545+
/// E.g. `/home/user/projects/my-repo`.
543546
root_path: RcStr,
544547

545-
/// A path where to emit the build outputs. next.config.js's distDir.
546-
dist_dir: RcStr,
548+
/// A path which contains the app/pages directories, relative to [`Project::root_path`], always
549+
/// a Unix path.
550+
/// E.g. `apps/my-app`
551+
project_path: RcStr,
547552

548-
/// A path inside the root_path which contains the app/pages directories.
549-
pub project_path: RcStr,
553+
/// A path where to emit the build outputs, relative to [`Project::project_path`], always a
554+
/// Unix path. Corresponds to next.config.js's `distDir`.
555+
/// E.g. `.next`
556+
dist_dir: RcStr,
550557

551558
/// Filesystem watcher options.
552559
watch: WatchOptions,
@@ -685,21 +692,30 @@ impl Project {
685692
}
686693

687694
#[turbo_tasks::function]
688-
pub fn dist_dir(&self) -> Vc<RcStr> {
689-
Vc::cell(self.dist_dir.clone())
695+
pub fn dist_dir_absolute(&self) -> Result<Vc<RcStr>> {
696+
Ok(Vc::cell(
697+
format!(
698+
"{}{}{}",
699+
self.root_path,
700+
std::path::MAIN_SEPARATOR,
701+
unix_to_sys(
702+
&join_path(&self.project_path, &self.dist_dir)
703+
.context("expected project_path to be inside of root_path")?
704+
)
705+
)
706+
.into(),
707+
))
690708
}
691709

692710
#[turbo_tasks::function]
693711
pub async fn node_root(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
694712
let this = self.await?;
695-
let relative_from_root_to_project_path =
696-
get_relative_path_to(&this.root_path, &this.project_path);
697713
Ok(self
698714
.output_fs()
699715
.root()
700716
.await?
701-
.join(&relative_from_root_to_project_path)?
702-
.join(&this.dist_dir.clone())?
717+
.join(&this.project_path)?
718+
.join(&this.dist_dir)?
703719
.cell())
704720
}
705721

@@ -726,28 +742,24 @@ impl Project {
726742
.cell())
727743
}
728744

745+
/// Returns the relative path from the node root to the output root.
746+
/// E.g. from `[project]/test/e2e/app-dir/non-root-project-monorepo/apps/web/app/
747+
/// import-meta-url-ssr/page.tsx` to `[project]/`.
729748
#[turbo_tasks::function]
730749
pub async fn node_root_to_root_path(self: Vc<Self>) -> Result<Vc<RcStr>> {
731-
let this = self.await?;
732-
let output_root_to_root_path = self
733-
.project_path()
734-
.await?
735-
.join(&this.dist_dir.clone())?
736-
.get_relative_path_to(&*self.project_root_path().await?)
737-
.context("Project path need to be in root path")?;
738-
Ok(Vc::cell(output_root_to_root_path))
750+
Ok(Vc::cell(
751+
self.node_root()
752+
.await?
753+
.get_relative_path_to(&*self.output_fs().root().await?)
754+
.context("Expected node root to be inside of output fs")?,
755+
))
739756
}
740757

741758
#[turbo_tasks::function]
742759
pub async fn project_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
743760
let this = self.await?;
744761
let root = self.project_root_path().await?;
745-
let project_relative = this.project_path.strip_prefix(&*this.root_path).unwrap();
746-
let project_relative = project_relative
747-
.strip_prefix(MAIN_SEPARATOR)
748-
.unwrap_or(project_relative)
749-
.replace(MAIN_SEPARATOR, "/");
750-
Ok(root.join(&project_relative)?.cell())
762+
Ok(root.join(&this.project_path)?.cell())
751763
}
752764

753765
#[turbo_tasks::function]

packages/next/src/build/swc/generated-native.d.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,20 @@ export interface NapiWatchOptions {
100100
}
101101
export interface NapiProjectOptions {
102102
/**
103-
* A root path from which all files must be nested under. Trying to access
104-
* a file outside this root will fail. Think of this as a chroot.
103+
* An absolute root path from which all files must be nested under. Trying to access
104+
* a file outside this root will fail, so think of this as a chroot.
105+
* E.g. `/home/user/projects/my-repo`.
105106
*/
106107
rootPath: RcStr
107-
/** A path inside the root_path which contains the app/pages directories. */
108+
/**
109+
* A path which contains the app/pages directories, relative to [`Project::root_path`].
110+
* E.g. `apps/my-app`
111+
*/
108112
projectPath: RcStr
109113
/**
110-
* next.config's distDir. Project initialization occurs earlier than
111-
* deserializing next.config, so passing it as separate option.
114+
* A path where to emit the build outputs, relative to [`Project::project_path`].
115+
* Corresponds to next.config.js's `distDir`.
116+
* E.g. `.next`
112117
*/
113118
distDir: RcStr
114119
/** Filesystem watcher options. */
@@ -146,15 +151,20 @@ export interface NapiProjectOptions {
146151
/** [NapiProjectOptions] with all fields optional. */
147152
export interface NapiPartialProjectOptions {
148153
/**
149-
* A root path from which all files must be nested under. Trying to access
150-
* a file outside this root will fail. Think of this as a chroot.
154+
* An absolute root path from which all files must be nested under. Trying to access
155+
* a file outside this root will fail, so think of this as a chroot.
156+
* E.g. `/home/user/projects/my-repo`.
151157
*/
152158
rootPath?: RcStr
153-
/** A path inside the root_path which contains the app/pages directories. */
159+
/**
160+
* A path which contains the app/pages directories, relative to [`Project::root_path`].
161+
* E.g. `apps/my-app`
162+
*/
154163
projectPath?: RcStr
155164
/**
156-
* next.config's distDir. Project initialization occurs earlier than
157-
* deserializing next.config, so passing it as separate option.
165+
* A path where to emit the build outputs, relative to [`Project::project_path`].
166+
* Corresponds to next.config.js's `distDir`.
167+
* E.g. `.next`
158168
*/
159169
distDir?: RcStr | undefined | null
160170
/** Filesystem watcher options. */

packages/next/src/build/swc/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ function bindingToApi(
621621
...options,
622622
nextConfig: await serializeNextConfig(
623623
options.nextConfig,
624-
options.projectPath!
624+
path.join(options.rootPath, options.projectPath)
625625
),
626626
jsConfig: JSON.stringify(options.jsConfig),
627627
env: rustifyEnv(options.env),
@@ -635,7 +635,10 @@ function bindingToApi(
635635
...options,
636636
nextConfig:
637637
options.nextConfig &&
638-
(await serializeNextConfig(options.nextConfig, options.projectPath!)),
638+
(await serializeNextConfig(
639+
options.nextConfig,
640+
path.join(options.rootPath!, options.projectPath!)
641+
)),
639642
jsConfig: options.jsConfig && JSON.stringify(options.jsConfig),
640643
env: options.env && rustifyEnv(options.env),
641644
}

packages/next/src/build/swc/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,18 +346,22 @@ export type WrittenEndpoint =
346346

347347
export interface ProjectOptions {
348348
/**
349-
* A root path from which all files must be nested under. Trying to access
350-
* a file outside this root will fail. Think of this as a chroot.
349+
* An absolute root path (Unix or Windows path) from which all files must be nested under. Trying
350+
* to access a file outside this root will fail, so think of this as a chroot.
351+
* E.g. `/home/user/projects/my-repo`.
351352
*/
352353
rootPath: string
353354

354355
/**
355-
* A path inside the root_path which contains the app/pages directories.
356+
* A path which contains the app/pages directories, relative to `root_path`, always a Unix path.
357+
* E.g. `apps/my-app`
356358
*/
357359
projectPath: string
358360

359361
/**
360-
* The path to the .next directory.
362+
* A path where to emit the build outputs, relative to [`Project::project_path`], always a Unix
363+
* path. Corresponds to next.config.js's `distDir`.
364+
* E.g. `.next`
361365
*/
362366
distDir: string
363367

packages/next/src/build/turbopack-build/impl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { setGlobal } from '../../trace'
2222
import { isCI } from '../../server/ci-info'
2323
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'
2424
import { getSupportedBrowsers } from '../utils'
25+
import { normalizePath } from '../../lib/normalize-path'
2526

2627
export async function turbopackBuild(): Promise<{
2728
duration: number
@@ -52,10 +53,11 @@ export async function turbopackBuild(): Promise<{
5253
const supportedBrowsers = getSupportedBrowsers(dir, dev)
5354

5455
const persistentCaching = isPersistentCachingEnabled(config)
56+
const rootPath = config.turbopack?.root || config.outputFileTracingRoot || dir
5557
const project = await bindings.turbo.createProject(
5658
{
57-
projectPath: dir,
5859
rootPath: config.turbopack?.root || config.outputFileTracingRoot || dir,
60+
projectPath: normalizePath(path.relative(rootPath, dir) || '.'),
5961
distDir,
6062
nextConfig: config,
6163
jsConfig: await getTurbopackJsConfig(dir, config),

packages/next/src/build/webpack/loaders/css-loader/src/utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import localByDefault from 'next/dist/compiled/postcss-modules-local-by-default'
1111
import extractImports from 'next/dist/compiled/postcss-modules-extract-imports'
1212
import modulesScope from 'next/dist/compiled/postcss-modules-scope'
1313
import camelCase from './camelcase'
14+
import { normalizePath } from '../../../../../lib/normalize-path'
1415

1516
const whitespace = '[\\x20\\t\\r\\n\\f]'
1617
const unescapeRegExp = new RegExp(
@@ -39,10 +40,6 @@ function unescape(str: string) {
3940
})
4041
}
4142

42-
function normalizePath(file: string) {
43-
return path.sep === '\\' ? file.replace(/\\/g, '/') : file
44-
}
45-
4643
function fixedEncodeURIComponent(str: string) {
4744
return str.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16)}`)
4845
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import path from 'path'
2+
3+
export function normalizePath(file: string) {
4+
return path.sep === '\\' ? file.replace(/\\/g, '/') : file
5+
}

0 commit comments

Comments
 (0)