diff --git a/crates/swc_webpack_ast/scripts/test-next.sh b/crates/swc_webpack_ast/scripts/test-next.sh index 381b9001afa..d6924866ac1 100755 --- a/crates/swc_webpack_ast/scripts/test-next.sh +++ b/crates/swc_webpack_ast/scripts/test-next.sh @@ -9,6 +9,7 @@ export RUST_BACKTRACE=1 cargo test --no-run UPDATE=1 cargo test -q -(cd next.js/packages/next-swc && yarn build-native) +(cd next.js/packages/next-swc && yarn build-native --release) -(cd next.js && NEXT_PRIVATE_LOCAL_WEBPACK5=1 yarn next dev test/integration/production) +(cd next.js && NEXT_PRIVATE_LOCAL_WEBPACK5=1 yarn testheadless $@ || true) +(cd next.js && NEXT_PRIVATE_LOCAL_WEBPACK5=1 yarn next dev $@) diff --git a/crates/swc_webpack_ast/src/lib.rs b/crates/swc_webpack_ast/src/lib.rs index 14f94b398e6..3dae40737f5 100644 --- a/crates/swc_webpack_ast/src/lib.rs +++ b/crates/swc_webpack_ast/src/lib.rs @@ -1,7 +1,12 @@ use crate::reducer::ast_reducer; -use anyhow::{Context, Error}; -use swc_common::{sync::Lrc, Mark, SourceFile, SourceMap}; +use anyhow::{anyhow, Context, Error}; +use serde::Serialize; +use swc_common::{ + errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceFile, SourceMap, + GLOBALS, +}; use swc_ecma_ast::*; +use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax, TsConfig}; use swc_ecma_transforms_base::resolver::resolver_with_mark; use swc_ecma_visit::VisitMutWith; use swc_estree_ast::flavor::Flavor; @@ -10,6 +15,73 @@ use swc_timer::timer; pub mod reducer; +#[derive(Serialize)] +pub struct AstOutput { + ast: String, + src: Option>, +} + +pub fn process_file(load_file: F, include_src: bool) -> Result +where + F: FnOnce(&Lrc) -> Result, Error>, +{ + let globals = Globals::new(); + let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + + let fm = load_file(&cm).context("failed to load file")?; + + // Default + let syntax = Syntax::Es(EsConfig { + jsx: true, + ..Default::default() + }); + let syntax = match &fm.name { + FileName::Real(path) => match path.extension() { + Some(ext) => { + if ext == "tsx" { + Syntax::Typescript(TsConfig { + tsx: true, + no_early_errors: true, + ..Default::default() + }) + } else if ext == "ts" { + Syntax::Typescript(TsConfig { + no_early_errors: true, + ..Default::default() + }) + } else { + syntax + } + } + _ => syntax, + }, + _ => syntax, + }; + + let module = { + let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None); + let mut parser = Parser::new_from(lexer); + + parser.parse_module().map_err(|err| { + HANDLER.with(|h| { + err.into_diagnostic(h).emit(); + anyhow!("failed to parse module") + }) + })? + }; + + let ast = GLOBALS.set(&globals, || webpack_ast(cm.clone(), fm.clone(), module))?; + + Ok(AstOutput { + ast, + src: if include_src { + Some(fm.src.clone()) + } else { + None + }, + }) +} + /// `n` is expected to be pure (`resolver` is not applied) pub fn webpack_ast( cm: Lrc, @@ -27,6 +99,7 @@ pub fn webpack_ast( let _timer = timer!("resolver"); n.visit_mut_with(&mut resolver_with_mark(top_level_mark)); } + { n.visit_mut_with(&mut ast_reducer(top_level_mark)); } diff --git a/crates/swc_webpack_ast/src/reducer/mod.rs b/crates/swc_webpack_ast/src/reducer/mod.rs index 35f468a6f2b..6a70e085f8e 100644 --- a/crates/swc_webpack_ast/src/reducer/mod.rs +++ b/crates/swc_webpack_ast/src/reducer/mod.rs @@ -1,11 +1,11 @@ -use self::flatten::contains_import; +use self::{flatten::contains_import, typescript::ts_remover}; use std::{iter::once, sync::Arc}; use swc_atoms::js_word; use swc_common::{ - collections::AHashSet, + collections::{AHashMap, AHashSet}, pass::{Repeat, Repeated}, util::take::Take, - Mark, Span, Spanned, SyntaxContext, + Mark, Span, Spanned, SyntaxContext, DUMMY_SP, }; use swc_ecma_ast::*; use swc_ecma_utils::{ident::IdentLike, Id, IsEmpty, StmtLike, StmtOrModuleItem}; @@ -13,6 +13,7 @@ use swc_ecma_visit::{Visit, VisitMut, VisitMutWith, VisitWith}; use swc_timer::timer; mod flatten; +mod typescript; /// # Usage /// @@ -68,11 +69,24 @@ pub fn ast_reducer(top_level_mark: Mark) -> impl VisitMut { }) } +#[derive(Debug, Clone, Copy, Default)] +struct BindingInfo { + used_as_type: bool, + used_as_var: bool, +} + struct Analyzer { amd_requires: AHashSet, + used_refs: AHashMap, } impl Visit for Analyzer { + fn visit_assign_pat_prop(&mut self, p: &AssignPatProp) { + p.visit_children_with(self); + + self.used_refs.entry(p.key.to_id()).or_default().used_as_var = true; + } + fn visit_call_expr(&mut self, e: &CallExpr) { e.visit_children_with(self); @@ -124,6 +138,64 @@ impl Visit for Analyzer { _ => {} } } + + fn visit_expr(&mut self, e: &Expr) { + e.visit_children_with(self); + + match e { + Expr::Ident(i) => { + self.used_refs.entry(i.to_id()).or_default().used_as_var = true; + } + + _ => {} + } + } + + fn visit_member_expr(&mut self, e: &MemberExpr) { + e.obj.visit_with(self); + + if e.computed { + e.prop.visit_with(self); + } + } + + fn visit_pat(&mut self, p: &Pat) { + p.visit_children_with(self); + + match p { + Pat::Ident(s) => { + self.used_refs.entry(s.to_id()).or_default().used_as_var = true; + } + + _ => {} + } + } + + fn visit_prop(&mut self, p: &Prop) { + p.visit_children_with(self); + + match p { + Prop::Shorthand(s) => { + self.used_refs.entry(s.to_id()).or_default().used_as_var = true; + } + + _ => {} + } + } + + fn visit_ts_type_ref(&mut self, ty: &TsTypeRef) { + fn left_most(n: &TsEntityName) -> &Ident { + match n { + TsEntityName::Ident(i) => i, + TsEntityName::TsQualifiedName(q) => left_most(&q.left), + } + } + + ty.visit_children_with(self); + + let left = left_most(&ty.type_name); + self.used_refs.entry(left.to_id()).or_default().used_as_type = true; + } } #[derive(Default)] @@ -131,6 +203,9 @@ struct ScopeData { imported_ids: AHashSet, /// amd `require` modules. amd_requires: AHashSet, + + #[allow(unused)] + used_refs: AHashMap, } impl ScopeData { @@ -160,7 +235,8 @@ impl ScopeData { } let mut analyzer = Analyzer { - amd_requires: AHashSet::default(), + amd_requires: Default::default(), + used_refs: Default::default(), }; items.visit_with(&mut analyzer); @@ -168,6 +244,7 @@ impl ScopeData { ScopeData { imported_ids, amd_requires: analyzer.amd_requires, + used_refs: analyzer.used_refs, } } @@ -791,22 +868,6 @@ impl VisitMut for ReduceAst { *e = *expr.arg.take(); } - Expr::TsAs(expr) => { - *e = *expr.expr.take(); - } - - Expr::TsConstAssertion(expr) => { - *e = *expr.expr.take(); - } - - Expr::TsTypeAssertion(expr) => { - *e = *expr.expr.take(); - } - - Expr::TsNonNull(expr) => { - *e = *expr.expr.take(); - } - Expr::TaggedTpl(expr) => { let mut exprs = Vec::with_capacity(expr.tpl.exprs.len() + 1); exprs.push(expr.tag.take()); @@ -1318,9 +1379,17 @@ impl VisitMut for ReduceAst { fn visit_mut_module_items(&mut self, stmts: &mut Vec) { if !self.collected_data { - let _timer = timer!("analyze before reducing"); self.collected_data = true; - self.data = Arc::new(ScopeData::analyze(&stmts)); + + { + let _timer = timer!("analyze before reducing"); + self.data = Arc::new(ScopeData::analyze(&stmts)); + } + { + let _timer = timer!("remove typescript nodes"); + + stmts.visit_mut_with(&mut ts_remover()); + } } let _timer = timer!("reduce ast (single pass)"); @@ -1438,7 +1507,12 @@ impl VisitMut for ReduceAst { } match pat { - Pat::Assign(..) => {} + Pat::Assign(..) => { + let old = self.can_remove_pat; + self.can_remove_pat = false; + pat.visit_mut_children_with(self); + self.can_remove_pat = old; + } _ => { pat.visit_mut_children_with(self); } @@ -1566,7 +1640,7 @@ impl VisitMut for ReduceAst { fn visit_mut_stmt(&mut self, stmt: &mut Stmt) { match stmt { Stmt::Debugger(_) | Stmt::Break(_) | Stmt::Continue(_) => { - *stmt = Stmt::Empty(EmptyStmt { span: stmt.span() }); + *stmt = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); return; } diff --git a/crates/swc_webpack_ast/src/reducer/typescript.rs b/crates/swc_webpack_ast/src/reducer/typescript.rs new file mode 100644 index 00000000000..a9eb83c8d16 --- /dev/null +++ b/crates/swc_webpack_ast/src/reducer/typescript.rs @@ -0,0 +1,106 @@ +use swc_common::util::take::Take; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; + +pub fn ts_remover() -> impl VisitMut { + TsRemover {} +} + +struct TsRemover {} + +impl VisitMut for TsRemover { + fn visit_mut_array_pat(&mut self, p: &mut ArrayPat) { + p.visit_mut_children_with(self); + + p.optional = false; + } + + fn visit_mut_expr(&mut self, e: &mut Expr) { + e.visit_mut_children_with(self); + + match e { + Expr::TsAs(expr) => { + *e = *expr.expr.take(); + } + + Expr::TsConstAssertion(expr) => { + *e = *expr.expr.take(); + } + + Expr::TsTypeAssertion(expr) => { + *e = *expr.expr.take(); + } + + Expr::TsNonNull(expr) => { + *e = *expr.expr.take(); + } + + _ => {} + } + } + + fn visit_mut_ident(&mut self, i: &mut Ident) { + i.visit_mut_children_with(self); + + i.optional = false; + } + + fn visit_mut_module_item(&mut self, s: &mut ModuleItem) { + s.visit_mut_children_with(self); + + match s { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: Decl::TsInterface(_) | Decl::TsTypeAlias(_), + .. + })) + | ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + type_only: true, .. + })) + | ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { + type_only: true, + .. + })) => { + s.take(); + return; + } + _ => {} + } + } + + fn visit_mut_object_pat(&mut self, p: &mut ObjectPat) { + p.visit_mut_children_with(self); + + p.optional = false; + } + + fn visit_mut_opt_ts_type(&mut self, ty: &mut Option>) { + *ty = None; + } + + fn visit_mut_opt_ts_type_ann(&mut self, ty: &mut Option) { + *ty = None; + } + + fn visit_mut_opt_ts_type_param_decl(&mut self, t: &mut Option) { + *t = None; + } + + fn visit_mut_opt_ts_type_param_instantiation( + &mut self, + t: &mut Option, + ) { + *t = None; + } + + fn visit_mut_stmt(&mut self, s: &mut Stmt) { + s.visit_mut_children_with(self); + + match s { + Stmt::Decl(Decl::TsTypeAlias(..) | Decl::TsInterface(..)) => { + s.take(); + } + + _ => {} + } + } +} diff --git a/crates/swc_webpack_ast/tests/fixture.rs b/crates/swc_webpack_ast/tests/fixture.rs index ac02886a44b..27631203a35 100644 --- a/crates/swc_webpack_ast/tests/fixture.rs +++ b/crates/swc_webpack_ast/tests/fixture.rs @@ -1,5 +1,5 @@ use std::{fs, path::PathBuf}; -use swc_common::{chain, Mark}; +use swc_common::{chain, Mark, Span}; use swc_ecma_ast::*; use swc_ecma_parser::{EsConfig, Parser, StringInput, Syntax, TsConfig}; use swc_ecma_transforms_base::resolver::resolver_with_mark; @@ -33,14 +33,24 @@ fn fixture(input: PathBuf) { struct AssertValid; impl VisitMut for AssertValid { + fn visit_mut_array_pat(&mut self, a: &mut ArrayPat) { + if a.optional { + panic!("found an optional pattern: {:?}", a) + } + + a.visit_mut_children_with(self); + } + fn visit_mut_empty_stmt(&mut self, i: &mut EmptyStmt) { panic!("found empty: {:?}", i) } fn visit_mut_ident(&mut self, i: &mut Ident) { - if i.span.is_dummy() { - panic!("found an identifier with dummy span: {:?}", i) + if i.optional { + panic!("found an optional pattern: {:?}", i) } + + i.visit_mut_children_with(self); } fn visit_mut_if_stmt(&mut self, s: &mut IfStmt) { @@ -59,11 +69,27 @@ impl VisitMut for AssertValid { fn visit_mut_module(&mut self, m: &mut Module) { dbg!(&*m); - m.visit_mut_children_with(self); + m.body.visit_mut_with(self); + } + + fn visit_mut_object_pat(&mut self, pat: &mut ObjectPat) { + if pat.optional { + panic!("found a optional pattern: {:?}", pat) + } + + pat.visit_mut_children_with(self); + } + + fn visit_mut_span(&mut self, sp: &mut Span) { + assert!(!sp.is_dummy()); + } + + fn visit_mut_ts_type(&mut self, ty: &mut TsType) { + panic!("found a typescript type: {:?}", ty) } } -#[testing::fixture("../swc_ecma_parser/tests/typescript/tsc/**/input.ts")] +#[testing::fixture("../swc_ecma_parser/tests/typescript/**/input.ts")] #[testing::fixture("../swc/tests/tsc-references/**/output.js")] fn assert_no_invalid(input: PathBuf) { testing::run_test(false, |cm, _handler| { diff --git a/crates/swc_webpack_ast/tests/fixture/issue/1/output.js b/crates/swc_webpack_ast/tests/fixture/issue/1/output.js index 7fe8d3dc1f8..d9e9f381463 100644 --- a/crates/swc_webpack_ast/tests/fixture/issue/1/output.js +++ b/crates/swc_webpack_ast/tests/fixture/issue/1/output.js @@ -53,8 +53,7 @@ class Container extends React.Component { } export const emitter = mitt(); let CachedComponent; -export async function initNext(opts = { -}) { +export async function initNext() { let initialErr; const appEntrypoint = null; const { component: app , exports: mod } = null; diff --git a/crates/swc_webpack_ast/tests/fixture/next/client/index/output.js b/crates/swc_webpack_ast/tests/fixture/next/client/index/output.js index 1c5f6b6f424..b901efd7c6d 100644 --- a/crates/swc_webpack_ast/tests/fixture/next/client/index/output.js +++ b/crates/swc_webpack_ast/tests/fixture/next/client/index/output.js @@ -78,8 +78,7 @@ class Container { const emitter = null; exports.emitter = null; let CachedComponent; -(function*(opts = { -}) { +(function*() { let initialErr; const appEntrypoint = null; const { component: app , exports: mod } = null;