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

Add shake exports transform to next-swc #32253

Merged
merged 3 commits into from Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -54,6 +54,7 @@ pub mod next_ssg;
pub mod page_config;
pub mod react_remove_properties;
pub mod remove_console;
pub mod shake_exports;
pub mod styled_jsx;
mod top_level_binding_collector;

Expand Down Expand Up @@ -86,6 +87,9 @@ pub struct TransformOptions {

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

#[serde(default)]
pub shake_exports: Option<shake_exports::Config>,
}

pub fn custom_before_pass(file: Arc<SourceFile>, opts: &TransformOptions) -> impl Fold {
Expand Down Expand Up @@ -124,6 +128,10 @@ pub fn custom_before_pass(file: Arc<SourceFile>, opts: &TransformOptions) -> imp
Either::Left(react_remove_properties::remove_properties(config.clone())),
_ => Either::Right(noop()),
},
match &opts.shake_exports {
Some(config) => Either::Left(shake_exports::shake_exports(config.clone())),
None => Either::Right(noop()),
}
)
}

Expand Down
115 changes: 115 additions & 0 deletions packages/next-swc/crates/core/src/shake_exports.rs
@@ -0,0 +1,115 @@
use serde::Deserialize;
use swc_atoms::js_word;
use swc_ecmascript::ast::*;
use swc_ecmascript::transforms::optimization::simplify::dce::{dce, Config as DCEConfig};
use swc_ecmascript::visit::{Fold, FoldWith};

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub ignore: Vec<String>,
}

pub fn shake_exports(config: Config) -> impl Fold {
ExportShaker {
ignore: config.ignore,
..Default::default()
}
}

#[derive(Debug, Default)]
struct ExportShaker {
ignore: Vec<String>,
remove_export: bool,
}

impl Fold for ExportShaker {
fn fold_module(&mut self, module: Module) -> Module {
let module = module.fold_children_with(self);
let module = module.fold_with(&mut dce(DCEConfig::default()));

module
}

fn fold_module_items(&mut self, items: Vec<ModuleItem>) -> Vec<ModuleItem> {
let mut new_items = vec![];
for item in items {
let item = item.fold_children_with(self);
if !self.remove_export {
new_items.push(item)
}
self.remove_export = false;
}
new_items
}

fn fold_export_decl(&mut self, mut decl: ExportDecl) -> ExportDecl {
match &mut decl.decl {
Decl::Fn(fn_decl) => {
if !self.ignore.contains(&fn_decl.ident.sym.to_string()) {
padmaia marked this conversation as resolved.
Show resolved Hide resolved
self.remove_export = true;
}
}
Decl::Class(class_decl) => {
if !self.ignore.contains(&class_decl.ident.sym.to_string()) {
self.remove_export = true;
}
}
Decl::Var(var_decl) => {
var_decl.decls = var_decl
.decls
.iter()
.filter_map(|var_decl| {
if let Pat::Ident(BindingIdent { id, .. }) = &var_decl.name {
if self.ignore.contains(&id.sym.to_string()) {
return Some(var_decl.to_owned());
}
}
None
})
.collect();
if var_decl.decls.is_empty() {
self.remove_export = true;
}
}
_ => {}
}
decl
}

fn fold_named_export(&mut self, mut export: NamedExport) -> NamedExport {
export.specifiers = export
.specifiers
.into_iter()
.filter_map(|spec| {
if let ExportSpecifier::Named(named_spec) = spec {
if let Some(ident) = &named_spec.exported {
if self.ignore.contains(&ident.sym.to_string()) {
return Some(ExportSpecifier::Named(named_spec));
}
} else if self.ignore.contains(&named_spec.orig.sym.to_string()) {
return Some(ExportSpecifier::Named(named_spec));
}
}
None
})
.collect();
if export.specifiers.is_empty() {
self.remove_export = true
}
export
}

fn fold_export_default_decl(&mut self, decl: ExportDefaultDecl) -> ExportDefaultDecl {
if !self.ignore.contains(&js_word!("default").to_string()) {
padmaia marked this conversation as resolved.
Show resolved Hide resolved
self.remove_export = true
}
decl
}

fn fold_export_default_expr(&mut self, expr: ExportDefaultExpr) -> ExportDefaultExpr {
if !self.ignore.contains(&js_word!("default").to_string()) {
self.remove_export = true
}
expr
}
}
47 changes: 44 additions & 3 deletions packages/next-swc/crates/core/tests/fixture.rs
@@ -1,7 +1,12 @@
use next_swc::{
amp_attributes::amp_attributes, next_dynamic::next_dynamic, next_ssg::next_ssg,
page_config::page_config_test, react_remove_properties::remove_properties,
remove_console::remove_console, styled_jsx::styled_jsx,
amp_attributes::amp_attributes,
next_dynamic::next_dynamic,
next_ssg::next_ssg,
page_config::page_config_test,
react_remove_properties::remove_properties,
remove_console::remove_console,
shake_exports::{shake_exports, Config as ShakeExportsConfig},
styled_jsx::styled_jsx,
};
use std::path::PathBuf;
use swc_common::{chain, comments::SingleThreadedComments, FileName, Mark, Span, DUMMY_SP};
Expand Down Expand Up @@ -159,3 +164,39 @@ fn react_remove_properties_custom_fixture(input: PathBuf) {
&output,
);
}

#[fixture("tests/fixture/shake-exports/most-usecases/input.js")]
fn shake_exports_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
shake_exports(ShakeExportsConfig {
ignore: vec![
String::from("keep"),
String::from("keep1"),
String::from("keep2"),
String::from("keep3"),
String::from("keep4"),
],
})
},
&input,
&output,
);
}

#[fixture("tests/fixture/shake-exports/keep-default/input.js")]
fn shake_exports_fixture_default(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
shake_exports(ShakeExportsConfig {
ignore: vec![String::from("default")],
})
},
&input,
&output,
);
}
@@ -0,0 +1,9 @@
let shouldBeRemoved = 'should be removed'
export function removeFunction() {
console.log(shouldBeRemoved);
}

let shouldBeKept = 'should be kept'
export default function shouldBeKept() {
console.log(shouldBeKept)
}
@@ -0,0 +1,4 @@
let shouldBeKept = "should be kept";
export default function shouldBeKept() {
console.log(shouldBeKept);
}
@@ -0,0 +1,36 @@
let shouldBeKept = 'should be kept'
export async function keep() {
console.log(shouldBeKept)
}

let shouldBeRemoved = 'should be removed'
export function removeFunction() {
console.log(shouldBeRemoved);
}

export let removeVarDeclaration = 'should be removed'
export let removeVarDeclarationUndefined // should also be removed
export let multipleDecl = 'should be removed', keep1 = 'should be kept'
export let keep2 = 'should be kept'

export class RemoveClass {
remove() {
console.log('should be removed')
}
}

let x = 'x'
let y = 'y'

// This should be removed
export {x, y as z}

let keep3 = 'should be kept'
let asKeep = 'should be kept'
let removeNamed = 'should be removed'

export {keep3, asKeep as keep4, removeNamed}

export default function removeDefault() {
console.log('should be removed')
}
@@ -0,0 +1,9 @@
let shouldBeKept = "should be kept";
export async function keep() {
console.log(shouldBeKept);
}
export let keep1 = "should be kept";
export let keep2 = "should be kept";
let keep3 = "should be kept";
let asKeep = "should be kept";
export { keep3, asKeep as keep4 };
1 change: 1 addition & 0 deletions packages/next-swc/crates/core/tests/full.rs
Expand Up @@ -59,6 +59,7 @@ fn test(input: &Path, minify: bool) {
styled_components: Some(assert_json("{}")),
remove_console: None,
react_remove_properties: None,
shake_exports: None,
};

let options = options.patch(&fm);
Expand Down