typeorm-codebase-sync
Version:
Automatically update your codebase to add migrations, entities and subscribers to your `data-source.ts` file
413 lines • 24.6 kB
JavaScript
import path from "path";
import ts from "typescript";
import { Codebase } from "../utils/Codebase.js";
import { isWithTypeExpression, updateWithTypeExpression } from "../utils/updateWithTypeExpression.js";
export class ImportAndAddItemToInitializerArrayProperty {
/**
* @param {string} filePath - file path of the file to edit
* @param {string} initializerName - name of the class while its instantiation should be manipulated
* @param {string} initializerPropertyName - name of the property that should be manipulated
* @param {string} importedFilePath - file path of the file to import
* @param {string} importedFileImportName - name of the import to use in the manipulated array
* @param {undefined | string} importedFileExportName - name of the export from `importedFilePath` file
* @param {boolean} importDefault - is the export from `importedFilePath` file a default export?
* @param {undefined | "esm" | "commonjs"} moduleSystem - determines whether an `.js` extension should be used in the import,
* if omitted then it's determined automatically
* @param {boolean} updateOtherRelevantFiles - should other files other than the `filePath` can be updated if necessary?
* @param {boolean} treatImportNamespaceAsList - given `import * as value from "./something"` treat `value` as a valid list
* @param {boolean} exportImportAllFromFileWhenImportingNamespace - in `treatImportNamespaceAsList` case,
* add `export * from "./importedFilePath` instead of `export { importedFileImportName } from "./importedFilePath`
* @param {boolean} treatObjectLiteralExpressionValuesAsList - given `{Something, Something2: Something3}`
* the result will be `{Something, Something2: Something3, importedFileImportName}`
* @param {boolean} instantiateObjectLiteralExpressionValuesByDefault - use object literal expression values when
* creating the `initializerPropertyName` property. See `treatObjectLiteralExpressionValuesAsList` parameter
*/
constructor({ filePath, initializerName, initializerPropertyName, importedFilePath, importedFileImportName, importedFileExportName, importDefault, updateOtherRelevantFiles, treatImportNamespaceAsList = false, exportImportAllFromFileWhenImportingNamespace = true, treatObjectLiteralExpressionValuesAsList = false, instantiateObjectLiteralExpressionValuesByDefault = false, moduleSystem }) {
this.referenceAdded = false;
this.importAdded = false;
this.filesToAddImportInFilePaths = new Set();
this.declarationReplacementMap = new Map();
this.linkingError = false;
this.filePath = filePath;
this.initializerName = initializerName;
this.initializerPropertyName = initializerPropertyName;
this.importedFilePath = importedFilePath;
this.importedFileImportName = importedFileImportName;
this.importedFileExportName = importedFileExportName;
this.importDefault = importDefault;
this.updateOtherRelevantFiles = updateOtherRelevantFiles;
this.treatImportNamespaceAsList = treatImportNamespaceAsList;
this.exportImportAllFromFileWhenImportingNamespace = exportImportAllFromFileWhenImportingNamespace;
this.treatObjectLiteralExpressionValuesAsList = treatObjectLiteralExpressionValuesAsList;
this.instantiateObjectLiteralExpressionValuesByDefault = instantiateObjectLiteralExpressionValuesByDefault;
this.addToExistingInitializerArgumentsPropertyArgumentsHandler =
this.addToExistingInitializerArgumentsPropertyArgumentsHandler.bind(this);
this.addNewInitializerArgumentsPropertyArgumentsHandler = this.addNewInitializerArgumentsPropertyArgumentsHandler.bind(this);
this.updateDeclaration = this.updateDeclaration.bind(this);
this.codebase = new Codebase({
entryFilePath: this.filePath,
moduleSystem
});
}
/**
* @returns {string[]} - list of modified file paths. empty list means no files could be modified.
*/
async manipulateCodebase() {
await this.codebase.initialize();
let entrySourceFile = this.codebase.getSourceFile(this.codebase.entryFilePath);
// try to add to existing property
entrySourceFile = this.findAndUpdateInitializer(entrySourceFile, this.addToExistingInitializerArgumentsPropertyArgumentsHandler);
if (!this.referenceAdded) // try to add new property
entrySourceFile = this.findAndUpdateInitializer(entrySourceFile, this.addNewInitializerArgumentsPropertyArgumentsHandler);
// updated modified global declarations in files, such as `const value = []`
this.codebase.updateSourceFile(entrySourceFile);
for (const sourceFile of this.codebase.getModifiedSourceFiles()) {
this.codebase.updateSourceFile(this.updateGlobalFileDeclarations(sourceFile));
}
// add import statement to files that depend on it
for (const filePath of this.filesToAddImportInFilePaths) {
const sourceFile = this.codebase.getSourceFile(filePath);
if (sourceFile == null) {
this.linkingError = true;
break;
}
this.codebase.updateSourceFile(this.addImportToSourceFile(sourceFile));
this.importAdded = true;
}
if (!this.referenceAdded || this.linkingError || !this.importAdded)
return [];
const updatedFilePaths = this.codebase.writeChangesToFilesystem();
return updatedFilePaths;
}
findAndUpdateInitializer(sourceFile, initializerArgumentsHandler) {
return ts.factory.updateSourceFile(sourceFile, sourceFile.statements.map((statement) => {
if (this.referenceAdded)
return statement;
// const *something* = new Initializer()
// export const *something* = new Initializer()
if (ts.isVariableStatement(statement)) {
return ts.factory.updateVariableStatement(statement, statement.modifiers, ts.factory.updateVariableDeclarationList(statement.declarationList, statement.declarationList.declarations.map((declaration) => {
// const something = *new Initializer()*
// export const something = *new Initializer()*
if (!this.referenceAdded &&
ts.isVariableDeclaration(declaration) &&
declaration.initializer != null &&
ts.isNewExpression(declaration.initializer) &&
declaration.initializer.arguments != null) {
return ts.factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, ts.factory.updateNewExpression(declaration.initializer, declaration.initializer.expression, declaration.initializer.typeArguments, initializerArgumentsHandler(declaration.initializer.arguments, sourceFile)));
}
return declaration;
})
.map(this.updateDeclaration)));
}
// *new Initializer()*
if (ts.isExpressionStatement(statement) &&
ts.isNewExpression(statement.expression) &&
statement.expression.arguments != null &&
ts.isIdentifier(statement.expression.expression) &&
statement.expression.expression.getText() === this.initializerName) {
return ts.factory.updateExpressionStatement(statement, ts.factory.updateNewExpression(statement.expression, statement.expression.expression, statement.expression.typeArguments, initializerArgumentsHandler(statement.expression.arguments, sourceFile)));
}
// *export default* new Initializer()
if (ts.isExportAssignment(statement) &&
ts.isNewExpression(statement.expression) &&
statement.expression.arguments != null &&
ts.isIdentifier(statement.expression.expression) &&
statement.expression.expression.getText() === this.initializerName) {
return ts.factory.updateExportAssignment(statement, statement.decorators, statement.modifiers, ts.factory.updateNewExpression(statement.expression, statement.expression.expression, statement.expression.typeArguments, initializerArgumentsHandler(statement.expression.arguments, sourceFile)));
}
return statement;
}));
}
// new Initializer(*{ property: [] }*)
addToExistingInitializerArgumentsPropertyArgumentsHandler(initializerArguments, sourceFile) {
return initializerArguments.map((argument, index) => {
if (index > 0 || !ts.isObjectLiteralExpression(argument))
return argument;
return ts.factory.updateObjectLiteralExpression(argument, argument.properties.map((property) => {
if (ts.isPropertyAssignment(property) && property.name.getText() === this.initializerPropertyName) {
return ts.factory.updatePropertyAssignment(property, property.name, this.updateExpression(property.initializer, sourceFile));
}
else if (ts.isShorthandPropertyAssignment(property) &&
property.name.getText() === this.initializerPropertyName) {
const refSymbol = this.codebase.checker.getSymbolAtLocation(property.name);
if (refSymbol == null)
return property;
const refShorthandSymbol = this.codebase.checker.getShorthandAssignmentValueSymbol(refSymbol.valueDeclaration);
if (refShorthandSymbol == null || refShorthandSymbol.declarations == null)
return property;
for (const declaration of refShorthandSymbol.declarations)
this.handleDeclaration(declaration, sourceFile);
}
return property;
}));
});
}
// const value = *[ImportName]*
// const value = *{ImportName}*
// new Initializer({property: *[ImportName]*})
// new Initializer({property: *{ImportName}*})
createNewPropertyAssignment(sourceFile) {
if (this.instantiateObjectLiteralExpressionValuesByDefault)
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(this.initializerPropertyName), this.updateObjectLiteralExpression(ts.factory.createObjectLiteralExpression([], true), sourceFile));
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(this.initializerPropertyName), this.updateArrayLiteralExpression(ts.factory.createArrayLiteralExpression([], true), sourceFile));
}
// new Initializer(**)
addNewInitializerArgumentsPropertyArgumentsHandler(initializerArguments, sourceFile) {
if (initializerArguments.length === 0) {
return [
ts.factory.createObjectLiteralExpression([
this.createNewPropertyAssignment(sourceFile)
], true)
];
}
return initializerArguments.map((argument, index) => {
if (index > 0 || !ts.isObjectLiteralExpression(argument))
return argument;
const propertyNameAlreadyExists = argument.properties.some((property) => {
if (ts.isPropertyAssignment(property) &&
property.name.getText() === this.initializerPropertyName)
return true;
if (ts.isShorthandPropertyAssignment(property) &&
property.name.getText() === this.initializerPropertyName)
return true;
return false;
});
if (propertyNameAlreadyExists)
return argument;
return ts.factory.updateObjectLiteralExpression(argument, argument.properties.concat([
this.createNewPropertyAssignment(sourceFile)
]));
});
}
// Initializer({property: *value*})
// Initializer({property: *[]*})
updateExpression(expression, sourceFile) {
// [] as any[]
// <any[]>[]
if (isWithTypeExpression(expression))
return updateWithTypeExpression(expression, (valueExpression) => this.updateExpression(valueExpression, sourceFile));
// Initializer({property: *[]*})
if (ts.isArrayLiteralExpression(expression))
return this.updateArrayLiteralExpression(expression);
// Initializer({property: *{}*})
if (ts.isObjectLiteralExpression(expression) &&
this.treatObjectLiteralExpressionValuesAsList)
return this.updateObjectLiteralExpression(expression);
// Initializer({property: *value*})
// refSymbol is where this variable expression is referencing to
const refSymbol = this.codebase.checker.getSymbolAtLocation(expression);
if (refSymbol?.declarations == null)
return expression;
for (const declaration of refSymbol.declarations)
this.handleDeclaration(declaration, sourceFile);
return expression;
}
// [Something, *ImportName*]
updateArrayLiteralExpression(arrayLiteralExpression, sourceFile) {
if (sourceFile == null)
sourceFile = arrayLiteralExpression.getSourceFile();
if (sourceFile == null) {
this.linkingError = true;
return arrayLiteralExpression;
}
const itemAlreadyExists = arrayLiteralExpression.elements.some((element => {
if (ts.isIdentifier(element))
return element.getText() === this.importedFileImportName;
return false;
}));
if (itemAlreadyExists) {
this.referenceAdded = true;
this.importAdded = true;
return arrayLiteralExpression;
}
const res = ts.factory.updateArrayLiteralExpression(arrayLiteralExpression, arrayLiteralExpression.elements.concat([
ts.factory.createIdentifier(this.importedFileImportName)
]));
this.codebase.markSourceFileAsUpdated(sourceFile);
this.filesToAddImportInFilePaths.add(path.resolve(sourceFile.fileName));
this.referenceAdded = true;
return res;
}
// {Something, Something2: Something2, *ImportName*}
updateObjectLiteralExpression(objectLiteralExpression, sourceFile) {
if (sourceFile == null)
sourceFile = objectLiteralExpression.getSourceFile();
if (sourceFile == null) {
this.linkingError = true;
return objectLiteralExpression;
}
const itemAlreadyExists = objectLiteralExpression.properties.some((property => {
if (ts.isShorthandPropertyAssignment(property))
return property.name.getText() === this.importedFileImportName;
else if (ts.isPropertyAssignment(property))
return property.initializer.getText() === this.importedFileImportName;
return false;
}));
if (itemAlreadyExists) {
this.referenceAdded = true;
this.importAdded = true;
return objectLiteralExpression;
}
const res = ts.factory.updateObjectLiteralExpression(objectLiteralExpression, objectLiteralExpression.properties.concat([
ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(this.importedFileImportName))
]));
this.codebase.markSourceFileAsUpdated(sourceFile);
this.filesToAddImportInFilePaths.add(path.resolve(sourceFile.fileName));
this.referenceAdded = true;
return res;
}
handleDeclaration(declaration, sourceFile) {
// *const value = []*
if (ts.isVariableDeclaration(declaration) &&
declaration.initializer != null) {
if (!this.updateOtherRelevantFiles &&
path.resolve(sourceFile.fileName) != path.resolve(this.codebase.entryFilePath))
return;
let declarationUpdated = false;
const newDeclaration = ts.factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, updateWithTypeExpression(declaration.initializer, (valueExpression) => {
const resExpression = this.updateExpression(valueExpression, valueExpression.getSourceFile());
if (valueExpression != resExpression)
declarationUpdated = true;
return resExpression;
}));
if (declarationUpdated) {
this.declarationReplacementMap.set(declaration, newDeclaration);
this.codebase.markSourceFileAsUpdated(declaration.getSourceFile());
}
}
else if (
// *export []*
// *export {}*
ts.isExportAssignment(declaration)) {
if (!this.updateOtherRelevantFiles &&
path.resolve(sourceFile.fileName) != path.resolve(this.codebase.entryFilePath))
return;
let declarationUpdated = false;
const newDeclaration = ts.factory.updateExportAssignment(declaration, declaration.decorators, declaration.modifiers, updateWithTypeExpression(declaration.expression, (valueExpression) => {
const resExpression = this.updateExpression(valueExpression, valueExpression.getSourceFile());
if (valueExpression != resExpression)
declarationUpdated = true;
return resExpression;
}));
if (declarationUpdated) {
this.declarationReplacementMap.set(declaration, newDeclaration);
this.codebase.markSourceFileAsUpdated(declaration.getSourceFile());
}
}
else if (
// *import {value} from "./somewhere"*
ts.isImportSpecifier(declaration) ||
ts.isImportClause(declaration)) {
if (declaration.name == null)
return;
const declarationNamedImportSymbol = this.codebase.checker.getSymbolAtLocation(declaration.name);
if (declarationNamedImportSymbol == null)
return;
// get the referenced declaration in the imported file
const sourceDeclarationSymbol = this.codebase.checker.getAliasedSymbol(declarationNamedImportSymbol);
if (sourceDeclarationSymbol == null || sourceDeclarationSymbol.declarations == null)
return;
for (const declaration of sourceDeclarationSymbol.declarations)
this.handleDeclaration(declaration, sourceFile);
}
else if (
// *import * as value from "./somewhere"*
this.treatImportNamespaceAsList &&
ts.isNamespaceImport(declaration)) {
const declarationNamedImportSymbol = this.codebase.checker.getSymbolAtLocation(declaration.name);
if (declarationNamedImportSymbol == null)
return;
const sourceDeclarationSymbol = this.codebase.checker.getAliasedSymbol(declarationNamedImportSymbol);
if (sourceDeclarationSymbol.valueDeclaration == null ||
!ts.isSourceFile(sourceDeclarationSymbol.valueDeclaration))
return;
// get the latest file version known to the codebase for this imported file
let referenceSourceFile = this.codebase.getSourceFile(sourceDeclarationSymbol.valueDeclaration.fileName);
if (referenceSourceFile == null)
return;
if (!this.updateOtherRelevantFiles &&
path.resolve(referenceSourceFile.fileName) != path.resolve(this.codebase.entryFilePath))
return;
referenceSourceFile = this.codebase.updateSourceFile(this.addExportFromImportToSourceFile(referenceSourceFile));
this.codebase.markSourceFileAsUpdated(referenceSourceFile);
this.importAdded = true;
this.referenceAdded = true;
}
}
// export * from "./somewhere"
// export { Something } from "./somewhere"
// export { ExportName as ImportName } from "./somewhere"
addExportFromImportToSourceFile(sourceFile) {
let lastExportStatementIndex = 0;
for (let i = sourceFile.statements.length - 1; i >= 0; i--) {
const statement = sourceFile.statements[i];
if (ts.isExportDeclaration(statement) ||
ts.isImportDeclaration(statement)) {
lastExportStatementIndex = i;
break;
}
}
return ts.factory.updateSourceFile(sourceFile, [
...sourceFile.statements.slice(0, lastExportStatementIndex + 1),
ts.factory.createExportDeclaration(undefined, undefined, false, this.exportImportAllFromFileWhenImportingNamespace
? undefined // export * from "./file"
: ts.factory.createNamedExports([
ts.factory.createExportSpecifier(false, this.importDefault
? ts.factory.createIdentifier("default") // export { default as Something } from "./file"
: ((this.importedFileImportName == this.importedFileExportName || this.importedFileExportName == null)
? undefined // export { Something } from "./file"
: ts.factory.createIdentifier(
// export { ExportName as ImportName } from "./file"
this.importedFileExportName)), ts.factory.createIdentifier(this.importedFileImportName))
]), ts.factory.createStringLiteral(this.codebase.getRelativeImportPath(sourceFile.fileName, this.importedFilePath))),
...sourceFile.statements.slice(lastExportStatementIndex + 1)
]);
}
updateDeclaration(declaration) {
const res = this.declarationReplacementMap.get(declaration);
if (res != null) {
this.declarationReplacementMap.delete(declaration);
return res;
}
return declaration;
}
updateGlobalFileDeclarations(sourceFile) {
return ts.factory.updateSourceFile(sourceFile, sourceFile.statements.map((statement) => {
if (ts.isVariableStatement(statement)) {
return ts.factory.updateVariableStatement(statement, statement.modifiers, ts.factory.updateVariableDeclarationList(statement.declarationList, statement.declarationList.declarations.map(this.updateDeclaration)));
}
else if (ts.isExportAssignment(statement))
return this.updateDeclaration(statement);
return statement;
}));
}
// import Something from "./something"
// import { Something } from "./something"
// import { ExportName as ImportName } from "./something"
addImportToSourceFile(sourceFile) {
let lastImportStatementIndex = 0;
for (let i = sourceFile.statements.length - 1; i >= 0; i--) {
const statement = sourceFile.statements[i];
if (ts.isImportDeclaration(statement)) {
lastImportStatementIndex = i;
break;
}
}
return ts.factory.updateSourceFile(sourceFile, [
...sourceFile.statements.slice(0, lastImportStatementIndex + 1),
ts.factory.createImportDeclaration(undefined, undefined, this.importDefault
// import Something from "./something"
? ts.factory.createImportClause(false, ts.factory.createIdentifier(this.importedFileImportName), undefined)
: ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
ts.factory.createImportSpecifier(false, (this.importedFileImportName == this.importedFileExportName || this.importedFileExportName == null)
? undefined // import { Something } from "./something"
: ts.factory.createIdentifier(
// import { ExportName as ImportName } from "./something"
this.importedFileExportName), ts.factory.createIdentifier(this.importedFileImportName))
])), ts.factory.createStringLiteral(this.codebase.getRelativeImportPath(sourceFile.fileName, this.importedFilePath))),
...sourceFile.statements.slice(lastImportStatementIndex + 1)
]);
}
}
//# sourceMappingURL=ImportAndAddItemToInitializerArrayProperty.js.map