From 2f47a89ce4876f8ce6c442c76200e61cd26d9ae4 Mon Sep 17 00:00:00 2001 From: OJ Kwon <1210596+kwonoj@users.noreply.github.com.> Date: Wed, 15 May 2024 10:33:50 -0700 Subject: [PATCH] feat(next): apply babel react compiler plugins --- .../crates/next-core/src/next_config.rs | 62 ++++++++- .../next/src/build/babel/loader/get-config.ts | 123 +++++++++++------- .../next/src/build/babel/loader/types.d.ts | 43 +++++- .../next/src/build/get-babel-loader-config.ts | 86 ++++++++++++ packages/next/src/build/swc/index.ts | 64 ++++++++- packages/next/src/build/webpack-config.ts | 39 +++--- 6 files changed, 337 insertions(+), 80 deletions(-) create mode 100644 packages/next/src/build/get-babel-loader-config.ts diff --git a/packages/next-swc/crates/next-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index 93cf4bda13ae7..490bbcdae34b5 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -419,7 +419,7 @@ pub struct ExperimentalTurboConfig { pub use_swc_css: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct RuleConfigItemOptions { pub loaders: Vec, @@ -427,14 +427,14 @@ pub struct RuleConfigItemOptions { pub rename_as: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase", untagged)] pub enum RuleConfigItemOrShortcut { Loaders(Vec), Advanced(RuleConfigItem), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase", untagged)] pub enum RuleConfigItem { Options(RuleConfigItemOptions), @@ -442,7 +442,7 @@ pub enum RuleConfigItem { Boolean(bool), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] #[serde(untagged)] pub enum LoaderItem { LoaderName(String), @@ -456,6 +456,36 @@ pub enum MdxRsOptions { Option(MdxTransformOptions), } +#[turbo_tasks::value(shared)] +#[derive(Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub enum ReactCompilerMode { + Infer, + Annotation, + All, +} + +/// Subset of react compiler options +#[turbo_tasks::value(shared)] +#[derive(Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReactCompilerOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub compilation_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub panic_threshold: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[serde(untagged)] +pub enum ReactCompilerOptionsOrBoolean { + Boolean(bool), + Option(ReactCompilerOptions), +} + +#[turbo_tasks::value(transparent)] +pub struct OptionalReactCompilerOptions(Option>); + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ExperimentalConfig { @@ -489,6 +519,7 @@ pub struct ExperimentalConfig { pub web_vitals_attribution: Option>, pub server_actions: Option, pub sri: Option, + react_compiler: Option, // --- // UNSUPPORTED @@ -961,6 +992,29 @@ impl NextConfig { Ok(options.cell()) } + #[turbo_tasks::function] + pub async fn react_compiler(self: Vc) -> Result> { + let options = &self.await?.experimental.react_compiler; + + let options = match options { + Some(ReactCompilerOptionsOrBoolean::Boolean(true)) => { + OptionalReactCompilerOptions(Some( + ReactCompilerOptions { + compilation_mode: None, + panic_threshold: None, + } + .cell(), + )) + } + Some(ReactCompilerOptionsOrBoolean::Option(options)) => OptionalReactCompilerOptions( + Some(ReactCompilerOptions { ..options.clone() }.cell()), + ), + _ => OptionalReactCompilerOptions(None), + }; + + Ok(options.cell()) + } + #[turbo_tasks::function] pub async fn sass_config(self: Vc) -> Result> { Ok(Vc::cell( diff --git a/packages/next/src/build/babel/loader/get-config.ts b/packages/next/src/build/babel/loader/get-config.ts index 7f6421e2ff585..abed82f147d3f 100644 --- a/packages/next/src/build/babel/loader/get-config.ts +++ b/packages/next/src/build/babel/loader/get-config.ts @@ -70,7 +70,11 @@ function getPlugins( const { isServer, isPageFile, isNextDist, hasModuleExports } = cacheCharacteristics - const { hasReactRefresh, development } = loaderOptions + const { development } = loaderOptions + const hasReactRefresh = + loaderOptions.transformMode !== 'standalone' + ? loaderOptions.hasReactRefresh + : false const applyCommonJsItem = hasModuleExports ? createConfigItem(require('../plugins/commonjs'), { type: 'plugin' }) @@ -260,14 +264,7 @@ function getFreshConfig( filename: string, inputSourceMap?: object | null ) { - let { isServer, pagesDir, development, hasJsxRuntime, configFile, srcDir } = - loaderOptions - - let customConfig: any = configFile - ? getCustomBabelConfig(configFile) - : undefined - - checkCustomBabelConfigDeprecation(customConfig) + let { isServer, pagesDir, srcDir, development } = loaderOptions let options = { babelrc: false, @@ -275,29 +272,75 @@ function getFreshConfig( filename, inputSourceMap: inputSourceMap || undefined, - // Set the default sourcemap behavior based on Webpack's mapping flag, - // but allow users to override if they want. - sourceMaps: - loaderOptions.sourceMaps === undefined - ? this.sourceMap - : loaderOptions.sourceMaps, - // Ensure that Webpack will get a full absolute path in the sourcemap // so that it can properly map the module back to its internal cached // modules. sourceFileName: filename, + sourceMaps: this.sourceMap, + } as any + + const baseCaller = { + name: 'next-babel-turbo-loader', + supportsStaticESM: true, + supportsDynamicImport: true, + + // Provide plugins with insight into webpack target. + // https://github.com/babel/babel-loader/issues/787 + target: target, + + // Webpack 5 supports TLA behind a flag. We enable it by default + // for Babel, and then webpack will throw an error if the experimental + // flag isn't enabled. + supportsTopLevelAwait: true, + + isServer, + srcDir, + pagesDir, + isDev: development, + + ...loaderOptions.caller, + } + + if (loaderOptions.transformMode === 'standalone') { + options.plugins = [ + '@babel/plugin-syntax-jsx', + ...(loaderOptions.plugins ?? []), + ] + options.presets = [ + [ + require('next/dist/compiled/babel/preset-typescript'), + { allowNamespaces: true }, + ], + ] + options.caller = baseCaller + } else { + let { configFile, plugins, hasJsxRuntime } = loaderOptions + let customConfig: any = configFile + ? getCustomBabelConfig(configFile) + : undefined + + checkCustomBabelConfigDeprecation(customConfig) - plugins: [ + // Set the default sourcemap behavior based on Webpack's mapping flag, + // but allow users to override if they want. + options.sourceMaps = + loaderOptions.sourceMaps === undefined + ? this.sourceMap + : loaderOptions.sourceMaps + + options.plugins = [ ...getPlugins(loaderOptions, cacheCharacteristics), + ...(plugins || []), ...(customConfig?.plugins || []), - ], + ] // target can be provided in babelrc - target: isServer ? undefined : customConfig?.target, + options.target = isServer ? undefined : customConfig?.target + // env can be provided in babelrc - env: customConfig?.env, + options.env = customConfig?.env - presets: (() => { + options.presets = (() => { // If presets is defined the user will have next/babel in their babelrc if (customConfig?.presets) { return customConfig.presets @@ -310,33 +353,15 @@ function getFreshConfig( // If no custom config is provided the default is to use next/babel return ['next/babel'] - })(), - - overrides: loaderOptions.overrides, + })() - caller: { - name: 'next-babel-turbo-loader', - supportsStaticESM: true, - supportsDynamicImport: true, + options.overrides = loaderOptions.overrides - // Provide plugins with insight into webpack target. - // https://github.com/babel/babel-loader/issues/787 - target: target, - - // Webpack 5 supports TLA behind a flag. We enable it by default - // for Babel, and then webpack will throw an error if the experimental - // flag isn't enabled. - supportsTopLevelAwait: true, - - isServer, - srcDir, - pagesDir, - isDev: development, + options.caller = { + ...baseCaller, hasJsxRuntime, - - ...loaderOptions.caller, - }, - } as any + } + } // Babel does strict checks on the config so undefined is not allowed if (typeof options.target === 'undefined') { @@ -405,7 +430,7 @@ export default function getConfig( filename ) - if (loaderOptions.configFile) { + if (loaderOptions.transformMode === 'default' && loaderOptions.configFile) { // Ensures webpack invalidates the cache for this loader when the config file changes this.addDependency(loaderOptions.configFile) } @@ -426,7 +451,11 @@ export default function getConfig( } } - if (loaderOptions.configFile && !configFiles.has(loaderOptions.configFile)) { + if ( + loaderOptions.transformMode === 'default' && + loaderOptions.configFile && + !configFiles.has(loaderOptions.configFile) + ) { configFiles.add(loaderOptions.configFile) Log.info( `Using external babel configuration from ${loaderOptions.configFile}` diff --git a/packages/next/src/build/babel/loader/types.d.ts b/packages/next/src/build/babel/loader/types.d.ts index ddf223ac33ec1..7115e56126315 100644 --- a/packages/next/src/build/babel/loader/types.d.ts +++ b/packages/next/src/build/babel/loader/types.d.ts @@ -6,16 +6,45 @@ export interface NextJsLoaderContext extends webpack.LoaderContext<{}> { target: string } -export interface NextBabelLoaderOptions { - hasJsxRuntime: boolean - hasReactRefresh: boolean +export interface NextBabelLoaderBaseOptions { isServer: boolean - development: boolean + distDir: string pagesDir: string + cwd: string + srcDir: string + caller: any + development: boolean + + // Custom plugins to be added to the generated babel options. + plugins?: Array +} + +/** + * Options to create babel loader for the default transformations. + * + * This is primary usecase of babel-loader configuration for running + * all of the necessary transforms for the ecmascript instead of swc loader. + */ +export type NextBabelLoaderOptionDefaultPresets = NextBabelLoaderBaseOptions & { + transformMode: 'default' + hasJsxRuntime: boolean + hasReactRefresh: boolean sourceMaps?: any[] overrides: any - caller: any configFile: string | undefined - cwd: string - srcDir: string } + +/** + * Options to create babel loader for 'standalone' transformations. + * + * This'll create a babel loader does not enable any of the default presets or plugins, + * only the ones specified in the options where swc loader is enabled but need to inject + * a babel specific plugins like react compiler. + */ +export type NextBabelLoaderOptionStandalone = NextBabelLoaderBaseOptions & { + transformMode: 'standalone' +} + +export type NextBabelLoaderOptions = + | NextBabelLoaderOptionDefaultPresets + | NextBabelLoaderOptionStandalone diff --git a/packages/next/src/build/get-babel-loader-config.ts b/packages/next/src/build/get-babel-loader-config.ts new file mode 100644 index 0000000000000..d4088efc2627e --- /dev/null +++ b/packages/next/src/build/get-babel-loader-config.ts @@ -0,0 +1,86 @@ +import path from 'path' +import type { ReactCompilerOptions } from '../server/config-shared' + +const getReactCompilerPlugins = ( + options: boolean | ReactCompilerOptions | undefined, + isDev: boolean +) => { + if (!options) { + return undefined + } + + const compilerOptions = typeof options === 'boolean' ? {} : options + if (options) { + return [ + [ + 'babel-plugin-react-compiler', + { + panicThreshold: isDev ? undefined : 'NONE', + ...compilerOptions, + }, + ], + ] + } + return undefined +} + +const getBabelLoader = ( + useSWCLoader: boolean | undefined, + babelConfigFile: string | undefined, + isServer: boolean, + distDir: string, + pagesDir: string | undefined, + cwd: string, + srcDir: string, + dev: boolean, + isClient: boolean, + reactCompilerOptions: boolean | ReactCompilerOptions | undefined +) => { + if (!useSWCLoader) { + return { + loader: 'next/dist/build/babel/loader', + options: { + transformMode: 'default', + configFile: babelConfigFile, + isServer, + distDir, + pagesDir, + cwd, + srcDir: path.dirname(srcDir), + development: dev, + hasReactRefresh: dev && isClient, + hasJsxRuntime: true, + plugins: getReactCompilerPlugins(reactCompilerOptions, dev), + }, + } + } + + return undefined +} + +/** + * Get a separate babel loader for the react compiler, **if** there aren't babel loader + * configured. If user have babel config, this should be configured in the babel loader itself. + * Note from react compiler: + * > For best results, compiler must run as the first plugin in your Babel pipeline so it receives input as close to the original source as possible. + */ +const getReactCompilerLoader = ( + options: boolean | ReactCompilerOptions | undefined, + cwd: string, + isDev: boolean +) => { + if (!options) { + return undefined + } + + return { + loader: 'next/dist/build/babel/loader', + options: { + transformMode: 'standalone', + cwd, + plugins: getReactCompilerPlugins(options, isDev), + }, + } +} + +export { getBabelLoader, getReactCompilerLoader } diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 8578d8a8b8e86..b5d30d30b57d5 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -16,10 +16,13 @@ import type { TurboRuleConfigItemOptions, } from '../../server/config-shared' import { isDeepStrictEqual } from 'util' -import type { DefineEnvPluginOptions } from '../webpack/plugins/define-env-plugin' -import { getDefineEnv } from '../webpack/plugins/define-env-plugin' +import { + type DefineEnvPluginOptions, + getDefineEnv, +} from '../webpack/plugins/define-env-plugin' import type { PageExtensions } from '../page-extensions-type' import type { __ApiPreviewProps } from '../../server/api-utils' +import { getReactCompilerLoader } from '../get-babel-loader-config' const nextVersion = process.env.__NEXT_VERSION as string @@ -1130,12 +1133,61 @@ function bindingToApi( } } + /** + * Returns a new copy of next.js config object to avoid mutating the original. + * + * Also it does some augmentation to the configuration as well, for example set the + * turbopack's rules if `experimental.reactCompilerOptions` is set. + */ + function augmentNextConfig( + originalNextConfig: NextConfigComplete, + projectPath: string + ): Record { + let nextConfig = { ...(originalNextConfig as any) } + + const reactCompilerOptions = nextConfig.experimental?.reactCompiler + + // It is not easy to set the rules inside of rust as resolving, and passing the context identical to the webpack + // config is bit hard, also we can reuse same codes between webpack config in here. + if (reactCompilerOptions) { + if (!nextConfig.experimental.turbo) { + nextConfig.experimental.turbo = {} + } + + if (!nextConfig.experimental.turbo.rules) { + nextConfig.experimental.turbo.rules = {} + } + + // [TODO]: need to enable against '*.ts' + for (const key of ['*.ts', '*.js', '*.jsx', '*.tsx']) { + if (nextConfig.experimental.turbo.rules[key]) { + Log.warn( + `The React Compiler cannot be enabled automatically because 'experimental.turbo' contains a rule for '${key}'. Remove this rule, or add 'babel-loader' and 'babel-plugin-react-compiler' to the Turbopack configuration manually.` + ) + break + } + nextConfig.experimental.turbo.rules[key] = { + foreign: false, + loaders: [ + getReactCompilerLoader( + originalNextConfig.experimental.reactCompiler, + projectPath, + nextConfig.dev + ), + ], + } + } + } + + return nextConfig + } + async function serializeNextConfig( nextConfig: NextConfigComplete, projectPath: string ): Promise { // Avoid mutating the existing `nextConfig` object. - let nextConfigSerializable = { ...(nextConfig as any) } + let nextConfigSerializable = augmentNextConfig(nextConfig, projectPath) nextConfigSerializable.generateBuildId = await nextConfig.generateBuildId?.() @@ -1144,8 +1196,10 @@ function bindingToApi( nextConfigSerializable.exportPathMap = {} nextConfigSerializable.webpack = nextConfig.webpack && {} - if (nextConfig.experimental?.turbo?.rules) { - ensureLoadersHaveSerializableOptions(nextConfig.experimental.turbo?.rules) + if (nextConfigSerializable.experimental?.turbo?.rules) { + ensureLoadersHaveSerializableOptions( + nextConfigSerializable.experimental.turbo?.rules + ) } nextConfigSerializable.modularizeImports = diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index d0a5f322bc504..32cc8b9c751e6 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -84,6 +84,10 @@ import { } from './create-compiler-aliases' import { hasCustomExportOutput } from '../export/utils' import { CssChunkingPlugin } from './webpack/plugins/css-chunking-plugin' +import { + getBabelLoader, + getReactCompilerLoader, +} from './get-babel-loader-config' type ExcludesFalse = (x: T | false) => x is T type ClientEntries = { @@ -405,23 +409,22 @@ export default async function getBaseWebpackConfig( loggedIgnoredCompilerOptions = true } - const babelLoader = (function getBabelLoader() { - if (useSWCLoader) return undefined - return { - loader: require.resolve('./babel/loader/index'), - options: { - configFile: babelConfigFile, - isServer: isNodeOrEdgeCompilation, - distDir, - pagesDir, - srcDir: path.dirname((appDir || pagesDir)!), - cwd: dir, - development: dev, - hasReactRefresh: dev && isClient, - hasJsxRuntime: true, - }, - } - })() + const babelLoader = getBabelLoader( + useSWCLoader, + babelConfigFile, + isNodeOrEdgeCompilation, + distDir, + pagesDir, + dir, + (appDir || pagesDir)!, + dev, + isClient, + config.experimental?.reactCompiler + ) + + const reactCompilerLoader = babelLoader + ? undefined + : getReactCompilerLoader(config.experimental?.reactCompiler, dir, dev) let swcTraceProfilingInitialized = false const getSwcLoader = (extraOptions: Partial) => { @@ -491,6 +494,7 @@ export default async function getBaseWebpackConfig( // acceptable as Babel will not be recommended. swcServerLayerLoader, babelLoader, + reactCompilerLoader, ].filter(Boolean) : [] @@ -540,6 +544,7 @@ export default async function getBaseWebpackConfig( // acceptable as Babel will not be recommended. isBrowserLayer ? swcBrowserLayerLoader : swcSSRLayerLoader, babelLoader, + reactCompilerLoader, ].filter(Boolean) : []), ]