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
2 changes: 2 additions & 0 deletions .github/workflows/build_reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ env:
NEXT_TEST_JOB: 1
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
VERCEL_TEST_TEAM: vtest314-next-e2e-tests
VERCEL_ADAPTER_TEST_TOKEN: ${{ secrets.VERCEL_ADAPTER_TEST_TOKEN }}
VERCEL_ADAPTER_TEST_TEAM: vtest314-next-adapter-e2e-tests
NEXT_TEST_PREFER_OFFLINE: 1
NEXT_CI_RUNNER: ${{ inputs.runs_on_labels }}
NEXT_TEST_PROXY_ADDRESS: ${{ inputs.overrideProxyAddress || '' }}
Expand Down
26 changes: 21 additions & 5 deletions .github/workflows/test_e2e_deploy_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ on:
type: string

env:
DD_ENV: 'ci'
DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }}
VERCEL_TEST_TEAM: vtest314-next-e2e-tests
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }}
DD_ENV: 'ci'
VERCEL_ADAPTER_TEST_TEAM: vtest314-next-adapter-e2e-tests
VERCEL_ADAPTER_TEST_TOKEN: ${{ secrets.VERCEL_ADAPTER_TEST_TOKEN }}

run-name: test-e2e-deploy ${{ inputs.nextVersion || (github.event_name == 'release' && github.event.release.tag_name) || 'canary' }}

Expand Down Expand Up @@ -98,7 +100,21 @@ jobs:
strategy:
fail-fast: false
matrix:
group: [1/1]
group:
[
1/12,
2/12,
3/12,
4/12,
5/12,
6/12,
7/12,
8/12,
9/12,
10/12,
11/12,
12/12,
]
with:
afterBuild: |
npm i -g vercel@latest && \
Expand All @@ -109,10 +125,10 @@ jobs:
NEXT_EXTERNAL_TESTS_FILTERS="test/deploy-tests-manifest.json" \
NEXT_TEST_VERSION="${{ github.event.inputs.nextVersion || needs.setup.outputs.next-version || 'canary' }}" \
VERCEL_CLI_VERSION="${{ github.event.inputs.vercelCliVersion || 'vercel@latest' }}" \
node run-tests.js --timings -g ${{ matrix.group }} -c 2 test/e2e/app-dir/segment-cache/client-params/client-params.test.ts test/e2e/app-dir/app-static/app-static.test.ts test/e2e/app-dir/cache-components/cache-components.test.ts test/e2e/app-dir/app/index.test.ts test/e2e/app-dir/app-edge/app-edge.test.ts test/e2e/edge-pages-support/index.test.ts test/e2e/prerender.test.ts test/e2e/middleware-general/test/index.test.ts test/e2e/middleware-rewrites/test/index.test.ts test/e2e/app-dir/actions/app-action-node-middleware.test.ts
node run-tests.js --timings -g ${{ matrix.group }} -c 2 --type e2e
skipNativeBuild: 'yes'
skipNativeInstall: 'no'
stepName: 'test-deploy-adapter-${{ matrix.group }}'
stepName: 'test-deploy-deploy-${{ matrix.group }}'
timeout_minutes: 180
runs_on_labels: '["ubuntu-latest"]'
overrideProxyAddress: ${{ inputs.overrideProxyAddress || '' }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test_e2e_project_reset_cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
env:
VERCEL_TEST_TEAM: vtest314-next-e2e-tests
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
VERCEL_ADAPTER_TEST_TEAM: vtest314-next-adapter-e2e-tests
VERCEL_ADAPTER_TEST_TOKEN: ${{ secrets.VERCEL_ADAPTER_TEST_TOKEN }}
NODE_LTS_VERSION: 20
TURBO_TEAM: 'vercel'
TURBO_CACHE: 'remote:rw'
Expand Down
202 changes: 193 additions & 9 deletions crates/next-api/src/nft_json.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::collections::{BTreeSet, VecDeque};
use std::collections::{BTreeSet, HashSet, VecDeque};

use anyhow::{Result, bail};
use serde_json::json;
use tracing::{Instrument, Level, Span};
use turbo_rcstr::RcStr;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{
FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
graph::{AdjacencyMap, GraphTraversal, Visit},
Expand All @@ -14,6 +14,7 @@ use turbo_tasks_fs::{
};
use turbopack_core::{
asset::{Asset, AssetContent},
issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
output::{OutputAsset, OutputAssets, OutputAssetsReference},
};

Expand Down Expand Up @@ -147,10 +148,16 @@ impl Asset for NftJsonAsset {
);
async move {
let mut result: BTreeSet<RcStr> = BTreeSet::new();
let project_path = this.project.project_path().owned().await?;

let output_root_ref = this.project.output_fs().root().await?;
let project_root_ref = this.project.project_fs().root().await?;
let next_config = this.project.next_config();
let next_config_path = this
.project
.next_config()
.config_file_path(project_path.clone())
.await?;

let output_file_tracing_includes = &*next_config.output_file_tracing_includes().await?;
let output_file_tracing_excludes = &*next_config.output_file_tracing_excludes().await?;
Expand All @@ -173,7 +180,6 @@ impl Asset for NftJsonAsset {
.chain(std::iter::once(chunk))
.collect();

let project_path = this.project.project_path().owned().await?;
let exclude_glob = if let Some(route) = &this.page_name {
if let Some(excludes_config) = output_file_tracing_excludes {
let mut combined_excludes = BTreeSet::new();
Expand Down Expand Up @@ -230,13 +236,11 @@ impl Asset for NftJsonAsset {
None
};

let entries = Vc::cell(entries);
// Collect base assets first
let all_assets = all_assets_from_entries_filtered(
Vc::cell(entries),
Some(client_root),
exclude_glob,
)
.await?;
let all_assets =
all_assets_from_entries_filtered(entries, Some(client_root.clone()), exclude_glob)
.await?;

for referenced_chunk in all_assets.iter().copied() {
if chunk.eq(&referenced_chunk) {
Expand All @@ -245,6 +249,19 @@ impl Asset for NftJsonAsset {

let referenced_chunk_path = referenced_chunk.path().await?;

if referenced_chunk_path == next_config_path {
// If next.config.js was traced, assume that the whole project was traced
// (unintentionally). Print a message in this case to avoid deploying
// unnecessary files.
error_unexpected_file(
entries,
Some(client_root.clone()),
exclude_glob,
*referenced_chunk,
)
.await?;
}

if referenced_chunk_path.has_extension(".map") {
continue;
}
Expand Down Expand Up @@ -442,6 +459,173 @@ pub async fn all_assets_from_entries_filtered(
))
}

#[turbo_tasks::function]
pub async fn error_unexpected_file(
entries: Vc<OutputAssets>,
client_root: Option<FileSystemPath>,
exclude_glob: Option<Vc<Glob>>,
referenced_chunk: ResolvedVc<Box<dyn OutputAsset>>,
) -> Result<()> {
let exclude_glob = if let Some(exclude_glob) = exclude_glob {
Some(exclude_glob.await?)
} else {
None
};
let emit_spans = tracing::enabled!(Level::INFO);
let map = AdjacencyMap::new()
.visit(
entries
.await?
.iter()
.map(async |asset| {
Ok((
*asset,
if emit_spans {
// INVALIDATION: we don't need to invalidate the list of assets when
// the span name changes
Some(asset.path_string().untracked().await?)
} else {
None
},
))
})
.try_join()
.await?,
OutputAssetFilteredVisit {
client_root,
exclude_glob,
emit_spans,
},
)
.await
.completed()?;

let reversed = map.reversed();

let mut path = vec![];
// Find any path from the referenced chunk back to one of the roots
{
let mut visited = HashSet::new();
let mut current = (
referenced_chunk,
if emit_spans {
// INVALIDATION: we don't need to invalidate the list of assets when
// the span name changes
Some(referenced_chunk.path_string().untracked().await?)
} else {
None
},
);
while let Some((from, _)) = reversed.get(&current).and_then(|mut edges| edges.next()) {
current = from.clone();
if !visited.insert(current.0) {
break;
}
path.push(current.0);
}
}

ForbiddenTracedFileIssue {
file: referenced_chunk,
path,
}
.resolved_cell()
.emit();

Ok(())
}

#[turbo_tasks::value(shared)]
struct ForbiddenTracedFileIssue {
file: ResolvedVc<Box<dyn OutputAsset>>,
path: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
}

#[turbo_tasks::value_impl]
impl Issue for ForbiddenTracedFileIssue {
fn severity(&self) -> IssueSeverity {
// Ideally this would be an error, but for now we keep it a warning to avoid breaking
// existing apps
IssueSeverity::Warning
}

#[turbo_tasks::function]
fn stage(&self) -> Vc<IssueStage> {
IssueStage::Misc.cell()
}

#[turbo_tasks::function]
fn file_path(&self) -> Vc<FileSystemPath> {
self.file.path()
}

#[turbo_tasks::function]
fn title(&self) -> Vc<StyledString> {
StyledString::Text(rcstr!("Encountered unexpected file in NFT list")).cell()
}

#[turbo_tasks::function]
async fn description(&self) -> Result<Vc<OptionStyledString>> {
let mut stack = vec![
StyledString::Text(rcstr!(
"A file was traced that indicates that the whole project was traced \
unintentionally. Somewhere in the import trace below, there are:"
)),
StyledString::Line(vec![
StyledString::Text(rcstr!("- filesystem operations (like ")),
StyledString::Code(rcstr!("path.join")),
StyledString::Text(rcstr!(", ")),
StyledString::Code(rcstr!("path.resolve")),
StyledString::Text(rcstr!(" or ")),
StyledString::Code(rcstr!("fs.readFile")),
StyledString::Text(rcstr!("), or")),
]),
StyledString::Line(vec![
StyledString::Text(rcstr!("- very dynamic requires (like ")),
StyledString::Code(rcstr!("require('./' + foo)")),
StyledString::Text(rcstr!(").")),
]),
StyledString::Text(rcstr!("To resolve this, you can")),
StyledString::Text(rcstr!("- remove them if possible, or")),
StyledString::Text(rcstr!("- only use them in development, or")),
StyledString::Line(vec![
StyledString::Text(rcstr!(
"- make sure they are statically scoped to some subfolder: "
)),
StyledString::Code(rcstr!("path.join(process.cwd(), 'data', bar)")),
StyledString::Text(rcstr!(", or")),
]),
StyledString::Line(vec![
StyledString::Text(rcstr!("- add ignore comments: ")),
StyledString::Code(rcstr!(
"path.join(/*turbopackIgnore: true*/ process.cwd(), bar)"
)),
]),
];

if self.path.len() > 1 {
stack.extend([
StyledString::Text(rcstr!("")),
StyledString::Text(
format!(
"Output asset trace:\n{}",
self.path
.iter()
.rev()
.map(async |a| Ok(format!(" {}", a.path_string().await?)))
.try_join()
.await?
.join("\n")
)
.into(),
),
])
}

Ok(Vc::cell(Some(StyledString::Stack(stack).resolved_cell())))
}
}

struct OutputAssetFilteredVisit {
client_root: Option<FileSystemPath>,
exclude_glob: Option<ReadRef<Glob>>,
Expand Down
14 changes: 10 additions & 4 deletions docs/01-app/02-guides/content-security-policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ Every time a page is viewed, a fresh nonce should be generated. This means that

For example:

> **Good to know**: In development, `'unsafe-eval'` is required because React uses `eval` to provide enhanced debugging information, such as reconstructing server-side error stacks in the browser. `unsafe-eval` is not required for production. Neither React nor Next.js use `eval` in production by default.

```ts filename="proxy.ts" switcher
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
Expand Down Expand Up @@ -88,9 +91,10 @@ import { NextResponse } from 'next/server'

export function proxy(request) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
Expand Down Expand Up @@ -483,9 +487,11 @@ When SRI is enabled, you can continue using your existing CSP policies. SRI work
> **Good to know**: For dynamic rendering scenarios, you can still generate nonces with proxy if needed, combining both SRI integrity attributes and nonce-based CSP approaches.

```js filename="next.config.js"
const isDev = process.env.NODE_ENV === 'development'

const cspHeader = `
default-src 'self';
script-src 'self';
script-src 'self'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self';
img-src 'self' blob: data:;
font-src 'self';
Expand Down Expand Up @@ -540,7 +546,7 @@ CSP implementation differs between development and production environments:

### Development Environment

In development, you will need to enable `'unsafe-eval'` to support APIs that provide additional debugging information:
In development, you will need to enable `'unsafe-eval'` because React uses `eval` to provide enhanced debugging information, such as reconstructing server-side error stacks in the browser to show you where errors originated on the server:

```ts filename="proxy.ts" switcher
export function proxy(request: NextRequest) {
Expand Down
Loading
Loading