[REFACTOR] Twenty UI multi barrel (#11301)

# Introduction
closes https://github.com/twentyhq/core-team-issues/issues/591
Same than for `twenty-shared` made in
https://github.com/twentyhq/twenty/pull/11083.

## TODO
- [x] Manual migrate twenty-website twenty-ui imports

## What's next:
- Generate barrel and migration script factorization within own package
+ tests
- Refactoring using preconstruct ? TimeBox
- Lint circular dependencies
- Lint import from barrel and forbid them

### Preconstruct
We need custom rollup plugins addition, but preconstruct does not expose
its rollup configuration. It might be possible to handle this using the
babel overrides. But was a big tunnel.
We could give it a try afterwards ! ( allowing cjs interop and stuff
like that )
Stuck to vite lib app

Closed related PRs:
- https://github.com/twentyhq/twenty/pull/11294
- https://github.com/twentyhq/twenty/pull/11203
This commit is contained in:
Paul Rastoin
2025-04-03 11:47:55 +02:00
committed by GitHub
parent 8c9fcfe5a4
commit 4a4e65fe4a
1009 changed files with 5757 additions and 2828 deletions

View File

@ -1,105 +0,0 @@
import * as fs from 'fs';
import path from 'path';
import slash from 'slash';
const extensions = ['.ts', '.tsx'];
const excludedExtensions = [
'.test.ts',
'.test.tsx',
'.spec.ts',
'.spec.tsx',
'.stories.ts',
'.stories.tsx',
];
const excludedDirectories = [
'__tests__',
'__mocks__',
'__stories__',
'internal',
];
const srcPath = path.resolve('packages/twenty-ui/src');
/**
* @param {string} directoryPath
* @returns {string[]}
*/
const getSubDirectoryPaths = (directoryPath) =>
fs
.readdirSync(directoryPath)
.filter(
(fileOrDirectoryName) =>
!excludedDirectories.includes(fileOrDirectoryName) &&
fs
.statSync(path.join(directoryPath, fileOrDirectoryName))
.isDirectory(),
)
.map((subDirectoryName) => path.join(directoryPath, subDirectoryName));
/**
*
* @param {string} directoryPath
* @returns {string[]}
*/
const getDirectoryPathsRecursive = (directoryPath) => [
directoryPath,
...getSubDirectoryPaths(directoryPath).flatMap(getDirectoryPathsRecursive),
];
/**
*
* @param {string} directoryPath
* @returns {string[]}
*/
const getFilesPaths = (directoryPath) =>
fs
.readdirSync(directoryPath)
.filter(
(filePath) =>
fs.statSync(path.join(directoryPath, filePath)).isFile() &&
!filePath.startsWith('index.') &&
extensions.some((extension) => filePath.endsWith(extension)) &&
excludedExtensions.every(
(excludedExtension) => !filePath.endsWith(excludedExtension),
),
);
const moduleDirectories = getSubDirectoryPaths(srcPath);
moduleDirectories.forEach((moduleDirectoryPath) => {
const directoryPaths = getDirectoryPathsRecursive(moduleDirectoryPath);
const moduleExports = directoryPaths
.flatMap((directoryPath) => {
const directFilesPaths = getFilesPaths(directoryPath);
return directFilesPaths.map((filePath) => {
const fileName = filePath.split('.').slice(0, -1).join('.');
return `export * from './${slash(path.relative(
moduleDirectoryPath,
path.join(directoryPath, fileName),
))}';`;
});
})
.sort((a, b) => a.localeCompare(b))
.join('\n');
fs.writeFileSync(
path.join(moduleDirectoryPath, 'index.ts'),
`${moduleExports}\n`,
'utf-8',
);
});
const mainBarrelExports = moduleDirectories
.map(
(moduleDirectoryPath) =>
`export * from './${slash(path.relative(srcPath, moduleDirectoryPath))}';`,
)
.sort((a, b) => a.localeCompare(b))
.join('\n');
fs.writeFileSync(
path.join(srcPath, 'index.ts'),
`${mainBarrelExports}\n`,
'utf-8',
);

View File

@ -0,0 +1,484 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } from 'glob';
import path from 'path';
import { Options } from 'prettier';
import slash from 'slash';
import ts from 'typescript';
// TODO prastoin refactor this file in several one into its dedicated package and make it a TypeScript CLI
const INDEX_FILENAME = 'index';
const PACKAGE_JSON_FILENAME = 'package.json';
const NX_PROJECT_CONFIGURATION_FILENAME = 'project.json';
const PACKAGE_PATH = path.resolve('packages/twenty-ui');
const SRC_PATH = path.resolve(`${PACKAGE_PATH}/src`);
const PACKAGE_JSON_PATH = path.join(PACKAGE_PATH, PACKAGE_JSON_FILENAME);
const NX_PROJECT_CONFIGURATION_PATH = path.join(
PACKAGE_PATH,
NX_PROJECT_CONFIGURATION_FILENAME,
);
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
}
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
const prettierFormat = (str: string, parser: Options['parser']) =>
prettier.format(str, {
...prettierConfiguration,
parser,
});
type createTypeScriptFileArgs = {
path: string;
content: string;
filename: string;
};
const createTypeScriptFile = ({
content,
path: filePath,
filename,
}: createTypeScriptFileArgs) => {
const header = `
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
* |___/
*/
`;
const formattedContent = prettierFormat(
`${header}\n${content}\n`,
'typescript',
);
fs.writeFileSync(
path.join(filePath, `${filename}.ts`),
formattedContent,
'utf-8',
);
};
const getLastPathFolder = (pathStr: string) => path.basename(pathStr);
const getSubDirectoryPaths = (directoryPath: string): string[] => {
const pattern = slash(path.join(directoryPath, '*/'));
return globSync(pattern, {
ignore: [...EXCLUDED_DIRECTORIES],
cwd: SRC_PATH,
nodir: false,
maxDepth: 1,
}).sort((a, b) => a.localeCompare(b));
};
const partitionFileExportsByType = (declarations: DeclarationOccurence[]) => {
return declarations.reduce<{
typeAndInterfaceDeclarations: DeclarationOccurence[];
otherDeclarations: DeclarationOccurence[];
}>(
(acc, { kind, name }) => {
if (kind === 'type' || kind === 'interface') {
return {
...acc,
typeAndInterfaceDeclarations: [
...acc.typeAndInterfaceDeclarations,
{ kind, name },
],
};
}
return {
...acc,
otherDeclarations: [...acc.otherDeclarations, { kind, name }],
};
},
{
typeAndInterfaceDeclarations: [],
otherDeclarations: [],
},
);
};
const generateModuleIndexFiles = (exportByBarrel: ExportByBarrel[]) => {
return exportByBarrel.map<createTypeScriptFileArgs>(
({ barrel: { moduleDirectory }, allFileExports }) => {
const content = allFileExports
.sort((a, b) => a.file.localeCompare(b.file))
.map(({ exports, file }) => {
const { otherDeclarations, typeAndInterfaceDeclarations } =
partitionFileExportsByType(exports);
const fileWithoutExtension = path.parse(file).name;
const pathToImport = slash(
path.relative(
moduleDirectory,
path.join(path.dirname(file), fileWithoutExtension),
),
);
const mapDeclarationNameAndJoin = (
declarations: DeclarationOccurence[],
) => declarations.map(({ name }) => name).join(', ');
const typeExport =
typeAndInterfaceDeclarations.length > 0
? `export type { ${mapDeclarationNameAndJoin(typeAndInterfaceDeclarations)} } from "./${pathToImport}"`
: '';
const othersExport =
otherDeclarations.length > 0
? `export { ${mapDeclarationNameAndJoin(otherDeclarations)} } from "./${pathToImport}"`
: '';
return [typeExport, othersExport]
.filter((el) => el !== '')
.join('\n');
})
.join('\n');
return {
content,
path: moduleDirectory,
filename: INDEX_FILENAME,
};
},
);
};
type JsonUpdate = Record<string, any>;
type WriteInJsonFileArgs = {
content: JsonUpdate;
file: string;
};
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
const updatedJsonFile = JSON.stringify(content);
const formattedContent = prettierFormat(updatedJsonFile, 'json-stringify');
fs.writeFileSync(file, formattedContent, 'utf-8');
};
const writeInPackageJson = (update: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: PACKAGE_JSON_PATH,
content: {
...initialJsonFile,
...update,
},
});
};
const updateNxProjectConfigurationBuildOutputs = (outputs: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(NX_PROJECT_CONFIGURATION_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: NX_PROJECT_CONFIGURATION_PATH,
content: {
...initialJsonFile,
targets: {
...initialJsonFile.targets,
build: {
...initialJsonFile.targets.build,
outputs,
},
},
},
});
};
type ExportOccurence = {
types: string;
import: string;
require: string;
};
type ExportsConfig = Record<string, ExportOccurence | string>;
const generateModulePackageExports = (moduleDirectories: string[]) => {
return moduleDirectories.reduce<ExportsConfig>(
(acc, moduleDirectory) => {
const moduleName = getLastPathFolder(moduleDirectory);
if (moduleName === undefined) {
throw new Error(
`Should never occur, moduleName is undefined ${moduleDirectory}`,
);
}
return {
...acc,
[`./${moduleName}`]: {
types: `./dist/${moduleName}/index.d.ts`,
import: `./dist/${moduleName}.mjs`,
require: `./dist/${moduleName}.cjs`,
},
};
},
{
'./style.css': './dist/style.css',
},
);
};
const computePackageJsonFilesAndExportsConfig = (
moduleDirectories: string[],
) => {
const entrypoints = moduleDirectories.map(getLastPathFolder);
const exports = generateModulePackageExports(moduleDirectories);
return {
exports,
files: ['dist', 'assets', ...entrypoints],
};
};
const computeProjectNxBuildOutputsPath = (moduleDirectories: string[]) => {
const dynamicOutputsPath = moduleDirectories
.map(getLastPathFolder)
.flatMap((barrelName) =>
['package.json', 'dist'].map(
(subPath) => `{projectRoot}/${barrelName}/${subPath}`,
),
);
return ['{projectRoot}/dist', ...dynamicOutputsPath];
};
const EXCLUDED_EXTENSIONS = [
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.stories.ts',
'**/*.stories.tsx',
] as const;
const EXCLUDED_DIRECTORIES = [
'**/__tests__/**',
'**/__mocks__/**',
'**/__stories__/**',
'**/internal/**',
'**/assets/**',
] as const;
function getTypeScriptFiles(
directoryPath: string,
includeIndex: boolean = false,
): string[] {
const pattern = slash(path.join(directoryPath, '**', '*.{ts,tsx}'));
const files = globSync(pattern, {
cwd: SRC_PATH,
nodir: true,
ignore: [...EXCLUDED_EXTENSIONS, ...EXCLUDED_DIRECTORIES],
});
return files.filter(
(file) =>
!file.endsWith('.d.ts') &&
(includeIndex ? true : !file.endsWith('index.ts')),
);
}
const getKind = (
node: ts.VariableStatement,
): Extract<ExportKind, 'const' | 'let' | 'var'> => {
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 extractExportsFromSourceFile(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 && !ts.isExportDeclaration(node)) {
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) => {
const kind = getKind(node);
if (ts.isIdentifier(decl.name)) {
exports.push({
kind,
name: decl.name.text,
});
} else if (ts.isObjectBindingPattern(decl.name)) {
decl.name.elements.forEach((element) => {
if (
!ts.isBindingElement(element) ||
!ts.isIdentifier(element.name)
) {
return;
}
exports.push({
kind,
name: element.name.text,
});
});
}
});
break;
case ts.isClassDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'class',
name: node.name.text,
});
break;
case ts.isExportDeclaration(node):
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const exportName = element.name.text;
// Check both the declaration and the individual specifier for type-only exports
const isTypeExport =
node.isTypeOnly || ts.isTypeOnlyExportDeclaration(node);
if (isTypeExport) {
// should handle kind
exports.push({
kind: 'type',
name: exportName,
});
return;
}
exports.push({
kind: 'const',
name: exportName,
});
});
}
break;
}
return ts.forEachChild(node, visit);
}
visit(sourceFile);
return exports;
}
type ExportKind =
| 'type'
| 'interface'
| 'enum'
| 'function'
| 'const'
| 'let'
| 'var'
| 'class';
type DeclarationOccurence = { kind: ExportKind; name: string };
type FileExports = Array<{
file: string;
exports: DeclarationOccurence[];
}>;
function findAllExports(directoryPath: string): FileExports {
const results: FileExports = [];
const files = getTypeScriptFiles(directoryPath);
for (const file of files) {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const exports = extractExportsFromSourceFile(sourceFile);
if (exports.length > 0) {
results.push({
file,
exports,
});
}
}
return results;
}
type ExportByBarrel = {
barrel: {
moduleName: string;
moduleDirectory: string;
};
allFileExports: FileExports;
};
const retrieveExportsByBarrel = (barrelDirectories: string[]) => {
return barrelDirectories.map<ExportByBarrel>((moduleDirectory) => {
const moduleExportsPerFile = findAllExports(moduleDirectory);
const moduleName = getLastPathFolder(moduleDirectory);
if (!moduleName) {
throw new Error(
`Should never occur moduleName not found ${moduleDirectory}`,
);
}
return {
barrel: {
moduleName,
moduleDirectory,
},
allFileExports: moduleExportsPerFile,
};
});
};
const main = () => {
const moduleDirectories = getSubDirectoryPaths(SRC_PATH);
const exportsByBarrel = retrieveExportsByBarrel(moduleDirectories);
const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel);
const packageJsonConfig =
computePackageJsonFilesAndExportsConfig(moduleDirectories);
const nxBuildOutputsPath =
computeProjectNxBuildOutputsPath(moduleDirectories);
updateNxProjectConfigurationBuildOutputs(nxBuildOutputsPath);
writeInPackageJson(packageJsonConfig);
moduleIndexFiles.forEach(createTypeScriptFile);
};
main();

View File

@ -0,0 +1,527 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } 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,d.ts}');
const files = globSync(pattern);
return files.filter(
(file) =>
(includeIndex ? true : !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 && !ts.isExportDeclaration(node)) {
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) => {
const kind = getKind(node);
if (ts.isIdentifier(decl.name)) {
exports.push({
kind,
name: decl.name.text,
});
} else if (ts.isObjectBindingPattern(decl.name)) {
decl.name.elements.forEach((element) => {
if (
!ts.isBindingElement(element) ||
!ts.isIdentifier(element.name)
) {
return;
}
exports.push({
kind,
name: element.name.text,
});
});
}
});
break;
case ts.isClassDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'class',
name: node.name.text,
});
break;
case ts.isExportDeclaration(node):
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const exportName = element.name.text;
// Check both the declaration and the individual specifier for type-only exports
const isTypeExport =
node.isTypeOnly || ts.isTypeOnlyExportDeclaration(node);
if (isTypeExport) {
// should handle kind
exports.push({
kind: 'type',
name: exportName,
});
return;
}
exports.push({
kind: 'const',
name: exportName,
});
});
}
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-ui'` && modulePath !== '"twenty-ui"') {
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<ExportPerModule[number]>((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<string, NewImport>;
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-ui/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-ui',
});
insertImportAtTop({
filePath: file,
importSpecifier: `twenty-ui/${barrel}`,
namedImports: modules,
});
}
}
};
const main = () => {
const packageSrcPath = 'packages/twenty-ui/src';
const exportsPerModule = retrievePackageExportsPerModule(packageSrcPath);
const packagesToMigrate = ['twenty-front'];
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();