Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 93 additions & 6 deletions crates/next-api/src/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use turbo_tasks::{
FxIndexMap, FxIndexSet, ReadRef, ResolvedVc, TraitRef, TryFlatJoinIterExt, TryJoinIterExt, Vc,
};
use turbo_tasks_fs::{
DirectoryEntry, FileSystemPath,
DirectoryEntry, FileSystemEntryType, FileSystemPath,
glob::{Glob, GlobOptions},
};
use turbo_tasks_hash::HashAlgorithm;
Expand Down Expand Up @@ -193,19 +193,28 @@ async fn get_glob_includes(
let glob_result = project_root_path.read_glob(glob).await?;

// Walk the full glob_result using an explicit stack to avoid async recursion overheads.
// Use a BTreeSet to get deterministic order (return value of `read_glob` has random order).
let mut result = vec![];
// Deduplicate symlinks shared by many matches. The return value of `read_glob` has random
// order, so the result is sorted below.
let mut result = FxHashSet::default();
let mut stack = VecDeque::new();
stack.push_back(glob_result);
while let Some(glob_result) = stack.pop_back() {
// Process direct results (files and directories at this level)
// Process direct results (files and directories at this level).
for entry in glob_result.results.values() {
let (DirectoryEntry::File(file_path) | DirectoryEntry::Symlink(file_path)) = entry
else {
continue;
};

result.push(file_path.clone());
// ReadGlobResult paths are logical by contract. Resolve each match here so the NFT
// includes both the physical file and every symlink needed to reach it.
let realpath = file_path.realpath_with_links().await?;
result.extend(realpath.symlinks.iter().cloned());
if let Ok(resolved_path) = &realpath.path_result
&& matches!(*resolved_path.get_type().await?, FileSystemEntryType::File)
{
result.insert(resolved_path.clone());
}
}

for nested_result in glob_result.inner.values() {
Expand All @@ -216,8 +225,8 @@ async fn get_glob_includes(

// All paths were matched from project_root_path, so they must all have the same `fs`. So it's
// enough to sort by path.
let mut result: Vec<_> = result.into_iter().collect();
result.sort_by(|a, b| a.path.cmp(&b.path));

Ok(result)
}

Expand Down Expand Up @@ -564,3 +573,81 @@ impl Issue for ForbiddenTracedFileIssue {
Ok(Some(StyledString::Stack(stack)))
}
}

#[cfg(all(test, unix))]
mod tests {
use std::{
fs::{create_dir_all, write},
os::unix::fs::symlink,
};

use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::Vc;
use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
use turbo_tasks_fs::{
DiskFileSystem, FileSystem,
glob::{Glob, GlobOptions},
};

use crate::nft::get_glob_includes;

#[turbo_tasks::function(operation, root)]
async fn assert_glob_includes_operation(disk_root: RcStr) -> anyhow::Result<()> {
let root = DiskFileSystem::new(rcstr!("test"), Vc::cell(disk_root))
.root()
.owned()
.await?;
let includes = get_glob_includes(
root,
Glob::new(
rcstr!("**"),
GlobOptions {
contains: true,
..Default::default()
},
),
)
.await?;

assert_eq!(
includes
.iter()
.map(|path| path.path.as_str())
.collect::<Vec<_>>(),
[
"alias",
"alias-chain",
"dangling",
"file-link",
"real/file.txt",
]
);
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn glob_includes_resolve_files_and_retain_symlinks() {
let scratch = tempfile::tempdir().unwrap();
let root = scratch.path();
create_dir_all(root.join("real")).unwrap();
write(root.join("real/file.txt"), "content").unwrap();
symlink("real", root.join("alias")).unwrap();
symlink("alias", root.join("alias-chain")).unwrap();
symlink("real/file.txt", root.join("file-link")).unwrap();
symlink("missing", root.join("dangling")).unwrap();

let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
BackendOptions::default(),
noop_backing_storage(),
));
let disk_root: RcStr = root.to_str().unwrap().into();
tt.run_once(async move {
assert_glob_includes_operation(disk_root)
.read_strongly_consistent()
.await?;
anyhow::Ok(())
})
.await
.unwrap();
}
}
1 change: 1 addition & 0 deletions crates/next-core/src/next_client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ pub async fn get_client_module_options_context(
source_maps,
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?,
mangle_export_names: *next_config.turbopack_mangle_export_names(mode).await?,
cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?,
cross_module_constants: *next_config.turbopack_cross_module_constants().await?,
preset_env_config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ impl ChunkableModule for EcmascriptClientReferenceModule {
impl EcmascriptChunkPlaceable for EcmascriptClientReferenceModule {
#[turbo_tasks::function]
fn get_exports(self: Vc<Self>) -> Vc<EcmascriptExports> {
self.proxy_module().get_exports()
// Borrowed from the proxy module, a separate module identity, so they must not carry a
// mangling decision — see `EcmascriptExports::borrowed`.
self.proxy_module().get_exports().borrowed()
}

#[turbo_tasks::function]
Expand Down
18 changes: 18 additions & 0 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,8 @@ pub struct ExperimentalConfig {
turbopack_infer_module_side_effects: Option<bool>,
/// Enable tree shaking of unused exports from static CommonJS modules. Defaults to false.
turbopack_cjs_tree_shaking: Option<bool>,
/// Shorten ("mangle") the export names modules expose to each other. Defaults to false.
turbopack_mangle_export_names: Option<bool>,
/// Enable scope hoisting of static CommonJS modules. Defaults to false.
turbopack_cjs_scope_hoisting: Option<bool>,
/// Enable cross-module constant inlining. Defaults to false.
Expand Down Expand Up @@ -2587,6 +2589,22 @@ impl NextConfig {
)
}

/// Whether Turbopack should shorten ("mangle") the export names modules expose to each other.
///
/// An explicit value always wins, in either direction — setting this to `true` in development
/// is honoured. `mode` only supplies the default when the option is unset: on in production
/// builds, off in development, where the extra module splitting costs rebuild time and the
/// short names make debugging harder for no benefit.
#[turbo_tasks::function]
pub async fn turbopack_mangle_export_names(&self, mode: Vc<NextMode>) -> Result<Vc<bool>> {
Ok(Vc::cell(
match self.experimental.turbopack_mangle_export_names {
Some(explicit) => explicit,
None => !mode.await?.is_development(),
},
))
}

#[turbo_tasks::function]
pub fn turbopack_cjs_scope_hoisting(&self) -> Vc<bool> {
Vc::cell(
Expand Down
1 change: 1 addition & 0 deletions crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ pub async fn get_server_module_options_context(
source_maps,
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?,
mangle_export_names: *next_config.turbopack_mangle_export_names(mode).await?,
cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?,
cross_module_constants: *next_config.turbopack_cross_module_constants().await?,
..Default::default()
Expand Down
Loading
Loading