@itrocks/uses
Version:
Apply reusable mixins to your classes effortlessly with the @Uses decorator
158 lines • 7.39 kB
JavaScript
import ts from 'typescript';
class UpdateOptions {
createImports = new Map;
updateClasses = new Set;
createExport = '';
createInterfaces = new Map;
}
let updateDeclarations = new Map;
function usesDecoratorValues(node) {
const mixins = [];
for (const decorator of ts.getDecorators(node) ?? []) {
if (!ts.isCallExpression(decorator.expression))
continue;
if (decorator.expression.expression.getText() !== 'Uses')
continue;
for (const argument of decorator.expression.arguments) {
if (!ts.isIdentifier(argument))
continue;
mixins.push(argument.text);
}
}
return mixins;
}
export default () => function transformer(context) {
const { factory } = context;
function createExport(className) {
return factory.createExportAssignment(undefined, undefined, factory.createIdentifier(className));
}
function createInterface(node, className, mixins) {
const modifiers = [];
if (node.modifiers?.some(modifier => (modifier.kind === ts.SyntaxKind.ExportKeyword))) {
modifiers.push(factory.createModifier(ts.SyntaxKind.ExportKeyword));
}
const interfaceName = factory.createIdentifier(className);
const heritageClause = factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, mixins.map(mixin => factory.createExpressionWithTypeArguments(factory.createIdentifier(mixin), undefined)));
return factory.createInterfaceDeclaration(modifiers, interfaceName, undefined, [heritageClause], []);
}
function updateClass(node) {
const modifiers = node.modifiers?.filter(modifier => (modifier.kind !== ts.SyntaxKind.DefaultKeyword)) ?? [];
modifiers.push(factory.createModifier(ts.SyntaxKind.DeclareKeyword));
return factory.updateClassDeclaration(node, modifiers, node.name, node.typeParameters, node.heritageClauses, node.members);
}
function visitSourceFile(sourceFile) {
const imports = new Map;
const updateOptions = new UpdateOptions;
const alreadyInterfaces = new Set;
function visit(node) {
if (ts.isImportDeclaration(node) && node.importClause && node.moduleSpecifier) {
const importPath = node.moduleSpecifier.text;
const namedBindings = node.importClause.namedBindings;
const name = node.importClause.name;
if (name) {
imports.set(name.text, { default: true, path: importPath });
}
if (namedBindings && ts.isNamedImports(namedBindings)) {
namedBindings.elements.forEach(element => {
imports.set(element.name.text, { default: false, path: importPath });
});
}
return node;
}
if (ts.isClassDeclaration(node)) {
let className;
let mixins;
if (!(className = node.name?.text)
|| !ts.canHaveDecorators(node)
|| !(mixins = usesDecoratorValues(node)).length) {
return node;
}
const isDefault = node.modifiers?.some(modifier => (modifier.kind === ts.SyntaxKind.DefaultKeyword));
if (isDefault) {
updateOptions.updateClasses.add(className);
}
if (!alreadyInterfaces.has(className)) {
updateOptions.createInterfaces.set(className, mixins);
for (const mixin of mixins) {
const importOptions = imports.get(mixin);
if (importOptions) {
updateOptions.createImports.set(mixin, importOptions);
}
}
}
if (isDefault) {
updateOptions.createExport = className;
}
updateDeclarations.set(sourceFile.fileName, updateOptions);
return node;
}
if (ts.isInterfaceDeclaration(node)) {
const className = node.name?.text;
if (className && node.modifiers?.some(modifier => (modifier.kind === ts.SyntaxKind.ExportKeyword))) {
alreadyInterfaces.add(className);
updateOptions.createInterfaces.delete(className);
}
return node;
}
return ts.visitEachChild(node, visit, context);
}
return ts.visitNode(sourceFile, visit);
}
function visitDeclarationFile(sourceFile) {
const updateOptions = updateDeclarations.get(sourceFile.fileName);
if (!updateOptions)
return sourceFile;
let doneImports = false;
const imports = new Set;
function visit(node) {
if (ts.isSourceFile(node))
return ts.visitEachChild(node, visit, context);
if (ts.isImportDeclaration(node)) {
if (node.importClause && node.moduleSpecifier) {
const namedBindings = node.importClause.namedBindings;
const name = node.importClause.name;
if (name)
imports.add(name.text);
if (namedBindings && ts.isNamedImports(namedBindings)) {
namedBindings.elements.forEach(element => imports.add(element.name.text));
}
return node;
}
return node;
}
const nodes = [];
const options = updateOptions;
if (!doneImports) {
doneImports = true;
options.createImports.forEach((importOptions, mixin) => {
if (imports.has(mixin))
return;
nodes.push(factory.createImportDeclaration(undefined, factory.createImportClause(false, importOptions.default ? factory.createIdentifier(mixin) : undefined, importOptions.default ? undefined : factory.createNamedImports([
factory.createImportSpecifier(false, undefined, factory.createIdentifier(mixin))
])), factory.createStringLiteral(importOptions.path)));
});
}
if (ts.isClassDeclaration(node)) {
const className = node.name?.text;
if (className) {
nodes.push(options.updateClasses.has(className) ? updateClass(node) : node);
const mixins = options.createInterfaces.get(className);
if (mixins?.length) {
nodes.push(createInterface(node, className, mixins));
}
if (options.createExport) {
nodes.push(createExport(className));
}
}
}
return (nodes.length) ? [...nodes] : node;
}
const result = ts.visitNode(sourceFile, visit);
updateDeclarations.delete(sourceFile.fileName);
return result;
}
return (sourceFile) => sourceFile.isDeclarationFile
? visitDeclarationFile(sourceFile)
: visitSourceFile(sourceFile);
};
//# sourceMappingURL=uses-interface-plugin.js.map