Skip to content

Commit

Permalink
Introduce PathRegex for patch matching with named parameters (vercel/…
Browse files Browse the repository at this point in the history
…turborepo#511)

This is another attempt at fixing https://github.com/vercel/turbo-tooling/pull/507. It also handles the root index page properly ("pages/index.js").
  • Loading branch information
alexkirsz authored and Brooooooklyn committed Oct 13, 2022
1 parent b3594f7 commit 21c0c6e
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 90 deletions.
1 change: 1 addition & 0 deletions crates/next-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod env;
pub mod next_client;
mod next_import_map;
mod nodejs;
mod path_regex;
pub mod react_refresh;
mod server_rendered_source;
mod web_entry_source;
Expand Down
24 changes: 7 additions & 17 deletions crates/next-core/src/nodejs/node_rendered_source.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use anyhow::{anyhow, Result};
use indexmap::IndexMap;
use turbo_tasks::primitives::RegexVc;
use turbo_tasks_fs::FileSystemPathVc;
use turbopack_core::chunk::ChunkingContextVc;
use turbopack_dev_server::source::{
Expand All @@ -13,6 +12,7 @@ use turbopack_dev_server::source::{
use turbopack_ecmascript::{chunk::EcmascriptChunkPlaceablesVc, EcmascriptModuleAssetVc};

use super::{external_asset_entrypoints, render_static, RenderData};
use crate::path_regex::PathRegexVc;

/// Trait that allows to get the entry module for rendering something in Node.js
#[turbo_tasks::value_trait]
Expand All @@ -21,23 +21,23 @@ pub trait NodeRenderer {
}

/// Creates a content source that renders something in Node.js with the passed
/// `renderer` when it matches a `regular_expression`. Once rendered it serves
/// `renderer` when it matches a `path_regex`. Once rendered it serves
/// all assets referenced by the `renderer` that are within the `server_root`.
/// It needs a temporary directory (`intermediate_output_path`) to place file
/// for Node.js execution during rendering. The `chunking_context` should emit
/// to this directory.
#[turbo_tasks::function]
pub fn create_node_rendered_source(
server_root: FileSystemPathVc,
regular_expression: RegexVc,
path_regex: PathRegexVc,
renderer: NodeRendererVc,
chunking_context: ChunkingContextVc,
runtime_entries: EcmascriptChunkPlaceablesVc,
intermediate_output_path: FileSystemPathVc,
) -> ContentSourceVc {
let source = NodeRenderContentSource {
server_root,
regular_expression,
path_regex,
renderer,
chunking_context,
runtime_entries,
Expand All @@ -59,7 +59,7 @@ pub fn create_node_rendered_source(
#[turbo_tasks::value]
struct NodeRenderContentSource {
server_root: FileSystemPathVc,
regular_expression: RegexVc,
path_regex: PathRegexVc,
renderer: NodeRendererVc,
chunking_context: ChunkingContextVc,
runtime_entries: EcmascriptChunkPlaceablesVc,
Expand All @@ -69,23 +69,13 @@ struct NodeRenderContentSource {
impl NodeRenderContentSource {
/// Checks if a path matches the regular expression
async fn is_matching_path(&self, path: &str) -> Result<bool> {
Ok(self.regular_expression.await?.is_match(path))
Ok(self.path_regex.await?.is_match(path))
}

/// Matches a path with the regular expression and returns a JSON object
/// with the named captures
async fn get_matches(&self, path: &str) -> Result<Option<IndexMap<String, String>>> {
let regexp = self.regular_expression.await?;
Ok(regexp.captures(path).map(|capture| {
regexp
.capture_names()
.flatten()
.filter_map(|name| {
let value = capture.name(name)?;
Some((name.to_string(), value.as_str().to_string()))
})
.collect()
}))
Ok(self.path_regex.await?.get_matches(path))
}
}

Expand Down
127 changes: 127 additions & 0 deletions crates/next-core/src/path_regex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use anyhow::{Context, Result};
use indexmap::IndexMap;
use turbo_tasks::primitives::Regex;

/// A regular expression that matches a path, with named capture groups for the
/// dynamic parts of the path.
#[turbo_tasks::value(shared)]
#[derive(Debug)]
pub struct PathRegex {
regex: Regex,
named_params: Vec<String>,
}

impl PathRegex {
/// Returns true if the given path matches the regular expression.
pub fn is_match(&self, path: &str) -> bool {
self.regex.is_match(path)
}

/// Matches a path with the regular expression and returns a map with the
/// named captures.
pub fn get_matches(&self, path: &str) -> Option<IndexMap<String, String>> {
self.regex.captures(path).map(|capture| {
self.named_params
.iter()
.enumerate()
.filter_map(|(idx, name)| {
let value = capture.get(idx + 1)?;
Some((name.to_string(), value.as_str().to_string()))
})
.collect()
})
}
}

/// Builder for [PathRegex].
pub struct PathRegexBuilder {
regex_str: String,
named_params: Vec<String>,
}

impl PathRegexBuilder {
/// Creates a new [PathRegexBuilder].
pub fn new() -> Self {
Self {
regex_str: "^".to_string(),
named_params: Default::default(),
}
}

fn include_slash(&self) -> bool {
self.regex_str.len() > 1
}

fn push_str(&mut self, str: &str) {
self.regex_str.push_str(str);
}

/// Pushes an optional catch all segment to the regex.
pub fn push_optional_catch_all<N, R>(&mut self, name: N, rem: R)
where
N: Into<String>,
R: AsRef<str>,
{
self.push_str(if self.include_slash() {
"(/[^?]+)?"
} else {
"([^?]+)?"
});
self.push_str(&regex::escape(rem.as_ref()));
self.named_params.push(name.into());
}

/// Pushes a catch all segment to the regex.
pub fn push_catch_all<N, R>(&mut self, name: N, rem: R)
where
N: Into<String>,
R: AsRef<str>,
{
if self.include_slash() {
self.push_str("/");
}
self.push_str("([^?]+)");
self.push_str(&regex::escape(rem.as_ref()));
self.named_params.push(name.into());
}

/// Pushes a dynamic segment to the regex.
pub fn push_dynamic_segment<N, R>(&mut self, name: N, rem: R)
where
N: Into<String>,
R: AsRef<str>,
{
if self.include_slash() {
self.push_str("/");
}
self.push_str("([^?/]+)");
self.push_str(&regex::escape(rem.as_ref()));
self.named_params.push(name.into());
}

/// Pushes a static segment to the regex.
pub fn push_static_segment<S>(&mut self, segment: S)
where
S: AsRef<str>,
{
if self.include_slash() {
self.push_str("/");
}
self.push_str(&regex::escape(segment.as_ref()));
}

/// Builds and returns the [PathRegex].
pub fn build(mut self) -> Result<PathRegex> {
self.regex_str += "$";
Ok(PathRegex {
regex: Regex(regex::Regex::new(&self.regex_str).with_context(|| "invalid path regex")?),
named_params: self.named_params,
})
}
}

impl Default for PathRegexBuilder {
fn default() -> Self {
Self::new()
}
}
106 changes: 33 additions & 73 deletions crates/next-core/src/server_rendered_source.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use std::{collections::HashMap, fmt::Write};
use std::collections::HashMap;

use anyhow::{anyhow, bail, Result};
use regex::Regex;
use turbo_tasks::{primitives::RegexVc, Value, ValueToString};
use turbo_tasks::{Value, ValueToString};
use turbo_tasks_env::ProcessEnvVc;
use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPathVc};
use turbopack::{
Expand Down Expand Up @@ -40,6 +39,7 @@ use crate::{
},
next_import_map::get_next_import_map,
nodejs::node_rendered_source::{create_node_rendered_source, NodeRenderer, NodeRendererVc},
path_regex::{PathRegexBuilder, PathRegexVc},
};

/// Create a content source serving the `pages` or `src/pages` directory as
Expand Down Expand Up @@ -138,7 +138,7 @@ pub async fn create_server_rendered_source(
async fn regular_expression_for_path(
server_root: FileSystemPathVc,
server_path: FileSystemPathVc,
) -> Result<RegexVc> {
) -> Result<PathRegexVc> {
let server_path_value = &*server_path.await?;
let path = if let Some(path) = server_root.await?.get_path_to(server_path_value) {
path
Expand All @@ -152,88 +152,48 @@ async fn regular_expression_for_path(
let (path, _) = path
.rsplit_once('.')
.ok_or_else(|| anyhow!("path ({}) has no extension", path))?;
let path = path.strip_suffix("/index").unwrap_or(path);
let mut reg_exp_source = "^".to_string();
let path = if path == "index" {
""
} else {
path.strip_suffix("/index").unwrap_or(path)
};
let mut path_regex = PathRegexBuilder::new();
for segment in path.split('/') {
/// Writes regex for handling an optional catch all placeholder.
fn write_optional_catch_all(
reg_exp_source: &mut String,
segment: &str,
include_slash: bool,
path: &str,
) -> Result<()> {
if let Some((placeholder, rem)) = segment.split_once("]]") {
if include_slash {
write!(*reg_exp_source, "(/?P<{placeholder}>[^?]+)?")?;
if let Some(segment) = segment.strip_prefix('[') {
if let Some(segment) = segment.strip_prefix("[...") {
if let Some((placeholder, rem)) = segment.split_once("]]") {
path_regex.push_optional_catch_all(placeholder, rem);
} else {
write!(*reg_exp_source, "(?P<{placeholder}>[^?]+)?")?;
bail!(
"path ({}) contains '[[' without matching ']]' at '[[...{}'",
path,
segment
);
}
*reg_exp_source += &regex::escape(rem);
Ok(())
} else {
bail!(
"path ({}) contains '[[' without matching ']]' at '[[...{}'",
path,
segment
);
}
}
/// Writes regex for handling a required catch all placeholder.
fn write_catch_all(reg_exp_source: &mut String, segment: &str, path: &str) -> Result<()> {
if let Some((placeholder, rem)) = segment.split_once(']') {
write!(*reg_exp_source, "(?P<{placeholder}>[^?]+)")?;
*reg_exp_source += &regex::escape(rem);
Ok(())
} else {
bail!(
"path ({}) contains '[' without matching ']' at '[...{}'",
path,
segment
);
}
}
/// Writes regex for handling a normal placeholder.
fn write_dynamic_segment(
reg_exp_source: &mut String,
segment: &str,
path: &str,
) -> Result<()> {
if let Some((placeholder, rem)) = segment.split_once(']') {
write!(*reg_exp_source, "(?P<{placeholder}>[^?/]+)")?;
*reg_exp_source += &regex::escape(rem);
Ok(())
} else if let Some(segment) = segment.strip_prefix("...") {
if let Some((placeholder, rem)) = segment.split_once(']') {
path_regex.push_catch_all(placeholder, rem);
} else {
bail!(
"path ({}) contains '[' without matching ']' at '[...{}'",
path,
segment
);
}
} else if let Some((placeholder, rem)) = segment.split_once(']') {
path_regex.push_dynamic_segment(placeholder, rem);
} else {
bail!(
"path ({}) contains '[' without matching ']' at '[{}'",
path,
segment
);
}
}

let include_slash = reg_exp_source.len() > 1;
if let Some(segment) = segment.strip_prefix('[') {
if let Some(segment) = segment.strip_prefix("[...") {
write_optional_catch_all(&mut reg_exp_source, segment, include_slash, path)?;
} else {
if include_slash {
reg_exp_source += "/";
}
if let Some(segment) = segment.strip_prefix("...") {
write_catch_all(&mut reg_exp_source, segment, path)?;
} else {
write_dynamic_segment(&mut reg_exp_source, segment, path)?;
}
}
} else {
if include_slash {
reg_exp_source += "/";
}
reg_exp_source += &regex::escape(segment);
path_regex.push_static_segment(segment);
}
}
reg_exp_source += "$";
Ok(RegexVc::cell(Regex::new(&reg_exp_source)?))
Ok(PathRegexVc::cell(path_regex.build()?))
}

/// Handles a single page file in the pages directory
Expand Down

0 comments on commit 21c0c6e

Please sign in to comment.