rsbuild-plugin-react-router
Version:
React Router plugin for Rsbuild
315 lines (294 loc) • 9.29 kB
text/typescript
import {
NAMED_COMPONENT_EXPORTS,
NAMED_COMPONENT_EXPORTS_SET,
} from './constants.js';
import type { ParseResult } from 'yuku-parser';
import {
callExpression,
exportNamedDeclaration,
exportSpecifier,
getExportedName,
getProgram,
identifier,
importDeclaration,
patternIncludesName,
variableDeclaration,
type AnyNode,
} from './route-ast.js';
export function toFunctionExpression(decl: AnyNode): AnyNode {
return {
...decl,
type: 'FunctionExpression',
declare: undefined,
};
}
export function toClassExpression(decl: AnyNode): AnyNode {
return {
...decl,
type: 'ClassExpression',
declare: undefined,
};
}
const getComponentExportName = (exportedName: string): string | null => {
if (exportedName === 'default') {
return 'Component';
}
return isNamedComponentExport(exportedName) ? exportedName : null;
};
const getImportInsertionIndex = (program: AnyNode): number => {
let index = 0;
for (const statement of program.body ?? []) {
if (
statement.type !== 'ExpressionStatement' ||
(statement.directive === undefined &&
statement.expression?.type !== 'Literal')
) {
break;
}
index += 1;
}
return index;
};
const declarationIncludesName = (
declaration: AnyNode,
name: string
): boolean => {
if (declaration.type === 'VariableDeclaration') {
return (declaration.declarations ?? []).some((declarator: AnyNode) =>
patternIncludesName(declarator.id, name)
);
}
if (
(declaration.type === 'FunctionDeclaration' ||
declaration.type === 'ClassDeclaration' ||
declaration.type === 'TSEnumDeclaration') &&
declaration.id?.name
) {
return declaration.id.name === name;
}
if (declaration.type === 'ImportDeclaration') {
return (declaration.specifiers ?? []).some(
(specifier: AnyNode) => specifier.local?.name === name
);
}
return false;
};
const hasTopLevelBindingName = (program: AnyNode, name: string): boolean => {
for (const statement of program.body ?? []) {
if (statement.type === 'ImportDeclaration') {
if (declarationIncludesName(statement, name)) {
return true;
}
continue;
}
if (statement.type === 'ExportDefaultDeclaration') {
if (statement.declaration?.id?.name === name) {
return true;
}
continue;
}
const declaration =
statement.type === 'ExportNamedDeclaration'
? statement.declaration
: statement;
if (declaration && declarationIncludesName(declaration, name)) {
return true;
}
}
return false;
};
export const transformRoute = (ast: ParseResult | AnyNode): void => {
const program = getProgram(ast);
const usedNames = new Set<string>();
const hocs: Array<[string, string]> = [];
const componentWrapperDeclarations: AnyNode[] = [];
function getUid(name: string) {
let uid = `_${name}`;
let index = 2;
while (usedNames.has(uid) || hasTopLevelBindingName(program, uid)) {
uid = `_${name}${index++}`;
}
usedNames.add(uid);
return uid;
}
function getHocUid(hocName: string) {
const uid = getUid(hocName);
hocs.push([hocName, uid]);
return identifier(uid);
}
function wrapNamedComponentDeclaration(name: string, declaration: AnyNode) {
const uid = getHocUid(`with${name}Props`);
const expression =
declaration.type === 'FunctionDeclaration'
? toFunctionExpression(declaration)
: declaration.type === 'ClassDeclaration'
? toClassExpression(declaration)
: declaration;
return variableDeclaration(name, callExpression(uid, [expression]));
}
for (const statement of [...(program.body ?? [])]) {
if (statement.type === 'ExportDefaultDeclaration') {
const declaration = statement.declaration;
if (!declaration) {
continue;
}
if (
declaration.declare === true ||
declaration.type === 'TSInterfaceDeclaration'
) {
continue;
}
const uid = getHocUid('withComponentProps');
if (
(declaration.type === 'FunctionDeclaration' ||
declaration.type === 'ClassDeclaration') &&
declaration.id?.name
) {
const statementIndex = program.body.indexOf(statement);
program.body.splice(statementIndex, 0, declaration);
statement.declaration = callExpression(uid, [
identifier(declaration.id.name),
]);
continue;
}
const expression =
declaration.type === 'FunctionDeclaration'
? toFunctionExpression(declaration)
: declaration.type === 'ClassDeclaration'
? toClassExpression(declaration)
: declaration;
statement.declaration = callExpression(uid, [expression]);
continue;
}
if (statement.type !== 'ExportNamedDeclaration') {
continue;
}
if (statement.exportKind === 'type') {
continue;
}
const declaration = statement.declaration;
if (declaration?.type === 'VariableDeclaration') {
for (const declarator of declaration.declarations ?? []) {
if (
declarator.id?.type !== 'Identifier' ||
!declarator.init ||
!isNamedComponentExport(declarator.id.name)
) {
continue;
}
const uid = getHocUid(`with${declarator.id.name}Props`);
declarator.init = callExpression(uid, [declarator.init]);
}
continue;
}
if (
(declaration?.type === 'FunctionDeclaration' ||
declaration?.type === 'ClassDeclaration') &&
declaration.id?.name &&
isNamedComponentExport(declaration.id.name)
) {
const name = declaration.id.name;
statement.declaration = wrapNamedComponentDeclaration(name, declaration);
continue;
}
if (statement.source) {
const importSpecifiers: Array<{ local: string; imported: string }> = [];
const sourceWrapperDeclarations: AnyNode[] = [];
const wrappedExportSpecifiers: AnyNode[] = [];
statement.specifiers = (statement.specifiers ?? []).filter(
(specifier: AnyNode) => {
if (
specifier.type !== 'ExportSpecifier' ||
specifier.exportKind === 'type'
) {
return true;
}
const exportedName = getExportedName(specifier);
const importedName = getExportedName(specifier.local);
const componentExportName = exportedName
? getComponentExportName(exportedName)
: null;
if (!exportedName || !importedName || !componentExportName) {
return true;
}
const sourceLocalName = getUid(`${exportedName}Source`);
const wrappedLocalName = getUid(exportedName);
const uid = getHocUid(`with${componentExportName}Props`);
importSpecifiers.push({
imported: importedName,
local: sourceLocalName,
});
sourceWrapperDeclarations.push(
variableDeclaration(
wrappedLocalName,
callExpression(uid, [identifier(sourceLocalName)])
)
);
wrappedExportSpecifiers.push(
exportSpecifier(wrappedLocalName, exportedName)
);
return false;
}
);
if (importSpecifiers.length > 0) {
const statementIndex = program.body.indexOf(statement);
const replacementStatements = [
importDeclaration(importSpecifiers, String(statement.source.value)),
];
if (statement.specifiers.length > 0) {
replacementStatements.push(statement);
}
replacementStatements.push(...sourceWrapperDeclarations);
replacementStatements.push(
exportNamedDeclaration(wrappedExportSpecifiers)
);
program.body.splice(statementIndex, 1, ...replacementStatements);
}
continue;
}
for (const specifier of statement.specifiers ?? []) {
if (
specifier.type !== 'ExportSpecifier' ||
specifier.exportKind === 'type'
) {
continue;
}
const exportedName = getExportedName(specifier);
const componentExportName = exportedName
? getComponentExportName(exportedName)
: null;
if (!exportedName || !componentExportName) {
continue;
}
const localName = specifier.local?.name;
if (!localName) {
continue;
}
const wrappedLocalName = getUid(exportedName);
const uid = getHocUid(`with${componentExportName}Props`);
componentWrapperDeclarations.push(
variableDeclaration(
wrappedLocalName,
callExpression(uid, [identifier(localName)])
)
);
specifier.local = identifier(wrappedLocalName);
}
}
program.body.push(...componentWrapperDeclarations);
if (hocs.length > 0) {
program.body.splice(
getImportInsertionIndex(program),
0,
importDeclaration(
hocs.map(([name, local]) => ({ imported: name, local })),
'virtual/react-router/with-props'
)
);
}
};
function isNamedComponentExport(
name: string
): name is (typeof NAMED_COMPONENT_EXPORTS)[number] {
return NAMED_COMPONENT_EXPORTS_SET.has(name);
}