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
8 changes: 4 additions & 4 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -399,13 +399,13 @@ indexmap = "2.7.1"
indoc = "2.0.0"
inventory = "0.3.21"
itertools = "0.10.5"
lightningcss = { version = "1.0.0-alpha.68", features = [
lightningcss = { version = "1.0.0-alpha.70", features = [
"serde",
"visitor",
"into_owned",
"browserslist",
] }
lightningcss-napi = { version = "0.4.5", default-features = false, features = [
lightningcss-napi = { version = "0.4.6", default-features = false, features = [
"visitor",
] }
markdown = "1.0.0-alpha.18"
Expand Down
70 changes: 31 additions & 39 deletions crates/next-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use next_core::{
instrumentation::instrumentation_files,
middleware::middleware_files,
mode::NextMode,
next_app::{AppPage, AppPath},
next_client::{
ClientChunkingContextOptions, get_client_chunking_context, get_client_compile_time_info,
},
Expand Down Expand Up @@ -218,24 +219,13 @@ struct DebugBuildPathsRouteKeys {
}

impl DebugBuildPathsRouteKeys {
fn from_debug_build_paths(paths: &DebugBuildPaths) -> Self {
Self {
fn from_debug_build_paths(paths: &DebugBuildPaths) -> Result<Self> {
Ok(Self {
app: paths
.app
.iter()
.map(|path| {
// App router: "/blog/[slug]/page.tsx" -> "/blog/[slug]"
if let Some(last_slash_idx) = path.rfind('/') {
if last_slash_idx == 0 {
"/".into() // Root: "/page.tsx" -> "/"
} else {
path[..last_slash_idx].into()
}
} else {
path.clone()
}
})
.collect(),
.map(|path| Ok(AppPath::from(AppPage::parse(path)?).to_string().into()))
.collect::<Result<FxHashSet<_>>>()?,
pages: paths
.pages
.iter()
Expand All @@ -248,7 +238,23 @@ impl DebugBuildPathsRouteKeys {
}
})
.collect(),
})
}

fn should_include_app_route(&self, route_key: &RcStr) -> bool {
// Special app router framework routes
if matches!(route_key.as_str(), "/_not-found" | "/_global-error") {
return true;
}
self.app.contains(route_key)
}

fn should_include_pages_route(&self, route_key: &RcStr) -> bool {
// Special pages router framework routes
if matches!(route_key.as_str(), "/_error" | "/_document" | "/_app") {
return true;
}
self.pages.contains(route_key)
}
}

Expand Down Expand Up @@ -524,24 +530,6 @@ fn define_env_diff_report(old: &DefineEnv, new: &DefineEnv) -> String {
report
}

/// Checks if an app router route should be included based on pre-converted route keys.
fn should_include_app_route(route_key: &RcStr, route_keys: &FxHashSet<RcStr>) -> bool {
// Special app router framework routes
if matches!(route_key.as_str(), "/_not-found" | "/_global-error") {
return true;
}
route_keys.contains(route_key)
}

/// Checks if a pages router route should be included based on pre-converted route keys.
fn should_include_pages_route(route_key: &RcStr, route_keys: &FxHashSet<RcStr>) -> bool {
// Special pages router framework routes
if matches!(route_key.as_str(), "/_error" | "/_document" | "/_app") {
return true;
}
route_keys.contains(route_key)
}

impl ProjectContainer {
pub async fn initialize(self: ResolvedVc<Self>, options: ProjectOptions) -> Result<()> {
let span = tracing::info_span!(
Expand Down Expand Up @@ -1635,7 +1623,8 @@ impl Project {
let debug_build_paths_route_keys = this
.debug_build_paths
.as_ref()
.map(DebugBuildPathsRouteKeys::from_debug_build_paths);
.map(DebugBuildPathsRouteKeys::from_debug_build_paths)
.transpose()?;

if let Some(app_project) = &*app_project.await? {
let app_routes = app_project.routes();
Expand All @@ -1646,17 +1635,20 @@ impl Project {
.filter(|(k, _)| {
debug_build_paths_route_keys
.as_ref()
.is_none_or(|keys| should_include_app_route(k, &keys.app))
.is_none_or(|keys| keys.should_include_app_route(k))
})
.map(|(k, v)| (k.clone(), v.clone())),
);
}

for (pathname, page_route) in pages_project.routes().await?.iter().filter(|(k, _)| {
debug_build_paths_route_keys
for (pathname, page_route) in &pages_project.routes().await? {
if debug_build_paths_route_keys
.as_ref()
.is_none_or(|keys| should_include_pages_route(k, &keys.pages))
}) {
.is_some_and(|keys| !keys.should_include_pages_route(pathname))
{
continue;
}

match routes.entry(pathname.clone()) {
Entry::Occupied(mut entry) => {
ConflictIssue {
Expand Down
78 changes: 78 additions & 0 deletions crates/next-core/src/next_app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ impl AppPage {
app_page.push_str(segment)?;
}

if let Some(last) = app_page.0.last_mut()
&& let PageSegment::Static(last_name) = &*last
{
// Next.js internals sometimes omit extensions when creating synthetic page entries
if last_name == "page" || last_name.starts_with("page.") {
*last = PageSegment::PageType(PageType::Page);
} else if last_name == "route" || last_name.starts_with("route.") {
*last = PageSegment::PageType(PageType::Route);
}
// can also be metadata (and be neither Page nor Route)
}

Ok(app_page)
}

Expand Down Expand Up @@ -529,3 +541,69 @@ impl From<AppPage> for AppPath {
)
}
}

#[cfg(test)]
mod test {
use crate::next_app::{AppPage, PageSegment, PageType};

#[test]
fn test_normalize_metadata_route() {
assert_eq!(
AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
AppPage(vec![
PageSegment::Group("group".into()),
PageSegment::Static("foo".into()),
PageSegment::Parallel("par".into()),
PageSegment::Static("bar".into()),
PageSegment::PageType(PageType::Page),
])
);
assert_eq!(
AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
AppPage(vec![
PageSegment::Group("group".into()),
PageSegment::Static("foo".into()),
PageSegment::Parallel("par".into()),
PageSegment::Static("bar".into()),
PageSegment::PageType(PageType::Page),
])
);

assert_eq!(
AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
AppPage(vec![
PageSegment::Group("group".into()),
PageSegment::Static("foo".into()),
PageSegment::Parallel("par".into()),
PageSegment::Static("bar".into()),
PageSegment::PageType(PageType::Route),
])
);
assert_eq!(
AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
AppPage(vec![
PageSegment::Group("group".into()),
PageSegment::Static("foo".into()),
PageSegment::Parallel("par".into()),
PageSegment::Static("bar".into()),
PageSegment::PageType(PageType::Route),
])
);

assert_eq!(
AppPage::parse("foo/sitemap").unwrap(),
AppPage(vec![
PageSegment::Static("foo".into()),
PageSegment::Static("sitemap".into()),
])
);

assert_eq!(
AppPage::parse("foo/robots.txt").unwrap(),
AppPage(vec![
PageSegment::Static("foo".into()),
PageSegment::Static("robots.txt".into()),
])
);
}
}
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/instrumentation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Instrumentation is the process of using code to integrate monitoring and logging

To set up instrumentation, create `instrumentation.ts|js` file in the **root directory** of your project (or inside the [`src`](/docs/app/api-reference/file-conventions/src-folder) folder if using one).

Then, export a `register` function in the file. This function will be called **once** when a new Next.js server instance is initiated.
Then, export a `register` function in the file. This function will be called **once** when a new Next.js server instance is initiated, and must complete before the server is ready to handle requests.

For example, to use Next.js with [OpenTelemetry](https://opentelemetry.io/) and [@vercel/otel](https://vercel.com/docs/observability/otel-overview):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To use it, place the file in the **root** of your application or inside a [`src`

### `register` (optional)

The file exports a `register` function that is called **once** when a new Next.js server instance is initiated. `register` can be an async function.
The file exports a `register` function that is called **once** when a new Next.js server instance is initiated, and must complete before the server is ready to handle requests. `register` can be an async function.

```ts filename="instrumentation.ts" switcher
import { registerOTel } from '@vercel/otel'
Expand Down
64 changes: 32 additions & 32 deletions test/integration/css/test/css-compilation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ module.exports = {

if (process.env.IS_TURBOPACK_TEST && useLightningcss) {
expect(cssContentWithoutSourceMap).toMatchInlineSnapshot(
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px,0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px, 0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
)
} else if (process.env.IS_TURBOPACK_TEST && !useLightningcss) {
expect(cssContentWithoutSourceMap).toMatchInlineSnapshot(
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px,0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px, 0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
)
} else if (process.env.NEXT_RSPACK && useLightningcss) {
expect(cssContentWithoutSourceMap).toMatchInlineSnapshot(
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px,0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
`"@media (min-width:480px) and (not (min-width:768px)){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0px, 0px)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`
)
} else if (useLightningcss) {
expect(cssContentWithoutSourceMap).toMatchInlineSnapshot(
Expand Down Expand Up @@ -129,36 +129,36 @@ module.exports = {
if (process.env.IS_TURBOPACK_TEST) {
// Turbopack always uses lightningcss
expect(sourceMapContentParsed).toMatchInlineSnapshot(`
{
"mappings": "AAAA,qDACE,2BAKF,0DAIA,kDAIA,uCAIA",
"names": [],
"sourcesContent": [
"@media (480px <= width < 768px) {
::placeholder {
color: green;
}
}

.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}

.transform-parsing {
transform: translate3d(0px, 0px);
}

.css-grid-shorthand {
grid-column: span 2;
}

.g-docs-sidenav .filter::-webkit-input-placeholder {
opacity: 80%;
{
"mappings": "AAAA,qDACE,2BAKF,0DAIA,mDAIA,uCAIA",
"names": [],
"sourcesContent": [
"@media (480px <= width < 768px) {
::placeholder {
color: green;
}
",
],
"version": 3,
}
`)
}

.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}

.transform-parsing {
transform: translate3d(0px, 0px);
}

.css-grid-shorthand {
grid-column: span 2;
}

.g-docs-sidenav .filter::-webkit-input-placeholder {
opacity: 80%;
}
",
],
"version": 3,
}
`)
} else if (process.env.NEXT_RSPACK && !useLightningcss) {
expect(sourceMapContentParsed).toMatchInlineSnapshot(`
{
Expand Down
26 changes: 26 additions & 0 deletions test/production/debug-build-path/debug-build-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,32 @@ describe('debug-build-paths', () => {
expect(buildResult.cliOutput).toContain('○ /foo')
expect(buildResult.cliOutput).not.toContain('/with-type-error')
})

it('should build routes inside route groups', async () => {
const buildResult = await next.build({
args: ['--debug-build-paths', 'app/(group)/**/page.tsx'],
})
expect(buildResult.exitCode).toBe(0)
expect(buildResult.cliOutput).toContain('Route (app)')
// Route groups are stripped from the path, so /nested instead of /(group)/nested
expect(buildResult.cliOutput).toContain('/nested')
// Should not build other routes
expect(buildResult.cliOutput).not.toContain('○ /about')
expect(buildResult.cliOutput).not.toContain('○ /dashboard')
})

it('should build routes with parallel routes', async () => {
const buildResult = await next.build({
args: ['--debug-build-paths', 'app/parallel-test/**/page.tsx'],
})
expect(buildResult.exitCode).toBe(0)
expect(buildResult.cliOutput).toContain('Route (app)')
// Parallel route segments (@sidebar) are stripped from the path
expect(buildResult.cliOutput).toContain('/parallel-test')
// Should not build other routes
expect(buildResult.cliOutput).not.toContain('○ /about')
expect(buildResult.cliOutput).not.toContain('○ /dashboard')
})
})

describe('typechecking with debug-build-paths', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>Grouped nested page</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Sidebar() {
return <div>Sidebar parallel route</div>
}
Loading
Loading