Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5a2cc3e
Don't transform nested functions in exported call expressions
unstubbable Nov 11, 2025
e4aee80
Update expected output for fixture 63
unstubbable Nov 11, 2025
bc0eb4d
Better expected output
unstubbable Nov 11, 2025
5c491cf
Intermediate step, still broken
unstubbable Nov 11, 2025
0b1576b
Remove duplicate exports in 63, includes other regressions
unstubbable Nov 11, 2025
1aac81f
Fix regressions after boolean inversion
unstubbable Nov 11, 2025
2b3b88a
Fix regression with client graph references
unstubbable Nov 11, 2025
5252a7b
Fix wrong cache import idents
unstubbable Nov 11, 2025
0e5f549
Fix reference ID map insertion
unstubbable Nov 11, 2025
ef0fd8c
Support re-export of imported thing
unstubbable Nov 11, 2025
438932a
Fix warning
unstubbable Nov 11, 2025
b926cbc
Support direct re-exports
unstubbable Nov 11, 2025
07fd910
Copy test fixture to client-graph fixtures
unstubbable Nov 11, 2025
914c193
Preserve comments
unstubbable Nov 11, 2025
d43e556
Preserve original spans
unstubbable Nov 11, 2025
3551909
Move name assignment into `if` block
unstubbable Nov 11, 2025
341a6e9
Clarify runtime wrapping of inbound functions
unstubbable Nov 11, 2025
8a74959
Allow sync functions for runtime-wrapped `'use cache'`
unstubbable Nov 11, 2025
23db8cc
Fix auto-format changes
unstubbable Nov 11, 2025
0ec2d8b
Extract `create_cache_wrapper` helper function
unstubbable Nov 11, 2025
4a30984
Add `type` imports/exports to fixture
unstubbable Nov 11, 2025
26321e5
Clean up some of the comments
unstubbable Nov 11, 2025
5c4c3ce
Remove unnecessary `.into()` calls
unstubbable Nov 11, 2025
792d038
Fix formatting
unstubbable Nov 11, 2025
e02f513
Dedupe expression runtime wrapper check
unstubbable Nov 11, 2025
1124616
Remove unnecessary defaults
unstubbable Nov 11, 2025
075fb78
Restore features in Cargo.toml
unstubbable Nov 11, 2025
982c8e3
Update `has_cache` when emitting cache runtime wrapper
unstubbable Nov 11, 2025
19cdea5
Fix function names
unstubbable Nov 11, 2025
ff83209
Add an end-to-end test
unstubbable Nov 11, 2025
0f83258
Fix auto-applied formatting
unstubbable Nov 11, 2025
a327d05
Update fixture output after rebase
unstubbable Nov 11, 2025
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
529 changes: 448 additions & 81 deletions crates/next-custom-transforms/src/transforms/server_actions.rs

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions crates/next-custom-transforms/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use swc_core::{
common::{comments::SingleThreadedComments, FileName, Mark, SyntaxContext},
ecma::{
ast::Pass,
parser::{EsSyntax, Syntax},
parser::{EsSyntax, Syntax, TsSyntax},
transforms::{
base::resolver,
react::jsx,
Expand Down Expand Up @@ -534,10 +534,16 @@ fn next_font_loaders_fixture(input: PathBuf) {

#[fixture("tests/fixture/server-actions/**/input.*")]
fn server_actions_fixture(input: PathBuf) {
let (input_syntax, extension) = if input.extension() == Some("ts".as_ref()) {
(Syntax::Typescript(Default::default()), "ts")
} else {
(syntax(), "js")
let (input_syntax, extension) = match input.extension().and_then(|e| e.to_str()) {
Some("ts") => (Syntax::Typescript(Default::default()), "ts"),
Some("tsx") => (
Syntax::Typescript(TsSyntax {
tsx: true,
..Default::default()
}),
"tsx",
),
_ => (syntax(), "js"),
};

let output = input.parent().unwrap().join(format!("output.{extension}"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use cache'

// @ts-ignore
import { getCachedStuff, wrap } from './foo'
// @ts-ignore
export { getData } from './data'

export const getCachedData = async () => {
// This one already worked before.
return getCachedStuff()
}

export const aliased = getCachedStuff

const Layout = wrap(async () => <div>Layout</div>)
const Other = wrap(async () => <div>Other</div>)
export const Sync = wrap(() => <div>Sync</div>)

export const wrapped = wrap(
async () => 'foo',
async () => 'bar',
async () => async () => 'baz',
() => 'sync'
)

export default Layout
export { Other, getCachedStuff }

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use cache'

// @ts-ignore
import { getStuff, wrap, type Stuff } from './foo'
// @ts-ignore
export { getData, type Data } from './data'

export const getCachedData = async (): Stuff => {
// This is not using the wrapped version of getStuff, as we're only
// runtime-wrapping what flows out of the module, not into it. Would one
// expect this to be cached?
return getStuff()
}

export const aliased = getStuff

const Layout = wrap(async () => <div>Layout</div>)
const Other = wrap(async () => <div>Other</div>)
export const Sync = wrap(() => <div>Sync</div>)

export const wrapped = wrap(
async () => 'foo',
async () => 'bar',
async () => async () => 'baz',
() => 'sync'
)

export default Layout
export { Other, getStuff }
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0","ff1acff246876a467753785a92d1f95ac6fe32c9b9":"Other","ff27fadf3eeb97c777cea9f14a407b5c0b42ac65bb":"aliased","ff438bb59117ff1af890c80ca3e39d9e888fc93033":"wrapped","ff84effee663e5ce4e0948b55df129a8df904c67aa":"Sync","ff8fa22f08e492db15701f58a1458cc4ebf782f855":"getData","ff980f8c891ae27674b86a4804d306bdb3065c2e4f":"getStuff","ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d":"default"} */ import { registerServerReference } from "private-next-rsc-server-reference";
import { cache as $$cache__ } from "private-next-rsc-cache-wrapper";
import { cache as $$reactCache__ } from "react";
// @ts-ignore
import { getStuff, wrap, type Stuff } from './foo';
const $$RSC_SERVER_CACHE_0_INNER = async function getCachedData() {
// This is not using the wrapped version of getStuff, as we're only
// runtime-wrapping what flows out of the module, not into it. Would one
// expect this to be cached?
return getStuff();
};
export var $$RSC_SERVER_CACHE_0 = $$reactCache__(function getCachedData() {
return $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, $$RSC_SERVER_CACHE_0_INNER, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_0, "name", {
value: "getCachedData"
});
export const getCachedData = $$RSC_SERVER_CACHE_0;
// @ts-ignore
import { getData } from './data';
const aliased = getStuff;
const Layout = wrap(async ()=><div>Layout</div>);
const Other = wrap(async ()=><div>Other</div>);
const Sync = wrap(()=><div>Sync</div>);
const wrapped = wrap(async ()=>'foo', async ()=>'bar', async ()=>async ()=>'baz', ()=>'sync');
let $$RSC_SERVER_CACHE_getData = getData;
if (typeof getData === "function") {
$$RSC_SERVER_CACHE_getData = $$reactCache__(function() {
return $$cache__("default", "ff8fa22f08e492db15701f58a1458cc4ebf782f855", 0, getData, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_getData, "ff8fa22f08e492db15701f58a1458cc4ebf782f855", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_getData, "name", {
value: "getData"
});
}
export { $$RSC_SERVER_CACHE_getData as getData };
let $$RSC_SERVER_CACHE_aliased = aliased;
if (typeof aliased === "function") {
$$RSC_SERVER_CACHE_aliased = $$reactCache__(function() {
return $$cache__("default", "ff27fadf3eeb97c777cea9f14a407b5c0b42ac65bb", 0, aliased, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_aliased, "ff27fadf3eeb97c777cea9f14a407b5c0b42ac65bb", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_aliased, "name", {
value: "aliased"
});
}
export { $$RSC_SERVER_CACHE_aliased as aliased };
let $$RSC_SERVER_CACHE_Sync = Sync;
if (typeof Sync === "function") {
$$RSC_SERVER_CACHE_Sync = $$reactCache__(function() {
return $$cache__("default", "ff84effee663e5ce4e0948b55df129a8df904c67aa", 0, Sync, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_Sync, "ff84effee663e5ce4e0948b55df129a8df904c67aa", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_Sync, "name", {
value: "Sync"
});
}
export { $$RSC_SERVER_CACHE_Sync as Sync };
let $$RSC_SERVER_CACHE_wrapped = wrapped;
if (typeof wrapped === "function") {
$$RSC_SERVER_CACHE_wrapped = $$reactCache__(function() {
return $$cache__("default", "ff438bb59117ff1af890c80ca3e39d9e888fc93033", 0, wrapped, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_wrapped, "ff438bb59117ff1af890c80ca3e39d9e888fc93033", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_wrapped, "name", {
value: "wrapped"
});
}
export { $$RSC_SERVER_CACHE_wrapped as wrapped };
let $$RSC_SERVER_CACHE_default = Layout;
if (typeof Layout === "function") {
$$RSC_SERVER_CACHE_default = $$reactCache__(function() {
return $$cache__("default", "ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d", 0, Layout, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_default, "ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_default, "name", {
value: "Layout"
});
}
export default $$RSC_SERVER_CACHE_default;
let $$RSC_SERVER_CACHE_Other = Other;
if (typeof Other === "function") {
$$RSC_SERVER_CACHE_Other = $$reactCache__(function() {
return $$cache__("default", "ff1acff246876a467753785a92d1f95ac6fe32c9b9", 0, Other, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_Other, "ff1acff246876a467753785a92d1f95ac6fe32c9b9", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_Other, "name", {
value: "Other"
});
}
export { $$RSC_SERVER_CACHE_Other as Other };
let $$RSC_SERVER_CACHE_getStuff = getStuff;
if (typeof getStuff === "function") {
$$RSC_SERVER_CACHE_getStuff = $$reactCache__(function() {
return $$cache__("default", "ff980f8c891ae27674b86a4804d306bdb3065c2e4f", 0, getStuff, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_getStuff, "ff980f8c891ae27674b86a4804d306bdb3065c2e4f", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_getStuff, "name", {
value: "getStuff"
});
}
export { $$RSC_SERVER_CACHE_getStuff as getStuff };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use cache'

// @ts-ignore
import { withSlug } from './with-slug'

const Page = withSlug(function Page({ slug }) {
return <p>Slug: {slug}</p>
})

export default Page
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* __next_internal_action_entry_do_not_use__ {"ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d":"default"} */ import { registerServerReference } from "private-next-rsc-server-reference";
import { cache as $$cache__ } from "private-next-rsc-cache-wrapper";
import { cache as $$reactCache__ } from "react";
// @ts-ignore
import { withSlug } from './with-slug';
const Page = withSlug(function Page({ slug }) {
return <p>Slug: {slug}</p>;
});
let $$RSC_SERVER_CACHE_default = Page;
if (typeof Page === "function") {
$$RSC_SERVER_CACHE_default = $$reactCache__(function() {
return $$cache__("default", "ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d", 0, Page, arguments);
});
registerServerReference($$RSC_SERVER_CACHE_default, "ffc18c215a6b7cdc64bf709f3a714ffdef1bf9651d", null);
Object["defineProperty"]($$RSC_SERVER_CACHE_default, "name", {
value: "Page"
});
}
export default $$RSC_SERVER_CACHE_default;
6 changes: 3 additions & 3 deletions packages/next/src/server/lib/lazy-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ export type LazyResult<TValue> = PromiseLike<TValue> & { value?: TValue }
export type ResolvedLazyResult<TValue> = PromiseLike<TValue> & { value: TValue }

/**
* Calls the given async function only when the returned promise-like object is
* Calls the given function only when the returned promise-like object is
* awaited. Afterwards, it provides the resolved value synchronously as `value`
* property.
*/
export function createLazyResult<TValue>(
fn: () => Promise<TValue>
fn: () => Promise<TValue> | TValue
): LazyResult<TValue> {
let pendingResult: Promise<TValue> | undefined

const result: LazyResult<TValue> = {
then(onfulfilled, onrejected) {
if (!pendingResult) {
pendingResult = fn()
pendingResult = Promise.resolve(fn())
}

pendingResult
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/app-dir/use-cache/app/(dynamic)/hoc/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use cache: remote'

import { withSlug } from './with-slug'

const Page = withSlug(function PageWithSlug({ slug }: { slug: string }) {
return (
<div>
<p>
Slug: <span id="slug">{slug}</span>
</p>
<p>
Date: <span id="date">{new Date().toISOString()}</span>
</p>
</div>
)
})

export default Page
10 changes: 10 additions & 0 deletions test/e2e/app-dir/use-cache/app/(dynamic)/hoc/[slug]/with-slug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function withSlug<P extends { params: Promise<{ slug: string }> }>(
Component: React.ComponentType<{ slug: string }>
): React.ComponentType<{ params: Promise<{ slug: string }> }> {
return async function ComponentWithSlug(props: P) {
const params = await props.params
const slug = params.slug

return <Component slug={slug} />
}
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/use-cache/use-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,16 @@ describe('use-cache', () => {
expect(initialScale2).toBe(initialScale)
expect(maximumScale2).toBe(maximumScale)
})

it('caches a higher-order component in a "use cache" module', async () => {
const browser = await next.browser('/hoc/foo')
const slug = await browser.elementById('slug').text()
expect(slug).toBe('foo')
const date = await browser.elementById('date').text()
expect(date).toBeDateString()
await browser.refresh()
expect(await browser.elementById('date').text()).toBe(date)
})
}
})

Expand Down
Loading