rsbuild-plugin-react-router
Version:
React Router plugin for Rsbuild
669 lines (624 loc) • 19.2 kB
text/typescript
import { walk, type ParseResult } from 'yuku-parser';
import {
getExportedName,
getPatternIdentifierNames,
getProgram,
removeFromArray,
type AnyNode,
type ProgramNode,
} from './route-ast.js';
export function validateDestructuredExports(
id: AnyNode,
exportsToRemove: readonly string[]
): void {
validateBindingTarget(id, new Set(exportsToRemove));
}
export function invalidDestructureError(name: string): Error {
return new Error(`Cannot remove destructured export "${name}"`);
}
const assertAllowedBindingName = (
name: string,
exportsToRemove: ReadonlySet<string>
): void => {
if (exportsToRemove.has(name)) {
throw invalidDestructureError(name);
}
};
const validateRestElement = (
element: AnyNode,
exportsToRemove: ReadonlySet<string>
): void => {
if (element.argument?.type === 'Identifier' && element.argument.name) {
assertAllowedBindingName(element.argument.name, exportsToRemove);
}
};
const validateObjectProperty = (
property: AnyNode,
exportsToRemove: ReadonlySet<string>
): void => {
if (property.type === 'RestElement') {
validateRestElement(property, exportsToRemove);
return;
}
if (property.type === 'Property') {
validateBindingTarget(
property.value as AnyNode | null | undefined,
exportsToRemove
);
}
};
const validateBindingTarget = (
node: AnyNode | null | undefined,
exportsToRemove: ReadonlySet<string>
): void => {
if (!node) {
return;
}
switch (node.type) {
case 'Identifier':
if (node.name) {
assertAllowedBindingName(node.name, exportsToRemove);
}
return;
case 'AssignmentPattern':
validateBindingTarget(node.left, exportsToRemove);
return;
case 'ArrayPattern':
for (const element of node.elements ?? []) {
if (element?.type === 'RestElement') {
validateRestElement(element, exportsToRemove);
} else {
validateBindingTarget(element, exportsToRemove);
}
}
return;
case 'ObjectPattern':
for (const property of node.properties ?? []) {
validateObjectProperty(property, exportsToRemove);
}
}
};
const getDeclaredNames = (node: AnyNode): Set<string> => {
const names = new Set<string>();
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations ?? []) {
getPatternIdentifierNames(declarator.id, names);
}
} else if (
(node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') &&
node.id?.name
) {
names.add(node.id.name);
} else if (node.type === 'ImportDeclaration') {
for (const specifier of node.specifiers ?? []) {
if (specifier.local?.name) {
names.add(specifier.local.name);
}
}
}
return names;
};
const isIdentifierDeclaration = (node: AnyNode, parent: AnyNode | null) => {
if (!parent || node.type !== 'Identifier') {
return false;
}
if (
(parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ClassDeclaration' ||
parent.type === 'ClassExpression') &&
parent.id === node
) {
return true;
}
if (parent.type === 'VariableDeclarator') {
return Boolean(
node.name && getPatternIdentifierNames(parent.id).has(node.name)
);
}
if (
(parent.type === 'ImportSpecifier' ||
parent.type === 'ImportDefaultSpecifier' ||
parent.type === 'ImportNamespaceSpecifier') &&
parent.local === node
) {
return true;
}
if (
(parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression') &&
node.name
) {
const name = node.name;
return (parent.params ?? []).some((param: AnyNode) =>
getPatternIdentifierNames(param).has(name)
);
}
return false;
};
const isNonReferenceIdentifier = (node: AnyNode, parent: AnyNode | null) => {
if (!parent || node.type !== 'Identifier') {
return false;
}
if (isIdentifierDeclaration(node, parent)) {
return true;
}
if (
parent.type === 'MemberExpression' &&
parent.property === node &&
!parent.computed
) {
return true;
}
if (
parent.type === 'Property' &&
parent.key === node &&
!parent.computed &&
!parent.shorthand
) {
return true;
}
if (
parent.type === 'MethodDefinition' &&
parent.key === node &&
!parent.computed
) {
return true;
}
if (
parent.type === 'ExportSpecifier' ||
parent.type === 'ExportDefaultSpecifier' ||
parent.type === 'ExportNamespaceSpecifier'
) {
return true;
}
if (parent.type === 'ImportSpecifier' && parent.imported === node) {
return true;
}
if (
parent.type === 'LabeledStatement' ||
parent.type === 'BreakStatement' ||
parent.type === 'ContinueStatement'
) {
return true;
}
return false;
};
const isUppercaseName = (name: string): boolean => /^[A-Z]/.test(name);
const collectReferencedNames = (node: AnyNode): Set<string> => {
const referenced = new Set<string>();
walk(node as never, {
Identifier(node, ctx) {
const current = node as unknown as AnyNode;
const parent = (ctx as { parent?: unknown }).parent as AnyNode | null;
if (!isNonReferenceIdentifier(current, parent) && current.name) {
referenced.add(current.name);
}
},
JSXIdentifier(node, ctx) {
const current = node as unknown as AnyNode;
const parent = (ctx as { parent?: unknown }).parent as AnyNode | null;
if (!parent) {
return;
}
if (parent.type === 'JSXMemberExpression' && parent.object === current) {
if (current.name) {
referenced.add(current.name);
}
return;
}
if (!current.name || !isUppercaseName(current.name)) {
return;
}
if (
(parent.type === 'JSXOpeningElement' ||
parent.type === 'JSXClosingElement') &&
parent.name === current
) {
referenced.add(current.name);
return;
}
},
ExportSpecifier(node, ctx) {
const current = node as unknown as AnyNode;
const declaration = (ctx as { parent?: unknown })
.parent as AnyNode | null;
if (
!declaration?.source &&
declaration?.exportKind !== 'type' &&
current.local?.name &&
current.exportKind !== 'type'
) {
referenced.add(current.local.name);
}
},
});
return referenced;
};
type TopLevelDeclaration = {
referencedNames: Set<string>;
};
type TopLevelDeclarationGraph = {
declarationsByNode: Map<AnyNode, TopLevelDeclaration>;
declarationsByName: Map<string, Set<TopLevelDeclaration>>;
};
const createTopLevelDeclarationGraph = (
program: ProgramNode
): TopLevelDeclarationGraph => {
const declarationsByNode = new Map<AnyNode, TopLevelDeclaration>();
const declarationsByName = new Map<string, Set<TopLevelDeclaration>>();
const registerDeclaration = (
node: AnyNode,
declarationNode: AnyNode,
declaredNames: Set<string>
) => {
const declaration: TopLevelDeclaration = {
referencedNames: collectReferencedNames(declarationNode),
};
declarationsByNode.set(node, declaration);
for (const name of declaredNames) {
const namedDeclarations = declarationsByName.get(name) ?? new Set();
namedDeclarations.add(declaration);
declarationsByName.set(name, namedDeclarations);
}
};
for (const statement of [...(program.body ?? [])]) {
if (statement.type === 'VariableDeclaration') {
for (const declarator of statement.declarations ?? []) {
registerDeclaration(
declarator,
declarator,
getPatternIdentifierNames(declarator.id)
);
}
continue;
}
if (
statement.type === 'FunctionDeclaration' ||
statement.type === 'ClassDeclaration'
) {
registerDeclaration(statement, statement, getDeclaredNames(statement));
}
}
return { declarationsByNode, declarationsByName };
};
const collectLiveTopLevelDeclarations = (
program: ProgramNode,
graph: TopLevelDeclarationGraph
): Set<TopLevelDeclaration> => {
const pendingNames: string[] = [];
for (const statement of program.body ?? []) {
if (statement.type === 'VariableDeclaration') {
continue;
}
if (graph.declarationsByNode.has(statement)) {
continue;
}
for (const name of collectReferencedNames(statement)) {
pendingNames.push(name);
}
}
// This is intentionally name-based and conservative: shadowing may retain a
// declaration, but it must never make a live declaration removable.
const visitedNames = new Set<string>();
const liveDeclarations = new Set<TopLevelDeclaration>();
while (pendingNames.length > 0) {
const name = pendingNames.pop();
if (!name || visitedNames.has(name)) {
continue;
}
visitedNames.add(name);
for (const declaration of graph.declarationsByName.get(name) ?? []) {
if (!liveDeclarations.has(declaration)) {
liveDeclarations.add(declaration);
for (const referencedName of declaration.referencedNames) {
pendingNames.push(referencedName);
}
}
}
}
return liveDeclarations;
};
const declarationReferencesName = (
declaration: TopLevelDeclaration,
names: ReadonlySet<string>,
graph: TopLevelDeclarationGraph,
cache: Map<TopLevelDeclaration, boolean>,
visitedNames = new Set<string>()
): boolean => {
const cached = cache.get(declaration);
if (cached !== undefined) {
return cached;
}
for (const referencedName of declaration.referencedNames) {
if (names.has(referencedName)) {
cache.set(declaration, true);
return true;
}
if (visitedNames.has(referencedName)) {
continue;
}
visitedNames.add(referencedName);
for (const referencedDeclaration of graph.declarationsByName.get(
referencedName
) ?? []) {
if (
declarationReferencesName(
referencedDeclaration,
names,
graph,
cache,
visitedNames
)
) {
cache.set(declaration, true);
return true;
}
}
}
cache.set(declaration, false);
return false;
};
const removeNewlyDeadTopLevelDeclarations = (
program: ProgramNode,
graph: TopLevelDeclarationGraph,
previouslyLive: ReadonlySet<TopLevelDeclaration>,
removedExportReferencedNames: ReadonlySet<string>
): void => {
const currentlyLive = collectLiveTopLevelDeclarations(program, graph);
const removedReferenceCache = new Map<TopLevelDeclaration, boolean>();
const isRemovableDeadDeclaration = (node: AnyNode) => {
const declaration = graph.declarationsByNode.get(node);
if (!declaration || currentlyLive.has(declaration)) {
return false;
}
return (
previouslyLive.has(declaration) ||
declarationReferencesName(
declaration,
removedExportReferencedNames,
graph,
removedReferenceCache
)
);
};
program.body = program.body.filter((statement: AnyNode) => {
if (statement.type === 'VariableDeclaration') {
statement.declarations = (statement.declarations ?? []).filter(
(declarator: AnyNode) => !isRemovableDeadDeclaration(declarator)
);
return statement.declarations.length > 0;
}
return !isRemovableDeadDeclaration(statement);
});
};
const hasRemovableExport = (
program: ProgramNode,
exportsToRemove: ReadonlySet<string>
): boolean => {
const removesNamedExports = [...exportsToRemove].some(
name => name !== 'default'
);
for (const statement of program.body ?? []) {
if (statement.type === 'ExportAllDeclaration') {
const exportedName = statement.exported
? getExportedName({ exported: statement.exported })
: null;
if (exportedName && exportsToRemove.has(exportedName)) {
return true;
}
if (!exportedName && removesNamedExports) {
return true;
}
continue;
}
if (statement.type === 'ExportDefaultDeclaration') {
if (exportsToRemove.has('default')) {
return true;
}
continue;
}
if (statement.type !== 'ExportNamedDeclaration') {
continue;
}
for (const specifier of statement.specifiers ?? []) {
if (specifier.type !== 'ExportSpecifier') {
continue;
}
const exportedName = getExportedName(specifier);
if (exportedName && exportsToRemove.has(exportedName)) {
return true;
}
}
const declaration = statement.declaration;
if (declaration?.type === 'VariableDeclaration') {
for (const declarator of declaration.declarations ?? []) {
for (const name of getPatternIdentifierNames(declarator.id)) {
if (exportsToRemove.has(name)) {
return true;
}
}
}
continue;
}
if (
(declaration?.type === 'FunctionDeclaration' ||
declaration?.type === 'ClassDeclaration') &&
declaration.id?.name &&
exportsToRemove.has(declaration.id.name)
) {
return true;
}
}
return false;
};
export const removeExports = (
ast: ParseResult | AnyNode,
exportsToRemove: readonly string[],
exportsToRemoveSet: ReadonlySet<string> = new Set(exportsToRemove)
): boolean => {
const program = getProgram(ast);
if (!hasRemovableExport(program, exportsToRemoveSet)) {
return false;
}
const declarationGraph = createTopLevelDeclarationGraph(program);
const previouslyLive = collectLiveTopLevelDeclarations(
program,
declarationGraph
);
let exportsChanged = false;
const removedExportLocalNames = new Set<string>();
const removedExportReferencedNames = new Set<string>();
const removesNamedExports = exportsToRemove.some(name => name !== 'default');
const trackRemovedExportReferences = (node: AnyNode | null | undefined) => {
if (!node) {
return;
}
const declaration = declarationGraph.declarationsByNode.get(node);
for (const name of declaration?.referencedNames ??
collectReferencedNames(node)) {
removedExportReferencedNames.add(name);
}
};
for (const statement of [...program.body]) {
if (statement.type === 'ExportAllDeclaration') {
const exportedName = statement.exported
? getExportedName({ exported: statement.exported })
: null;
if (exportedName && exportsToRemoveSet.has(exportedName)) {
exportsChanged = true;
removeFromArray(program.body, statement);
}
if (!exportedName && removesNamedExports) {
throw new Error(
'Cannot remove named exports from `export *`; use explicit named re-exports.'
);
}
continue;
}
if (statement.type === 'ExportNamedDeclaration') {
if (statement.specifiers?.length) {
statement.specifiers = statement.specifiers.filter(
(specifier: AnyNode) => {
if (specifier.type !== 'ExportSpecifier') {
return true;
}
const exportedName = getExportedName(specifier);
if (exportedName && exportsToRemoveSet.has(exportedName)) {
exportsChanged = true;
if (specifier.local?.name) {
removedExportLocalNames.add(specifier.local.name);
removedExportReferencedNames.add(specifier.local.name);
}
return false;
}
return true;
}
);
if (statement.specifiers.length === 0 && !statement.declaration) {
removeFromArray(program.body, statement);
}
}
const declaration = statement.declaration;
if (declaration?.type === 'VariableDeclaration') {
declaration.declarations = (declaration.declarations ?? []).filter(
(declarator: AnyNode) => {
const id = declarator.id;
if (id?.type === 'Identifier') {
if (id.name && exportsToRemoveSet.has(id.name)) {
exportsChanged = true;
removedExportLocalNames.add(id.name);
removedExportReferencedNames.add(id.name);
trackRemovedExportReferences(declarator);
return false;
}
return true;
}
if (id) {
validateDestructuredExports(id, exportsToRemove);
}
return true;
}
);
if (declaration.declarations.length === 0) {
removeFromArray(program.body, statement);
}
}
if (
(declaration?.type === 'FunctionDeclaration' ||
declaration?.type === 'ClassDeclaration') &&
declaration.id?.name &&
exportsToRemoveSet.has(declaration.id.name)
) {
exportsChanged = true;
removedExportLocalNames.add(declaration.id.name);
removedExportReferencedNames.add(declaration.id.name);
trackRemovedExportReferences(statement);
removeFromArray(program.body, statement);
}
}
if (
statement.type === 'ExportDefaultDeclaration' &&
exportsToRemoveSet.has('default')
) {
exportsChanged = true;
const declaration = statement.declaration;
if (declaration?.type === 'Identifier' && declaration.name) {
removedExportLocalNames.add(declaration.name);
removedExportReferencedNames.add(declaration.name);
} else if (declaration?.id?.name) {
removedExportLocalNames.add(declaration.id.name);
removedExportReferencedNames.add(declaration.id.name);
}
trackRemovedExportReferences(statement);
removeFromArray(program.body, statement);
}
}
for (const statement of [...program.body]) {
const expression =
statement.type === 'ExpressionStatement' ? statement.expression : null;
const left =
expression?.type === 'AssignmentExpression' ? expression.left : null;
if (
left?.type === 'MemberExpression' &&
left.object?.type === 'Identifier' &&
left.object.name &&
removedExportLocalNames.has(left.object.name)
) {
removeFromArray(program.body, statement);
}
}
if (exportsChanged) {
removeNewlyDeadTopLevelDeclarations(
program,
declarationGraph,
previouslyLive,
removedExportReferencedNames
);
}
return exportsChanged;
};
export const removeUnusedImports = (ast: ParseResult | AnyNode): void => {
const program = getProgram(ast);
const referenced = collectReferencedNames(program);
for (const statement of [...program.body]) {
if (statement.type !== 'ImportDeclaration') {
continue;
}
if ((statement.specifiers ?? []).length === 0) {
continue;
}
statement.specifiers = (statement.specifiers ?? []).filter(
(specifier: AnyNode) => {
if (specifier.importKind === 'type') {
return false;
}
return !specifier.local?.name || referenced.has(specifier.local.name);
}
);
if (statement.specifiers.length === 0) {
removeFromArray(program.body, statement);
}
}
};