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

feat(next-swc): Implement CJS optimizer #49972

Merged
merged 45 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4c359cb
Add a module
kdy1 May 17, 2023
2ebe0c2
Data
kdy1 May 18, 2023
ab5a0d6
Use visitor
kdy1 May 18, 2023
20ac12d
config
kdy1 May 18, 2023
03e01cf
field
kdy1 May 18, 2023
f228dc7
Collect require calls
kdy1 May 18, 2023
6424f4c
Dep on rustc-hash
kdy1 May 18, 2023
19c3d17
cargo lockfile
kdy1 May 18, 2023
b67710e
unresolved
kdy1 May 18, 2023
13efa00
VisitMut
kdy1 May 18, 2023
a5541c0
Replace
kdy1 May 18, 2023
40fb7a9
Add a test
kdy1 May 18, 2023
6ea0b8a
extra_stmts
kdy1 May 18, 2023
cd42939
prepend
kdy1 May 18, 2023
2cd9d84
Add a todo
kdy1 May 18, 2023
3d1a49e
Update test refs
kdy1 May 18, 2023
8ed27f5
clippy
kdy1 May 18, 2023
144b65e
Dep
kdy1 May 19, 2023
b0fe6ac
cargo lockfile
kdy1 May 19, 2023
30dbafe
test
kdy1 May 19, 2023
f2cdb95
Dep on `convert_case`
kdy1 May 19, 2023
025727f
cargo lockfile
kdy1 May 19, 2023
2e70615
`ImportRecord`
kdy1 May 19, 2023
658e66f
Store only if it should be rewrite
kdy1 May 19, 2023
d77a74e
WIP: Rewrite
kdy1 May 19, 2023
1e2a538
Rename
kdy1 May 19, 2023
249bb0b
Implement
kdy1 May 19, 2023
9b5ee8e
lint
kdy1 May 19, 2023
bcaf412
test
kdy1 May 19, 2023
9308abb
Harden
kdy1 May 19, 2023
35ed85f
Add a test
kdy1 May 19, 2023
4b9a9f8
ignore
kdy1 May 19, 2023
dbe7fff
Drop old requires
kdy1 May 19, 2023
b5fddae
Update test refs
kdy1 May 19, 2023
333944b
config
kdy1 May 22, 2023
fb7a29b
Drop dep
kdy1 May 22, 2023
d657c6e
cargo lockfile
kdy1 May 22, 2023
c9938d7
Clippy
kdy1 May 22, 2023
b42840e
json
kdy1 May 22, 2023
5945ecc
add config andtest in next.js
shuding May 22, 2023
9c47597
unresolved_mark
kdy1 May 23, 2023
7f900fd
fixup
kdy1 May 23, 2023
5dbc105
fix cases and add test
shuding May 23, 2023
3974cef
Merge branch 'canary' into kdy1/next-swc-opt
shuding May 23, 2023
b34b5cf
Merge branch 'canary' into kdy1/next-swc-opt
shuding May 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/next-swc/crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugin = ["turbopack-binding/__swc_core_binding_napi_plugin"]

[dependencies]
chrono = "0.4"
convert_case = "0.5.0"
easy-error = "1.0.0"
either = "1"
fxhash = "0.2.1"
Expand All @@ -17,6 +18,7 @@ once_cell = { workspace = true }
next-transform-font = {workspace = true}
pathdiff = "0.2.0"
regex = "1.5"
rustc-hash = "1"
serde = "1"
serde_json = "1"
sha1 = "0.10.1"
Expand Down
279 changes: 279 additions & 0 deletions packages/next-swc/crates/core/src/cjs_optimizer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Deserialize;
use turbopack_binding::swc::core::{
common::{util::take::Take, SyntaxContext, DUMMY_SP},
ecma::{
ast::{
CallExpr, Callee, Decl, Expr, Id, Ident, Lit, MemberExpr, MemberProp, Module,
ModuleItem, Pat, Script, Stmt, VarDecl, VarDeclKind, VarDeclarator,
},
atoms::{Atom, JsWord},
utils::{prepend_stmts, private_ident, ExprFactory, IdentRenamer},
visit::{
as_folder, noop_visit_mut_type, noop_visit_type, Fold, Visit, VisitMut, VisitMutWith,
VisitWith,
},
},
};

pub fn cjs_optimizer(config: Config, unresolved_ctxt: SyntaxContext) -> impl Fold + VisitMut {
as_folder(CjsOptimizer {
data: State::default(),
packages: config.packages,
unresolved_ctxt,
})
}

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub packages: FxHashMap<String, PackageConfig>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageConfig {
pub transforms: FxHashMap<JsWord, JsWord>,
}

struct CjsOptimizer {
data: State,
packages: FxHashMap<String, PackageConfig>,
unresolved_ctxt: SyntaxContext,
}

#[derive(Debug, Default)]
struct State {
/// List of `require` calls **which should be replaced**.
///
/// `(identifier): (module_record)`
imports: FxHashMap<Id, ImportRecord>,

/// `(module_specifier, property): (identifier)`
replaced: FxHashMap<(Atom, JsWord), Id>,

extra_stmts: Vec<Stmt>,

rename_map: FxHashMap<Id, Id>,

/// Ignored identifiers for `obj` of [MemberExpr].
ignored: FxHashSet<Id>,

is_prepass: bool,
}

#[derive(Debug)]
struct ImportRecord {
module_specifier: Atom,
}

impl CjsOptimizer {
fn should_rewrite(&self, module_specifier: &str) -> Option<&FxHashMap<JsWord, JsWord>> {
self.packages.get(module_specifier).map(|v| &v.transforms)
}
}

impl VisitMut for CjsOptimizer {
noop_visit_mut_type!();

fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
self.data.is_prepass = true;
stmts.visit_mut_children_with(self);
self.data.is_prepass = false;
stmts.visit_mut_children_with(self);
}

fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);

if let Expr::Member(n) = e {
if let MemberProp::Ident(prop) = &n.prop {
if let Expr::Ident(obj) = &*n.obj {
let key = obj.to_id();
if self.data.ignored.contains(&key) {
return;
}

if let Some(record) = self.data.imports.get(&key) {
let mut replaced = false;

let new_id = self
.data
.replaced
.entry((record.module_specifier.clone(), prop.sym.clone()))
.or_insert_with(|| private_ident!(prop.sym.clone()).to_id())
.clone();

if let Some(map) = self.should_rewrite(&record.module_specifier) {
if let Some(renamed) = map.get(&prop.sym) {
replaced = true;
if !self.data.is_prepass {
// Transform as `require('foo').bar`
let var = VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(new_id.clone().into()),
init: Some(Box::new(Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: Ident::new(
"require".into(),
DUMMY_SP.with_ctxt(self.unresolved_ctxt),
)
.as_callee(),
args: vec![Expr::Lit(Lit::Str(
renamed.clone().into(),
))
.as_arg()],
type_args: None,
})),
prop: MemberProp::Ident(Ident::new(
prop.sym.clone(),
DUMMY_SP.with_ctxt(self.unresolved_ctxt),
)),
}))),
definite: false,
};

self.data.extra_stmts.push(Stmt::Decl(Decl::Var(Box::new(
VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Const,
declare: false,
decls: vec![var],
},
))));

*e = Expr::Ident(new_id.into());
}
}
}

if !replaced {
self.data.ignored.insert(key);
}
}
}
}
}
}

fn visit_mut_module(&mut self, n: &mut Module) {
n.visit_children_with(&mut Analyzer {
data: &mut self.data,
in_member_or_var: false,
});

n.visit_mut_children_with(self);

prepend_stmts(
&mut n.body,
self.data.extra_stmts.drain(..).map(ModuleItem::Stmt),
);

n.visit_mut_children_with(&mut IdentRenamer::new(&self.data.rename_map));
}

fn visit_mut_script(&mut self, n: &mut Script) {
n.visit_children_with(&mut Analyzer {
data: &mut self.data,
in_member_or_var: false,
});

n.visit_mut_children_with(self);

prepend_stmts(&mut n.body, self.data.extra_stmts.drain(..));

n.visit_mut_children_with(&mut IdentRenamer::new(&self.data.rename_map));
}

fn visit_mut_stmt(&mut self, n: &mut Stmt) {
n.visit_mut_children_with(self);

if let Stmt::Decl(Decl::Var(v)) = n {
if v.decls.is_empty() {
n.take();
}
}
}

fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
n.visit_mut_children_with(self);

// Find `require('foo')`
if let Some(Expr::Call(CallExpr {
callee: Callee::Expr(callee),
args,
..
})) = n.init.as_deref()
{
if let Expr::Ident(ident) = &**callee {
if ident.span.ctxt == self.unresolved_ctxt && ident.sym == *"require" {
if let Some(arg) = args.get(0) {
if let Expr::Lit(Lit::Str(v)) = &*arg.expr {
// TODO: Config

if let Pat::Ident(name) = &n.name {
if let Some(..) = self.should_rewrite(&v.value) {
let key = name.to_id();

if !self.data.is_prepass {
if !self.data.ignored.contains(&key) {
// Drop variable declarator.
n.name.take();
}
} else {
self.data.imports.insert(
key,
ImportRecord {
module_specifier: v.value.clone().into(),
},
);
}
}
}
}
}
}
}
}
}

fn visit_mut_var_declarators(&mut self, n: &mut Vec<VarDeclarator>) {
n.visit_mut_children_with(self);

// We make `name` invalid if we should drop it.
n.retain(|v| !v.name.is_invalid());
}
}

struct Analyzer<'a> {
in_member_or_var: bool,
data: &'a mut State,
}

impl Visit for Analyzer<'_> {
noop_visit_type!();

fn visit_var_declarator(&mut self, n: &VarDeclarator) {
self.in_member_or_var = true;
n.visit_children_with(self);
self.in_member_or_var = false;
}

fn visit_member_expr(&mut self, e: &MemberExpr) {
self.in_member_or_var = true;
e.visit_children_with(self);
self.in_member_or_var = false;

if let (Expr::Ident(obj), MemberProp::Computed(..)) = (&*e.obj, &e.prop) {
self.data.ignored.insert(obj.to_id());
}
}

fn visit_ident(&mut self, i: &Ident) {
i.visit_children_with(self);
if !self.in_member_or_var {
self.data.ignored.insert(i.to_id());
}
}
}
16 changes: 15 additions & 1 deletion packages/next-swc/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ use fxhash::FxHashSet;
use next_transform_font::next_font_loaders;
use serde::Deserialize;
use turbopack_binding::swc::core::{
common::{chain, comments::Comments, pass::Optional, FileName, SourceFile, SourceMap},
common::{
chain, comments::Comments, pass::Optional, FileName, Mark, SourceFile, SourceMap,
SyntaxContext,
},
ecma::{
ast::EsVersion, parser::parse_file_as_module, transforms::base::pass::noop, visit::Fold,
},
};

pub mod amp_attributes;
mod auto_cjs;
pub mod cjs_optimizer;
pub mod disallow_re_export_all_in_page;
pub mod next_dynamic;
pub mod next_ssg;
Expand Down Expand Up @@ -125,6 +129,9 @@ pub struct TransformOptions {

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

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

pub fn custom_before_pass<'a, C: Comments + 'a>(
Expand All @@ -133,6 +140,7 @@ pub fn custom_before_pass<'a, C: Comments + 'a>(
opts: &'a TransformOptions,
comments: C,
eliminated_packages: Rc<RefCell<FxHashSet<String>>>,
unresolved_mark: Mark,
) -> impl Fold + 'a
where
C: Clone,
Expand Down Expand Up @@ -277,6 +285,12 @@ where
)),
None => Either::Right(noop()),
},
match &opts.cjs_require_optimizer {
Some(config) => {
Either::Left(cjs_optimizer::cjs_optimizer(config.clone(), SyntaxContext::empty().apply_mark(unresolved_mark)))
},
None => Either::Right(noop()),
},
)
}

Expand Down
Loading