swc-export-order 0.1.0

SWC plugin for injecting export order
Documentation
//! This is an swc plugin which injects a constant named `__namedExportsOrder`
//! defined as an array of strings representing an ordered list of named
//! exports.

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},
};

/// The name of the constant containing the export order
const NAMED_EXPORTS_ORDER: &str = "__namedExportsOrder";

/// This visitor collects the names of the exports in the module in order, then
/// adds an exported constant named `__namedExportsOrder` containing an array of
/// strings of those export names.
#[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 {
				// Match `export { foo }`
				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());
					}
				}
				// Match `export ...`
				ModuleDecl::ExportDecl(export) => match &export.decl {
					// Match `const foo = ...`
					Decl::Var(var) => {
						for decl in &var.decls {
							names.append(&mut extract_bindings(&decl.name));
						}
					}
					// Match `function foo()`
					Decl::Fn(function) => {
						names.push(function.ident.sym.clone());
					}
					// Match `class foo`
					Decl::Class(class) => {
						names.push(class.ident.sym.clone());
					}
					_ => (),
				},
				_ => (),
			}
		}

		// Convert the list of export names to string literal expressions
		let export_exprs: Vec<_> = names
			.drain(..)
			.map(|atom| {
				Some(ExprOrSpread {
					spread: None,
					expr: Box::new(Expr::Lit(Lit::Str(Str::from(atom)))),
				})
			})
			.collect();

		// Define the export declaration
		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()
			})),
		}));

		// Add it to the list of module items
		items.push(declaration);
	}
}

/// Iterate over all of the bindings in a pattern recursively, to capture every
/// bound name in an arbitrarily deep destructuring assignment.
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 {
			// Match `const foo`
			Pat::Ident(ident) => names.push(ident.sym.clone()),
			// Match `const [foo]`
			Pat::Array(array) => {
				// We iterate in reverse to add to the stack in the right order
				for elem in array.elems.iter().rev().flatten() {
					stack.push(elem);
				}
			}
			// Match `const { foo }`
			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
}

/// Entry point for the plugin.
#[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!(
		// Syntax
		Default::default(),
		// Test transform
		|_| visit_mut_pass(ExportOrderVisitor),
		// Test name
		test,
		// Test input
		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 {}
		"#,
		// Expected output
		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"];
		"#
	);
}