Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimize_barrel SWC transform and new optimizePackageImports config #54572

Merged
merged 4 commits into from Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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";