Fix client boundary defined in a module (#46171)
This PR fixes the bug where a client boundary (`"use client"`) is defined in a module (`"type": "module"`). Currently Next.js throws this error: ``` error - Error: Cannot find module 'private-next-rsc-mod-ref-proxy' ``` ...that will be resolved with this PR. The only limitation after this fix is, you can't have `export *` under a client boundary in a module. Added a error message for that. NEXT-595 ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
This commit is contained in:
parent
9e86fd1d58
commit
0ab8dcb09d
13 changed files with 172 additions and 14 deletions
|
@ -43,6 +43,7 @@ struct ReactServerComponents<C: Comments> {
|
|||
filepath: String,
|
||||
app_dir: Option<PathBuf>,
|
||||
comments: C,
|
||||
export_names: Vec<String>,
|
||||
invalid_server_imports: Vec<JsWord>,
|
||||
invalid_client_imports: Vec<JsWord>,
|
||||
invalid_server_react_apis: Vec<JsWord>,
|
||||
|
@ -78,7 +79,7 @@ impl<C: Comments> ReactServerComponents<C> {
|
|||
// Collects top level directives and imports, then removes specific ones
|
||||
// from the AST.
|
||||
fn collect_top_level_directives_and_imports(
|
||||
&self,
|
||||
&mut self,
|
||||
module: &mut Module,
|
||||
) -> (bool, Vec<ModuleImports>) {
|
||||
let mut imports: Vec<ModuleImports> = vec![];
|
||||
|
@ -170,6 +171,52 @@ impl<C: Comments> ReactServerComponents<C> {
|
|||
|
||||
finished_directives = true;
|
||||
}
|
||||
// Collect all export names.
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
|
||||
for specifier in &e.specifiers {
|
||||
self.export_names.push(match specifier {
|
||||
ExportSpecifier::Default(_) => "default".to_string(),
|
||||
ExportSpecifier::Namespace(_) => "*".to_string(),
|
||||
ExportSpecifier::Named(named) => match &named.exported {
|
||||
Some(exported) => match &exported {
|
||||
ModuleExportName::Ident(i) => i.sym.to_string(),
|
||||
ModuleExportName::Str(s) => s.value.to_string(),
|
||||
},
|
||||
_ => match &named.orig {
|
||||
ModuleExportName::Ident(i) => i.sym.to_string(),
|
||||
ModuleExportName::Str(s) => s.value.to_string(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
|
||||
match decl {
|
||||
Decl::Class(ClassDecl { ident, .. }) => {
|
||||
self.export_names.push(ident.sym.to_string());
|
||||
}
|
||||
Decl::Fn(FnDecl { ident, .. }) => {
|
||||
self.export_names.push(ident.sym.to_string());
|
||||
}
|
||||
Decl::Var(var) => {
|
||||
for decl in &var.decls {
|
||||
if let Pat::Ident(ident) = &decl.name {
|
||||
self.export_names.push(ident.id.sym.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
|
||||
decl: _,
|
||||
..
|
||||
})) => {
|
||||
self.export_names.push("default".to_string());
|
||||
}
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
|
||||
self.export_names.push("*".to_string());
|
||||
}
|
||||
_ => {
|
||||
finished_directives = true;
|
||||
}
|
||||
|
@ -245,7 +292,11 @@ impl<C: Comments> ReactServerComponents<C> {
|
|||
Comment {
|
||||
span: DUMMY_SP,
|
||||
kind: CommentKind::Block,
|
||||
text: " __next_internal_client_entry_do_not_use__ ".into(),
|
||||
text: format!(
|
||||
" __next_internal_client_entry_do_not_use__ {} ",
|
||||
self.export_names.join(",")
|
||||
)
|
||||
.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -471,6 +522,7 @@ pub fn server_components<C: Comments>(
|
|||
comments,
|
||||
filepath: filename.to_string(),
|
||||
app_dir,
|
||||
export_names: vec![],
|
||||
invalid_server_imports: vec![
|
||||
JsWord::from("client-only"),
|
||||
JsWord::from("react-dom/client"),
|
||||
|
|
|
@ -25,3 +25,10 @@ import "fs"
|
|||
export default function () {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const a = 1
|
||||
const b = 1
|
||||
export { b }
|
||||
export { c } from 'c'
|
||||
export * from 'd'
|
||||
export { e as f } from 'e'
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// This is a comment.
|
||||
/* __next_internal_client_entry_do_not_use__ */ const { createProxy } = require("private-next-rsc-mod-ref-proxy");
|
||||
/* __next_internal_client_entry_do_not_use__ default,a,b,c,*,f */ const { createProxy } = require("private-next-rsc-mod-ref-proxy");
|
||||
module.exports = createProxy("/some-project/src/some-file.js");
|
||||
|
|
|
@ -17,6 +17,7 @@ import { tryToParsePath } from '../../lib/try-to-parse-path'
|
|||
import { isAPIRoute } from '../../lib/is-api-route'
|
||||
import { isEdgeRuntime } from '../../lib/is-edge-runtime'
|
||||
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'
|
||||
import type { RSCMeta } from '../webpack/loaders/get-module-build-info'
|
||||
|
||||
export interface MiddlewareConfig {
|
||||
matchers: MiddlewareMatcher[]
|
||||
|
@ -39,21 +40,18 @@ export interface PageStaticInfo {
|
|||
middleware?: Partial<MiddlewareConfig>
|
||||
}
|
||||
|
||||
const CLIENT_MODULE_LABEL = '/* __next_internal_client_entry_do_not_use__ */'
|
||||
const CLIENT_MODULE_LABEL =
|
||||
/\/\* __next_internal_client_entry_do_not_use__ ([^ ]*) \*\//
|
||||
const ACTION_MODULE_LABEL =
|
||||
/\/\* __next_internal_action_entry_do_not_use__ ([^ ]+) \*\//
|
||||
|
||||
export type RSCModuleType = 'server' | 'client'
|
||||
export function getRSCModuleInformation(source: string): {
|
||||
type: RSCModuleType
|
||||
actions?: string[]
|
||||
} {
|
||||
const type = source.includes(CLIENT_MODULE_LABEL)
|
||||
? RSC_MODULE_TYPES.client
|
||||
: RSC_MODULE_TYPES.server
|
||||
|
||||
export function getRSCModuleInformation(source: string): RSCMeta {
|
||||
const clientRefs = source.match(CLIENT_MODULE_LABEL)?.[1]?.split(',')
|
||||
const actions = source.match(ACTION_MODULE_LABEL)?.[1]?.split(',')
|
||||
return { type, actions }
|
||||
|
||||
const type = clientRefs ? RSC_MODULE_TYPES.client : RSC_MODULE_TYPES.server
|
||||
return { type, actions, clientRefs }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,9 @@ export function getModuleBuildInfo(webpackModule: webpack.Module) {
|
|||
}
|
||||
|
||||
export interface RSCMeta {
|
||||
type?: RSCModuleType
|
||||
type: RSCModuleType
|
||||
actions?: string[]
|
||||
clientRefs?: string[]
|
||||
requests?: string[] // client requests in flight client entry
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,46 @@ export default async function transformSource(
|
|||
const buildInfo = getModuleBuildInfo(this._module)
|
||||
buildInfo.rsc = getRSCModuleInformation(source)
|
||||
|
||||
const isESM = this._module?.parser?.sourceType === 'module'
|
||||
|
||||
// A client boundary.
|
||||
if (isESM && buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
|
||||
const clientRefs = buildInfo.rsc.clientRefs!
|
||||
if (clientRefs.includes('*')) {
|
||||
return callback(
|
||||
new Error(
|
||||
`It's currently unsupport to use "export *" in a client boundary. Please use named exports instead.`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// For ESM, we can't simply export it as a proxy via `module.exports`.
|
||||
// Use multiple named exports instead.
|
||||
const proxyFilepath = source.match(/createProxy\((.+)\)/)?.[1]
|
||||
if (!proxyFilepath) {
|
||||
return callback(
|
||||
new Error(
|
||||
`Failed to find the proxy file path in the client boundary. This is a bug in Next.js.`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let esmSource = `
|
||||
import { createProxy } from "private-next-rsc-mod-ref-proxy"
|
||||
const proxy = createProxy(${proxyFilepath})
|
||||
`
|
||||
let cnt = 0
|
||||
for (const ref of clientRefs) {
|
||||
if (ref === 'default') {
|
||||
esmSource += `\nexport default proxy.default`
|
||||
} else {
|
||||
esmSource += `\nconst e${cnt} = proxy["${ref}"]\nexport { e${cnt++} as ${ref} }`
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, esmSource, sourceMap)
|
||||
}
|
||||
|
||||
if (buildInfo.rsc?.type !== RSC_MODULE_TYPES.client) {
|
||||
if (noopHeadPath === this.resourcePath) {
|
||||
warnOnce(
|
||||
|
|
|
@ -177,5 +177,34 @@ createNextDescribe(
|
|||
expect(v1).toBe(v2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should export client module references in esm', async () => {
|
||||
const html = await next.render('/esm-client-ref')
|
||||
expect(html).toContain('hello')
|
||||
})
|
||||
|
||||
if ((global as any).isNextDev) {
|
||||
it('should error for wildcard exports of client module references in esm', async () => {
|
||||
const page = 'app/esm-client-ref/page.js'
|
||||
const pageSource = await next.readFile(page)
|
||||
|
||||
try {
|
||||
await next.patchFile(
|
||||
page,
|
||||
pageSource.replace(
|
||||
"'client-esm-module'",
|
||||
"'client-esm-module-wildcard'"
|
||||
)
|
||||
)
|
||||
await next.render('/esm-client-ref')
|
||||
} finally {
|
||||
await next.patchFile(page, pageSource)
|
||||
}
|
||||
|
||||
expect(next.cliOutput).toInclude(
|
||||
`It's currently unsupport to use "export *" in a client boundary. Please use named exports instead.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
9
test/e2e/app-dir/app-external/app/esm-client-ref/page.js
Normal file
9
test/e2e/app-dir/app-external/app/esm-client-ref/page.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Hello } from 'client-esm-module'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<h1>
|
||||
<Hello />
|
||||
</h1>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
'use client'
|
||||
|
||||
export * from './lib'
|
|
@ -0,0 +1,3 @@
|
|||
export function Hello() {
|
||||
return 'hello'
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "client-esm-module-wildcard",
|
||||
"type": "module",
|
||||
"exports": "./index.js"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
'use client'
|
||||
|
||||
export function Hello() {
|
||||
return 'hello'
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "client-esm-module",
|
||||
"type": "module",
|
||||
"exports": "./index.js"
|
||||
}
|
Loading…
Reference in a new issue