Skip to content

[pull] canary from vercel:canary #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 4, 2025
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
56 changes: 53 additions & 3 deletions crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use turbo_tasks_fs::FileSystemPath;
use turbopack::{
css::chunk::CssChunkType,
module_options::{
CssOptionsContext, EcmascriptOptionsContext, JsxTransformOptions, ModuleOptionsContext,
ModuleRule, TypeofWindow, TypescriptTransformOptions,
CssOptionsContext, EcmascriptOptionsContext, ExternalsTracingOptions, JsxTransformOptions,
ModuleOptionsContext, ModuleRule, TypeofWindow, TypescriptTransformOptions,
},
resolve_options_context::ResolveOptionsContext,
transition::Transition,
Expand All @@ -19,6 +19,7 @@ use turbopack_core::{
ChunkingConfig, MangleType, MinifyType, SourceMapsType,
module_id_strategies::ModuleIdStrategy,
},
compile_time_defines,
compile_time_info::{CompileTimeDefines, CompileTimeInfo, FreeVarReferences},
environment::{
Environment, ExecutionEnvironment, NodeJsEnvironment, NodeJsVersion, RuntimeVersions,
Expand Down Expand Up @@ -387,6 +388,49 @@ pub async fn get_server_compile_time_info(
.await
}

#[turbo_tasks::function]
pub async fn get_tracing_compile_time_info() -> Result<Vc<CompileTimeInfo>> {
CompileTimeInfo::builder(
Environment::new(ExecutionEnvironment::NodeJsLambda(
NodeJsEnvironment::default().resolved_cell(),
))
.to_resolved()
.await?,
)
/*
We'd really like to set `process.env.NODE_ENV = "production"` here, but with that,
`react/cjs/react.development.js` won't be copied anymore (as expected).
However if you `import` react from native ESM: `import {createContext} from 'react';`, it fails with
```
import {createContext} from 'react';
^^^^^^^^^^^^^
SyntaxError: Named export 'createContext' not found. The requested module 'react' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
```
This is because Node's import-cjs-from-esm feature can correctly find all named exports in
```
// `react/index.js`
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.js');
} else {
module.exports = require('./cjs/react.development.js');
}
```
if both files exist (which is what's happening so far).
If `react.development.js` doesn't exist, then it bails with that error message.
Also just removing that second branch works fine, but a `require` to a non-existent file fails.
*/
.defines(
compile_time_defines!(
process.env.TURBOPACK = true,
// process.env.NODE_ENV = "production",
)
.resolved_cell(),
)
.cell()
.await
}

#[turbo_tasks::function]
pub async fn get_server_module_options_context(
project_path: FileSystemPath,
Expand Down Expand Up @@ -544,7 +588,13 @@ pub async fn get_server_module_options_context(
tree_shaking_mode: tree_shaking_mode_for_user_code,
side_effect_free_packages: next_config.optimize_package_imports().owned().await?,
enable_externals_tracing: if next_mode.is_production() {
Some(project_path)
Some(
ExternalsTracingOptions {
tracing_root: project_path,
compile_time_info: get_tracing_compile_time_info().to_resolved().await?,
}
.resolved_cell(),
)
} else {
None
},
Expand Down
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -771,5 +771,6 @@
"770": "createParamsFromClient should not be called in a runtime prerender.",
"771": "\\`%s\\` was called during a runtime prerender. Next.js should be preventing %s from being included in server components statically, but did not in this case.",
"772": "FetchStrategy.PPRRuntime should never be used when `experimental.clientSegmentCache` is disabled",
"773": "Missing workStore in createPrerenderParamsForClientSegment"
"773": "Missing workStore in createPrerenderParamsForClientSegment",
"774": "Route %s used %s outside of a Server Component. This is not allowed."
}
19 changes: 10 additions & 9 deletions packages/next/src/server/request/root-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ export function getRootParam(paramName: string): Promise<ParamValue> {
throw new InvariantError(`Missing workStore in ${apiName}`)
}

const workUnitStore = workUnitAsyncStorage.getStore()
if (!workUnitStore) {
throw new Error(
`Route ${workStore.route} used ${apiName} outside of a Server Component. This is not allowed.`
)
}

const actionStore = actionAsyncStorage.getStore()
if (actionStore) {
if (actionStore.isAppRoute) {
Expand All @@ -211,23 +218,17 @@ export function getRootParam(paramName: string): Promise<ParamValue> {
`Route ${workStore.route} used ${apiName} inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js.`
)
}
if (actionStore.isAction) {
if (actionStore.isAction && workUnitStore.phase === 'action') {
// Actions are not fundamentally tied to a route (even if they're always submitted from some page),
// so root params would be inconsistent if an action is called from multiple roots.
// Make sure we check if the phase is "action" - we should not error in the rerender
// after an action revalidates or updates cookies (which will still have `actionStore.isAction === true`)
throw new Error(
`${apiName} was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route.`
)
}
}

const workUnitStore = workUnitAsyncStorage.getStore()

if (!workUnitStore) {
throw new Error(
`Route ${workStore.route} used ${apiName} in Pages Router. This API is only available within App Router.`
)
}

switch (workUnitStore.type) {
case 'unstable-cache':
case 'cache': {
Expand Down
18 changes: 9 additions & 9 deletions test/development/app-dir/ssr-in-rsc/ssr-in-rsc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,16 +794,16 @@ describe('react-dom/server in React Server environment', () => {
if (isTurbopack) {
if (isReact18) {
expect(redbox).toMatchInlineSnapshot(`
{
"description": "Cannot read properties of undefined (reading 'ReactCurrentDispatcher')",
"source": "internal-pkg/server.node.js (1:1) @ {module evaluation}
{
"description": "Cannot read properties of undefined (reading 'ReactCurrentDispatcher')",
"source": "internal-pkg/server.node.js (1:1) @ {module evaluation}
> 1 | import * as ReactDOMServerEdge from 'react-dom/server.node'
| ^
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerEdgeDefault from 'react-dom/server.node'
4 |",
}
> 1 | import * as ReactDOMServerEdge from 'react-dom/server.node'
| ^
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerEdgeDefault from 'react-dom/server.node'
4 |",
}
`)
} else {
expect(redbox).toMatchInlineSnapshot(`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { lang, locale } from 'next/root-params'
import { connection } from 'next/server'
import { cookies } from 'next/headers'
import { Suspense } from 'react'

export default async function Page() {
const currentLang = await lang()
const currentLocale = await locale()
return (
<main>
<div>
Root params are{' '}
<span id="root-params">
{currentLang} {currentLocale}
</span>
</div>
<Suspense fallback="Loading...">
<Timestamp />
</Suspense>
<form
action={async () => {
'use server'
// rerender the page and return it alongside the action result
const cookieStore = await cookies()
cookieStore.set('my-cookie', Date.now() + '')
}}
>
<button type="submit">Submit form</button>
</form>
</main>
)
}

async function Timestamp() {
await connection()
return <div id="timestamp">{Date.now()}</div>
}
64 changes: 62 additions & 2 deletions test/e2e/app-dir/app-root-params-getters/simple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ import { outdent } from 'outdent'
import { createRequestTracker } from '../../../lib/e2e-utils/request-tracker'

describe('app-root-param-getters - simple', () => {
let currentCliOutputIndex = 0
beforeEach(() => {
resetCliOutput()
})

const getCliOutput = () => {
if (next.cliOutput.length < currentCliOutputIndex) {
// cliOutput shrank since we started the test, so something (like a `sandbox`) reset the logs
currentCliOutputIndex = 0
}
return next.cliOutput.slice(currentCliOutputIndex)
}

const resetCliOutput = () => {
currentCliOutputIndex = next.cliOutput.length
}

const { next, isNextDev, isTurbopack, isNextDeploy } = nextTestSetup({
files: join(__dirname, 'fixtures', 'simple'),
})
Expand Down Expand Up @@ -109,12 +126,55 @@ describe('app-root-param-getters - simple', () => {
)
expect(response.status()).toBe(500)
if (!isNextDeploy) {
expect(next.cliOutput).toInclude(
expect(getCliOutput()).toInclude(
"`import('next/root-params').lang()` was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route."
)
}
})

it('should not error when rerendering the page after a server action', async () => {
const params = { lang: 'en', locale: 'us' }
const browser = await next.browser(
`/${params.lang}/${params.locale}/rerender-after-server-action`
)
expect(await browser.elementById('root-params').text()).toBe(
`${params.lang} ${params.locale}`
)
const initialDate = await browser.elementById('timestamp')

// Run a server action and rerender the page
const tracker = createRequestTracker(browser)
const [, response] = await tracker.captureResponse(
async () => {
await browser.elementByCss('button[type="submit"]').click()
},
{
request: {
method: 'POST',
pathname: `/${params.lang}/${params.locale}/rerender-after-server-action`,
},
}
)
// We're using lang() outside of an action, so we should see no errors
expect(response.status()).toBe(200)
if (!isNextDeploy) {
expect(getCliOutput()).not.toInclude(
"`import('next/root-params').lang()` was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route."
)
}

await retry(async () => {
// The page should've been rerendered because of the cookie update
const updatedDate = await browser.elementById('timestamp')
expect(initialDate).not.toEqual(updatedDate)
})

// It should still display correct root params
expect(await browser.elementById('root-params').text()).toBe(
`${params.lang} ${params.locale}`
)
})

// TODO(root-params): add support for route handlers
it('should error when used in a route handler (until we implement it)', async () => {
const params = { lang: 'en', locale: 'us' }
Expand All @@ -123,7 +183,7 @@ describe('app-root-param-getters - simple', () => {
)
expect(response.status).toBe(500)
if (!isNextDeploy) {
expect(next.cliOutput).toInclude(
expect(getCliOutput()).toInclude(
"Route /[lang]/[locale]/route-handler used `import('next/root-params').lang()` inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js."
)
}
Expand Down
30 changes: 15 additions & 15 deletions turbopack/crates/turbopack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ use turbopack_core::{
chunk::SourceMapsType,
compile_time_info::CompileTimeInfo,
context::{AssetContext, ProcessResult},
environment::{Environment, ExecutionEnvironment, NodeJsEnvironment},
ident::Layer,
issue::{IssueExt, IssueSource, StyledString, module::ModuleIssue},
module::Module,
Expand Down Expand Up @@ -643,15 +642,12 @@ async fn process_default_internal(
}

#[turbo_tasks::function]
async fn externals_tracing_module_context(ty: ExternalType) -> Result<Vc<ModuleAssetContext>> {
let env = Environment::new(ExecutionEnvironment::NodeJsLambda(
NodeJsEnvironment::default().resolved_cell(),
))
.to_resolved()
.await?;

async fn externals_tracing_module_context(
ty: ExternalType,
compile_time_info: Vc<CompileTimeInfo>,
) -> Result<Vc<ModuleAssetContext>> {
let resolve_options = ResolveOptionsContext {
emulate_environment: Some(env),
emulate_environment: Some(compile_time_info.await?.environment),
loose_errors: true,
custom_conditions: match ty {
ExternalType::CommonJs => vec![rcstr!("require")],
Expand All @@ -663,7 +659,7 @@ async fn externals_tracing_module_context(ty: ExternalType) -> Result<Vc<ModuleA

Ok(ModuleAssetContext::new_without_replace_externals(
Default::default(),
CompileTimeInfo::builder(env).cell().await?,
compile_time_info,
// Keep these options more or less in sync with
// turbopack/crates/turbopack/tests/node-file-trace.rs to ensure that the NFT unit tests
// are actually representative of what Turbopack does.
Expand Down Expand Up @@ -784,7 +780,7 @@ impl AssetContext for ModuleAssetContext {
ResolveResultItem::External { name, ty, traced } => {
let replacement = if replace_externals {
let tracing_mode = if traced == ExternalTraced::Traced
&& let Some(tracing_root) = &self
&& let Some(options) = &self
.module_options_context()
.await?
.enable_externals_tracing
Expand All @@ -793,13 +789,17 @@ impl AssetContext for ModuleAssetContext {
// request will later be resolved relative to tracing_root
// anyway.

let options = options.await?;
CachedExternalTracingMode::Traced {
externals_context: ResolvedVc::upcast(
externals_tracing_module_context(ty)
.to_resolved()
.await?,
externals_tracing_module_context(
ty,
*options.compile_time_info,
)
.to_resolved()
.await?,
),
root_origin: tracing_root.join("_")?,
root_origin: options.tracing_root.join("_")?,
}
} else {
CachedExternalTracingMode::Untraced
Expand Down
Loading
Loading