Skip to content

Commit

Permalink
Merge 25bd59d into 8936f1d
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkirsz authored Jul 3, 2023
2 parents 8936f1d + 25bd59d commit 9dc695a
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 34 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/turbo-tasks-fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ criterion = { workspace = true, features = ["async_tokio"] }
rstest = { workspace = true }
sha2 = "0.10.2"
tempfile = { workspace = true }
turbo-tasks-memory = { path = "../turbo-tasks-memory" }
turbo-tasks-memory = { workspace = true }
turbo-tasks-testing = { workspace = true }

[build-dependencies]
turbo-tasks-build = { path = "../turbo-tasks-build" }
turbo-tasks-build = { workspace = true }
185 changes: 163 additions & 22 deletions crates/turbo-tasks-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod retry;
pub mod rope;
pub mod source_context;
pub mod util;
pub(crate) mod virtual_fs;

use std::{
borrow::Cow,
Expand Down Expand Up @@ -54,13 +55,14 @@ use tokio::{
use tracing::{instrument, Level};
use turbo_tasks::{
mark_stateful,
primitives::{BoolVc, StringReadRef, StringVc},
primitives::{BoolVc, OptionStringVc, StringReadRef, StringVc},
spawn_thread,
trace::TraceRawVcs,
CompletionVc, InvalidationReason, Invalidator, ValueToString, ValueToStringVc,
};
use turbo_tasks_hash::hash_xxh3_hash64;
use util::{extract_disk_access, join_path, normalize_path, sys_to_unix, unix_to_sys};
pub use virtual_fs::VirtualFileSystemVc;

use self::{invalidation::WatchStart, json::UnparseableJson, mutex_map::MutexMap};
use crate::{
Expand Down Expand Up @@ -928,17 +930,57 @@ impl FileSystemPath {
/// Returns the final component of the FileSystemPath, or an empty string
/// for the root path.
pub fn file_name(&self) -> &str {
// rsplit will always give at least one item
self.path.rsplit('/').next().unwrap()
let (_, file_name) = self.split_file_name();
file_name
}

pub fn extension(&self) -> Option<&str> {
if let Some((_, ext)) = self.path.rsplit_once('.') {
if !ext.contains('/') {
return Some(ext);
let (_, extension) = self.split_extension();
extension
}

/// Splits the path into two components:
/// 1. The path without the extension;
/// 2. The extension, if any.
fn split_extension(&self) -> (&str, Option<&str>) {
if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
if extension.contains('/') ||
// The file name begins with a `.` and has no other `.`s within.
path_before_extension.ends_with('/') || path_before_extension.is_empty()
{
(self.path.as_str(), None)
} else {
(path_before_extension, Some(extension))
}
} else {
(self.path.as_str(), None)
}
}

/// Splits the path into two components:
/// 1. The parent directory, if any;
/// 2. The file name;
fn split_file_name(&self) -> (Option<&str>, &str) {
// Since the path is normalized, we know `parent`, if any, must not be empty.
if let Some((parent, file_name)) = self.path.rsplit_once('/') {
(Some(parent), file_name)
} else {
(None, self.path.as_str())
}
}

/// Splits the path into three components:
/// 1. The parent directory, if any;
/// 2. The file stem;
/// 3. The extension, if any.
fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
let (path_before_extension, extension) = self.split_extension();

if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
(Some(parent), file_stem, extension)
} else {
(None, path_before_extension, extension)
}
None
}
}

Expand Down Expand Up @@ -1014,15 +1056,11 @@ impl FileSystemPathVc {
appending
)
}
if let Some((path, ext)) = this.path.rsplit_once('.') {
// check if `ext` is a real extension, and not a "." in a directory name or a
// .dotfile
if !(ext.contains('/') || (path.ends_with('/') && !path.is_empty())) {
return Ok(Self::new_normalized(
this.fs,
format!("{}{}.{}", path, appending, ext),
));
}
if let (path, Some(ext)) = this.split_extension() {
return Ok(Self::new_normalized(
this.fs,
format!("{}{}.{}", path, appending, ext),
));
}
Ok(Self::new_normalized(
this.fs,
Expand Down Expand Up @@ -1089,6 +1127,42 @@ impl FileSystemPathVc {
pub async fn is_inside_or_equal(self, other: FileSystemPathVc) -> Result<BoolVc> {
Ok(BoolVc::cell(self.await?.is_inside_or_equal(&*other.await?)))
}

/// Creates a new [`FileSystemPathVc`] like `self` but with the given
/// extension.
#[turbo_tasks::function]
pub async fn with_extension(self, extension: &str) -> Result<FileSystemPathVc> {
let this = self.await?;
let (path_without_extension, _) = this.split_extension();
Ok(Self::new_normalized(
this.fs,
// Like `Path::with_extension` and `PathBuf::set_extension`, if the extension is empty,
// we remove the extension altogether.
match extension.is_empty() {
true => format!("{path_without_extension}"),
false => format!("{path_without_extension}.{extension}"),
},
))
}

/// Extracts the stem (non-extension) portion of self.file_name.
///
/// The stem is:
///
/// * [`None`], if there is no file name;
/// * The entire file name if there is no embedded `.`;
/// * The entire file name if the file name begins with `.` and has no other
/// `.`s within;
/// * Otherwise, the portion of the file name before the final `.`
#[turbo_tasks::function]
pub async fn file_stem(self) -> Result<OptionStringVc> {
let this = self.await?;
let (_, file_stem, _) = this.split_file_stem_extension();
if file_stem.is_empty() {
return Ok(OptionStringVc::cell(None));
}
Ok(OptionStringVc::cell(Some(file_stem.to_string())))
}
}

impl Display for FileSystemPath {
Expand Down Expand Up @@ -1213,12 +1287,8 @@ impl FileSystemPathVc {
Ok(FileSystemEntryTypeVc::cell(FileSystemEntryType::NotFound))
}
DirectoryContent::Entries(entries) => {
let basename = if let Some((_, basename)) = this.path.rsplit_once('/') {
basename
} else {
&this.path
};
if let Some(entry) = entries.get(basename) {
let (_, file_name) = this.split_file_name();
if let Some(entry) = entries.get(file_name) {
Ok(FileSystemEntryTypeVc::cell(entry.into()))
} else {
Ok(FileSystemEntryTypeVc::cell(FileSystemEntryType::NotFound))
Expand Down Expand Up @@ -1960,3 +2030,74 @@ pub fn register() {
turbo_tasks::register();
include!(concat!(env!("OUT_DIR"), "/register.rs"));
}

#[cfg(test)]
mod tests {
use super::{virtual_fs::VirtualFileSystemVc, *};

#[tokio::test]
async fn with_extension() {
crate::register();

turbo_tasks_testing::VcStorage::with(async {
let fs = VirtualFileSystemVc::new().as_file_system();

let path_txt = FileSystemPathVc::new_normalized(fs, "foo/bar.txt".into());

let path_json = path_txt.with_extension("json");
assert_eq!(&*path_json.await.unwrap().path, "foo/bar.json");

let path_no_ext = path_txt.with_extension("");
assert_eq!(&*path_no_ext.await.unwrap().path, "foo/bar");

let path_new_ext = path_no_ext.with_extension("json");
assert_eq!(&*path_new_ext.await.unwrap().path, "foo/bar.json");

let path_no_slash_txt = FileSystemPathVc::new_normalized(fs, "bar.txt".into());

let path_no_slash_json = path_no_slash_txt.with_extension("json");
assert_eq!(path_no_slash_json.await.unwrap().path.as_str(), "bar.json");

let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
assert_eq!(path_no_slash_no_ext.await.unwrap().path.as_str(), "bar");

let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
assert_eq!(
path_no_slash_new_ext.await.unwrap().path.as_str(),
"bar.json"
);

anyhow::Ok(())
})
.await
.unwrap()
}

#[tokio::test]
async fn file_stem() {
crate::register();

turbo_tasks_testing::VcStorage::with(async {
let fs = VirtualFileSystemVc::new().as_file_system();

let path = FileSystemPathVc::new_normalized(fs, "".into());
assert_eq!(path.file_stem().await.unwrap().as_deref(), None);

let path = FileSystemPathVc::new_normalized(fs, "foo/bar.txt".into());
assert_eq!(path.file_stem().await.unwrap().as_deref(), Some("bar"));

let path = FileSystemPathVc::new_normalized(fs, "bar.txt".into());
assert_eq!(path.file_stem().await.unwrap().as_deref(), Some("bar"));

let path = FileSystemPathVc::new_normalized(fs, "foo/bar".into());
assert_eq!(path.file_stem().await.unwrap().as_deref(), Some("bar"));

let path = FileSystemPathVc::new_normalized(fs, "foo/.bar".into());
assert_eq!(path.file_stem().await.unwrap().as_deref(), Some(".bar"));

anyhow::Ok(())
})
.await
.unwrap()
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
use anyhow::{bail, Result};
use turbo_tasks::{primitives::StringVc, CompletionVc, ValueToString, ValueToStringVc};
use turbo_tasks_fs::{

use super::{
DirectoryContentVc, FileContentVc, FileMetaVc, FileSystem, FileSystemPathVc, FileSystemVc,
LinkContentVc,
};

#[turbo_tasks::value]
pub struct VirtualFileSystem;

#[turbo_tasks::value_impl]
impl VirtualFileSystemVc {
#[turbo_tasks::function]
/// Creates a new [`VirtualFileSystemVc`].
///
/// NOTE: This function is not a `turbo_tasks::function` to avoid instances
/// being equivalent identity-wise. This ensures that a [`FileSystemPathVc`]
/// created from this [`VirtualFileSystemVc`] will never be equivalent,
/// nor be interoperable, with a [`FileSystemPathVc`] created from another
/// [`VirtualFileSystemVc`].
pub fn new() -> Self {
Self::cell(VirtualFileSystem)
}
Expand Down
1 change: 1 addition & 0 deletions crates/turbo-tasks-testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ bench = false
[dependencies]
anyhow = { workspace = true }
auto-hash-map = { workspace = true }
futures = { workspace = true }
lazy_static = { workspace = true }
tokio = { workspace = true }
turbo-tasks = { workspace = true }
28 changes: 23 additions & 5 deletions crates/turbo-tasks-testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@ use std::{
collections::HashMap,
future::Future,
mem::replace,
panic::AssertUnwindSafe,
sync::{Arc, Mutex, Weak},
};

use anyhow::Result;
use anyhow::{anyhow, Result};
use auto_hash_map::AutoSet;
use futures::FutureExt;
use turbo_tasks::{
backend::CellContent,
event::{Event, EventListener},
primitives::RawVcSetVc,
registry,
test_helpers::with_turbo_tasks_for_testing,
util::StaticOrArc,
util::{SharedError, StaticOrArc},
CellId, InvalidationReason, RawVc, TaskId, TraitTypeId, TurboTasksApi, TurboTasksCallApi,
};

enum Task {
Spawned(Event),
Finished(RawVc),
Finished(Result<RawVc, SharedError>),
}

#[derive(Default)]
Expand Down Expand Up @@ -57,7 +59,20 @@ impl TurboTasksCallApi for VcStorage {
this.clone(),
TaskId::from(i),
async move {
let result = future.await.unwrap();
let result = AssertUnwindSafe(future).catch_unwind().await;

// Convert the unwind panic to an anyhow error that can be cloned.
let result = result
.map_err(|any| match any.downcast::<String>() {
Ok(owned) => anyhow!(owned),
Err(any) => match any.downcast::<&'static str>() {
Ok(str) => anyhow!(str),
Err(_) => anyhow!("unknown panic"),
},
})
.and_then(|r| r)
.map_err(|err| SharedError::new(err));

let mut tasks = this.tasks.lock().unwrap();
if let Task::Spawned(event) = replace(&mut tasks[i], Task::Finished(result)) {
event.notify(usize::MAX);
Expand Down Expand Up @@ -133,7 +148,10 @@ impl TurboTasksApi for VcStorage {
let task = tasks.get(*task).unwrap();
match task {
Task::Spawned(event) => Ok(Err(event.listen())),
Task::Finished(result) => Ok(Ok(*result)),
Task::Finished(result) => match result {
Ok(vc) => Ok(Ok(vc.clone())),
Err(err) => Err(anyhow!(err.clone())),
},
}
}

Expand Down
5 changes: 4 additions & 1 deletion crates/turbopack-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ pub mod target;
mod utils;
pub mod version;
pub mod virtual_asset;
pub mod virtual_fs;

pub mod virtual_fs {
pub use turbo_tasks_fs::VirtualFileSystemVc;
}

pub const PROJECT_FILESYSTEM_NAME: &str = "project";
pub const SOURCE_MAP_ROOT_NAME: &str = "turbopack";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function instantiateModule(id: ModuleId, source: SourceInfo): Module {
}

module.loaded = true;
if (module.namespaceObject) {
if (module.namespaceObject && module.exports !== module.namespaceObject) {
// in case of a circular dependency: cjs1 -> esm2 -> cjs1
interopEsm(module.exports, module.namespaceObject);
}
Expand Down

0 comments on commit 9dc695a

Please sign in to comment.