diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 2c3e0e6aab59e..019ca39f214cc 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -56,6 +56,7 @@ pub mod amp_attributes; mod auto_cjs; pub mod cjs_optimizer; pub mod disallow_re_export_all_in_page; +pub mod named_import_transform; pub mod next_dynamic; pub mod next_ssg; pub mod page_config; @@ -128,6 +129,9 @@ pub struct TransformOptions { #[serde(default)] pub modularize_imports: Option, + #[serde(default)] + pub auto_modularize_imports: Option, + #[serde(default)] pub font_loaders: Option, @@ -245,6 +249,10 @@ where Some(config) => Either::Left(shake_exports::shake_exports(config.clone())), None => Either::Right(noop()), }, + match &opts.auto_modularize_imports { + Some(config) => Either::Left(named_import_transform::named_import_transform(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 new file mode 100644 index 0000000000000..1799d2d024e7c --- /dev/null +++ b/packages/next-swc/crates/core/src/named_import_transform.rs @@ -0,0 +1,83 @@ +use serde::Deserialize; +use turbopack_binding::swc::core::{ + common::DUMMY_SP, + ecma::{ast::*, visit::Fold}, +}; + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + pub packages: Vec, +} + +pub fn named_import_transform(config: Config) -> impl Fold { + NamedImportTransform { + packages: config.packages, + } +} + +#[derive(Debug, Default)] +struct NamedImportTransform { + packages: Vec, +} + +impl Fold for NamedImportTransform { + fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl { + // Match named imports and check if it's included in the packages + let src_value = decl.src.value.clone(); + + if self.packages.iter().any(|p| src_value == *p) { + let mut specifier_names = vec![]; + + // Skip the transform if the default or namespace import is present + let mut skip_transform = false; + + for specifier in &decl.specifiers { + match specifier { + ImportSpecifier::Named(specifier) => { + // Push the import name as string to the vec + if let Some(imported) = &specifier.imported { + match imported { + ModuleExportName::Ident(ident) => { + specifier_names.push(ident.sym.to_string()); + } + ModuleExportName::Str(str_) => { + specifier_names.push(str_.value.to_string()); + } + } + } else { + specifier_names.push(specifier.local.sym.to_string()); + } + } + ImportSpecifier::Default(_) => { + skip_transform = true; + break; + } + ImportSpecifier::Namespace(_) => { + skip_transform = true; + break; + } + } + } + + if !skip_transform { + let new_src = format!( + "barrel-optimize-loader?names={}!{}", + specifier_names.join(","), + src_value + ); + + // Create a new import declaration, keep everything the same except the source + let mut new_decl = decl.clone(); + new_decl.src = Box::new(Str { + span: DUMMY_SP, + value: new_src.into(), + raw: None, + }); + + return new_decl; + } + } + + decl + } +} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 150d2f4d00b96..9a38810175ff2 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -3,6 +3,7 @@ use std::{env::current_dir, path::PathBuf}; use next_swc::{ amp_attributes::amp_attributes, cjs_optimizer::cjs_optimizer, + named_import_transform::named_import_transform, next_dynamic::next_dynamic, next_ssg::next_ssg, page_config::page_config_test, @@ -454,6 +455,32 @@ fn cjs_optimize_fixture(input: PathBuf) { ); } +#[fixture("tests/fixture/named-import-transform/**/input.js")] +fn named_import_transform_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), + named_import_transform(json( + r#" + { + "packages": ["foo", "bar"] + } + "# + )) + ) + }, + &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/input.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/input.js new file mode 100644 index 0000000000000..b9709c0cf8c22 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/input.js @@ -0,0 +1,3 @@ +import { A, B, C as F } from 'foo' +import D from 'bar' +import E from 'baz' 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 new file mode 100644 index 0000000000000..f0d084c179e09 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js @@ -0,0 +1,3 @@ +import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"; +import D from 'bar'; +import E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/input.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/input.js new file mode 100644 index 0000000000000..a48d2ef999064 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/input.js @@ -0,0 +1,3 @@ +import { A, B, C as F } from 'foo' +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 new file mode 100644 index 0000000000000..f2874acce5044 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js @@ -0,0 +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 E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index 0c1d5f83731fb..b7cb7f4a5d87f 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -78,6 +78,7 @@ fn test(input: &Path, minify: bool) { app_dir: None, server_actions: None, cjs_require_optimizer: None, + auto_modularize_imports: 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 31d88d534d8c7..62a5a86f9be78 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -370,6 +370,11 @@ export function getLoaderSWCOptions({ }, }, } + baseOptions.autoModularizeImports = { + packages: [ + // TODO: Add a list of packages that should be optimized by default + ], + } const isNextDist = nextDistPath.test(filename) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 3e21e1309d5eb..47cc4377ccbf0 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1936,6 +1936,7 @@ 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) diff --git a/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts b/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts new file mode 100644 index 0000000000000..e294d7f7b9675 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts @@ -0,0 +1,5 @@ +export default function transformSource(this: any, source: string) { + // const { names }: any = this.getOptions() + // const { resourcePath } = this + return source +} diff --git a/test/development/basic/auto-modularize-imports/app/layout.js b/test/development/basic/auto-modularize-imports/app/layout.js new file mode 100644 index 0000000000000..8525f5f8c0b2a --- /dev/null +++ b/test/development/basic/auto-modularize-imports/app/layout.js @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000000000..501ce5d145923 --- /dev/null +++ b/test/development/basic/auto-modularize-imports/app/page.js @@ -0,0 +1,11 @@ +'use client' + +import { IceCream } from 'lucide-react' + +export default function Page() { + return ( +
+ +
+ ) +}