swc_export_order/
lib.rs

1//! This is an swc plugin which injects a constant named `__namedExportsOrder`
2//! defined as an array of strings representing an ordered list of named
3//! exports.
4
5use swc_core::{
6	atoms::Atom,
7	common::DUMMY_SP,
8	ecma::{
9		ast::{
10			ArrayLit,
11			BindingIdent,
12			Decl,
13			ExportDecl,
14			ExportSpecifier,
15			Expr,
16			ExprOrSpread,
17			Ident,
18			Lit,
19			ModuleDecl,
20			ModuleItem,
21			ObjectPatProp,
22			Pat,
23			Program,
24			Str,
25			VarDecl,
26			VarDeclKind,
27			VarDeclarator,
28		},
29		visit::{VisitMut, VisitMutWith},
30	},
31	plugin::{plugin_transform, proxies::TransformPluginProgramMetadata},
32};
33
34/// The name of the constant containing the export order
35const NAMED_EXPORTS_ORDER: &str = "__namedExportsOrder";
36
37/// This visitor collects the names of the exports in the module in order, then
38/// adds an exported constant named `__namedExportsOrder` containing an array of
39/// strings of those export names.
40#[derive(Debug, Clone)]
41struct ExportOrderVisitor;
42
43impl VisitMut for ExportOrderVisitor {
44	fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
45		items.visit_mut_children_with(self);
46
47		let mut names: Vec<Atom> = Vec::new();
48
49		for item in items.iter() {
50			let ModuleItem::ModuleDecl(decl) = item else {
51				continue;
52			};
53
54			match decl {
55				// Match `export { foo }`
56				ModuleDecl::ExportNamed(named) => {
57					for specifier in &named.specifiers {
58						let ExportSpecifier::Named(specifier) = specifier else {
59							continue;
60						};
61						if specifier.is_type_only {
62							continue;
63						}
64						let export_name = specifier.exported.as_ref().unwrap_or(&specifier.orig);
65						names.push(export_name.atom().clone());
66					}
67				}
68				// Match `export ...`
69				ModuleDecl::ExportDecl(export) => match &export.decl {
70					// Match `const foo = ...`
71					Decl::Var(var) => {
72						for decl in &var.decls {
73							names.append(&mut extract_bindings(&decl.name));
74						}
75					}
76					// Match `function foo()`
77					Decl::Fn(function) => {
78						names.push(function.ident.sym.clone());
79					}
80					// Match `class foo`
81					Decl::Class(class) => {
82						names.push(class.ident.sym.clone());
83					}
84					_ => (),
85				},
86				_ => (),
87			}
88		}
89
90		// Convert the list of export names to string literal expressions
91		let export_exprs: Vec<_> = names
92			.drain(..)
93			.map(|atom| {
94				Some(ExprOrSpread {
95					spread: None,
96					expr: Box::new(Expr::Lit(Lit::Str(Str::from(atom)))),
97				})
98			})
99			.collect();
100
101		// Define the export declaration
102		let declaration = ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
103			span: DUMMY_SP,
104			decl: Decl::Var(Box::new(VarDecl {
105				kind: VarDeclKind::Const,
106				decls: vec![VarDeclarator {
107					name: Pat::Ident(BindingIdent {
108						id: Ident {
109							sym: Atom::new(NAMED_EXPORTS_ORDER),
110							optional: false,
111							..Default::default()
112						},
113						type_ann: None,
114					}),
115					init: Some(Box::new(Expr::Array(ArrayLit {
116						elems: export_exprs,
117						..Default::default()
118					}))),
119					span: DUMMY_SP,
120					definite: false,
121				}],
122				..Default::default()
123			})),
124		}));
125
126		// Add it to the list of module items
127		items.push(declaration);
128	}
129}
130
131/// Iterate over all of the bindings in a pattern recursively, to capture every
132/// bound name in an arbitrarily deep destructuring assignment.
133pub fn extract_bindings(pat: &Pat) -> Vec<Atom> {
134	let mut names: Vec<Atom> = Vec::new();
135	let mut stack: Vec<&Pat> = vec![pat];
136	while let Some(item) = stack.pop() {
137		match item {
138			// Match `const foo`
139			Pat::Ident(ident) => names.push(ident.sym.clone()),
140			// Match `const [foo]`
141			Pat::Array(array) => {
142				// We iterate in reverse to add to the stack in the right order
143				for elem in array.elems.iter().rev().flatten() {
144					stack.push(elem);
145				}
146			}
147			// Match `const { foo }`
148			Pat::Object(object) => {
149				for prop in object.props.iter().rev() {
150					match prop {
151						ObjectPatProp::Assign(assign) => names.push(assign.key.sym.clone()),
152						ObjectPatProp::KeyValue(kv) => stack.push(&*kv.value),
153						ObjectPatProp::Rest(rest) => stack.push(&*rest.arg),
154					}
155				}
156			}
157			Pat::Assign(assign) => stack.push(&*assign.left),
158			Pat::Rest(rest) => stack.push(&*rest.arg),
159			_ => (),
160		}
161	}
162	names
163}
164
165/// Entry point for the plugin.
166#[plugin_transform]
167pub fn process_transform(
168	mut program: Program,
169	_metadata: TransformPluginProgramMetadata,
170) -> Program {
171	program.visit_mut_with(&mut ExportOrderVisitor);
172	program
173}
174
175#[cfg(test)]
176mod tests {
177	use swc_core::ecma::{transforms::testing::test_inline, visit::visit_mut_pass};
178
179	use super::ExportOrderVisitor;
180
181	test_inline!(
182		// Syntax
183		Default::default(),
184		// Test transform
185		|_| visit_mut_pass(ExportOrderVisitor),
186		// Test name
187		test,
188		// Test input
189		r#"
190		const z = 'zoo';
191		const y = 5;
192		const x = () => 5;
193
194		export { z }
195		export { y, x }
196
197		export const [w, v] = [1, 2]
198		export const {u: U = 1, T: { t }} = {u: 1, T: { t: 1 }}
199		export const s = 's'
200		export function r() {}
201		export class q {}
202		"#,
203		// Expected output
204		r#"
205		const z = 'zoo';
206		const y = 5;
207		const x = () => 5;
208
209		export { z }
210		export { y, x }
211
212		export const [w, v] = [1, 2]
213		export const {u: U = 1, T: { t }} = {u: 1, T: { t: 1 }}
214		export const s = 's'
215		export function r() {}
216		export class q {}
217		export const __namedExportsOrder = ["z", "y", "x", "w", "v", "U", "t", "s", "r", "q"];
218		"#
219	);
220}