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
31 changes: 21 additions & 10 deletions crates/next-api/src/next_server_nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use turbo_tasks::{
use turbo_tasks_fs::{
DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath, glob::Glob,
};
use turbo_tasks_hash::HashAlgorithm;
use turbopack::externals_tracing_module_context;
use turbopack_core::{
asset::{Asset, AssetContent},
Expand Down Expand Up @@ -117,9 +118,12 @@ impl Asset for ServerNftJsonAsset {
.await?
.iter()
.map(async |m| {
base_dir
.get_relative_path_to(&*m.path().await?)
.context("failed to compute relative path for server NFT JSON")
Ok((
base_dir
.get_relative_path_to(&*m.path().await?)
.context("failed to compute relative path for server NFT JSON")?,
m.content().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
))
})
.try_join()
.await?;
Expand All @@ -128,11 +132,15 @@ impl Asset for ServerNftJsonAsset {
for ty in ["app-page", "pages"] {
let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
let module_path = dir.join("module.compiled.js")?;
server_output_assets.push(
server_output_assets.push((
base_dir
.get_relative_path_to(&module_path)
.context("failed to compute relative path for server NFT JSON")?,
);
module_path
.read()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.await?,
));

let contexts_dir = dir.join("vendored/contexts")?;
let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
Expand All @@ -146,24 +154,27 @@ impl Asset for ServerNftJsonAsset {
continue;
};
if file.extension() == Some("js") {
server_output_assets.push(
server_output_assets.push((
base_dir
.get_relative_path_to(file)
.context("failed to compute relative path for server NFT JSON")?,
)
file.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
))
}
}
}

server_output_assets.sort();
server_output_assets.sort_unstable();
// Dedupe as some entries may be duplicates: a file might be referenced multiple times,
// e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
// was required.
server_output_assets.dedup();

let (files, file_hashes): (Vec<_>, Vec<_>) = server_output_assets.into_iter().unzip();
let json = json!({
"version": 1,
"files": server_output_assets
"version": 1,
"files": files,
"fileHashes": file_hashes
});

Ok(AssetContent::file(
Expand Down
34 changes: 27 additions & 7 deletions crates/next-api/src/nft_json.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};

use anyhow::{Result, bail};
use async_trait::async_trait;
Expand All @@ -14,6 +14,7 @@ use turbo_tasks_fs::{
DirectoryEntry, File, FileContent, FileSystem, FileSystemPath,
glob::{Glob, GlobOptions},
};
use turbo_tasks_hash::HashAlgorithm;
use turbopack_core::{
asset::{Asset, AssetContent},
issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString},
Expand Down Expand Up @@ -106,14 +107,14 @@ async fn apply_includes(
project_root_path: FileSystemPath,
glob: Vc<Glob>,
ident_folder: &FileSystemPath,
) -> Result<BTreeSet<RcStr>> {
) -> Result<BTreeMap<RcStr, ReadRef<RcStr>>> {
debug_assert_eq!(project_root_path.fs, ident_folder.fs);
// Read files matching the glob pattern from the project root
// This result itself has random order, but the BTreeSet will ensure a deterministic ordering.
let glob_result = project_root_path.read_glob(glob).await?;

// Walk the full glob_result using an explicit stack to avoid async recursion overheads.
let mut result = BTreeSet::new();
let mut result = BTreeMap::new();
let mut stack = VecDeque::new();
stack.push_back(glob_result);
while let Some(glob_result) = stack.pop_back() {
Expand All @@ -128,7 +129,10 @@ async fn apply_includes(
// unwrap is safe because project_root_path and ident_folder have the same filesystem
// and paths produced by read_glob stay in the filesystem
let relative_path = ident_folder.get_relative_path_to(file_path).unwrap();
result.insert(relative_path);
result.insert(
relative_path,
file_path.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
);
}

for nested_result in glob_result.inner.values() {
Expand All @@ -149,7 +153,7 @@ impl Asset for NftJsonAsset {
path = display(self.path().to_string().await?)
);
async move {
let mut result: BTreeSet<RcStr> = BTreeSet::new();
let mut result: BTreeMap<RcStr, ReadRef<RcStr>> = BTreeMap::new();
let project_path = this.project.project_path().owned().await?;

let output_root_ref = this.project.output_fs().root().await?;
Expand Down Expand Up @@ -320,7 +324,13 @@ impl Asset for NftJsonAsset {
}
};

result.insert(specifier);
result.insert(
specifier,
referenced_chunk
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.await?,
);
}

// Apply outputFileTracingIncludes and outputFileTracingExcludes
Expand Down Expand Up @@ -371,9 +381,19 @@ impl Asset for NftJsonAsset {
result.extend(includes.into_iter().flatten());
}

let (files, file_hashes): (Vec<_>, Vec<_>) = result.into_iter().unzip();
// We can't just add this into "files" because Next.js sometimes decides to delete
// output files such as `.next/server/pages/index.js` if that page was prerendered and
// is fully static. An alternative would be to postprocess the nft file so that
// non-adapter consumers (which includes output:standalone) don't experience a breaking
// change, but instead we just add it as a separate field that only build-complete
// reads.
let entry_hash = chunk.content().hash(HashAlgorithm::Xxh3Hash128Hex).await?;
let json = json!({
"version": 1,
"files": result
"files": files,
"fileHashes": file_hashes,
"entryHash": entry_hash,
});

Ok(AssetContent::file(
Expand Down
8 changes: 6 additions & 2 deletions crates/next-api/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tracing::Instrument;
use turbo_rcstr::RcStr;
use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
use turbo_tasks_fs::FileSystemPath;
use turbo_tasks_hash::{HashAlgorithm, encode_hex};
use turbo_tasks_hash::HashAlgorithm;
use turbopack_core::{
asset::{Asset, no_hash_salt},
output::{OutputAsset, OutputAssets},
Expand Down Expand Up @@ -44,7 +44,11 @@ async fn asset_path(
.await?
.context("asset content not found")?
} else {
encode_hex(*asset.content().hash().await?).into()
asset
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.owned()
.await?
};
Some(AssetPath {
path: RcStr::from(path),
Expand Down
26 changes: 5 additions & 21 deletions crates/next-api/src/routes_hashes_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::Serialize;
use turbo_rcstr::RcStr;
use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
use turbo_tasks_fs::{FileContent, FileSystemPath};
use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
use turbo_tasks_hash::{DeterministicHash, HashAlgorithm, Xxh3Hash64Hasher, hash_xxh3_hash64};
use turbopack_core::{
asset::{Asset, AssetContent},
module::{Module, Modules},
Expand Down Expand Up @@ -77,19 +77,11 @@ pub async fn outputs_hash(outputs: Vc<OutputAssets>) -> Result<Vc<u64>> {
.await?;
let outputs_hashes = output_assets
.iter()
.map(|asset| asset.content().hash())
.map(|asset| asset.content().hash(HashAlgorithm::Xxh3Hash128Hex))
.try_join()
.await?;

let outputs_hash = {
let mut hasher = Xxh3Hash64Hasher::new();
for hash in outputs_hashes.iter() {
hash.deterministic_hash(&mut hasher);
}
hasher.finish()
};

Ok(Vc::cell(outputs_hash))
Ok(Vc::cell(hash_xxh3_hash64(outputs_hashes)))
}

#[turbo_tasks::function]
Expand Down Expand Up @@ -159,19 +151,11 @@ pub async fn sources_hash(module_graph: Vc<ModuleGraph>, modules: Vc<Modules>) -
.try_flat_join()
.await?
.into_iter()
.map(|source| source.content().hash())
.map(|source| source.content().hash(HashAlgorithm::Xxh3Hash128Hex))
.try_join()
.await?;

let sources_hash = {
let mut hasher = Xxh3Hash64Hasher::new();
for source in sources.iter() {
source.deterministic_hash(&mut hasher);
}
hasher.finish()
};

Ok(Vc::cell(sources_hash))
Ok(Vc::cell(hash_xxh3_hash64(sources)))
}

#[derive(Serialize)]
Expand Down
4 changes: 3 additions & 1 deletion crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,9 @@ pub struct OptionJsonValue(
);

fn turbopack_config_documentation_link() -> RcStr {
rcstr!("https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#configuring-webpack-loaders")
rcstr!(
"https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#configuring-webpack-loaders"
)
}

#[turbo_tasks::value(shared)]
Expand Down
5 changes: 4 additions & 1 deletion crates/next-napi-bindings/src/next_api/turbopack_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,10 @@ pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
let bug_report_url = format!(
"https://bugs.nextjs.org/search?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
&urlencoding::encode(&title),
&urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &internal_error_str))
&urlencoding::encode(&format!(
"{}\n\nError message:\n```\n{}\n```",
&version_str, &internal_error_str
))
);
let bug_report_message = if supports_hyperlinks::supports_hyperlinks() {
"clicking here.".hyperlink(&bug_report_url)
Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/03-api-reference/04-functions/cacheTag.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default async function submit() {
cacheTag('tag-one', 'tag-two')
```

- **Limits**: The max length for a custom tag is 256 characters and the max tag items is 128.
- **Limits**: A single `cacheTag()` call accepts up to 128 tags, each with a maximum length of 256 characters. Tags longer than 256 characters are skipped, and any tags past the 128th in one call are dropped. Both cases log a console warning.

## Examples

Expand Down
23 changes: 23 additions & 0 deletions docs/01-app/03-api-reference/04-functions/use-router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function Page() {
- `router.prefetch(href: string, options?: { onInvalidate?: () => void })`: [Prefetch](/docs/app/getting-started/linking-and-navigating#prefetching) the provided route for faster client-side transitions. The optional `onInvalidate` callback is called when the [prefetched data becomes stale](/docs/app/guides/prefetching#extending-or-ejecting-link).
- `router.back()`: Navigate back to the previous route in the browser’s history stack.
- `router.forward()`: Navigate forwards to the next page in the browser’s history stack.
- `router.bfcacheId`: An opaque string identifier scoped to the current route segment. It changes when the surrounding segment is freshly created by a push or replace navigation, and stays the same for back/forward navigations, `router.refresh()`, and search-param- or hash-only navigations. See [`bfcacheId`](#bfcacheid) below for details.

> **Good to know**:
>
Expand Down Expand Up @@ -156,6 +157,28 @@ export default function Page() {
}
```

### `bfcacheId`

`router.bfcacheId` is an opaque string identifier scoped to the current route segment. It changes when the surrounding segment is freshly created by a push or replace navigation, and stays the same for back/forward navigations, `router.refresh()`, and search-param- or hash-only navigations.

The recommended use is to pass it as a React `key` to opt out of state preservation on fresh navigations, while still restoring it during a back/forward navigation:

```tsx filename="app/example/page.tsx"
'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
const { bfcacheId } = useRouter()
return <form key={bfcacheId}>{/* ... */}</form>
}
```

When `cacheComponents` is enabled, the App Router preserves Client Component state across navigations using React `<Activity>`. Keying a component on `bfcacheId` resets it on each fresh navigation while still preserving its state across browser back/forward navigations.

> **Good to know**:
> Instead of `bfcacheId`, prefer resetting state explicitly in an event handler (for example, `onSubmit`) or deriving a key from your data (for example, a draft id from the server). Use `bfcacheId` only as a last resort, like when migrating an existing codebase.

## Version History

| Version | Changes |
Expand Down
Loading
Loading