Skip to content

Commit 15ac9e6

Browse files
committed
Ensure codeframe when calling Client Functions from the Server
1 parent cb3d8a0 commit 15ac9e6

File tree

6 files changed

+99
-45
lines changed

6 files changed

+99
-45
lines changed

crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ impl EcmascriptClientReferenceModule {
8585
writedoc!(
8686
code,
8787
r#"
88+
// This file is generated by next-core EcmascriptClientReferenceModule.
8889
import {{ registerClientReference }} from "react-server-dom-turbopack/server";
8990
"#,
9091
)?;
@@ -135,6 +136,7 @@ impl EcmascriptClientReferenceModule {
135136
writedoc!(
136137
code,
137138
r#"
139+
// This file is generated by next-core EcmascriptClientReferenceModule.
138140
const {{ createClientModuleProxy }} = require("react-server-dom-turbopack/server");
139141
140142
{TURBOPACK_EXPORT_NAMESPACE}(createClientModuleProxy({server_module_path}));
@@ -149,11 +151,15 @@ impl EcmascriptClientReferenceModule {
149151

150152
let proxy_source = VirtualSource::new(
151153
self.server_ident.path().await?.join(
152-
// Depending on the original format, we call the file `proxy.mjs` or `proxy.cjs`.
153-
// This is because we're placing the virtual module next to the original code, so
154-
// its parsing will be affected by `type` fields in package.json --
155-
// a bare `proxy.js` may end up being unexpectedly parsed as the wrong format.
156-
&format!("proxy.{}", if is_esm { "mjs" } else { "cjs" }),
154+
// We choose the extension based on the original file because we're placing the
155+
// virtual module next to the original code, so its parsing will be
156+
// affected by `type` fields in package.json -- a bare `proxy.js`
157+
// may end up being unexpectedly parsed as the wrong format.
158+
// The name special cased later to always ignore-list this module.
159+
&format!(
160+
"__nextjs-internal-proxy.{}",
161+
if is_esm { "mjs" } else { "cjs" }
162+
),
157163
)?,
158164
proxy_module_content,
159165
);

packages/next/src/build/webpack/config/blocks/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { getRspackCore } from '../../../../shared/lib/get-rspack'
99
function shouldIgnorePath(modulePath: string): boolean {
1010
return (
1111
modulePath.includes('node_modules') ||
12+
modulePath.endsWith('__nextjs-internal-proxy.cjs') ||
13+
modulePath.endsWith('__nextjs-internal-proxy.mjs') ||
1214
// Only relevant for when Next.js is symlinked e.g. in the Next.js monorepo
1315
modulePath.includes('next/dist')
1416
)

packages/next/src/build/webpack/loaders/next-flight-loader/index.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { NormalModule, webpack } from 'next/dist/compiled/webpack/webpack'
1+
import type {
2+
javascript,
3+
LoaderContext,
4+
NormalModule,
5+
webpack,
6+
} from 'next/dist/compiled/webpack/webpack'
27
import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants'
38
import {
49
BARREL_OPTIMIZATION_PREFIX,
@@ -8,10 +13,7 @@ import { warnOnce } from '../../../../shared/lib/utils/warn-once'
813
import { getRSCModuleInformation } from '../../../analysis/get-page-static-info'
914
import { formatBarrelOptimizedResource } from '../../utils'
1015
import { getModuleBuildInfo } from '../get-module-build-info'
11-
import type {
12-
javascript,
13-
LoaderContext,
14-
} from 'next/dist/compiled/webpack/webpack'
16+
import { ModuleFilenameHelpers } from 'next/dist/compiled/webpack/webpack'
1517

1618
type SourceType = javascript.JavascriptParser['sourceType'] | 'commonjs'
1719

@@ -72,6 +74,7 @@ export default function transformSource(
7274
prefix = `/* __rspack_internal_rsc_module_information_do_not_use__ ${rscModuleInformationJson} */\n`
7375
source = prefix + source
7476
}
77+
prefix += `// This file is generated by the Webpack next-flight-loader.\n`
7578

7679
// Resource key is the unique identifier for the resource. When RSC renders
7780
// a client module, that key is used to identify that module across all compiler
@@ -144,7 +147,28 @@ ${JSON.stringify(ref)},
144147
}
145148
}
146149

147-
return this.callback(null, esmSource, sourceMap)
150+
const compilation = this._compilation!
151+
const originalSourceURL = ModuleFilenameHelpers.createFilename(
152+
module,
153+
{
154+
moduleFilenameTemplate:
155+
'webpack://[namespace]/[resource-path]/__nextjs-internal-proxy.mjs',
156+
namespace: '_N_E',
157+
},
158+
{
159+
requestShortener: compilation.requestShortener,
160+
chunkGraph: compilation.chunkGraph,
161+
hashFunction: compilation.outputOptions.hashFunction,
162+
}
163+
)
164+
165+
return this.callback(null, esmSource, {
166+
version: 3,
167+
sources: [originalSourceURL],
168+
// minimal, parseable mappings
169+
mappings: 'AAAA',
170+
sourcesContent: [esmSource],
171+
})
148172
} else if (assumedSourceType === 'commonjs') {
149173
let cjsSource =
150174
prefix +
@@ -153,7 +177,29 @@ const { createProxy } = require("${MODULE_PROXY_PATH}")
153177
154178
module.exports = createProxy(${stringifiedResourceKey})
155179
`
156-
return this.callback(null, cjsSource, sourceMap)
180+
181+
const compilation = this._compilation!
182+
const originalSourceURL = ModuleFilenameHelpers.createFilename(
183+
module,
184+
{
185+
moduleFilenameTemplate:
186+
'webpack://[namespace]/[resource-path]/__nextjs-internal-proxy.cjs',
187+
namespace: '_N_E',
188+
},
189+
{
190+
requestShortener: compilation.requestShortener,
191+
chunkGraph: compilation.chunkGraph,
192+
hashFunction: compilation.outputOptions.hashFunction,
193+
}
194+
)
195+
196+
return this.callback(null, cjsSource, {
197+
version: 3,
198+
sources: [originalSourceURL],
199+
// minimal, parseable mappings
200+
mappings: 'AAAA',
201+
sourcesContent: [cjsSource],
202+
})
157203
}
158204
}
159205

test/development/app-dir/source-mapping/source-mapping.test.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -169,37 +169,18 @@ describe('source-mapping', () => {
169169
it('should show an error when client functions are called from server components', async () => {
170170
const browser = await next.browser('/server-client')
171171

172-
// TODO(veil): Top stack should be ignore-listed
173-
if (isTurbopack) {
174-
await expect(browser).toDisplayRedbox(`
175-
{
176-
"description": "Attempted to call useClient() from the server but useClient is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
177-
"environmentLabel": "Server",
178-
"label": "Runtime Error",
179-
"source": "app/server-client/client.js/proxy.mjs (3:24) @ <anonymous>
180-
> 3 | function() { throw new Error("Attempted to call useClient() from the server but useClient is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component."); },
181-
| ^",
182-
"stack": [
183-
"<anonymous> app/server-client/client.js/proxy.mjs (3:24)",
184-
"Component app/server-client/page.js (5:12)",
185-
],
186-
}
187-
`)
188-
} else {
189-
await expect(browser).toDisplayRedbox(`
190-
{
191-
"description": "Attempted to call useClient() from the server but useClient is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
192-
"environmentLabel": "Server",
193-
"label": "Runtime Error",
194-
"source": "app/server-client/page.js (5:12) @ Component
195-
> 5 | useClient()
196-
| ^",
197-
"stack": [
198-
"<FIXME-file-protocol>",
199-
"Component app/server-client/page.js (5:12)",
200-
],
201-
}
202-
`)
203-
}
172+
await expect(browser).toDisplayRedbox(`
173+
{
174+
"description": "Attempted to call useClient() from the server but useClient is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
175+
"environmentLabel": "Server",
176+
"label": "Runtime Error",
177+
"source": "app/server-client/page.js (5:12) @ Component
178+
> 5 | useClient()
179+
| ^",
180+
"stack": [
181+
"Component app/server-client/page.js (5:12)",
182+
],
183+
}
184+
`)
204185
})
205186
})

test/development/app-dir/use-cache-errors/use-cache-errors.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { nextTestSetup } from 'e2e-utils'
22
import { assertNoRedbox } from '../../../lib/next-test-utils'
33

44
describe('use-cache-errors', () => {
5-
const { next } = nextTestSetup({
5+
const { isTurbopack, next } = nextTestSetup({
66
files: __dirname,
77
})
88
const isRspack = Boolean(process.env.NEXT_RSPACK)
@@ -33,7 +33,24 @@ describe('use-cache-errors', () => {
3333
],
3434
}
3535
`)
36+
} else if (isTurbopack) {
37+
await expect(browser).toDisplayRedbox(`
38+
{
39+
"description": "Attempted to call useStuff() from the server but useStuff is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
40+
"environmentLabel": "Cache",
41+
"label": "Runtime Error",
42+
"source": "app/module-with-use-cache.ts (16:18) @ useCachedStuff
43+
> 16 | return useStuff()
44+
| ^",
45+
"stack": [
46+
"useCachedStuff app/module-with-use-cache.ts (16:18)",
47+
"Page app/page.tsx (22:10)",
48+
],
49+
}
50+
`)
3651
} else {
52+
// TODO(veil): Webpack does sourcemap the client reference module.
53+
// codeframe just lucks out due to being able to sourcemap.
3754
await expect(browser).toDisplayRedbox(`
3855
{
3956
"description": "Attempted to call useStuff() from the server but useStuff is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",

turbopack/crates/turbopack-core/src/source_map/utils.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub fn add_default_ignore_list(map: &mut swc_sourcemap::SourceMap) {
1919
if source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[next]"))
2020
|| source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[turbopack]"))
2121
|| source.contains("/node_modules/")
22+
|| source.ends_with("__nextjs-internal-proxy.cjs")
23+
|| source.ends_with("__nextjs-internal-proxy.mjs")
2224
{
2325
ignored_ids.insert(source_id);
2426
}

0 commit comments

Comments
 (0)