Skip to content

Commit

Permalink
Add support for CSS module composes: and fix CSS precedence issues (#…
Browse files Browse the repository at this point in the history
…3771)

Adds support for the CSS module `composes:` rule.

This also fixes a large issue with our chunk ordering and CSS precedence, where the BFS order of our chunks did not match the expected topological ordering.
  • Loading branch information
alexkirsz authored Feb 15, 2023
1 parent b9d1cf2 commit 44d6a46
Show file tree
Hide file tree
Showing 39 changed files with 1,125 additions and 316 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ opt-level = 3
# ref: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
[workspace.dependencies]
indexmap = { version = "1.9.2" }
indoc = "1.0"
# Keep consistent with preset_env_base through swc_core
browserslist-rs = { version = "0.12.2" }
swc_core = { version = "0.59.26" }
Expand Down
2 changes: 1 addition & 1 deletion crates/next-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ bench = false
anyhow = "1.0.47"
auto-hash-map = { path = "../auto-hash-map" }
indexmap = { workspace = true, features = ["serde"] }
indoc = "1.0"
indoc = { workspace = true }
mime = "0.3.16"
once_cell = "1.13.0"
qstring = "0.7.2"
Expand Down
28 changes: 14 additions & 14 deletions crates/next-core/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use indexmap::IndexMap;
use mime::{APPLICATION_JAVASCRIPT_UTF_8, APPLICATION_JSON};
use serde::Serialize;
use turbo_tasks::{
graph::{GraphTraversal, NonDeterministic},
primitives::{StringVc, StringsVc},
TryFlatMapRecursiveJoinIterExt, TryJoinIterExt,
TryJoinIterExt,
};
use turbo_tasks_fs::File;
use turbopack_core::asset::AssetContentVc;
Expand Down Expand Up @@ -60,19 +61,18 @@ impl DevManifestContentSourceVc {
Ok(content_source.get_children().await?.clone_value())
}

let mut routes = this
.page_roots
.iter()
.copied()
.try_flat_map_recursive_join(get_content_source_children)
.await?
.into_iter()
.map(content_source_to_pathname)
.try_join()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let mut routes = GraphTraversal::<NonDeterministic<_>>::visit(
this.page_roots.iter().copied(),
get_content_source_children,
)
.await?
.into_iter()
.map(content_source_to_pathname)
.try_join()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();

routes.sort_by_cached_key(|s| s.split('/').map(PageSortKey::from).collect::<Vec<_>>());

Expand Down
121 changes: 121 additions & 0 deletions crates/turbo-tasks/src/graph/get_children.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::{collections::HashSet, future::Future};

use anyhow::Result;

/// A trait that allows a graph traversal to get the children of a node.
pub trait GetChildren<T, A = ()> {
type Children: IntoIterator<Item = T>;
type Future: Future<Output = Result<Self::Children>>;

fn get_children(&mut self, item: &T) -> Option<Self::Future>;
}

// The different `Impl*` here are necessary in order to avoid the `Conflicting
// implementations of trait` error when implementing `GetChildren` on different
// kinds of `FnMut`.
// See https://users.rust-lang.org/t/conflicting-implementation-when-implementing-traits-for-fn/53359/3

pub struct ImplRef;

impl<T, C, F, CI> GetChildren<T, ImplRef> for C
where
C: FnMut(&T) -> F,
F: Future<Output = Result<CI>>,
CI: IntoIterator<Item = T>,
{
type Children = CI;
type Future = F;

fn get_children(&mut self, item: &T) -> Option<Self::Future> {
Some((self)(item))
}
}

pub struct ImplRefOption;

impl<T, C, F, CI> GetChildren<T, ImplRefOption> for C
where
C: FnMut(&T) -> Option<F>,
F: Future<Output = Result<CI>>,
CI: IntoIterator<Item = T>,
{
type Children = CI;
type Future = F;

fn get_children(&mut self, item: &T) -> Option<Self::Future> {
(self)(item)
}
}

pub struct ImplValue;

impl<T, C, F, CI> GetChildren<T, ImplValue> for C
where
T: Copy,
C: FnMut(T) -> F,
F: Future<Output = Result<CI>>,
CI: IntoIterator<Item = T>,
{
type Children = CI;
type Future = F;

fn get_children(&mut self, item: &T) -> Option<Self::Future> {
Some((self)(*item))
}
}

pub struct ImplValueOption;

impl<T, C, F, CI> GetChildren<T, ImplValueOption> for C
where
T: Copy,
C: FnMut(T) -> Option<F>,
F: Future<Output = Result<CI>>,
CI: IntoIterator<Item = T>,
{
type Children = CI;
type Future = F;

fn get_children(&mut self, item: &T) -> Option<Self::Future> {
(self)(*item)
}
}

/// A [`GetChildren`] implementation that skips nodes that have already been
/// visited. This is necessary to avoid repeated work when traversing non-tree
/// graphs (i.e. where a child can have more than one parent).
#[derive(Debug)]
pub struct SkipDuplicates<T, C, A> {
get_children: C,
visited: HashSet<T>,
_phantom: std::marker::PhantomData<A>,
}

impl<T, C, A> SkipDuplicates<T, C, A> {
/// Create a new [`SkipDuplicates`] that wraps the given [`GetChildren`].
pub fn new(get_children: C) -> Self {
Self {
get_children,
visited: HashSet::new(),
_phantom: std::marker::PhantomData,
}
}
}

impl<T, C, A> GetChildren<T> for SkipDuplicates<T, C, A>
where
T: Eq + std::hash::Hash + Clone,
C: GetChildren<T, A>,
{
type Children = C::Children;
type Future = C::Future;

fn get_children(&mut self, item: &T) -> Option<Self::Future> {
if !self.visited.contains(item) {
self.visited.insert(item.clone());
self.get_children.get_children(item)
} else {
None
}
}
}
10 changes: 10 additions & 0 deletions crates/turbo-tasks/src/graph/graph_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// A graph store is a data structure that will be built up during a graph
/// traversal. It is used to store the results of the traversal.
pub trait GraphStore<T>: Default {
type Handle: Clone;

// TODO(alexkirsz) An `entry(parent_handle) -> Entry` API would be more
// efficient, as right now we're getting the same key multiple times.
/// Inserts a node into the graph store, and returns a handle to it.
fn insert(&mut self, parent_handle: Option<Self::Handle>, node: T) -> (Self::Handle, &T);
}
131 changes: 131 additions & 0 deletions crates/turbo-tasks/src/graph/graph_traversal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use std::{future::Future, pin::Pin, task::ready};

use anyhow::Result;
use futures::{stream::FuturesUnordered, Stream};
use pin_project_lite::pin_project;

use super::{graph_store::GraphStore, GetChildren};

/// [`GraphTraversal`] is a utility type that can be used to traverse a graph of
/// nodes, where each node can have a variable number of children. The traversal
/// is done in parallel, and the order of the nodes in the traversal result is
/// determined by the [`GraphStore`] parameter.
pub struct GraphTraversal<S> {
_store: std::marker::PhantomData<S>,
}

impl<S> GraphTraversal<S> {
/// Visits the graph starting from the given `roots`, and returns a future
/// that will resolve to the traversal result.
pub fn visit<T, I, C, A>(roots: I, mut get_children: C) -> GraphTraversalFuture<T, S, C, A>
where
S: GraphStore<T>,
I: IntoIterator<Item = T>,
C: GetChildren<T, A>,
{
let mut store = S::default();
let futures = FuturesUnordered::new();
for item in roots {
let (parent_handle, item) = store.insert(None, item);
if let Some(future) = get_children.get_children(item) {
futures.push(WithHandle::new(future, parent_handle));
}
}
GraphTraversalFuture {
store,
futures,
get_children,
}
}
}

/// A future that resolves to a [`GraphStore`] containing the result of a graph
/// traversal.
pub struct GraphTraversalFuture<T, S, C, A>
where
S: GraphStore<T>,
C: GetChildren<T, A>,
{
store: S,
futures: FuturesUnordered<WithHandle<C::Future, S::Handle>>,
get_children: C,
}

impl<T, S, C, A> Future for GraphTraversalFuture<T, S, C, A>
where
S: GraphStore<T>,
C: GetChildren<T, A>,
{
type Output = Result<S>;

fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
loop {
let futures = unsafe { Pin::new_unchecked(&mut this.futures) };
if let Some((parent_handle, result)) = ready!(futures.poll_next(cx)) {
match result {
Ok(children) => {
for item in children {
let (child_handle, item) =
this.store.insert(Some(parent_handle.clone()), item);

if let Some(future) = this.get_children.get_children(item) {
this.futures.push(WithHandle::new(future, child_handle));
}
}
}
Err(err) => return std::task::Poll::Ready(Err(err)),
}
} else {
return std::task::Poll::Ready(Ok(std::mem::take(&mut this.store)));
}
}
}
}

pin_project! {
struct WithHandle<T, P>
where
T: Future,
{
#[pin]
future: T,
handle: Option<P>,
}
}

impl<T, H> WithHandle<T, H>
where
T: Future,
{
pub fn new(future: T, handle: H) -> Self {
Self {
future,
handle: Some(handle),
}
}
}

impl<T, H> Future for WithHandle<T, H>
where
T: Future,
{
type Output = (H, T::Output);

fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.project();
match this.future.poll(cx) {
std::task::Poll::Ready(result) => std::task::Poll::Ready((
this.handle.take().expect("polled after completion"),
result,
)),
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
}
10 changes: 10 additions & 0 deletions crates/turbo-tasks/src/graph/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mod get_children;
mod graph_store;
mod graph_traversal;
mod non_deterministic;
mod reverse_topological;

pub use get_children::{GetChildren, SkipDuplicates};
pub use graph_traversal::GraphTraversal;
pub use non_deterministic::NonDeterministic;
pub use reverse_topological::ReverseTopological;
31 changes: 31 additions & 0 deletions crates/turbo-tasks/src/graph/non_deterministic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use super::graph_store::GraphStore;

/// A graph traversal that does not guarantee any particular order, and may not
/// return the same order every time it is run.
pub struct NonDeterministic<T> {
output: Vec<T>,
}

impl<T> Default for NonDeterministic<T> {
fn default() -> Self {
Self { output: Vec::new() }
}
}

impl<T> GraphStore<T> for NonDeterministic<T> {
type Handle = ();

fn insert(&mut self, _parent_handle: Option<Self::Handle>, node: T) -> (Self::Handle, &T) {
self.output.push(node);
((), self.output.last().unwrap())
}
}

impl<T> IntoIterator for NonDeterministic<T> {
type Item = T;
type IntoIter = <Vec<T> as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.output.into_iter()
}
}
Loading

0 comments on commit 44d6a46

Please sign in to comment.