From 9f432cbc78ae3736e3de8a26ef1d9a8ed55dc390 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 20 Dec 2023 17:08:07 +0100 Subject: [PATCH] Transpile all code on app browser layer (#59569) --- packages/next/src/build/swc/options.ts | 41 +++++-- packages/next/src/build/webpack-config.ts | 111 ++++++++++-------- .../build/webpack/loaders/next-swc-loader.ts | 7 +- .../ReactRefreshLogBox-builtins.test.ts | 2 +- .../app-dir/app-external/app-external.test.ts | 44 ++++--- .../app-external/app/action/client/page.js | 16 +++ .../server-action-mod/index.js | 7 ++ .../server-action-mod/package.json | 3 + test/turbopack-tests-manifest.json | 3 +- 9 files changed, 157 insertions(+), 77 deletions(-) create mode 100644 test/e2e/app-dir/app-external/app/action/client/page.js create mode 100644 test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/index.js create mode 100644 test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/package.json diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 0336df67ace12..546f2ebfc743f 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -1,3 +1,4 @@ +import { WEBPACK_LAYERS, type WebpackLayerName } from '../../lib/constants' import type { NextConfig, ExperimentalConfig, @@ -9,6 +10,8 @@ import type { ResolvedBaseUrl } from '../load-jsconfig' const nextDistPath = /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ +const nodeModulesPath = /[\\/]node_modules[\\/]/ + const regeneratorRuntimePath = require.resolve( 'next/dist/compiled/regenerator-runtime' ) @@ -44,7 +47,7 @@ function getBaseSWCOptions({ jsConfig, swcCacheDir, serverComponents, - isReactServerLayer, + bundleLayer, }: { filename: string jest?: boolean @@ -59,8 +62,10 @@ function getBaseSWCOptions({ jsConfig: any swcCacheDir?: string serverComponents?: boolean - isReactServerLayer?: boolean + bundleLayer?: WebpackLayerName }) { + const isReactServerLayer = + bundleLayer === WEBPACK_LAYERS.reactServerComponents const parserConfig = getParserOptions({ filename, jsConfig }) const paths = jsConfig?.compilerOptions?.paths const enableDecorators = Boolean( @@ -177,7 +182,7 @@ function getBaseSWCOptions({ serverComponents: serverComponents && !jest ? { - isReactServerLayer: !!isReactServerLayer, + isReactServerLayer, } : undefined, serverActions: @@ -186,7 +191,7 @@ function getBaseSWCOptions({ // always enable server actions // TODO: remove this option enabled: true, - isReactServerLayer: !!isReactServerLayer, + isReactServerLayer, } : undefined, // For app router we prefer to bundle ESM, @@ -295,8 +300,8 @@ export function getJestSWCOptions({ resolvedBaseUrl, esm, // Don't apply server layer transformations for Jest - isReactServerLayer: false, // Disable server / client graph assertions for Jest + bundleLayer: undefined, serverComponents: false, }) @@ -339,7 +344,7 @@ export function getLoaderSWCOptions({ swcCacheDir, relativeFilePathFromRoot, serverComponents, - isReactServerLayer, + bundleLayer, esm, }: { filename: string @@ -362,7 +367,7 @@ export function getLoaderSWCOptions({ relativeFilePathFromRoot: string esm?: boolean serverComponents?: boolean - isReactServerLayer?: boolean + bundleLayer?: WebpackLayerName }) { let baseOptions: any = getBaseSWCOptions({ filename, @@ -375,7 +380,7 @@ export function getLoaderSWCOptions({ jsConfig, // resolvedBaseUrl, swcCacheDir, - isReactServerLayer, + bundleLayer, serverComponents, esm: !!esm, }) @@ -418,9 +423,12 @@ export function getLoaderSWCOptions({ } const isNextDist = nextDistPath.test(filename) + const isNodeModules = nodeModulesPath.test(filename) + const isAppBrowserLayer = bundleLayer === WEBPACK_LAYERS.appPagesBrowser + let options: any if (isServer) { - return { + options = { ...baseOptions, // Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages disableNextSsg: true, @@ -440,7 +448,7 @@ export function getLoaderSWCOptions({ ...getModuleOptions(esm), } } else { - const options = { + options = { ...baseOptions, // Ensure Next.js internals are output as commonjs modules ...(isNextDist @@ -468,6 +476,17 @@ export function getLoaderSWCOptions({ // Matches default @babel/preset-env behavior options.jsc.target = 'es5' } - return options } + + // For node_modules in app browser layer, we don't need to do any server side transformation. + // Only keep server actions transform to discover server actions from client components. + if (isAppBrowserLayer && isNodeModules) { + options.disableNextSsg = true + options.disablePageConfig = true + options.isPageFile = false + options.optimizeServerReact = undefined + options.cjsRequireOptimizer = undefined + } + + return options } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 816e73a1dd1e0..e2dd496a030e2 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -142,14 +142,14 @@ const devtoolRevertWarning = execOnce( let loggedSwcDisabled = false let loggedIgnoredCompilerOptions = false +const reactRefreshLoaderName = + 'next/dist/compiled/@next/react-refresh-utils/dist/loader' export function attachReactRefresh( webpackConfig: webpack.Configuration, targetLoader: webpack.RuleSetUseItem ) { let injections = 0 - const reactRefreshLoaderName = - 'next/dist/compiled/@next/react-refresh-utils/dist/loader' const reactRefreshLoader = require.resolve(reactRefreshLoaderName) webpackConfig.module?.rules?.forEach((rule) => { if (rule && typeof rule === 'object' && 'use' in rule) { @@ -445,18 +445,22 @@ export default async function getBaseWebpackConfig( // RSC loaders, prefer ESM, set `esm` to true const swcServerLayerLoader = getSwcLoader({ serverComponents: true, - isReactServerLayer: true, + bundleLayer: WEBPACK_LAYERS.reactServerComponents, esm: true, }) - const swcClientLayerLoader = getSwcLoader({ + const swcSSRLayerLoader = getSwcLoader({ serverComponents: true, - isReactServerLayer: false, + bundleLayer: WEBPACK_LAYERS.serverSideRendering, + esm: true, + }) + const swcBrowserLayerLoader = getSwcLoader({ + serverComponents: true, + bundleLayer: WEBPACK_LAYERS.appPagesBrowser, esm: true, }) // Default swc loaders for pages doesn't prefer ESM. const swcDefaultLoader = getSwcLoader({ serverComponents: true, - isReactServerLayer: false, esm: false, }) @@ -475,31 +479,30 @@ export default async function getBaseWebpackConfig( ].filter(Boolean) : [] - const swcLoaderForMiddlewareLayer = useSWCLoader - ? getSwcLoader({ - serverComponents: false, - isReactServerLayer: false, - }) - : // 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({ - serverComponents: false, - isReactServerLayer: false, - }), - ] + const swcLoaderForMiddlewareLayer = [ + // 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({ + serverComponents: false, + bundleLayer: WEBPACK_LAYERS.middleware, + }), + babelLoader, + ].filter(Boolean) - // client components layers: SSR + browser - const swcLoaderForClientLayer = [ - ...(dev && isClient - ? [ - require.resolve( - 'next/dist/compiled/@next/react-refresh-utils/dist/loader' - ), - ] - : []), + const reactRefreshLoaders = + dev && isClient ? [require.resolve(reactRefreshLoaderName)] : [] + + // client components layers: SSR or browser + const createSwcLoaderForClientLayer = ({ + isBrowserLayer, + reactRefresh, + }: { + isBrowserLayer: boolean + reactRefresh: boolean + }) => [ + ...(reactRefresh ? reactRefreshLoaders : []), { // This loader handles actions and client entries // in the client layer. @@ -511,12 +514,22 @@ export default async function getBaseWebpackConfig( // as an additional pass to handle RSC correctly. // This will cause some performance overhead but // acceptable as Babel will not be recommended. - swcClientLayerLoader, + isBrowserLayer ? swcBrowserLayerLoader : swcSSRLayerLoader, babelLoader, ].filter(Boolean) : []), ] + const swcLoaderForBrowserLayer = createSwcLoaderForClientLayer({ + isBrowserLayer: true, + // reactRefresh for browser layer is applied conditionally to user-land source + reactRefresh: false, + }) + const swcLoaderForSSRLayer = createSwcLoaderForClientLayer({ + isBrowserLayer: false, + reactRefresh: true, + }) + // 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 // be performed. @@ -524,7 +537,7 @@ export default async function getBaseWebpackConfig( hasAppDir && useSWCLoader ? getSwcLoader({ serverComponents: false, - isReactServerLayer: false, + bundleLayer: WEBPACK_LAYERS.api, }) : defaultLoaders.babel @@ -1379,6 +1392,20 @@ export default async function getBaseWebpackConfig( }, ] : []), + // Do not apply react-refresh-loader to node_modules for app router browser layer + ...(hasAppDir && dev && isClient + ? [ + { + test: codeCondition.test, + exclude: codeCondition.exclude, + issuerLayer: WEBPACK_LAYERS.appPagesBrowser, + use: reactRefreshLoaders, + resolve: { + mainFields: getMainField(compilerType, true), + }, + }, + ] + : []), { oneOf: [ { @@ -1412,17 +1439,16 @@ export default async function getBaseWebpackConfig( }, { test: codeCondition.test, - exclude: codeCondition.exclude, - issuerLayer: [WEBPACK_LAYERS.appPagesBrowser], - use: swcLoaderForClientLayer, + issuerLayer: WEBPACK_LAYERS.appPagesBrowser, + use: swcLoaderForBrowserLayer, resolve: { mainFields: getMainField(compilerType, true), }, }, { test: codeCondition.test, - issuerLayer: [WEBPACK_LAYERS.serverSideRendering], - use: swcLoaderForClientLayer, + issuerLayer: WEBPACK_LAYERS.serverSideRendering, + use: swcLoaderForSSRLayer, resolve: { mainFields: getMainField(compilerType, true), }, @@ -1431,18 +1457,11 @@ export default async function getBaseWebpackConfig( : []), { ...codeCondition, - use: - dev && isClient - ? [ - require.resolve( - 'next/dist/compiled/@next/react-refresh-utils/dist/loader' - ), - defaultLoaders.babel, - ] - : defaultLoaders.babel, + use: [...reactRefreshLoaders, defaultLoaders.babel], }, ], }, + ...(!config.images.disableStaticImages ? [ { 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 aff07d2700b61..b1650fa4b8334 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -27,6 +27,7 @@ DEALINGS IN THE SOFTWARE. */ import type { NextConfig } from '../../../../types' +import type { WebpackLayerName } from '../../../lib/constants' import { isWasm, transform } from '../../swc' import { getLoaderSWCOptions } from '../../swc/options' import path, { isAbsolute } from 'path' @@ -43,7 +44,7 @@ export interface SWCLoaderOptions { supportedBrowsers: string[] | undefined swcCacheDir: string serverComponents?: boolean - isReactServerLayer?: boolean + bundleLayer?: WebpackLayerName esm?: boolean } @@ -69,7 +70,7 @@ async function loaderTransform( supportedBrowsers, swcCacheDir, serverComponents, - isReactServerLayer, + bundleLayer, esm, } = loaderOptions const isPageFile = filename.startsWith(pagesDir) @@ -93,7 +94,7 @@ async function loaderTransform( swcCacheDir, relativeFilePathFromRoot, serverComponents, - isReactServerLayer, + bundleLayer, esm, }) diff --git a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts index 36278161698db..dfa2002ac6e9b 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts @@ -51,7 +51,7 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => { ) expect(await session.hasRedbox(true)).toBe(true) expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "./node_modules/my-package/index.js:1:0 + "./node_modules/my-package/index.js:1:12 Module not found: Can't resolve 'dns' https://nextjs.org/docs/messages/module-not-found diff --git a/test/e2e/app-dir/app-external/app-external.test.ts b/test/e2e/app-dir/app-external/app-external.test.ts index b734f53bcc358..5820bb4fe34fd 100644 --- a/test/e2e/app-dir/app-external/app-external.test.ts +++ b/test/e2e/app-dir/app-external/app-external.test.ts @@ -251,21 +251,35 @@ createNextDescribe( expect(html).toContain('success') }) - it('should not prefer to resolve esm over cjs for bundling optout packages', async () => { - const browser = await next.browser('/optout/action') - expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('') - - browser.elementByCss('#dual-pkg-outout button').click() - await check(async () => { - const text = await browser.elementByCss('#dual-pkg-outout p').text() - if (process.env.TURBOPACK) { - // The prefer esm won't effect turbopack resolving - expect(text).toBe('dual-pkg-optout:mjs') - } else { - expect(text).toBe('dual-pkg-optout:cjs') - } - return 'success' - }, /success/) + describe('server actions', () => { + it('should not prefer to resolve esm over cjs for bundling optout packages', async () => { + const browser = await next.browser('/optout/action') + expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('') + + browser.elementByCss('#dual-pkg-outout button').click() + await check(async () => { + const text = await browser.elementByCss('#dual-pkg-outout p').text() + if (process.env.TURBOPACK) { + // The prefer esm won't effect turbopack resolving + expect(text).toBe('dual-pkg-optout:mjs') + } else { + expect(text).toBe('dual-pkg-optout:cjs') + } + return 'success' + }, /success/) + }) + + it('should compile server actions from node_modules in client components', async () => { + // before action there's no action log + expect(next.cliOutput).not.toContain('action-log:server:action1') + const browser = await next.browser('/action/client') + await browser.elementByCss('#action').click() + + await check(() => { + expect(next.cliOutput).toContain('action-log:server:action1') + return 'success' + }, /success/) + }) }) } ) diff --git a/test/e2e/app-dir/app-external/app/action/client/page.js b/test/e2e/app-dir/app-external/app/action/client/page.js new file mode 100644 index 0000000000000..48cc3ebb7f6ec --- /dev/null +++ b/test/e2e/app-dir/app-external/app/action/client/page.js @@ -0,0 +1,16 @@ +'use client' + +import { action1 } from 'server-action-mod' + +export default function Page() { + return ( + + ) +} diff --git a/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/index.js b/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/index.js new file mode 100644 index 0000000000000..18fd3ad6b939e --- /dev/null +++ b/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/index.js @@ -0,0 +1,7 @@ +'use server' + +export async function action1() { + console.log( + `action-log:${typeof window === 'undefined' ? 'server' : 'client'}:action1` + ) +} diff --git a/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/package.json b/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/package.json new file mode 100644 index 0000000000000..1aa8764b1da0d --- /dev/null +++ b/test/e2e/app-dir/app-external/node_modules_bak/server-action-mod/package.json @@ -0,0 +1,3 @@ +{ + "exports": "./index.js" +} diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index 3d2aa5da42bc8..6f73c35e94b14 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -2494,7 +2494,8 @@ ], "failed": [ "app dir - external dependency should be able to opt-out 3rd party packages being bundled in server components", - "app dir - external dependency should have proper tree-shaking for known modules in CJS" + "app dir - external dependency should have proper tree-shaking for known modules in CJS", + "app dir - external dependency server actions should compile server actions from node_modules in client components" ], "pending": [], "flakey": [],