diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 019ca39f214c..11aea351c7b3 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -59,6 +59,7 @@ pub mod disallow_re_export_all_in_page; pub mod named_import_transform; pub mod next_dynamic; pub mod next_ssg; +pub mod optimize_barrel; pub mod page_config; pub mod react_remove_properties; pub mod react_server_components; @@ -132,6 +133,9 @@ pub struct TransformOptions { #[serde(default)] pub auto_modularize_imports: Option, + #[serde(default)] + pub optimize_barrel_exports: Option, + #[serde(default)] pub font_loaders: Option, @@ -253,6 +257,11 @@ where Some(config) => Either::Left(named_import_transform::named_import_transform(config.clone())), None => Either::Right(noop()), }, + match &opts.optimize_barrel_exports { + Some(config) => Either::Left(optimize_barrel::optimize_barrel( + file.name.clone(),config.clone())), + None => Either::Right(noop()), + }, opts.emotion .as_ref() .and_then(|config| { diff --git a/packages/next-swc/crates/core/src/named_import_transform.rs b/packages/next-swc/crates/core/src/named_import_transform.rs index 1799d2d024e7..4e95899cb4ee 100644 --- a/packages/next-swc/crates/core/src/named_import_transform.rs +++ b/packages/next-swc/crates/core/src/named_import_transform.rs @@ -60,10 +60,10 @@ impl Fold for NamedImportTransform { } if !skip_transform { + let names = specifier_names.join(","); let new_src = format!( - "barrel-optimize-loader?names={}!{}", - specifier_names.join(","), - src_value + "__barrel_optimize__?names={}!=!{}?__barrel_optimize_noop__={}", + names, src_value, names, ); // Create a new import declaration, keep everything the same except the source diff --git a/packages/next-swc/crates/core/src/optimize_barrel.rs b/packages/next-swc/crates/core/src/optimize_barrel.rs new file mode 100644 index 000000000000..0fee0a2e3e95 --- /dev/null +++ b/packages/next-swc/crates/core/src/optimize_barrel.rs @@ -0,0 +1,268 @@ +use serde::Deserialize; +use turbopack_binding::swc::core::{ + common::{FileName, DUMMY_SP}, + ecma::{ + ast::*, + utils::{private_ident, quote_str}, + visit::Fold, + }, +}; + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + pub names: Vec, +} + +pub fn optimize_barrel(filename: FileName, config: Config) -> impl Fold { + OptimizeBarrel { + filepath: filename.to_string(), + names: config.names, + } +} + +#[derive(Debug, Default)] +struct OptimizeBarrel { + filepath: String, + names: Vec, +} + +impl Fold for OptimizeBarrel { + fn fold_module_items(&mut self, items: Vec) -> Vec { + // One pre-pass to find all the local idents that we are referencing. + let mut local_idents = vec![]; + for item in &items { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_named)) = item { + if export_named.src.is_none() { + for spec in &export_named.specifiers { + if let ExportSpecifier::Named(s) = spec { + let str_name; + if let Some(name) = &s.exported { + str_name = match &name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + } else { + str_name = match &s.orig { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + } + + // If the exported name needs to be kept, track the local ident. + if self.names.contains(&str_name) { + if let ModuleExportName::Ident(i) = &s.orig { + local_idents.push(i.sym.clone()); + } + } + } + } + } + } + } + + // The second pass to rebuild the module items. + let mut new_items = vec![]; + + // We only apply this optimization to barrel files. Here we consider + // a barrel file to be a file that only exports from other modules. + // Besides that, lit expressions are allowed as well ("use client", etc.). + let mut is_barrel = true; + for item in &items { + match item { + ModuleItem::ModuleDecl(decl) => { + match decl { + // export { foo } from './foo'; + ModuleDecl::ExportNamed(export_named) => { + for spec in &export_named.specifiers { + match spec { + ExportSpecifier::Namespace(s) => { + let name_str = match &s.name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + if self.names.contains(&name_str) { + new_items.push(item.clone()); + } + } + ExportSpecifier::Named(s) => { + if let Some(name) = &s.exported { + let name_str = match &name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + + if self.names.contains(&name_str) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers: vec![ExportSpecifier::Named( + ExportNamedSpecifier { + span: DUMMY_SP, + orig: s.orig.clone(), + exported: Some( + ModuleExportName::Ident( + Ident::new( + name_str.into(), + DUMMY_SP, + ), + ), + ), + is_type_only: false, + }, + )], + src: export_named.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } else { + let name_str = match &s.orig { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + + if self.names.contains(&name_str) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers: vec![ExportSpecifier::Named( + ExportNamedSpecifier { + span: DUMMY_SP, + orig: s.orig.clone(), + exported: None, + is_type_only: false, + }, + )], + src: export_named.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + } + _ => { + is_barrel = false; + break; + } + } + } + } + // Keep import statements that create the local idents we need. + ModuleDecl::Import(import_decl) => { + for spec in &import_decl.specifiers { + match spec { + ImportSpecifier::Named(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Named( + ImportNamedSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + imported: s.imported.clone(), + is_type_only: false, + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + ImportSpecifier::Default(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Default( + ImportDefaultSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + ImportSpecifier::Namespace(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Namespace( + ImportStarAsSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + } + } + } + _ => { + // Export expressions are not allowed in barrel files. + is_barrel = false; + break; + } + } + } + ModuleItem::Stmt(stmt) => match stmt { + Stmt::Expr(expr) => match &*expr.expr { + Expr::Lit(_) => { + new_items.push(item.clone()); + } + _ => { + is_barrel = false; + break; + } + }, + _ => { + is_barrel = false; + break; + } + }, + } + } + + // If the file is not a barrel file, we need to create a new module that + // re-exports from the original file. + // This is to avoid creating multiple instances of the original module. + if !is_barrel { + new_items = vec![ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: self + .names + .iter() + .map(|name| { + ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(private_ident!(name.clone())), + exported: None, + is_type_only: false, + }) + }) + .collect(), + src: Some(Box::new(quote_str!(self.filepath.to_string()))), + type_only: false, + asserts: None, + }, + ))]; + } + + new_items + } +} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 9a38810175ff..8046417205f1 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -6,6 +6,7 @@ use next_swc::{ named_import_transform::named_import_transform, next_dynamic::next_dynamic, next_ssg::next_ssg, + optimize_barrel::optimize_barrel, page_config::page_config_test, react_remove_properties::remove_properties, react_server_components::server_components, @@ -481,6 +482,35 @@ fn named_import_transform_fixture(input: PathBuf) { ); } +#[fixture("tests/fixture/optimize-barrel/**/input.js")] +fn optimize_barrel_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|_tr| { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + chain!( + resolver(unresolved_mark, top_level_mark, false), + optimize_barrel( + FileName::Real(PathBuf::from("/some-project/node_modules/foo/file.js")), + json( + r#" + { + "names": ["x", "y", "z"] + } + "# + ) + ) + ) + }, + &input, + &output, + Default::default(), + ); +} + fn json(s: &str) -> T where T: DeserializeOwned, diff --git a/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js index f0d084c179e0..53fc2884247c 100644 --- a/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js @@ -1,3 +1,3 @@ -import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"; +import { A, B, C as F } from "__barrel_optimize__?names=A,B,C!=!foo?__barrel_optimize_noop__=A,B,C"; import D from 'bar'; import E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js index f2874acce504..b4d1a9b11dec 100644 --- a/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js @@ -1,3 +1,3 @@ -import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"; -import { D } from "barrel-optimize-loader?names=D!bar"; +import { A, B, C as F } from "__barrel_optimize__?names=A,B,C!=!foo?__barrel_optimize_noop__=A,B,C"; +import { D } from "__barrel_optimize__?names=D!=!bar?__barrel_optimize_noop__=D"; import E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js new file mode 100644 index 000000000000..747c0573c916 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js @@ -0,0 +1,3 @@ +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js new file mode 100644 index 000000000000..0edcd356f6fc --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js @@ -0,0 +1,3 @@ +export { b as y } from './1'; +export { x } from './2'; +export { z }; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js new file mode 100644 index 000000000000..6f255555ee99 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js @@ -0,0 +1,6 @@ +// De-optimize this file +const foo = 1 + +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js new file mode 100644 index 000000000000..3f5d1a74b87b --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js @@ -0,0 +1,2 @@ +// De-optimize this file +export { x, y, z } from "/some-project/node_modules/foo/file.js"; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js new file mode 100644 index 000000000000..630019266de2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js @@ -0,0 +1,6 @@ +// De-optimize this file +export * from 'x' + +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js new file mode 100644 index 000000000000..3f5d1a74b87b --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js @@ -0,0 +1,2 @@ +// De-optimize this file +export { x, y, z } from "/some-project/node_modules/foo/file.js"; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js new file mode 100644 index 000000000000..bd460bc9a27b --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js @@ -0,0 +1,9 @@ +'use client' + +import foo, { a, b } from 'foo' +import z from 'bar' + +export { a as x } +export { y } from '1' +export { b } +export { foo as default, z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js new file mode 100644 index 000000000000..2a8069f942f6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js @@ -0,0 +1,7 @@ +'use client'; +import { a } from 'foo' +import z from 'bar' + +export { a as x }; +export { y } from '1'; +export { z }; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js new file mode 100644 index 000000000000..605b446a453c --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js @@ -0,0 +1,2 @@ +import * as index from './icons/index.js' +export { index as x } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js new file mode 100644 index 000000000000..605b446a453c --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js @@ -0,0 +1,2 @@ +import * as index from './icons/index.js' +export { index as x } diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index b7cb7f4a5d87..92af9032f1e9 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -79,6 +79,7 @@ fn test(input: &Path, minify: bool) { server_actions: None, cjs_require_optimizer: None, auto_modularize_imports: None, + optimize_barrel_exports: None, }; let unresolved_mark = Mark::new(); diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 62a5a86f9be7..6af12afce99e 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -301,6 +301,7 @@ export function getLoaderSWCOptions({ isPageFile, hasReactRefresh, modularizeImports, + optimizePackageImports, swcPlugins, compilerOptions, jsConfig, @@ -310,6 +311,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, + optimizeBarrelExports, }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { @@ -321,6 +323,9 @@ export function getLoaderSWCOptions({ isPageFile: boolean hasReactRefresh: boolean modularizeImports: NextConfig['modularizeImports'] + optimizePackageImports?: NonNullable< + NextConfig['experimental'] + >['optimizePackageImports'] swcPlugins: ExperimentalConfig['swcPlugins'] compilerOptions: NextConfig['compiler'] jsConfig: any @@ -330,6 +335,7 @@ export function getLoaderSWCOptions({ hasServerComponents?: boolean isServerLayer: boolean isServerActionsEnabled?: boolean + optimizeBarrelExports?: string[] }) { let baseOptions: any = getBaseSWCOptions({ filename, @@ -370,10 +376,17 @@ export function getLoaderSWCOptions({ }, }, } - baseOptions.autoModularizeImports = { - packages: [ - // TODO: Add a list of packages that should be optimized by default - ], + + // Modularize import optimization for barrel files + if (optimizePackageImports) { + baseOptions.autoModularizeImports = { + packages: optimizePackageImports, + } + } + if (optimizeBarrelExports) { + baseOptions.optimizeBarrelExports = { + names: optimizeBarrelExports, + } } const isNextDist = nextDistPath.test(filename) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 47cc4377ccbf..2ca96b4d71d9 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -532,6 +532,27 @@ function getOptimizedAliases(): { [pkg: string]: string } { ) } +// Alias these modules to be resolved with "module" if possible. +function getModularizeImportAliases(packages: string[]) { + const aliases: { [pkg: string]: string } = {} + const mainFields = ['module', 'main'] + + for (const pkg of packages) { + try { + const descriptionFileData = require(`${pkg}/package.json`) + + for (const field of mainFields) { + if (descriptionFileData.hasOwnProperty(field)) { + aliases[pkg] = `${pkg}/${descriptionFileData[field]}` + break + } + } + } catch {} + } + + return aliases +} + export function attachReactRefresh( webpackConfig: webpack.Configuration, targetLoader: webpack.RuleSetUseItem @@ -1165,6 +1186,14 @@ export default async function getBaseWebpackConfig( ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), ...(reactProductionProfiling ? getReactProfilingInProduction() : {}), + // For Node server, we need to re-alias the package imports to prefer to + // resolve to the module export. + ...(isNodeServer + ? getModularizeImportAliases( + config.experimental.optimizePackageImports || [] + ) + : {}), + [RSC_ACTION_VALIDATE_ALIAS]: 'next/dist/build/webpack/loaders/next-flight-loader/action-validate', @@ -1397,6 +1426,12 @@ export default async function getBaseWebpackConfig( return } + // __barrel_optimize__ is a special marker that tells Next.js to + // optimize the import by removing unused exports. This has to be compiled. + if (request.startsWith('__barrel_optimize__')) { + return + } + // When in esm externals mode, and using import, we resolve with // ESM resolving options. // Also disable esm request when appDir is enabled @@ -1936,7 +1971,6 @@ export default async function getBaseWebpackConfig( 'next-invalid-import-error-loader', 'next-metadata-route-loader', 'modularize-import-loader', - 'barrel-optimize-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1951,6 +1985,25 @@ export default async function getBaseWebpackConfig( }, module: { rules: [ + { + test: /__barrel_optimize__/, + use: ({ + resourceQuery, + issuerLayer, + }: { + resourceQuery: string + issuerLayer: string + }) => { + const names = resourceQuery.slice('?names='.length).split(',') + return [ + getSwcLoader({ + isServerLayer: + issuerLayer === WEBPACK_LAYERS.reactServerComponents, + optimizeBarrelExports: names, + }), + ] + }, + }, ...(hasAppDir ? [ { @@ -2006,9 +2059,7 @@ export default async function getBaseWebpackConfig( ...(hasAppDir && !isClient ? [ { - issuerLayer: { - or: [isWebpackServerLayer], - }, + issuerLayer: isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2144,9 +2195,7 @@ export default async function getBaseWebpackConfig( ? [ { test: codeCondition.test, - issuerLayer: { - or: [isWebpackServerLayer], - }, + issuerLayer: isWebpackServerLayer, exclude: [asyncStoragesRegex], use: swcLoaderForServerLayer, }, diff --git a/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts b/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts deleted file mode 100644 index e294d7f7b967..000000000000 --- a/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function transformSource(this: any, source: string) { - // const { names }: any = this.getOptions() - // const { resourcePath } = this - return source -} 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 0c054724a8e4..9d26ab993f28 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -53,6 +53,7 @@ async function loaderTransform( swcCacheDir, hasServerComponents, isServerLayer, + optimizeBarrelExports, } = loaderOptions const isPageFile = filename.startsWith(pagesDir) const relativeFilePathFromRoot = path.relative(rootDir, filename) @@ -66,6 +67,7 @@ async function loaderTransform( development: this.mode === 'development', hasReactRefresh, modularizeImports: nextConfig?.modularizeImports, + optimizePackageImports: nextConfig?.experimental?.optimizePackageImports, swcPlugins: nextConfig?.experimental?.swcPlugins, compilerOptions: nextConfig?.compiler, jsConfig, @@ -75,6 +77,7 @@ async function loaderTransform( hasServerComponents, isServerActionsEnabled: nextConfig?.experimental?.serverActions, isServerLayer, + optimizeBarrelExports, }) const programmaticOptions = { diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index d51ee7c75959..b2f974bc69c2 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -473,6 +473,9 @@ const configSchema = { }, }, }, + optimizePackageImports: { + type: 'array', + }, instrumentationHook: { type: 'boolean', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 870528c8c204..a9e34c7c0b6c 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -236,6 +236,11 @@ export interface ExperimentalConfig { webVitalsAttribution?: Array<(typeof WEB_VITALS)[number]> + /** + * Automatically apply the "modularizeImports" optimization to imports of the specified packages. + */ + optimizePackageImports?: string[] + turbo?: ExperimentalTurboOptions turbotrace?: { logLevel?: diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 31ef671af833..b5b5e787b5aa 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -678,48 +678,6 @@ function assignDefaults( 'lodash-es': { transform: 'lodash-es/{{member}}', }, - 'lucide-react': { - // Note that we need to first resolve to the base path (`lucide-react`) and join the subpath, - // instead of just resolving `lucide-react/esm/icons/{{kebabCase member}}` because this package - // doesn't have proper `exports` fields for individual icons in its package.json. - transform: { - // Special aliases - '(SortAsc|LucideSortAsc|SortAscIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/arrow-up-narrow-wide!lucide-react', - '(SortDesc|LucideSortDesc|SortDescIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/arrow-down-wide-narrow!lucide-react', - '(Verified|LucideVerified|VerifiedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/badge-check!lucide-react', - '(Slash|LucideSlash|SlashIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/ban!lucide-react', - '(CurlyBraces|LucideCurlyBraces|CurlyBracesIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/braces!lucide-react', - '(CircleSlashed|LucideCircleSlashed|CircleSlashedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/circle-slash-2!lucide-react', - '(SquareGantt|LucideSquareGantt|SquareGanttIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/gantt-chart-square!lucide-react', - '(SquareKanbanDashed|LucideSquareKanbanDashed|SquareKanbanDashedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/kanban-square-dashed!lucide-react', - '(SquareKanban|LucideSquareKanban|SquareKanbanIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/kanban-square!lucide-react', - '(Edit3|LucideEdit3|Edit3Icon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen-line!lucide-react', - '(Edit|LucideEdit|EditIcon|PenBox|LucidePenBox|PenBoxIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen-square!lucide-react', - '(Edit2|LucideEdit2|Edit2Icon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen!lucide-react', - '(Stars|LucideStars|StarsIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/sparkles!lucide-react', - '(TextSelection|LucideTextSelection|TextSelectionIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/text-select!lucide-react', - // General rules - 'Lucide(.*)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase memberMatches.[1] }}!lucide-react', - '(.*)Icon': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase memberMatches.[1] }}!lucide-react', - '*': 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase member }}!lucide-react', - }, - }, '@headlessui/react': { transform: { Transition: @@ -775,6 +733,15 @@ function assignDefaults( }, } + const userProvidedOptimizePackageImports = + result.experimental?.optimizePackageImports || [] + if (!result.experimental) { + result.experimental = {} + } + result.experimental.optimizePackageImports = [ + ...new Set([...userProvidedOptimizePackageImports, 'lucide-react']), + ] + return result } diff --git a/test/development/basic/auto-modularize-imports/app/layout.js b/test/development/basic/auto-modularize-imports/app/layout.js deleted file mode 100644 index 8525f5f8c0b2..000000000000 --- a/test/development/basic/auto-modularize-imports/app/layout.js +++ /dev/null @@ -1,12 +0,0 @@ -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} - -export default function RootLayout({ children }) { - return ( - - {children} - - ) -} diff --git a/test/development/basic/auto-modularize-imports/app/page.js b/test/development/basic/auto-modularize-imports/app/page.js deleted file mode 100644 index 501ce5d14592..000000000000 --- a/test/development/basic/auto-modularize-imports/app/page.js +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import { IceCream } from 'lucide-react' - -export default function Page() { - return ( -
- -
- ) -}