Skip to content

Commit

Permalink
optimize_barrel SWC transform and new optimizePackageImports conf…
Browse files Browse the repository at this point in the history
…ig (#54572)

## Implementation

Base on #54530, we're implementing a `optimize_barrel` transform to
optimize barrel files to only export the names we need. If the
transformed file isn't a "barrel file", we just re-export the names from
it without any transformation.

Take `lucide-react` as an example, with #54530 we are able to transform

```js
import { IceCream } from 'lucide-react'
```

to 

```js
import { IceCream } from '__barrel_optimize__?names=IceCream!=!lucide-react?__barrel_optimize_noop__=IceCream'
```

And then, we apply that new request with a new Webpack module rule to
use the SWC loader with option `optimizeBarrelExports: ['IceCream']`,
which eventually got passed to this new `optimize_barrel` transform and
does the optimization.

## Notes

We'll have to add a new `getModularizeImportAliases` alias list to map
`lucide-react` to the ESM version, as we have the `['main', 'module']`
resolve order for the server compiler. Otherwise this optimization
doesn't work in that compiler.

There's no e2e test added because it's already covered by the
`modularize-imports` test as we removed the default `lucide-react`
transform rules and it still works.

We'll need to test other libs before migrating them to the new
`optimizePackageImports` option.

---

Closes #54571, closes #53605, closes #53789, closes #53894, closes
#54063.
  • Loading branch information
shuding committed Aug 25, 2023
1 parent eee1376 commit b4a5663
Show file tree
Hide file tree
Showing 26 changed files with 449 additions and 87 deletions.
9 changes: 9 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -132,6 +133,9 @@ pub struct TransformOptions {
#[serde(default)]
pub auto_modularize_imports: Option<named_import_transform::Config>,

#[serde(default)]
pub optimize_barrel_exports: Option<optimize_barrel::Config>,

#[serde(default)]
pub font_loaders: Option<next_transform_font::Config>,

Expand Down Expand Up @@ -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| {
Expand Down
6 changes: 3 additions & 3 deletions packages/next-swc/crates/core/src/named_import_transform.rs
Expand Up @@ -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
Expand Down
268 changes: 268 additions & 0 deletions 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<String>,
}

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<String>,
}

impl Fold for OptimizeBarrel {
fn fold_module_items(&mut self, items: Vec<ModuleItem>) -> Vec<ModuleItem> {
// 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
}
}
30 changes: 30 additions & 0 deletions packages/next-swc/crates/core/tests/fixture.rs
Expand Up @@ -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,
Expand Down Expand Up @@ -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<T>(s: &str) -> T
where
T: DeserializeOwned,
Expand Down
@@ -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';
@@ -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';
@@ -0,0 +1,3 @@
export { foo, b as y } from './1'
export { x, a } from './2'
export { z }
@@ -0,0 +1,3 @@
export { b as y } from './1';
export { x } from './2';
export { z };
@@ -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 }
@@ -0,0 +1,2 @@
// De-optimize this file
export { x, y, z } from "/some-project/node_modules/foo/file.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 }
@@ -0,0 +1,2 @@
// De-optimize this file
export { x, y, z } from "/some-project/node_modules/foo/file.js";

0 comments on commit b4a5663

Please sign in to comment.