import prettier from '@prettier/sync'; import * as fs from 'fs'; import glob from 'glob'; import * as path from 'path'; import ts from 'typescript'; const prettierConfigFile = prettier.resolveConfigFile(); if (prettierConfigFile == null) { throw new Error('Prettier config file not found'); } const prettierConfiguration = prettier.resolveConfig(prettierConfigFile); type DeclarationOccurence = { kind: string; name: string }; type ExtractedExports = Array<{ file: string; exports: DeclarationOccurence[]; }>; type ExtractedImports = Array<{ file: string; imports: string[] }>; type ExportPerModule = Array<{ moduleName: string; exports: ExtractedExports[number]['exports']; }>; function findAllExports(directoryPath: string): ExtractedExports { const results: ExtractedExports = []; const files = getTypeScriptFiles(directoryPath); for (const file of files) { const sourceFile = ts.createSourceFile( file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, ); const exports = extractExports(sourceFile); if (exports.length > 0) { results.push({ file, exports, }); } } return results; } function findAllImports(directoryPath: string): ExtractedImports { const results: ExtractedImports = []; const includeIndex = true; const files = getTypeScriptFiles(directoryPath, includeIndex); for (const file of files) { try { const sourceFile = ts.createSourceFile( file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, ); const imports = extractImports(sourceFile); if (imports.length > 0) { results.push({ file, imports, }); } } catch (e) { console.log(e); console.log('Because of file: ', file); throw e; } } return results; } function getTypeScriptFiles( directoryPath: string, includeIndex: boolean = false, ): string[] { const pattern = path.join(directoryPath, '**/*.{ts,tsx}'); const files = glob.sync(pattern); return files.filter( (file) => !file.endsWith('.d.ts') && (includeIndex ? true : !file.endsWith('index.ts')), ); } const getKind = (node: ts.VariableStatement) => { const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0; if (isConst) { return 'const'; } const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0; if (isLet) { return 'let'; } return 'var'; }; function extractExports(sourceFile: ts.SourceFile) { const exports: DeclarationOccurence[] = []; function visit(node: ts.Node) { if (!ts.canHaveModifiers(node)) { return ts.forEachChild(node, visit); } const modifiers = ts.getModifiers(node); const isExport = modifiers?.some( (mod) => mod.kind === ts.SyntaxKind.ExportKeyword, ); if (!isExport) { return ts.forEachChild(node, visit); } switch (true) { case ts.isTypeAliasDeclaration(node): exports.push({ kind: 'type', name: node.name.text, }); break; case ts.isInterfaceDeclaration(node): exports.push({ kind: 'interface', name: node.name.text, }); break; case ts.isEnumDeclaration(node): exports.push({ kind: 'enum', name: node.name.text, }); break; case ts.isFunctionDeclaration(node) && node.name !== undefined: exports.push({ kind: 'function', name: node.name.text, }); break; case ts.isVariableStatement(node): node.declarationList.declarations.forEach((decl) => { if (ts.isIdentifier(decl.name)) { const kind = getKind(node); exports.push({ kind, name: decl.name.text, }); } }); break; case ts.isClassDeclaration(node) && node.name !== undefined: exports.push({ kind: 'class', name: node.name.text, }); break; } return ts.forEachChild(node, visit); } visit(sourceFile); return exports; } function extractImports(sourceFile: ts.SourceFile): string[] { const imports: string[] = []; function visit(node: ts.Node) { if (!ts.isImportDeclaration(node)) { return ts.forEachChild(node, visit); } const modulePath = node.moduleSpecifier.getText(sourceFile); // Quite static if (modulePath !== `'twenty-shared'`) { return ts.forEachChild(node, visit); } if (!node.importClause) { return ts.forEachChild(node, visit); } if (!node.importClause.namedBindings) { return ts.forEachChild(node, visit); } if (ts.isNamedImports(node.importClause.namedBindings)) { const namedImports = node.importClause.namedBindings.elements.map( (element) => { if (element.propertyName) { return `${element.propertyName.text} as ${element.name.text}`; } return element.name.text; }, ); // imports.push(`import { ${namedImports} } from ${modulePath}`); namedImports.forEach((namedImport) => { imports.push(namedImport); }); } ts.forEachChild(node, visit); } visit(sourceFile); return imports; } const getSubDirectoryPaths = (directoryPath: string): string[] => fs .readdirSync(directoryPath) .filter((fileOrDirectoryName) => { const isDirectory = fs .statSync(path.join(directoryPath, fileOrDirectoryName)) .isDirectory(); return isDirectory; }) .map((subDirectoryName) => path.join(directoryPath, subDirectoryName)); const retrievePackageExportsPerModule = (srcPath: string) => { const subdirectories = getSubDirectoryPaths(srcPath); return subdirectories.map((moduleDirectory) => { const moduleExportsPerFile = findAllExports(moduleDirectory); const moduleName = moduleDirectory.split('/').pop(); if (!moduleName) { throw new Error( `Should never occurs moduleName not found ${moduleDirectory}`, ); } const flattenExports = Object.values(moduleExportsPerFile).flatMap( (arr) => arr.exports, ); return { moduleName, exports: flattenExports, }; }); }; type NewImport = { barrel: string; modules: string[] }; type MappedResolution = { newImports: Record; file: string; }; type MapSourceImportToBarrelArgs = { importsPerFile: ExtractedImports; exportsPerModule: ExportPerModule; }; const mapSourceImportToBarrel = ({ exportsPerModule, importsPerFile, }: MapSourceImportToBarrelArgs): MappedResolution[] => { const mappedResolution: MappedResolution[] = []; for (const fileImport of importsPerFile) { const { file, imports } = fileImport; let result: MappedResolution = { file, newImports: {}, }; for (const importedDeclaration of imports) { const findResult = exportsPerModule.find(({ exports }) => exports.some((el) => el.name === importedDeclaration), ); if (findResult === undefined) { throw new Error( `Should never occurs no barrel exports ${importedDeclaration}`, ); } const { moduleName } = findResult; if (result.newImports[moduleName]) { result.newImports[moduleName].modules.push(importedDeclaration); } else { result.newImports[moduleName] = { barrel: moduleName, modules: [importedDeclaration], }; } } mappedResolution.push(result); } return mappedResolution; }; const retrieveImportFromPackageInSource = (srcPath: string) => { return findAllImports(srcPath); }; /** * Inserts a new import statement at the top of a TypeScript file * @param filePath Path to the TypeScript file * @param importSpecifier The module to import from (e.g., 'twenty-shared/utils') * @param namedImports Array of named imports (e.g., ['useQuery', 'useMutation']) */ type InsertImportAtTopArgs = { filePath: string; importSpecifier: string; namedImports: string[]; }; function insertImportAtTop({ filePath, importSpecifier, namedImports, }: InsertImportAtTopArgs): void { // Read the file content const sourceText = fs.readFileSync(filePath, 'utf8'); // Create a source file const sourceFile = ts.createSourceFile( filePath, sourceText, ts.ScriptTarget.Latest, true, ); // Build the new import statement let newImport = `import { ${namedImports.join(', ')} } from '${importSpecifier}';\n`; // Find the position to insert the import let insertPos = 0; // Case 1: File has imports - insert after the last import let lastImportEnd = 0; ts.forEachChild(sourceFile, (node) => { if ( ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node) || (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && node.expression.expression.kind === ts.SyntaxKind.ImportKeyword) // Overkill ? ) { const end = node.getEnd(); if (end > lastImportEnd) { lastImportEnd = end; } } }); if (lastImportEnd > 0) { // Insert after the last import with a newline insertPos = lastImportEnd; // Check if there's already a newline after the last import if (sourceText[insertPos] !== '\n') { newImport = '\n' + newImport; } } // Insert the new import const updatedSourceText = sourceText.substring(0, insertPos) + newImport + sourceText.substring(insertPos); // Write back to file fs.writeFileSync( filePath, prettier.format(updatedSourceText, { parser: 'typescript', ...prettierConfiguration, }), 'utf8', ); } type RemoveSpecificImports = { filePath: string; moduleSpecifier: string; }; function removeSpecificImports({ filePath, moduleSpecifier, }: RemoveSpecificImports) { const sourceText = fs.readFileSync(filePath, 'utf8'); const sourceFile = ts.createSourceFile( filePath, sourceText, ts.ScriptTarget.Latest, true, ); type Replacement = { start: number; end: number; newText: string; }; let replacement: Replacement | undefined; function visit(node: ts.Node) { if (ts.isImportDeclaration(node)) { const importSource = node.moduleSpecifier .getText(sourceFile) .replace(/^['"]|['"]$/g, ''); if (importSource === moduleSpecifier && node.importClause) { replacement = { start: node.getFullStart(), end: node.getEnd(), newText: '', }; } } ts.forEachChild(node, visit); } visit(sourceFile); let updatedSourceText = sourceText; if (replacement) { const { end, newText, start } = replacement; updatedSourceText = updatedSourceText.substring(0, start) + newText + updatedSourceText.substring(end); fs.writeFileSync( filePath, prettier.format(updatedSourceText, { parser: 'typescript', ...prettierConfiguration, }), 'utf8', ); } } const migrateImports = (mappedResolutions: MappedResolution[]) => { for (const { file, newImports } of mappedResolutions) { for (const { barrel, modules } of Object.values(newImports)) { // TODO could refactor to avoid double source file and read removeSpecificImports({ filePath: file, moduleSpecifier: 'twenty-shared', }); insertImportAtTop({ filePath: file, importSpecifier: `twenty-shared/${barrel}`, namedImports: modules, }); } } }; const main = () => { const packageSrcPath = 'packages/twenty-shared/src'; const exportsPerModule = retrievePackageExportsPerModule(packageSrcPath); const packagesToMigrate = [ 'twenty-front', 'twenty-ui', 'twenty-server', 'twenty-emails', 'twenty-zapier', 'twenty-chrome-extension', ]; for (const currPackage of packagesToMigrate) { console.log(`About to run over ${currPackage}`); const importsPerFile = retrieveImportFromPackageInSource( `packages/${currPackage}`, ); const mappedResolutions = mapSourceImportToBarrel({ exportsPerModule, importsPerFile, }); migrateImports(mappedResolutions); console.log(`${currPackage} migrated`); } console.log('SUCCESSFULLY COMPLETED'); }; main();