diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 9d104a3a5215..3e8cd7dd85ba 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -46,7 +46,8 @@ use turbopack_binding::swc::{ SyntaxContext, }, ecma::{ - ast::EsVersion, parser::parse_file_as_module, transforms::base::pass::noop, visit::Fold, + ast::EsVersion, atoms::JsWord, parser::parse_file_as_module, + transforms::base::pass::noop, visit::Fold, }, }, custom_transform::modularize_imports, @@ -97,7 +98,7 @@ pub struct TransformOptions { pub is_server: bool, #[serde(default)] - pub disable_checks: bool, + pub bundle_target: JsWord, #[serde(default)] pub server_components: Option, @@ -198,7 +199,7 @@ where config.clone(), comments.clone(), opts.app_dir.clone(), - opts.disable_checks + opts.bundle_target.clone() )), _ => Either::Right(noop()), }, diff --git a/packages/next-swc/crates/core/src/react_server_components.rs b/packages/next-swc/crates/core/src/react_server_components.rs index 9c47c9af855c..8dd923dc61c7 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -50,7 +50,7 @@ struct ReactServerComponents { invalid_client_imports: Vec, invalid_server_react_apis: Vec, invalid_server_react_dom_apis: Vec, - disable_checks: bool, + bundle_target: String, } struct ModuleImports { @@ -67,15 +67,25 @@ impl VisitMut for ReactServerComponents { let is_cjs = contains_cjs(module); if self.is_server { - if !is_client_entry { - self.assert_server_graph(&imports, module); - } else { + if is_client_entry { self.to_module_ref(module, is_cjs); return; + } else if self.bundle_target == "server" { + // Only assert server graph if file's bundle target is "server", e.g. + // * server components pages + // * pages bundles on SSR layer + // * middleware + // * app/pages api routes + self.assert_server_graph(&imports, module); } } else { - if !is_action_file { - self.assert_client_graph(&imports, module); + // Only assert client graph if the file is not an action file, + // and bundle target is "client" e.g. + // * client components pages + // * pages bundles on browser layer + if !is_action_file && self.bundle_target == "client" { + self.assert_client_graph(&imports); + self.assert_invalid_api(module, true); } if is_client_entry { self.prepend_comment_node(module, is_cjs); @@ -129,7 +139,7 @@ impl ReactServerComponents { if is_action_file { panic_both_directives(expr_stmt.span) } - } else if !self.disable_checks { + } else if self.bundle_target != "default" { HANDLER.with(|handler| { handler .struct_span_err( @@ -334,9 +344,6 @@ impl ReactServerComponents { } fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) { - if self.disable_checks { - return; - } for import in imports { let source = import.source.0.clone(); if self.invalid_server_imports.contains(&source) { @@ -408,10 +415,7 @@ impl ReactServerComponents { } } - fn assert_client_graph(&self, imports: &[ModuleImports], module: &Module) { - if self.disable_checks { - return; - } + fn assert_client_graph(&self, imports: &[ModuleImports]) { for import in imports { let source = import.source.0.clone(); if self.invalid_client_imports.contains(&source) { @@ -425,8 +429,6 @@ impl ReactServerComponents { }) } } - - self.assert_invalid_api(module, true); } fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) { @@ -567,17 +569,17 @@ pub fn server_components( config: Config, comments: C, app_dir: Option, - disable_checks: bool, + bundle_target: JsWord, ) -> impl Fold + VisitMut { let is_server: bool = match &config { Config::WithOptions(x) => x.is_server, _ => true, }; as_folder(ReactServerComponents { - disable_checks, is_server, comments, filepath: filename.to_string(), + bundle_target: bundle_target.to_string(), app_dir, export_names: vec![], invalid_server_imports: vec![ diff --git a/packages/next-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index 0de686177a78..5ea07faf0ccf 100644 --- a/packages/next-swc/crates/core/tests/errors.rs +++ b/packages/next-swc/crates/core/tests/errors.rs @@ -97,7 +97,7 @@ fn react_server_components_server_graph_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false, + String::from("server").into(), ) }, &input, @@ -122,7 +122,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false, + String::from("client").into(), ) }, &input, @@ -169,7 +169,7 @@ fn react_server_actions_server_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false + String::from("default").into(), ), server_actions( &FileName::Real("/app/item.js".into()), @@ -205,7 +205,7 @@ fn react_server_actions_client_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false + String::from("client").into(), ), server_actions( &FileName::Real("/app/item.js".into()), diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index bebeccc364b6..2c68b6485425 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -326,7 +326,7 @@ fn react_server_components_server_graph_fixture(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false, + String::from("default").into(), ) }, &input, @@ -348,7 +348,7 @@ fn react_server_components_no_checks_server_graph_fixture(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - true, + String::from("default").into(), ) }, &input, @@ -370,7 +370,7 @@ fn react_server_components_client_graph_fixture(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false, + String::from("default").into(), ) }, &input, @@ -392,7 +392,7 @@ fn react_server_components_no_checks_client_graph_fixture(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - true, + String::from("default").into(), ) }, &input, diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index 592fe5d2cc52..86c588072a94 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -81,7 +81,7 @@ fn test(input: &Path, minify: bool) { auto_modularize_imports: None, optimize_barrel_exports: None, optimize_server_react: None, - disable_checks: false, + bundle_target: String::from("default").into(), }; let unresolved_mark = Mark::new(); diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 725ea768f1d2..a9989b365289 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -5,6 +5,8 @@ import type { StyledComponentsConfig, } from '../../server/config-shared' +type BundleType = 'client' | 'server' | 'default' + const nextDistPath = /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ @@ -42,6 +44,7 @@ function getBaseSWCOptions({ jsConfig, swcCacheDir, isServerLayer, + bundleTarget, hasServerComponents, isServerActionsEnabled, }: { @@ -55,6 +58,7 @@ function getBaseSWCOptions({ compilerOptions: NextConfig['compiler'] resolvedBaseUrl?: string jsConfig: any + bundleTarget: BundleType swcCacheDir?: string isServerLayer?: boolean hasServerComponents?: boolean @@ -184,7 +188,7 @@ function getBaseSWCOptions({ isServer: !!isServerLayer, } : undefined, - disableChecks: false, + bundleTarget, } } @@ -274,13 +278,14 @@ export function getJestSWCOptions({ resolvedBaseUrl, // Don't apply server layer transformations for Jest isServerLayer: false, + // Disable server / client graph assertions for Jest + bundleTarget: 'default', }) const isNextDist = nextDistPath.test(filename) return { ...baseOptions, - disableChecks: true, env: { targets: { // Targets the current version of Node.js @@ -317,6 +322,7 @@ export function getLoaderSWCOptions({ isServerLayer, isServerActionsEnabled, optimizeBarrelExports, + bundleTarget = 'client', }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { @@ -338,6 +344,7 @@ export function getLoaderSWCOptions({ supportedBrowsers: string[] swcCacheDir: string relativeFilePathFromRoot: string + bundleTarget: BundleType hasServerComponents?: boolean isServerLayer: boolean isServerActionsEnabled?: boolean @@ -357,6 +364,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, + bundleTarget, }) baseOptions.fontLoaders = { fontLoaders: [ diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 3af37d71a990..d9c90d8158e0 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -15,6 +15,7 @@ import type { MiddlewareManifest, } from './webpack/plugins/middleware-plugin' import type { StaticGenerationAsyncStorage } from '../client/components/static-generation-async-storage.external' +import type { WebpackLayerName } from '../lib/constants' import '../server/require-hook' import '../server/node-polyfill-fetch' @@ -35,6 +36,7 @@ import { SERVER_PROPS_SSG_CONFLICT, MIDDLEWARE_FILENAME, INSTRUMENTATION_HOOK_FILENAME, + WEBPACK_LAYERS, } from '../lib/constants' import { MODERN_BROWSERSLIST_TARGET } from '../shared/lib/constants' import prettyBytes from '../lib/pretty-bytes' @@ -2116,3 +2118,15 @@ export function getSupportedBrowsers( // Uses modern browsers as the default. return MODERN_BROWSERSLIST_TARGET } + +export function isWebpackServerLayer( + layer: WebpackLayerName | null | undefined +): boolean { + return Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any)) +} + +export function isWebpackDefaultLayer( + layer: WebpackLayerName | null | undefined +): boolean { + return layer === null || layer === undefined +} diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index d53986f3dd6c..218a26fd5d4b 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -19,6 +19,7 @@ import { WEBPACK_RESOURCE_QUERIES, WebpackLayerName, } from '../lib/constants' +import { isWebpackDefaultLayer, isWebpackServerLayer } from './utils' import { CustomRoutes } from '../lib/load-custom-routes.js' import { isEdgeRuntime } from '../lib/is-edge-runtime' import { @@ -86,9 +87,6 @@ const NEXT_PROJECT_ROOT_DIST_CLIENT = path.join( 'client' ) -const isWebpackServerLayer = (layer: WebpackLayerName | null) => - Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any)) - if (parseInt(React.version) < 18) { throw new Error('Next.js requires react >= 18.2.0 to be installed.') } @@ -455,19 +453,6 @@ function createRSCAliases( ] = `next/dist/compiled/react-dom${bundledReactChannel}/server-rendering-stub` } - // Alias `server-only` and `client-only` modules to their server/client only, vendored versions. - // These aliases are necessary if the user doesn't have those two packages installed manually. - if (typeof opts.reactServerCondition !== 'undefined') { - if (opts.reactServerCondition) { - // Alias to the `react-server` exports. - alias['server-only$'] = 'next/dist/compiled/server-only/empty' - alias['client-only$'] = 'next/dist/compiled/client-only/error' - } else { - alias['server-only$'] = 'next/dist/compiled/server-only/index' - alias['client-only$'] = 'next/dist/compiled/client-only/index' - } - } - if (opts.reactProductionProfiling) { alias[ 'react-dom$' @@ -936,29 +921,62 @@ export default async function getBaseWebpackConfig( const swcLoaderForServerLayer = hasServerComponents ? useSWCLoader - ? [getSwcLoader({ isServerLayer: true })] - : // When using Babel, we will have to add the SWC loader - // as an additional pass to handle RSC correctly. - // This will cause some performance overhead but - // acceptable as Babel will not be recommended. - [getSwcLoader({ isServerLayer: true }), getBabelLoader()] - : [] - const swcLoaderForClientLayer = hasServerComponents - ? useSWCLoader - ? [getSwcLoader({ hasServerComponents, isServerLayer: false })] + ? [getSwcLoader({ isServerLayer: true, bundleTarget: 'server' })] : // When using Babel, we will have to add the SWC loader // as an additional pass to handle RSC correctly. // This will cause some performance overhead but // acceptable as Babel will not be recommended. - [getSwcLoader({ isServerLayer: false }), getBabelLoader()] + [ + getSwcLoader({ isServerLayer: true, bundleTarget: 'server' }), + getBabelLoader(), + ] : [] + const swcLoaderForMiddlewareLayer = useSWCLoader - ? getSwcLoader({ hasServerComponents: false }) + ? getSwcLoader({ hasServerComponents: false, bundleTarget: 'server' }) : // When using Babel, we will have to use SWC to do the optimization // for middleware to tree shake the unused default optimized imports like "next/server". // This will cause some performance overhead but // acceptable as Babel will not be recommended. - [getSwcLoader({ hasServerComponents: false }), getBabelLoader()] + [ + getSwcLoader({ hasServerComponents: false, bundleTarget: 'server' }), + getBabelLoader(), + ] + + // client components layers: SSR + browser + const swcLoaderForClientLayer = [ + ...(dev && isClient + ? [ + require.resolve( + 'next/dist/compiled/@next/react-refresh-utils/dist/loader' + ), + ] + : []), + { + // This loader handles actions and client entries + // in the client layer. + loader: 'next-flight-client-module-loader', + }, + ...(hasServerComponents + ? useSWCLoader + ? [ + getSwcLoader({ + hasServerComponents, + isServerLayer: false, + }), + ] + : // When using Babel, we will have to add the SWC loader + // as an additional pass to handle RSC correctly. + // This will cause some performance overhead but + // acceptable as Babel will not be recommended. + [ + getSwcLoader({ + isServerLayer: false, + }), + getBabelLoader(), + ] + : []), + ] // Loader for API routes needs to be differently configured as it shouldn't // have RSC transpiler enabled, so syntax checks such as invalid imports won't @@ -969,6 +987,7 @@ export default async function getBaseWebpackConfig( loader: 'next-swc-loader', options: { ...getSwcLoader().options, + bundleTarget: 'server', hasServerComponents: false, }, } @@ -1782,9 +1801,7 @@ export default async function getBaseWebpackConfig( chunks: 'all', name: 'framework', // Ensures the framework chunk is not created for App Router. - layer(layer: any) { - return layer === null || layer === undefined - }, + layer: isWebpackDefaultLayer, test(module: any) { const resource = module.nameForCondition?.() return resource @@ -2028,6 +2045,66 @@ export default async function getBaseWebpackConfig( ] }, }, + // Alias server-only and client-only to proper exports based on bundling layers + { + issuerLayer: { + or: WEBPACK_LAYERS.GROUP.serverTarget, + }, + resolve: { + // Error on client-only but allow server-only + alias: { + 'server-only$': 'next/dist/compiled/server-only/empty', + 'client-only$': 'next/dist/compiled/client-only/error', + 'next/dist/compiled/server-only$': + 'next/dist/compiled/server-only/empty', + }, + }, + }, + { + issuerLayer: { + not: WEBPACK_LAYERS.GROUP.serverTarget, + }, + resolve: { + // Error on server-only but allow client-only + alias: { + 'server-only$': 'next/dist/compiled/server-only/index', + 'client-only$': 'next/dist/compiled/client-only/index', + 'next/dist/compiled/client-only$': + 'next/dist/compiled/client-only/index', + 'next/dist/compiled/server-only': + 'next/dist/compiled/server-only/index', + }, + }, + }, + // Detect server-only / client-only imports and error in build time + { + test: [ + /^client-only$/, + /next[\\/]dist[\\/]compiled[\\/]client-only[\\/]error/, + ], + loader: 'next-invalid-import-error-loader', + issuerLayer: { + or: WEBPACK_LAYERS.GROUP.serverTarget, + }, + options: { + message: + "'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.", + }, + }, + { + test: [ + /^server-only$/, + /next[\\/]dist[\\/]compiled[\\/]server-only[\\/]index/, + ], + loader: 'next-invalid-import-error-loader', + issuerLayer: { + not: WEBPACK_LAYERS.GROUP.serverTarget, + }, + options: { + message: + "'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.", + }, + }, ...(hasAppDir ? [ { @@ -2149,9 +2226,7 @@ export default async function getBaseWebpackConfig( oneOf: [ { exclude: [asyncStoragesRegex], - issuerLayer: { - or: [isWebpackServerLayer], - }, + issuerLayer: isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2238,28 +2313,12 @@ export default async function getBaseWebpackConfig( }, { ...codeCondition, - issuerLayer: { - or: [ - WEBPACK_LAYERS.serverSideRendering, - WEBPACK_LAYERS.appPagesBrowser, - ], - }, - exclude: [codeCondition.exclude], - use: [ - ...(dev && isClient - ? [ - require.resolve( - 'next/dist/compiled/@next/react-refresh-utils/dist/loader' - ), - ] - : []), - { - // This loader handles actions and client entries - // in the client layer. - loader: 'next-flight-client-module-loader', - }, - ...swcLoaderForClientLayer, + issuerLayer: [ + WEBPACK_LAYERS.appPagesBrowser, + WEBPACK_LAYERS.serverSideRendering, ], + exclude: [codeCondition.exclude], + use: swcLoaderForClientLayer, }, ] : []), @@ -2403,26 +2462,6 @@ export default async function getBaseWebpackConfig( }, ] : []), - { - test: /(node_modules|next[/\\]dist[/\\]compiled)[/\\]client-only[/\\]error.js/, - loader: 'next-invalid-import-error-loader', - issuerLayer: { - or: [isWebpackServerLayer], - }, - options: { - message: - "'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.", - }, - }, - { - test: /(node_modules|next[/\\]dist[/\\]compiled)[/\\]server-only[/\\]index.js/, - loader: 'next-invalid-import-error-loader', - issuerLayer: WEBPACK_LAYERS.serverSideRendering, - options: { - message: - "'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.", - }, - }, { // Mark `image-response.js` as side-effects free to make sure we can // tree-shake it if not used. diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index c52ae3e38803..6b23d7e7e24f 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -54,6 +54,7 @@ async function loaderTransform( hasServerComponents, isServerLayer, optimizeBarrelExports, + bundleTarget, } = loaderOptions const isPageFile = filename.startsWith(pagesDir) const relativeFilePathFromRoot = path.relative(rootDir, filename) @@ -86,6 +87,7 @@ async function loaderTransform( isServerActionsEnabled: nextConfig?.experimental?.serverActions, isServerLayer, optimizeBarrelExports, + bundleTarget, }) const programmaticOptions = { diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 92702f6231c1..e3f8f07838c4 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -113,7 +113,7 @@ const WEBPACK_LAYERS_NAMES = { /** * The browser client bundle layer for actions. */ - actionBrowser: 'actionBrowser', + actionBrowser: 'action-browser', /** * The layer for the API routes. */ @@ -143,7 +143,7 @@ const WEBPACK_LAYERS_NAMES = { export type WebpackLayerName = (typeof WEBPACK_LAYERS_NAMES)[keyof typeof WEBPACK_LAYERS_NAMES] -export const WEBPACK_LAYERS = { +const WEBPACK_LAYERS = { ...WEBPACK_LAYERS_NAMES, GROUP: { server: [ @@ -152,12 +152,24 @@ export const WEBPACK_LAYERS = { WEBPACK_LAYERS_NAMES.appMetadataRoute, WEBPACK_LAYERS_NAMES.appRouteHandler, ], + serverTarget: [ + // all GROUP.server + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.appMetadataRoute, + WEBPACK_LAYERS_NAMES.appRouteHandler, + // plus middleware and pages api + WEBPACK_LAYERS_NAMES.middleware, + WEBPACK_LAYERS_NAMES.api, + ], }, } -export const WEBPACK_RESOURCE_QUERIES = { +const WEBPACK_RESOURCE_QUERIES = { edgeSSREntry: '__next_edge_ssr_entry__', metadata: '__next_metadata__', metadataRoute: '__next_metadata_route__', metadataImageMeta: '__next_metadata_image_meta__', } + +export { WEBPACK_LAYERS, WEBPACK_RESOURCE_QUERIES } diff --git a/test/e2e/module-layer/app/app/client-edge/page.js b/test/e2e/module-layer/app/app/client-edge/page.js new file mode 100644 index 000000000000..5bebe6a87c9d --- /dev/null +++ b/test/e2e/module-layer/app/app/client-edge/page.js @@ -0,0 +1,9 @@ +'use client' + +import 'client-only' + +export default function Page() { + return 'app/client-edge/page.js' +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/app/app/client/page.js b/test/e2e/module-layer/app/app/client/page.js new file mode 100644 index 000000000000..9b1365837de7 --- /dev/null +++ b/test/e2e/module-layer/app/app/client/page.js @@ -0,0 +1,7 @@ +'use client' + +import 'client-only' + +export default function Page() { + return 'app/client/page.js' +} diff --git a/test/e2e/module-layer/app/app/route-edge/route.js b/test/e2e/module-layer/app/app/route-edge/route.js new file mode 100644 index 000000000000..43201073ba93 --- /dev/null +++ b/test/e2e/module-layer/app/app/route-edge/route.js @@ -0,0 +1,7 @@ +import 'server-only' + +export function GET() { + return new Response('app-route-edge/route.js') +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/app/app/route/route.js b/test/e2e/module-layer/app/app/route/route.js new file mode 100644 index 000000000000..3212d102f33a --- /dev/null +++ b/test/e2e/module-layer/app/app/route/route.js @@ -0,0 +1,5 @@ +import 'server-only' + +export function GET() { + return new Response('app-route/route.js') +} diff --git a/test/e2e/module-layer/app/app/server-edge/page.js b/test/e2e/module-layer/app/app/server-edge/page.js new file mode 100644 index 000000000000..c833828d4f3b --- /dev/null +++ b/test/e2e/module-layer/app/app/server-edge/page.js @@ -0,0 +1,7 @@ +import 'server-only' + +export default function Page() { + return 'app/server-edge/page.js' +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/app/app/server/page.js b/test/e2e/module-layer/app/app/server/page.js new file mode 100644 index 000000000000..aa6ff75951d9 --- /dev/null +++ b/test/e2e/module-layer/app/app/server/page.js @@ -0,0 +1,5 @@ +import 'server-only' + +export default function Page() { + return 'app/server/page.js' +} diff --git a/test/e2e/module-layer/app/layout.js b/test/e2e/module-layer/app/layout.js new file mode 100644 index 000000000000..4ee00a218505 --- /dev/null +++ b/test/e2e/module-layer/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/module-layer/index.test.ts b/test/e2e/module-layer/index.test.ts new file mode 100644 index 000000000000..6e4359b6a21a --- /dev/null +++ b/test/e2e/module-layer/index.test.ts @@ -0,0 +1,60 @@ +import { createNextDescribe } from 'e2e-utils' + +createNextDescribe( + 'module layer', + { + files: __dirname, + }, + ({ next, isNextStart }) => { + it('should render routes marked with restriction marks without errors', async () => { + const routes = [ + // app client components pages + '/app/client', + '/app/client-edge', + // app sever components pages + '/app/server', + '/app/server-edge', + // app routes + '/app/route', + '/app/route-edge', + // pages/api + '/api/hello', + '/api/hello-edge', + ] + + for (const route of routes) { + const { status } = await next.fetch(route) + expect([route, status]).toEqual([route, 200]) + } + }) + + if (isNextStart) { + it('should log the build info properly', async () => { + const cliOutput = next.cliOutput + expect(cliOutput).toContain('Middleware') + + const functionsManifest = JSON.parse( + await next.readFile('.next/server/functions-config-manifest.json') + ) + expect(functionsManifest.functions).toContainKeys([ + '/app/route-edge', + '/api/hello-edge', + '/app/client-edge', + '/app/server-edge', + ]) + const pagesManifest = JSON.parse( + await next.readFile('.next/server/pages-manifest.json') + ) + const middlewareManifest = JSON.parse( + await next.readFile('.next/server/middleware-manifest.json') + ) + expect(middlewareManifest.middleware).toBeTruthy() + expect(pagesManifest).toContainKeys([ + '/api/hello-edge', + '/pages-ssr', + '/api/hello', + ]) + }) + } + } +) diff --git a/test/e2e/module-layer/middleware.js b/test/e2e/module-layer/middleware.js new file mode 100644 index 000000000000..4780d4b6425b --- /dev/null +++ b/test/e2e/module-layer/middleware.js @@ -0,0 +1,6 @@ +import 'server-only' +import { NextResponse } from 'next/server' + +export function middleware() { + return NextResponse.next() +} diff --git a/test/e2e/module-layer/pages/api/hello-edge.js b/test/e2e/module-layer/pages/api/hello-edge.js new file mode 100644 index 000000000000..adcc54926079 --- /dev/null +++ b/test/e2e/module-layer/pages/api/hello-edge.js @@ -0,0 +1,7 @@ +import 'server-only' + +export default function handler() { + return new Response('api/hello-edge.js') +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/pages/api/hello.js b/test/e2e/module-layer/pages/api/hello.js new file mode 100644 index 000000000000..3779e0be22f3 --- /dev/null +++ b/test/e2e/module-layer/pages/api/hello.js @@ -0,0 +1,5 @@ +import 'server-only' + +export default function handler(req, res) { + return res.send('api/hello.js') +} diff --git a/test/e2e/module-layer/pages/pages-ssr.js b/test/e2e/module-layer/pages/pages-ssr.js new file mode 100644 index 000000000000..786b8b822e10 --- /dev/null +++ b/test/e2e/module-layer/pages/pages-ssr.js @@ -0,0 +1,5 @@ +import 'client-only' + +export default function Page() { + return 'pages/pages-ssr.js' +}