From 1b2e15da0248f6a72c6fe5180ca79701b7b93fe2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 14 Sep 2023 17:27:13 +0200 Subject: [PATCH 01/18] Alias server-only and client-only in bundled layers --- packages/next-swc/crates/core/src/lib.rs | 6 +- .../core/src/react_server_components.rs | 32 ++-- packages/next-swc/crates/core/tests/errors.rs | 8 +- .../next-swc/crates/core/tests/fixture.rs | 8 +- packages/next-swc/crates/core/tests/full.rs | 2 +- packages/next/src/build/swc/options.ts | 13 +- packages/next/src/build/webpack-config.ts | 168 ++++++++++-------- .../build/webpack/loaders/next-swc-loader.ts | 2 + packages/next/src/lib/constants.ts | 8 + .../module-layer/app/app/client-edge/page.js | 9 + test/e2e/module-layer/app/app/client/page.js | 7 + .../module-layer/app/app/route-edge/route.js | 7 + test/e2e/module-layer/app/app/route/route.js | 5 + .../module-layer/app/app/server-edge/page.js | 7 + test/e2e/module-layer/app/app/server/page.js | 5 + test/e2e/module-layer/app/layout.js | 7 + test/e2e/module-layer/index.test.ts | 61 +++++++ test/e2e/module-layer/middleware.js | 6 + test/e2e/module-layer/pages/api/hello-edge.js | 7 + test/e2e/module-layer/pages/api/hello.js | 5 + test/e2e/module-layer/pages/pages-ssr.js | 5 + 21 files changed, 277 insertions(+), 101 deletions(-) create mode 100644 test/e2e/module-layer/app/app/client-edge/page.js create mode 100644 test/e2e/module-layer/app/app/client/page.js create mode 100644 test/e2e/module-layer/app/app/route-edge/route.js create mode 100644 test/e2e/module-layer/app/app/route/route.js create mode 100644 test/e2e/module-layer/app/app/server-edge/page.js create mode 100644 test/e2e/module-layer/app/app/server/page.js create mode 100644 test/e2e/module-layer/app/layout.js create mode 100644 test/e2e/module-layer/index.test.ts create mode 100644 test/e2e/module-layer/middleware.js create mode 100644 test/e2e/module-layer/pages/api/hello-edge.js create mode 100644 test/e2e/module-layer/pages/api/hello.js create mode 100644 test/e2e/module-layer/pages/pages-ssr.js diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 9d104a3a5215b..be6d200ce677d 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -46,7 +46,7 @@ use turbopack_binding::swc::{ SyntaxContext, }, ecma::{ - ast::EsVersion, parser::parse_file_as_module, transforms::base::pass::noop, visit::Fold, + ast::EsVersion, parser::parse_file_as_module, transforms::base::pass::noop, visit::Fold, atoms::JsWord, }, }, custom_transform::modularize_imports, @@ -97,7 +97,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 +198,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 6ba72beb90915..0673a3275cb98 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,14 +67,24 @@ 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 { + // 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, module); } if is_client_entry { @@ -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) { @@ -409,9 +416,6 @@ impl ReactServerComponents { } fn assert_client_graph(&self, imports: &[ModuleImports], module: &Module) { - if self.disable_checks { - return; - } for import in imports { let source = import.source.0.clone(); if self.invalid_client_imports.contains(&source) { @@ -567,17 +571,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 0de686177a78a..405506b0fb6c2 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("default").into(), ) }, &input, @@ -122,7 +122,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - false, + String::from("default").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("default").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 bebeccc364b6e..2c68b6485425a 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 592fe5d2cc527..86c588072a94c 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 725ea768f1d22..9090f5a9d1e71 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,8 @@ function getBaseSWCOptions({ isServer: !!isServerLayer, } : undefined, - disableChecks: false, + // disableChecks: false, + bundleTarget, } } @@ -274,13 +279,14 @@ export function getJestSWCOptions({ resolvedBaseUrl, // Don't apply server layer transformations for Jest isServerLayer: false, + bundleTarget: 'default', }) const isNextDist = nextDistPath.test(filename) return { ...baseOptions, - disableChecks: true, + // disableChecks: true, env: { targets: { // Targets the current version of Node.js @@ -317,6 +323,7 @@ export function getLoaderSWCOptions({ isServerLayer, isServerActionsEnabled, optimizeBarrelExports, + bundleTarget = 'default', }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { @@ -338,6 +345,7 @@ export function getLoaderSWCOptions({ supportedBrowsers: string[] swcCacheDir: string relativeFilePathFromRoot: string + bundleTarget: BundleType hasServerComponents?: boolean isServerLayer: boolean isServerActionsEnabled?: boolean @@ -357,6 +365,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, + bundleTarget, }) baseOptions.fontLoaders = { fontLoaders: [ diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index aa80e4c6fbff4..6b0ae2fb3d0f1 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -86,8 +86,11 @@ const NEXT_PROJECT_ROOT_DIST_CLIENT = path.join( 'client' ) +const isWebpackBundleAllServerLayer = (layer: WebpackLayerName | null) => + Boolean(layer && WEBPACK_LAYERS.GROUP.serverBundleAll.includes(layer)) + const isWebpackServerLayer = (layer: WebpackLayerName | null) => - Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any)) + Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer)) if (parseInt(React.version) < 18) { throw new Error('Next.js requires react >= 18.2.0 to be installed.') @@ -455,19 +458,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$' @@ -932,29 +922,68 @@ export default async function getBaseWebpackConfig( const swcLoaderForServerLayer = hasServerComponents ? useSWCLoader - ? [getSwcLoader({ isServerLayer: true })] + ? [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: true }), getBabelLoader()] - : [] - const swcLoaderForClientLayer = 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()] + [ + getSwcLoader({ isServerLayer: true, bundleTarget: 'server' }), + getBabelLoader(), + ] : [] + + // for SSR and browser bundles + const createSwcLoaderForClientLayer = (isBrowser: boolean) => + hasServerComponents + ? useSWCLoader + ? [ + getSwcLoader({ + hasServerComponents, + isServerLayer: false, + bundleTarget: isBrowser ? 'client' : '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, + bundleTarget: isBrowser ? 'client' : '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(), + ] + + function createClientLoader(isBrowser: boolean) { + return [ + ...(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', + }, + ...createSwcLoaderForClientLayer(isBrowser), + ] + } // 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 @@ -966,6 +995,7 @@ export default async function getBaseWebpackConfig( options: { ...getSwcLoader().options, hasServerComponents: false, + bundleTarget: 'server', }, } : defaultLoaders.babel @@ -1176,6 +1206,8 @@ export default async function getBaseWebpackConfig( 'styled-jsx/style$': defaultOverrides['styled-jsx/style'], 'styled-jsx$': defaultOverrides['styled-jsx'], + 'server-only$': 'next/dist/compiled/server-only', + 'client-only$': 'next/dist/compiled/client-only', ...customAppAliases, ...customErrorAlias, @@ -1434,7 +1466,7 @@ export default async function getBaseWebpackConfig( // Don't bundle @vercel/og nodejs bundle for nodejs runtime. // TODO-APP: bundle route.js with different layer that externals common node_module deps. if ( - isWebpackServerLayer(layer) && + isWebpackBundleAllServerLayer(layer) && request === 'next/dist/compiled/@vercel/og/index.node.js' ) { return `module ${request}` @@ -1570,7 +1602,7 @@ export default async function getBaseWebpackConfig( (isEsm && isAppLayer) if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { - if (isWebpackServerLayer(layer)) { + if (isWebpackBundleAllServerLayer(layer)) { // All packages should be bundled for the server layer if they're not opted out. // This option takes priority over the transpilePackages option. @@ -2082,7 +2114,7 @@ export default async function getBaseWebpackConfig( ...(hasAppDir && !isClient ? [ { - issuerLayer: isWebpackServerLayer, + issuerLayer: isWebpackBundleAllServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2124,6 +2156,29 @@ export default async function getBaseWebpackConfig( } as any, ] : []), + { + issuerLayer: [isWebpackServerLayer], + 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', + }, + }, + }, + { + issuerLayer: [ + WEBPACK_LAYERS.appPagesBrowser, + WEBPACK_LAYERS.actionBrowser, + ], + 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', + }, + }, + }, ...(hasAppDir && isEdgeServer ? [ // The Edge bundle includes the server in its entrypoint, so it has to @@ -2146,7 +2201,7 @@ export default async function getBaseWebpackConfig( { exclude: [asyncStoragesRegex], issuerLayer: { - or: [isWebpackServerLayer], + or: [isWebpackBundleAllServerLayer], }, test: { // Resolve it if it is a source code file, and it has NOT been @@ -2221,7 +2276,7 @@ export default async function getBaseWebpackConfig( ? [ { test: codeCondition.test, - issuerLayer: isWebpackServerLayer, + issuerLayer: isWebpackBundleAllServerLayer, exclude: [asyncStoragesRegex], use: swcLoaderForServerLayer, }, @@ -2234,28 +2289,15 @@ export default async function getBaseWebpackConfig( }, { ...codeCondition, - issuerLayer: { - or: [ - WEBPACK_LAYERS.serverSideRendering, - WEBPACK_LAYERS.appPagesBrowser, - ], - }, + issuerLayer: 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, - ], + use: createClientLoader(true), + }, + { + ...codeCondition, + issuerLayer: WEBPACK_LAYERS.serverSideRendering, + exclude: [codeCondition.exclude], + use: createClientLoader(false), }, ] : []), @@ -2399,26 +2441,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 c52ae3e38803a..6b23d7e7e24f4 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 92702f6231c18..72f03d676b090 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -146,11 +146,19 @@ export type WebpackLayerName = export const WEBPACK_LAYERS = { ...WEBPACK_LAYERS_NAMES, GROUP: { + serverBundleAll: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.appMetadataRoute, + WEBPACK_LAYERS_NAMES.appRouteHandler, + ], server: [ WEBPACK_LAYERS_NAMES.reactServerComponents, WEBPACK_LAYERS_NAMES.actionBrowser, WEBPACK_LAYERS_NAMES.appMetadataRoute, WEBPACK_LAYERS_NAMES.appRouteHandler, + WEBPACK_LAYERS_NAMES.middleware, + WEBPACK_LAYERS_NAMES.api, ], }, } 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 0000000000000..14aca14e1b69f --- /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 '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 0000000000000..00cad0c929f40 --- /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 '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 0000000000000..43201073ba932 --- /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 0000000000000..3212d102f33a4 --- /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 0000000000000..75bdfa02d06fa --- /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 '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 0000000000000..c7886b97c51c5 --- /dev/null +++ b/test/e2e/module-layer/app/app/server/page.js @@ -0,0 +1,5 @@ +import 'server-only' + +export default function Page() { + return '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 0000000000000..4ee00a218505a --- /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 0000000000000..271fbdc4df1e3 --- /dev/null +++ b/test/e2e/module-layer/index.test.ts @@ -0,0 +1,61 @@ +import { createNextDescribe } from 'e2e-utils' + +createNextDescribe( + 'module layer', + { + files: __dirname, + skipDeployment: true, + }, + ({ 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(status).toBe(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 0000000000000..4780d4b6425b8 --- /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 0000000000000..adcc549260798 --- /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 0000000000000..3779e0be22f39 --- /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 0000000000000..786b8b822e109 --- /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' +} From 78a7aa3536ef315eb99a9867d080eb7f4a94f236 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 14 Sep 2023 17:31:54 +0200 Subject: [PATCH 02/18] fmt --- packages/next-swc/crates/core/src/lib.rs | 3 ++- packages/next-swc/crates/core/src/react_server_components.rs | 4 ++-- packages/next/src/build/swc/options.ts | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index be6d200ce677d..3e8cd7dd85ba7 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, atoms::JsWord, + ast::EsVersion, atoms::JsWord, parser::parse_file_as_module, + transforms::base::pass::noop, visit::Fold, }, }, custom_transform::modularize_imports, 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 0673a3275cb98..1261685c3a71c 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -80,8 +80,8 @@ impl VisitMut for ReactServerComponents { self.assert_server_graph(&imports, module); } } else { - // Only assert client graph if the file is not an action file, and bundle target is "client" - // e.g. + // 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" { diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 9090f5a9d1e71..d5392910e70c6 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -188,7 +188,6 @@ function getBaseSWCOptions({ isServer: !!isServerLayer, } : undefined, - // disableChecks: false, bundleTarget, } } @@ -286,7 +285,6 @@ export function getJestSWCOptions({ return { ...baseOptions, - // disableChecks: true, env: { targets: { // Targets the current version of Node.js From 0b668043b5dd8aa88cc3d8d073131bbc5566392d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 02:19:46 +0200 Subject: [PATCH 03/18] fix alias --- packages/next/src/build/swc/options.ts | 2 +- packages/next/src/build/webpack-config.ts | 86 +++++++++++++------ .../build/webpack/loaders/next-swc-loader.ts | 2 +- packages/next/src/server/import-overrides.ts | 6 ++ .../app/app/client/server-only-component.js | 5 ++ 5 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 test/e2e/module-layer/app/app/client/server-only-component.js diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index d5392910e70c6..5b073abc570dc 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -321,7 +321,7 @@ export function getLoaderSWCOptions({ isServerLayer, isServerActionsEnabled, optimizeBarrelExports, - bundleTarget = 'default', + bundleTarget, }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 6b0ae2fb3d0f1..79b4f0b383bed 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1206,8 +1206,8 @@ export default async function getBaseWebpackConfig( 'styled-jsx/style$': defaultOverrides['styled-jsx/style'], 'styled-jsx$': defaultOverrides['styled-jsx'], - 'server-only$': 'next/dist/compiled/server-only', - 'client-only$': 'next/dist/compiled/client-only', + // 'server-only$': 'next/dist/compiled/server-only', + // 'client-only$': 'next/dist/compiled/client-only', ...customAppAliases, ...customErrorAlias, @@ -2056,6 +2056,65 @@ export default async function getBaseWebpackConfig( ] }, }, + { + // test: /next[\\/]dist[\\/]compiled[\\/]server-only/, + issuerLayer: isWebpackServerLayer, + 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', + 'next/dist/compiled/client-only': + 'next/dist/compiled/client-only/error', + // 'next/dist/compiled/client-only': `next-invalid-import-error-loader?${JSON.stringify({ + // message: `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` + // })}`, + // 'client-only': `next-invalid-import-error-loader?${JSON.stringify({ + // message: `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` + // })}`, + }, + }, + }, + { + issuerLayer(layer: string | null) { + return !layer || !isWebpackServerLayer(layer) + }, + 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/server-only': + 'next/dist/compiled/server-only/index', + 'next/dist/compiled/client-only': + 'next/dist/compiled/client-only/index', + }, + }, + }, + { + test: /(node_modules|next[/\\]dist[/\\]compiled)[/\\]client-only[/\\]error.js/, + loader: 'next-invalid-import-error-loader', + issuerLayer: 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.actionBrowser, + WEBPACK_LAYERS.appPagesBrowser, + WEBPACK_LAYERS.serverSideRendering, + ], + options: { + message: + "'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.", + }, + }, ...(hasAppDir ? [ { @@ -2156,29 +2215,6 @@ export default async function getBaseWebpackConfig( } as any, ] : []), - { - issuerLayer: [isWebpackServerLayer], - 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', - }, - }, - }, - { - issuerLayer: [ - WEBPACK_LAYERS.appPagesBrowser, - WEBPACK_LAYERS.actionBrowser, - ], - 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', - }, - }, - }, ...(hasAppDir && isEdgeServer ? [ // The Edge bundle includes the server in its entrypoint, so it has to 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 6b23d7e7e24f4..c04ec230e1e85 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -87,7 +87,7 @@ async function loaderTransform( isServerActionsEnabled: nextConfig?.experimental?.serverActions, isServerLayer, optimizeBarrelExports, - bundleTarget, + bundleTarget: bundleTarget || 'default', }) const programmaticOptions = { diff --git a/packages/next/src/server/import-overrides.ts b/packages/next/src/server/import-overrides.ts index c72f42612eae3..483e0c44a5854 100644 --- a/packages/next/src/server/import-overrides.ts +++ b/packages/next/src/server/import-overrides.ts @@ -21,6 +21,12 @@ export const defaultOverrides = { 'styled-jsx/style': process.env.NEXT_MINIMAL ? resolve('styled-jsx/style') : resolve('styled-jsx/style', nextPaths), + 'server-only': process.env.NEXT_MINIMAL + ? resolve('next/dist/compiled/server-only') + : resolve('next/dist/compiled/server-only', nextPaths), + 'client-only': process.env.NEXT_MINIMAL + ? resolve('next/dist/compiled/client-only') + : resolve('next/dist/compiled/client-only', nextPaths), } const toResolveMap = (map: Record): [string, string][] => diff --git a/test/e2e/module-layer/app/app/client/server-only-component.js b/test/e2e/module-layer/app/app/client/server-only-component.js new file mode 100644 index 0000000000000..66393e79bf04c --- /dev/null +++ b/test/e2e/module-layer/app/app/client/server-only-component.js @@ -0,0 +1,5 @@ +import 'server-only' + +export function two() { + return 2 +} From c91310e4ccd043cf7120903042efb8f127164119 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 03:03:05 +0200 Subject: [PATCH 04/18] assert default and client as client graph --- .../core/src/react_server_components.rs | 2 +- packages/next/src/build/webpack-config.ts | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) 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 1261685c3a71c..73703870db008 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -84,7 +84,7 @@ impl VisitMut for ReactServerComponents { // is "client" e.g. // * client components pages // * pages bundles on browser layer - if !is_action_file && self.bundle_target == "client" { + if !is_action_file && self.bundle_target != "server" { self.assert_client_graph(&imports, module); } if is_client_entry { diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 79b4f0b383bed..48c78c507c2fc 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -2064,11 +2064,11 @@ export default async function getBaseWebpackConfig( 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$': 'next/dist/compiled/server-only/empty', - 'next/dist/compiled/client-only': - 'next/dist/compiled/client-only/error', - // 'next/dist/compiled/client-only': `next-invalid-import-error-loader?${JSON.stringify({ + // 'next/dist/compiled/client-only': + // 'next/dist/compiled/client-only/error', + // 'next/dist/compiled/client-only$': `next-invalid-import-error-loader?${JSON.stringify({ // message: `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` // })}`, // 'client-only': `next-invalid-import-error-loader?${JSON.stringify({ @@ -2084,17 +2084,22 @@ export default async function getBaseWebpackConfig( 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', + 'server-only$': 'next/dist/compiled/server-only/index', + 'client-only$': 'next/dist/compiled/client-only/index', + // 'next/dist/compiled/server-only$': `next-invalid-import-error-loader?${JSON.stringify({ + // message: `'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.` + // })}`, + 'next/dist/compiled/client-only$': + 'next/dist/compiled/client-only/index', 'next/dist/compiled/server-only': 'next/dist/compiled/server-only/index', - 'next/dist/compiled/client-only': - 'next/dist/compiled/client-only/index', + // 'next/dist/compiled/client-only': + // 'next/dist/compiled/client-only/index', }, }, }, { - test: /(node_modules|next[/\\]dist[/\\]compiled)[/\\]client-only[/\\]error.js/, + test: /client-only$|next[\\/]dist[\\/]compiled[\\/]client-only[\\/]error/, loader: 'next-invalid-import-error-loader', issuerLayer: isWebpackServerLayer, options: { @@ -2103,13 +2108,16 @@ export default async function getBaseWebpackConfig( }, }, { - test: /(node_modules|next[/\\]dist[/\\]compiled)[/\\]server-only[/\\]index.js/, + test: /server-only$|next[\\/]dist[\\/]compiled[\\/]server-only[\\/]index/, loader: 'next-invalid-import-error-loader', - issuerLayer: [ - WEBPACK_LAYERS.actionBrowser, - WEBPACK_LAYERS.appPagesBrowser, - WEBPACK_LAYERS.serverSideRendering, - ], + issuerLayer(layer: string | null) { + return !layer || !isWebpackServerLayer(layer) + }, + // issuerLayer: [ + // WEBPACK_LAYERS.actionBrowser, + // WEBPACK_LAYERS.appPagesBrowser, + // WEBPACK_LAYERS.serverSideRendering, + // ], options: { message: "'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.", From 416eb0725e38082977e423ed651198ff11d1bede Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 12:19:10 +0200 Subject: [PATCH 05/18] fix ssr layer --- packages/next/src/build/webpack-config.ts | 77 +++++++++++------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 48c78c507c2fc..5beafce394aa8 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -933,15 +933,38 @@ export default async function getBaseWebpackConfig( ] : [] - // for SSR and browser bundles - const createSwcLoaderForClientLayer = (isBrowser: boolean) => - hasServerComponents + const swcLoaderForMiddlewareLayer = useSWCLoader + ? 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, bundleTarget: 'server' }), + getBabelLoader(), + ] + + // client components layers: SSR + browser + const swcLoaderForClientLayers = [ + ...(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, - bundleTarget: isBrowser ? 'client' : 'server', + bundleTarget: 'client', }), ] : // When using Babel, we will have to add the SWC loader @@ -951,39 +974,12 @@ export default async function getBaseWebpackConfig( [ getSwcLoader({ isServerLayer: false, - bundleTarget: isBrowser ? 'client' : 'server', + bundleTarget: 'client', }), getBabelLoader(), ] - : [] - const swcLoaderForMiddlewareLayer = useSWCLoader - ? 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, bundleTarget: 'server' }), - getBabelLoader(), - ] - - function createClientLoader(isBrowser: boolean) { - return [ - ...(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', - }, - ...createSwcLoaderForClientLayer(isBrowser), - ] - } + : []), + ] // 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 @@ -2333,15 +2329,12 @@ export default async function getBaseWebpackConfig( }, { ...codeCondition, - issuerLayer: WEBPACK_LAYERS.appPagesBrowser, - exclude: [codeCondition.exclude], - use: createClientLoader(true), - }, - { - ...codeCondition, - issuerLayer: WEBPACK_LAYERS.serverSideRendering, + issuerLayer: [ + WEBPACK_LAYERS.appPagesBrowser, + WEBPACK_LAYERS.serverSideRendering, + ], exclude: [codeCondition.exclude], - use: createClientLoader(false), + use: swcLoaderForClientLayers, }, ] : []), From 9125b67ec7d8b8ef68ccd9347ae6b7a47eca72e2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 13:46:46 +0200 Subject: [PATCH 06/18] merge bundle target --- .../next-swc/crates/core/src/react_server_components.rs | 4 ++-- packages/next-swc/crates/core/tests/errors.rs | 6 +++--- packages/next/src/build/swc/options.ts | 2 +- packages/next/src/build/webpack-config.ts | 2 -- 4 files changed, 6 insertions(+), 8 deletions(-) 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 73703870db008..42b068f3e428d 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -84,7 +84,7 @@ impl VisitMut for ReactServerComponents { // is "client" e.g. // * client components pages // * pages bundles on browser layer - if !is_action_file && self.bundle_target != "server" { + if !is_action_file && self.bundle_target == "client" { self.assert_client_graph(&imports, module); } if is_client_entry { @@ -139,7 +139,7 @@ impl ReactServerComponents { if is_action_file { panic_both_directives(expr_stmt.span) } - } else if self.bundle_target == "default" { + } else if self.bundle_target != "default" { HANDLER.with(|handler| { handler .struct_span_err( diff --git a/packages/next-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index 405506b0fb6c2..5ea07faf0ccf9 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, - String::from("default").into(), + String::from("server").into(), ) }, &input, @@ -122,7 +122,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - String::from("default").into(), + String::from("client").into(), ) }, &input, @@ -205,7 +205,7 @@ fn react_server_actions_client_errors(input: PathBuf) { ), tr.comments.as_ref().clone(), None, - String::from("default").into(), + String::from("client").into(), ), server_actions( &FileName::Real("/app/item.js".into()), diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 5b073abc570dc..d27c915f0a7ec 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -363,7 +363,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, - bundleTarget, + bundleTarget: bundleTarget || 'client', }) baseOptions.fontLoaders = { fontLoaders: [ diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 5beafce394aa8..6e06d29f86486 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -964,7 +964,6 @@ export default async function getBaseWebpackConfig( getSwcLoader({ hasServerComponents, isServerLayer: false, - bundleTarget: 'client', }), ] : // When using Babel, we will have to add the SWC loader @@ -974,7 +973,6 @@ export default async function getBaseWebpackConfig( [ getSwcLoader({ isServerLayer: false, - bundleTarget: 'client', }), getBabelLoader(), ] From f4f0feeb2c0c3dc6a94fa507abacfa7e11bcce51 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 15:26:31 +0200 Subject: [PATCH 07/18] fix invalid api checking --- .../crates/core/src/react_server_components.rs | 13 ++++++++----- test/e2e/app-dir/hello-world/app/page.tsx | 7 ++++++- 2 files changed, 14 insertions(+), 6 deletions(-) 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 42b068f3e428d..71eec530fbc7a 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -84,8 +84,13 @@ impl VisitMut for ReactServerComponents { // 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, module); + if !is_action_file { + if self.bundle_target == "client" { + self.assert_client_graph(&imports); + } + if self.bundle_target != "server" { + self.assert_invalid_api(module, true); + } } if is_client_entry { self.prepend_comment_node(module, is_cjs); @@ -415,7 +420,7 @@ impl ReactServerComponents { } } - fn assert_client_graph(&self, imports: &[ModuleImports], module: &Module) { + fn assert_client_graph(&self, imports: &[ModuleImports]) { for import in imports { let source = import.source.0.clone(); if self.invalid_client_imports.contains(&source) { @@ -429,8 +434,6 @@ impl ReactServerComponents { }) } } - - self.assert_invalid_api(module, true); } fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) { diff --git a/test/e2e/app-dir/hello-world/app/page.tsx b/test/e2e/app-dir/hello-world/app/page.tsx index ff7159d9149fe..f46e7fef69430 100644 --- a/test/e2e/app-dir/hello-world/app/page.tsx +++ b/test/e2e/app-dir/hello-world/app/page.tsx @@ -1,3 +1,8 @@ +// export default function Page() { +// return

hello world

+// } + +'use client' export default function Page() { - return

hello world

+ return typeof window === 'undefined' ? 'HELLO' : 'WORLD' } From 44bf33485f6b50f1727e99338f740733d25139b9 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 16:21:06 +0200 Subject: [PATCH 08/18] fix test --- .../next-swc/crates/core/src/react_server_components.rs | 2 -- packages/next/src/build/webpack-config.ts | 9 +++------ packages/next/src/lib/constants.ts | 1 + test/e2e/app-dir/hello-world/app/page.tsx | 7 +------ test/e2e/module-layer/app/app/client-edge/page.js | 2 +- test/e2e/module-layer/app/app/client/page.js | 2 +- .../module-layer/app/app/client/server-only-component.js | 5 ----- test/e2e/module-layer/app/app/server-edge/page.js | 2 +- test/e2e/module-layer/app/app/server/page.js | 2 +- 9 files changed, 9 insertions(+), 23 deletions(-) delete mode 100644 test/e2e/module-layer/app/app/client/server-only-component.js 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 71eec530fbc7a..22f9e13837930 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -87,8 +87,6 @@ impl VisitMut for ReactServerComponents { if !is_action_file { if self.bundle_target == "client" { self.assert_client_graph(&imports); - } - if self.bundle_target != "server" { self.assert_invalid_api(module, true); } } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 6e06d29f86486..95320e8aeb760 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -962,6 +962,7 @@ export default async function getBaseWebpackConfig( ? useSWCLoader ? [ getSwcLoader({ + bundleTarget: 'client', hasServerComponents, isServerLayer: false, }), @@ -972,6 +973,7 @@ export default async function getBaseWebpackConfig( // acceptable as Babel will not be recommended. [ getSwcLoader({ + bundleTarget: 'client', isServerLayer: false, }), getBabelLoader(), @@ -988,8 +990,8 @@ export default async function getBaseWebpackConfig( loader: 'next-swc-loader', options: { ...getSwcLoader().options, - hasServerComponents: false, bundleTarget: 'server', + hasServerComponents: false, }, } : defaultLoaders.babel @@ -2107,11 +2109,6 @@ export default async function getBaseWebpackConfig( issuerLayer(layer: string | null) { return !layer || !isWebpackServerLayer(layer) }, - // issuerLayer: [ - // WEBPACK_LAYERS.actionBrowser, - // WEBPACK_LAYERS.appPagesBrowser, - // WEBPACK_LAYERS.serverSideRendering, - // ], options: { message: "'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.", diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 72f03d676b090..cc7882fd8c34b 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -157,6 +157,7 @@ export const WEBPACK_LAYERS = { WEBPACK_LAYERS_NAMES.actionBrowser, WEBPACK_LAYERS_NAMES.appMetadataRoute, WEBPACK_LAYERS_NAMES.appRouteHandler, + // Different from GROUP.server.bundleAll strategy, but still target as server-only bundle WEBPACK_LAYERS_NAMES.middleware, WEBPACK_LAYERS_NAMES.api, ], diff --git a/test/e2e/app-dir/hello-world/app/page.tsx b/test/e2e/app-dir/hello-world/app/page.tsx index f46e7fef69430..ff7159d9149fe 100644 --- a/test/e2e/app-dir/hello-world/app/page.tsx +++ b/test/e2e/app-dir/hello-world/app/page.tsx @@ -1,8 +1,3 @@ -// export default function Page() { -// return

hello world

-// } - -'use client' export default function Page() { - return typeof window === 'undefined' ? 'HELLO' : 'WORLD' + return

hello world

} diff --git a/test/e2e/module-layer/app/app/client-edge/page.js b/test/e2e/module-layer/app/app/client-edge/page.js index 14aca14e1b69f..5bebe6a87c9d2 100644 --- a/test/e2e/module-layer/app/app/client-edge/page.js +++ b/test/e2e/module-layer/app/app/client-edge/page.js @@ -3,7 +3,7 @@ import 'client-only' export default function Page() { - return 'page.js' + 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 index 00cad0c929f40..9b1365837de7a 100644 --- a/test/e2e/module-layer/app/app/client/page.js +++ b/test/e2e/module-layer/app/app/client/page.js @@ -3,5 +3,5 @@ import 'client-only' export default function Page() { - return 'page.js' + return 'app/client/page.js' } diff --git a/test/e2e/module-layer/app/app/client/server-only-component.js b/test/e2e/module-layer/app/app/client/server-only-component.js deleted file mode 100644 index 66393e79bf04c..0000000000000 --- a/test/e2e/module-layer/app/app/client/server-only-component.js +++ /dev/null @@ -1,5 +0,0 @@ -import 'server-only' - -export function two() { - return 2 -} diff --git a/test/e2e/module-layer/app/app/server-edge/page.js b/test/e2e/module-layer/app/app/server-edge/page.js index 75bdfa02d06fa..c833828d4f3bc 100644 --- a/test/e2e/module-layer/app/app/server-edge/page.js +++ b/test/e2e/module-layer/app/app/server-edge/page.js @@ -1,7 +1,7 @@ import 'server-only' export default function Page() { - return 'page.js' + 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 index c7886b97c51c5..aa6ff75951d9a 100644 --- a/test/e2e/module-layer/app/app/server/page.js +++ b/test/e2e/module-layer/app/app/server/page.js @@ -1,5 +1,5 @@ import 'server-only' export default function Page() { - return 'page.js' + return 'app/server/page.js' } From fbcf31d938dbf478d5cd0f82bfb28e65442cda84 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 17:13:00 +0200 Subject: [PATCH 09/18] fix graph assertion --- packages/next/src/build/swc/options.ts | 5 +++-- packages/next/src/build/webpack/loaders/next-swc-loader.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index d27c915f0a7ec..a9989b365289b 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -278,6 +278,7 @@ export function getJestSWCOptions({ resolvedBaseUrl, // Don't apply server layer transformations for Jest isServerLayer: false, + // Disable server / client graph assertions for Jest bundleTarget: 'default', }) @@ -321,7 +322,7 @@ export function getLoaderSWCOptions({ isServerLayer, isServerActionsEnabled, optimizeBarrelExports, - bundleTarget, + bundleTarget = 'client', }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { @@ -363,7 +364,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, - bundleTarget: bundleTarget || 'client', + bundleTarget, }) baseOptions.fontLoaders = { fontLoaders: [ 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 c04ec230e1e85..6b23d7e7e24f4 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -87,7 +87,7 @@ async function loaderTransform( isServerActionsEnabled: nextConfig?.experimental?.serverActions, isServerLayer, optimizeBarrelExports, - bundleTarget: bundleTarget || 'default', + bundleTarget, }) const programmaticOptions = { From 797739ab2c7f991fae2e252dd2b42abca536be64 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 17:15:07 +0200 Subject: [PATCH 10/18] clean up --- packages/next/src/build/webpack-config.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 95320e8aeb760..22b2afa9f269f 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -962,7 +962,6 @@ export default async function getBaseWebpackConfig( ? useSWCLoader ? [ getSwcLoader({ - bundleTarget: 'client', hasServerComponents, isServerLayer: false, }), @@ -973,7 +972,6 @@ export default async function getBaseWebpackConfig( // acceptable as Babel will not be recommended. [ getSwcLoader({ - bundleTarget: 'client', isServerLayer: false, }), getBabelLoader(), @@ -1202,8 +1200,6 @@ export default async function getBaseWebpackConfig( 'styled-jsx/style$': defaultOverrides['styled-jsx/style'], 'styled-jsx$': defaultOverrides['styled-jsx'], - // 'server-only$': 'next/dist/compiled/server-only', - // 'client-only$': 'next/dist/compiled/client-only', ...customAppAliases, ...customErrorAlias, @@ -2052,8 +2048,8 @@ export default async function getBaseWebpackConfig( ] }, }, + // Alias server-only and client-only to proper exports based on bundling layers { - // test: /next[\\/]dist[\\/]compiled[\\/]server-only/, issuerLayer: isWebpackServerLayer, resolve: { // Error on client-only but allow server-only @@ -2062,14 +2058,6 @@ export default async function getBaseWebpackConfig( 'client-only$': 'next/dist/compiled/client-only/error', 'next/dist/compiled/server-only$': 'next/dist/compiled/server-only/empty', - // 'next/dist/compiled/client-only': - // 'next/dist/compiled/client-only/error', - // 'next/dist/compiled/client-only$': `next-invalid-import-error-loader?${JSON.stringify({ - // message: `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` - // })}`, - // 'client-only': `next-invalid-import-error-loader?${JSON.stringify({ - // message: `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` - // })}`, }, }, }, @@ -2082,18 +2070,14 @@ export default async function getBaseWebpackConfig( alias: { 'server-only$': 'next/dist/compiled/server-only/index', 'client-only$': 'next/dist/compiled/client-only/index', - // 'next/dist/compiled/server-only$': `next-invalid-import-error-loader?${JSON.stringify({ - // message: `'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.` - // })}`, 'next/dist/compiled/client-only$': 'next/dist/compiled/client-only/index', 'next/dist/compiled/server-only': 'next/dist/compiled/server-only/index', - // 'next/dist/compiled/client-only': - // 'next/dist/compiled/client-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', From 7737a0d49655420ba023671deb884bab3e3b338b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 15 Sep 2023 17:22:03 +0200 Subject: [PATCH 11/18] rust check --- .../crates/core/src/react_server_components.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 22f9e13837930..1b0f9f1190d04 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -71,8 +71,7 @@ impl VisitMut for ReactServerComponents { 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. + // Only assert server graph if file's bundle target is "server", e.g. // * server components pages // * pages bundles on SSR layer // * middleware @@ -80,15 +79,13 @@ impl VisitMut for ReactServerComponents { self.assert_server_graph(&imports, module); } } else { - // Only assert client graph if the file is not an action file, and bundle target - // is "client" e.g. + // 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 { - if self.bundle_target == "client" { - self.assert_client_graph(&imports); - self.assert_invalid_api(module, true); - } + 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); From d38a001fd43edffd7409754facb36b95da87e1b6 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 01:21:05 +0200 Subject: [PATCH 12/18] refactor --- packages/next/src/build/webpack-config.ts | 56 ++++++++++++++--------- packages/next/src/lib/constants.ts | 20 ++++---- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 22b2afa9f269f..c71316c12e6af 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -86,12 +86,6 @@ const NEXT_PROJECT_ROOT_DIST_CLIENT = path.join( 'client' ) -const isWebpackBundleAllServerLayer = (layer: WebpackLayerName | null) => - Boolean(layer && WEBPACK_LAYERS.GROUP.serverBundleAll.includes(layer)) - -const isWebpackServerLayer = (layer: WebpackLayerName | null) => - Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer)) - if (parseInt(React.version) < 18) { throw new Error('Next.js requires react >= 18.2.0 to be installed.') } @@ -1458,7 +1452,7 @@ export default async function getBaseWebpackConfig( // Don't bundle @vercel/og nodejs bundle for nodejs runtime. // TODO-APP: bundle route.js with different layer that externals common node_module deps. if ( - isWebpackBundleAllServerLayer(layer) && + WEBPACK_LAYERS.isWebpackServerLayer(layer) && request === 'next/dist/compiled/@vercel/og/index.node.js' ) { return `module ${request}` @@ -1594,7 +1588,7 @@ export default async function getBaseWebpackConfig( (isEsm && isAppLayer) if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { - if (isWebpackBundleAllServerLayer(layer)) { + if (WEBPACK_LAYERS.isWebpackServerLayer(layer)) { // All packages should be bundled for the server layer if they're not opted out. // This option takes priority over the transpilePackages option. @@ -2050,7 +2044,7 @@ export default async function getBaseWebpackConfig( }, // Alias server-only and client-only to proper exports based on bundling layers { - issuerLayer: isWebpackServerLayer, + issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, resolve: { // Error on client-only but allow server-only alias: { @@ -2062,8 +2056,13 @@ export default async function getBaseWebpackConfig( }, }, { - issuerLayer(layer: string | null) { - return !layer || !isWebpackServerLayer(layer) + issuerLayer: { + not: [ + null, + WEBPACK_LAYERS.isWebpackServerLayer, + WEBPACK_LAYERS.api, + WEBPACK_LAYERS.middleware, + ], }, resolve: { // Error on server-only but allow client-only @@ -2079,19 +2078,36 @@ export default async function getBaseWebpackConfig( }, // Detect server-only / client-only imports and error in build time { - test: /client-only$|next[\\/]dist[\\/]compiled[\\/]client-only[\\/]error/, + test: [ + /client-only$/, + /next[\\/]dist[\\/]compiled[\\/]client-only[\\/]error/, + ], loader: 'next-invalid-import-error-loader', - issuerLayer: isWebpackServerLayer, + issuerLayer: { + or: [ + WEBPACK_LAYERS.isWebpackServerLayer, + WEBPACK_LAYERS.api, + WEBPACK_LAYERS.middleware, + ], + }, 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/, + test: [ + /server-only/, + /next[\\/]dist[\\/]compiled[\\/]server-only[\\/]index/, + ], loader: 'next-invalid-import-error-loader', - issuerLayer(layer: string | null) { - return !layer || !isWebpackServerLayer(layer) + issuerLayer: { + not: [ + null, + WEBPACK_LAYERS.isWebpackServerLayer, + WEBPACK_LAYERS.api, + WEBPACK_LAYERS.middleware, + ], }, options: { message: @@ -2156,7 +2172,7 @@ export default async function getBaseWebpackConfig( ...(hasAppDir && !isClient ? [ { - issuerLayer: isWebpackBundleAllServerLayer, + issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2219,9 +2235,7 @@ export default async function getBaseWebpackConfig( oneOf: [ { exclude: [asyncStoragesRegex], - issuerLayer: { - or: [isWebpackBundleAllServerLayer], - }, + issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2295,7 +2309,7 @@ export default async function getBaseWebpackConfig( ? [ { test: codeCondition.test, - issuerLayer: isWebpackBundleAllServerLayer, + issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, exclude: [asyncStoragesRegex], use: swcLoaderForServerLayer, }, diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index cc7882fd8c34b..ff424b8510d95 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,30 +143,26 @@ 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: { - serverBundleAll: [ - WEBPACK_LAYERS_NAMES.reactServerComponents, - WEBPACK_LAYERS_NAMES.actionBrowser, - WEBPACK_LAYERS_NAMES.appMetadataRoute, - WEBPACK_LAYERS_NAMES.appRouteHandler, - ], server: [ WEBPACK_LAYERS_NAMES.reactServerComponents, WEBPACK_LAYERS_NAMES.actionBrowser, WEBPACK_LAYERS_NAMES.appMetadataRoute, WEBPACK_LAYERS_NAMES.appRouteHandler, - // Different from GROUP.server.bundleAll strategy, but still target as server-only bundle - WEBPACK_LAYERS_NAMES.middleware, - WEBPACK_LAYERS_NAMES.api, ], }, + isWebpackServerLayer(layer: WebpackLayerName | null): boolean { + return Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any)) + }, } -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 } From 764bdd4a5cd1a2e9677ee0a1d4884c065af9d6f7 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 01:29:41 +0200 Subject: [PATCH 13/18] fix --- packages/next/src/build/webpack-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index c71316c12e6af..9404cbcebc103 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -2058,7 +2058,7 @@ export default async function getBaseWebpackConfig( { issuerLayer: { not: [ - null, + (layer: string | null) => layer === null, WEBPACK_LAYERS.isWebpackServerLayer, WEBPACK_LAYERS.api, WEBPACK_LAYERS.middleware, From 4b019f2bd0314680ee68b230b0298fbbe7fe7c3d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 01:33:53 +0200 Subject: [PATCH 14/18] fix --- packages/next/src/build/webpack-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 9404cbcebc103..e8baf0c57073f 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -2103,7 +2103,7 @@ export default async function getBaseWebpackConfig( loader: 'next-invalid-import-error-loader', issuerLayer: { not: [ - null, + (layer: string | null) => layer === null, WEBPACK_LAYERS.isWebpackServerLayer, WEBPACK_LAYERS.api, WEBPACK_LAYERS.middleware, From 33b45bbab291a2a4e1f56f0e3449e0c1d52b1a2e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 02:11:04 +0200 Subject: [PATCH 15/18] refactor --- packages/next/src/build/utils.ts | 12 +++++++ packages/next/src/build/webpack-config.ts | 43 ++++++++--------------- packages/next/src/lib/constants.ts | 13 +++++-- test/e2e/module-layer/index.test.ts | 2 +- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 3af37d71a9907..921c7d10d4766 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -2116,3 +2116,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 e8baf0c57073f..4ebeba5198f34 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 { @@ -1452,7 +1453,7 @@ export default async function getBaseWebpackConfig( // Don't bundle @vercel/og nodejs bundle for nodejs runtime. // TODO-APP: bundle route.js with different layer that externals common node_module deps. if ( - WEBPACK_LAYERS.isWebpackServerLayer(layer) && + isWebpackServerLayer(layer) && request === 'next/dist/compiled/@vercel/og/index.node.js' ) { return `module ${request}` @@ -1588,7 +1589,7 @@ export default async function getBaseWebpackConfig( (isEsm && isAppLayer) if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { - if (WEBPACK_LAYERS.isWebpackServerLayer(layer)) { + if (isWebpackServerLayer(layer)) { // All packages should be bundled for the server layer if they're not opted out. // This option takes priority over the transpilePackages option. @@ -1796,9 +1797,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 @@ -2044,7 +2043,9 @@ export default async function getBaseWebpackConfig( }, // Alias server-only and client-only to proper exports based on bundling layers { - issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, + issuerLayer: { + or: WEBPACK_LAYERS.GROUP.serverTarget, + }, resolve: { // Error on client-only but allow server-only alias: { @@ -2057,12 +2058,7 @@ export default async function getBaseWebpackConfig( }, { issuerLayer: { - not: [ - (layer: string | null) => layer === null, - WEBPACK_LAYERS.isWebpackServerLayer, - WEBPACK_LAYERS.api, - WEBPACK_LAYERS.middleware, - ], + not: WEBPACK_LAYERS.GROUP.serverTarget, }, resolve: { // Error on server-only but allow client-only @@ -2079,16 +2075,12 @@ export default async function getBaseWebpackConfig( // Detect server-only / client-only imports and error in build time { test: [ - /client-only$/, + /^client-only$/, /next[\\/]dist[\\/]compiled[\\/]client-only[\\/]error/, ], loader: 'next-invalid-import-error-loader', issuerLayer: { - or: [ - WEBPACK_LAYERS.isWebpackServerLayer, - WEBPACK_LAYERS.api, - WEBPACK_LAYERS.middleware, - ], + or: WEBPACK_LAYERS.GROUP.serverTarget, }, options: { message: @@ -2097,17 +2089,12 @@ export default async function getBaseWebpackConfig( }, { test: [ - /server-only/, + /^server-only$/, /next[\\/]dist[\\/]compiled[\\/]server-only[\\/]index/, ], loader: 'next-invalid-import-error-loader', issuerLayer: { - not: [ - (layer: string | null) => layer === null, - WEBPACK_LAYERS.isWebpackServerLayer, - WEBPACK_LAYERS.api, - WEBPACK_LAYERS.middleware, - ], + not: WEBPACK_LAYERS.GROUP.serverTarget, }, options: { message: @@ -2172,7 +2159,7 @@ export default async function getBaseWebpackConfig( ...(hasAppDir && !isClient ? [ { - issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, + issuerLayer: isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2235,7 +2222,7 @@ export default async function getBaseWebpackConfig( oneOf: [ { exclude: [asyncStoragesRegex], - issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, + issuerLayer: isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2309,7 +2296,7 @@ export default async function getBaseWebpackConfig( ? [ { test: codeCondition.test, - issuerLayer: WEBPACK_LAYERS.isWebpackServerLayer, + issuerLayer: isWebpackServerLayer, exclude: [asyncStoragesRegex], use: swcLoaderForServerLayer, }, diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index ff424b8510d95..e3f8f07838c4c 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -152,9 +152,16 @@ const WEBPACK_LAYERS = { WEBPACK_LAYERS_NAMES.appMetadataRoute, WEBPACK_LAYERS_NAMES.appRouteHandler, ], - }, - isWebpackServerLayer(layer: WebpackLayerName | null): boolean { - return Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any)) + 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, + ], }, } diff --git a/test/e2e/module-layer/index.test.ts b/test/e2e/module-layer/index.test.ts index 271fbdc4df1e3..a7c7ea854fdeb 100644 --- a/test/e2e/module-layer/index.test.ts +++ b/test/e2e/module-layer/index.test.ts @@ -25,7 +25,7 @@ createNextDescribe( for (const route of routes) { const { status } = await next.fetch(route) - expect(status).toBe(200) + expect([route, status]).toEqual([route, 200]) } }) From d3b0764710b1bfe7709e51d56bfb9f328508d14d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 02:12:15 +0200 Subject: [PATCH 16/18] rm default overrides, test against deployment --- packages/next/src/server/import-overrides.ts | 6 ------ test/e2e/module-layer/index.test.ts | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/next/src/server/import-overrides.ts b/packages/next/src/server/import-overrides.ts index 483e0c44a5854..c72f42612eae3 100644 --- a/packages/next/src/server/import-overrides.ts +++ b/packages/next/src/server/import-overrides.ts @@ -21,12 +21,6 @@ export const defaultOverrides = { 'styled-jsx/style': process.env.NEXT_MINIMAL ? resolve('styled-jsx/style') : resolve('styled-jsx/style', nextPaths), - 'server-only': process.env.NEXT_MINIMAL - ? resolve('next/dist/compiled/server-only') - : resolve('next/dist/compiled/server-only', nextPaths), - 'client-only': process.env.NEXT_MINIMAL - ? resolve('next/dist/compiled/client-only') - : resolve('next/dist/compiled/client-only', nextPaths), } const toResolveMap = (map: Record): [string, string][] => diff --git a/test/e2e/module-layer/index.test.ts b/test/e2e/module-layer/index.test.ts index a7c7ea854fdeb..6e4359b6a21aa 100644 --- a/test/e2e/module-layer/index.test.ts +++ b/test/e2e/module-layer/index.test.ts @@ -4,7 +4,6 @@ createNextDescribe( 'module layer', { files: __dirname, - skipDeployment: true, }, ({ next, isNextStart }) => { it('should render routes marked with restriction marks without errors', async () => { From 28b5f6c1fd9211a7329b400381e594aef2c7cb19 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 16 Sep 2023 02:16:04 +0200 Subject: [PATCH 17/18] fix type imports --- packages/next/src/build/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 921c7d10d4766..d9c90d8158e00 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' From 19a94033dc411ea614f13c3ad24b582909b23b77 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sun, 17 Sep 2023 01:00:29 +0200 Subject: [PATCH 18/18] rename --- packages/next/src/build/webpack-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 4ebeba5198f34..52301f6b0d70d 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -940,7 +940,7 @@ export default async function getBaseWebpackConfig( ] // client components layers: SSR + browser - const swcLoaderForClientLayers = [ + const swcLoaderForClientLayer = [ ...(dev && isClient ? [ require.resolve( @@ -2314,7 +2314,7 @@ export default async function getBaseWebpackConfig( WEBPACK_LAYERS.serverSideRendering, ], exclude: [codeCondition.exclude], - use: swcLoaderForClientLayers, + use: swcLoaderForClientLayer, }, ] : []),