use swc_core::{
atoms::Atom,
common::DUMMY_SP,
ecma::{
ast::{
ArrayLit,
BindingIdent,
Decl,
ExportDecl,
ExportSpecifier,
Expr,
ExprOrSpread,
Ident,
Lit,
ModuleDecl,
ModuleItem,
ObjectPatProp,
Pat,
Program,
Str,
VarDecl,
VarDeclKind,
VarDeclarator,
},
visit::{VisitMut, VisitMutWith},
},
plugin::{plugin_transform, proxies::TransformPluginProgramMetadata},
};
const NAMED_EXPORTS_ORDER: &str = "__namedExportsOrder";
#[derive(Debug, Clone)]
struct ExportOrderVisitor;
impl VisitMut for ExportOrderVisitor {
fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
items.visit_mut_children_with(self);
let mut names: Vec<Atom> = Vec::new();
for item in items.iter() {
let ModuleItem::ModuleDecl(decl) = item else {
continue;
};
match decl {
ModuleDecl::ExportNamed(named) => {
for specifier in &named.specifiers {
let ExportSpecifier::Named(specifier) = specifier else {
continue;
};
if specifier.is_type_only {
continue;
}
let export_name = specifier.exported.as_ref().unwrap_or(&specifier.orig);
names.push(export_name.atom().clone());
}
}
ModuleDecl::ExportDecl(export) => match &export.decl {
Decl::Var(var) => {
for decl in &var.decls {
names.append(&mut extract_bindings(&decl.name));
}
}
Decl::Fn(function) => {
names.push(function.ident.sym.clone());
}
Decl::Class(class) => {
names.push(class.ident.sym.clone());
}
_ => (),
},
_ => (),
}
}
let export_exprs: Vec<_> = names
.drain(..)
.map(|atom| {
Some(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str::from(atom)))),
})
})
.collect();
let declaration = ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: Decl::Var(Box::new(VarDecl {
kind: VarDeclKind::Const,
decls: vec![VarDeclarator {
name: Pat::Ident(BindingIdent {
id: Ident {
sym: Atom::new(NAMED_EXPORTS_ORDER),
optional: false,
..Default::default()
},
type_ann: None,
}),
init: Some(Box::new(Expr::Array(ArrayLit {
elems: export_exprs,
..Default::default()
}))),
span: DUMMY_SP,
definite: false,
}],
..Default::default()
})),
}));
items.push(declaration);
}
}
pub fn extract_bindings(pat: &Pat) -> Vec<Atom> {
let mut names: Vec<Atom> = Vec::new();
let mut stack: Vec<&Pat> = vec![pat];
while let Some(item) = stack.pop() {
match item {
Pat::Ident(ident) => names.push(ident.sym.clone()),
Pat::Array(array) => {
for elem in array.elems.iter().rev().flatten() {
stack.push(elem);
}
}
Pat::Object(object) => {
for prop in object.props.iter().rev() {
match prop {
ObjectPatProp::Assign(assign) => names.push(assign.key.sym.clone()),
ObjectPatProp::KeyValue(kv) => stack.push(&*kv.value),
ObjectPatProp::Rest(rest) => stack.push(&*rest.arg),
}
}
}
Pat::Assign(assign) => stack.push(&*assign.left),
Pat::Rest(rest) => stack.push(&*rest.arg),
_ => (),
}
}
names
}
#[plugin_transform]
pub fn process_transform(
mut program: Program,
_metadata: TransformPluginProgramMetadata,
) -> Program {
program.visit_mut_with(&mut ExportOrderVisitor);
program
}
#[cfg(test)]
mod tests {
use swc_core::ecma::{transforms::testing::test_inline, visit::visit_mut_pass};
use super::ExportOrderVisitor;
test_inline!(
Default::default(),
|_| visit_mut_pass(ExportOrderVisitor),
test,
r#"
const z = 'zoo';
const y = 5;
const x = () => 5;
export { z }
export { y, x }
export const [w, v] = [1, 2]
export const {u: U = 1, T: { t }} = {u: 1, T: { t: 1 }}
export const s = 's'
export function r() {}
export class q {}
"#,
r#"
const z = 'zoo';
const y = 5;
const x = () => 5;
export { z }
export { y, x }
export const [w, v] = [1, 2]
export const {u: U = 1, T: { t }} = {u: 1, T: { t: 1 }}
export const s = 's'
export function r() {}
export class q {}
export const __namedExportsOrder = ["z", "y", "x", "w", "v", "U", "t", "s", "r", "q"];
"#
);
}