diff --git a/eslint.config.mjs b/eslint.config.mjs index 1160ca4666..8f8685622e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,9 @@ import unicornPlugin from 'eslint-plugin-unicorn'; import unusedImportsPlugin from 'eslint-plugin-unused-imports'; import jsoncParser from 'jsonc-eslint-parser'; -const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules'); +const twentyRules = await nxPlugin.loadWorkspaceRules( + 'packages/twenty-eslint-rules', +); export default [ // Base JavaScript configuration @@ -65,6 +67,10 @@ export default [ sourceTag: 'scope:sdk', onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'], }, + { + sourceTag: 'scope:create-app', + onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'], + }, { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'], @@ -197,7 +203,7 @@ export default [ plugins: { ...mdxPlugin.flat.plugins, '@nx': nxPlugin, - 'twenty': { rules: twentyRules }, + twenty: { rules: twentyRules }, }, }, mdxPlugin.flatCodeBlocks, diff --git a/packages/create-twenty-app/eslint.config.mjs b/packages/create-twenty-app/eslint.config.mjs index 1609e83403..237549301d 100644 --- a/packages/create-twenty-app/eslint.config.mjs +++ b/packages/create-twenty-app/eslint.config.mjs @@ -1,111 +1,20 @@ -import js from '@eslint/js'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import prettierPlugin from 'eslint-plugin-prettier'; +import baseConfig from '../../eslint.config.mjs'; export default [ - js.configs.recommended, + ...baseConfig, { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parser: typescriptParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - globals: { - // Node.js globals - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - __dirname: 'readonly', - __filename: 'readonly', - global: 'readonly', - setTimeout: 'readonly', - clearTimeout: 'readonly', - setInterval: 'readonly', - clearInterval: 'readonly', - // Browser globals that Node.js also has - URL: 'readonly', - URLSearchParams: 'readonly', - // Node.js types - NodeJS: 'readonly', - }, - }, - plugins: { - '@typescript-eslint': typescriptEslint, - prettier: prettierPlugin, - }, + ignores: ['**/dist/**'], + }, + { + files: ['**/*.{js,jsx,ts,tsx}'], rules: { - ...typescriptEslint.configs.recommended.rules, 'prettier/prettier': 'error', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'no-useless-escape': 'off', }, }, { - files: ['**/*.js'], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - __dirname: 'readonly', - __filename: 'readonly', - global: 'readonly', - }, - }, - }, - { - files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'], - languageOptions: { - parser: typescriptParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - globals: { - // Node.js globals - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - __dirname: 'readonly', - __filename: 'readonly', - global: 'readonly', - // Jest globals - describe: 'readonly', - it: 'readonly', - test: 'readonly', - expect: 'readonly', - jest: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - }, - }, - plugins: { - '@typescript-eslint': typescriptEslint, - prettier: prettierPlugin, - }, rules: { - ...typescriptEslint.configs.recommended.rules, - 'prettier/prettier': 'error', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'no-useless-escape': 'off', + 'no-console': 'off', }, - }, - { - ignores: ['dist/**', 'node_modules/**'], + ignores: ['src/**/*.ts', '!src/cli/**/*.ts'], }, ]; diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index c2f7d4a4eb..3b0a82f504 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "0.4.3", + "version": "0.4.4", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", @@ -8,7 +8,9 @@ "dist/**/*" ], "scripts": { - "build": "npx rimraf dist && npx vite build" + "build": "npx rimraf dist && npx vite build", + "prepack": "tsx ../twenty-utils/pack-scripts/prepack.ts", + "postpack": "tsx ../twenty-utils/pack-scripts/postpack.ts" }, "keywords": [ "twenty", @@ -34,6 +36,7 @@ "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.startcase": "^4.4.0", + "twenty-shared": "workspace:*", "uuid": "^13.0.0" }, "devDependencies": { @@ -43,6 +46,7 @@ "@types/lodash.kebabcase": "^4.1.7", "@types/lodash.startcase": "^4", "@types/node": "^20.0.0", + "typescript": "^5.9.2", "vite": "^7.0.0", "vite-plugin-dts": "^4.5.4", "vite-tsconfig-paths": "^4.2.1" diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index b0c07711f4..958a5aac06 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -70,7 +70,7 @@ describe('copyBaseApplicationProject', () => { const packageJson = await fs.readJson(packageJsonPath); expect(packageJson.name).toBe('my-test-app'); expect(packageJson.version).toBe('0.1.0'); - expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.3'); + expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.4'); expect(packageJson.scripts['app:dev']).toBe('twenty app:dev'); }); diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 9fb5281b75..436cb96259 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -282,7 +282,7 @@ const createPackageJson = async ({ 'lint:fix': 'eslint --fix', }, dependencies: { - 'twenty-sdk': '0.4.3', + 'twenty-sdk': '0.4.4', }, devDependencies: { typescript: '^5.9.3', diff --git a/packages/create-twenty-app/tsconfig.json b/packages/create-twenty-app/tsconfig.json index d5bed1aa71..3e5c64e6d7 100644 --- a/packages/create-twenty-app/tsconfig.json +++ b/packages/create-twenty-app/tsconfig.json @@ -12,7 +12,8 @@ "types": ["jest", "node"], "paths": { "@/*": ["./src/*"] - } + }, + "jsx": "react" }, "include": [ "src/**/*.ts", diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 859177cd9e..9a21c41b99 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "0.4.3", + "version": "0.4.4", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", @@ -14,8 +14,8 @@ ], "scripts": { "build": "npx rimraf dist && npx vite build", - "prepack": "tsx ./scripts/prepack.ts", - "postpack": "tsx ./scripts/postpack.ts" + "prepack": "tsx ../twenty-utils/pack-scripts/prepack.ts", + "postpack": "tsx ../twenty-utils/pack-scripts/postpack.ts" }, "keywords": [ "twenty", diff --git a/packages/twenty-sdk/vite.config.ts b/packages/twenty-sdk/vite.config.ts index b8f4d20ae8..aeaa1cbaec 100644 --- a/packages/twenty-sdk/vite.config.ts +++ b/packages/twenty-sdk/vite.config.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs-extra'; import path from 'path'; import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; @@ -12,8 +11,6 @@ const entries = [ 'src/front-component/index.ts', ]; -export const PACKAGES_TO_VENDOR = ['twenty-ui', 'twenty-shared']; - const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => { if (!chunk.isEntry) { throw new Error( @@ -32,20 +29,6 @@ const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => { return `${modulePath}/index.${extension}`; }; -const copyTwentyPackagesInVendor = (packages: string[]) => { - return packages.map((packageName) => ({ - name: `copy-${packageName}-dist`, - closeBundle: async () => { - const sharedDist = path.resolve(__dirname, `../${packageName}/dist`); - const vendorDist = path.resolve(__dirname, `dist/vendor/${packageName}`); - - await fs.remove(vendorDist); - await fs.ensureDir(path.dirname(vendorDist)); - await fs.copy(sharedDist, vendorDist); - }, - })); -}; - export default defineConfig(() => { const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json'); @@ -61,36 +44,7 @@ export default defineConfig(() => { tsconfigPaths({ root: __dirname, }), - ...copyTwentyPackagesInVendor(PACKAGES_TO_VENDOR), - dts({ - entryRoot: './src', - tsconfigPath: tsConfigPath, - exclude: ['vite.config.ts'], - beforeWriteFile: (filePath, content) => { - const fromDir = path.dirname(filePath); - const vendorDir = path.resolve(process.cwd(), 'dist/vendor'); - - let rel = path - .relative(fromDir, vendorDir) - .split(path.sep) - .join(path.posix.sep); - if (!rel.startsWith('.')) rel = `./${rel}`; - - const formattedContent = PACKAGES_TO_VENDOR.reduce((acc, pkg) => { - const regex = new RegExp( - `(from\\s+["'])${pkg}(\\/[^"']*)?(["'])`, - 'g', - ); - - return acc.replace(regex, `$1${rel}/${pkg}$2$3`); - }, content); - - return { - filePath, - content: formattedContent, - }; - }, - }), + dts({ entryRoot: './src', tsconfigPath: tsConfigPath }), ], worker: { format: 'iife', @@ -124,9 +78,7 @@ export default defineConfig(() => { warn(warning); }, external: [ - ...Object.keys((packageJson as any).dependencies || {}).filter( - (dep) => !PACKAGES_TO_VENDOR.includes(dep), - ), + ...Object.keys((packageJson as any).dependencies || {}), 'path', 'fs', 'fs/promises', diff --git a/packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts b/packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts deleted file mode 100644 index 1e951cb089..0000000000 --- a/packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts +++ /dev/null @@ -1,494 +0,0 @@ -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 DeclarationOccurrence = { kind: string; name: string }; -type ExtractedExports = Array<{ - file: string; - exports: DeclarationOccurrence[]; -}>; -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 = globSync(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: DeclarationOccurrence[] = []; - - 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', - ]; - 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(); diff --git a/packages/twenty-ui/scripts/migrateFromSingleToMultiBarrelImport.ts b/packages/twenty-ui/scripts/migrateFromSingleToMultiBarrelImport.ts deleted file mode 100644 index 834209be7a..0000000000 --- a/packages/twenty-ui/scripts/migrateFromSingleToMultiBarrelImport.ts +++ /dev/null @@ -1,527 +0,0 @@ -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 DeclarationOccurrence = { kind: string; name: string }; -type ExtractedExports = Array<{ - file: string; - exports: DeclarationOccurrence[]; -}>; -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: DeclarationOccurrence[] = []; - - 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((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-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(); diff --git a/packages/twenty-utils/pack-scripts/constants.ts b/packages/twenty-utils/pack-scripts/constants.ts new file mode 100644 index 0000000000..4cc1b5cfef --- /dev/null +++ b/packages/twenty-utils/pack-scripts/constants.ts @@ -0,0 +1,7 @@ +import path from 'path'; + +export const PKG_PATH = path.resolve(process.cwd(), 'package.json'); +export const BACKUP_PATH = path.resolve( + process.cwd(), + 'package.__backup__.json', +); diff --git a/packages/twenty-sdk/scripts/postpack.ts b/packages/twenty-utils/pack-scripts/postpack.ts similarity index 79% rename from packages/twenty-sdk/scripts/postpack.ts rename to packages/twenty-utils/pack-scripts/postpack.ts index 586be7afc1..030cb8c1d6 100644 --- a/packages/twenty-sdk/scripts/postpack.ts +++ b/packages/twenty-utils/pack-scripts/postpack.ts @@ -1,10 +1,10 @@ import fs from 'fs-extra'; -import { BACKUP_PATH, PKG_PATH } from './prepack'; +import { BACKUP_PATH, PKG_PATH } from './constants'; const main = async () => { if (await fs.pathExists(BACKUP_PATH)) { const original = await fs.readJson(BACKUP_PATH); - await fs.rm(PKG_PATH); + await fs.remove(PKG_PATH); await fs.writeJson(PKG_PATH, original, { spaces: 2 }); await fs.remove(BACKUP_PATH); } diff --git a/packages/twenty-sdk/scripts/prepack.ts b/packages/twenty-utils/pack-scripts/prepack.ts similarity index 58% rename from packages/twenty-sdk/scripts/prepack.ts rename to packages/twenty-utils/pack-scripts/prepack.ts index 579795143d..455c4bf9ce 100644 --- a/packages/twenty-sdk/scripts/prepack.ts +++ b/packages/twenty-utils/pack-scripts/prepack.ts @@ -1,18 +1,9 @@ import fs from 'fs-extra'; -import path from 'path'; -import { PACKAGES_TO_VENDOR } from '../vite.config'; - -export const PKG_PATH = path.resolve(process.cwd(), 'package.json'); -export const BACKUP_PATH = path.resolve( - process.cwd(), - 'package.__backup__.json', -); +import { BACKUP_PATH, PKG_PATH } from './constants'; const stripWorkspace = (deps = {}) => Object.fromEntries( - Object.entries(deps).filter( - ([key, _]) => !PACKAGES_TO_VENDOR.includes(key), - ), + Object.entries(deps).filter(([_, value]) => !value.startsWith('workspace:')), ); const main = async () => { diff --git a/yarn.lock b/yarn.lock index 431986b7ad..3ddfa82e9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31097,6 +31097,8 @@ __metadata: lodash.camelcase: "npm:^4.3.0" lodash.kebabcase: "npm:^4.1.1" lodash.startcase: "npm:^4.4.0" + twenty-shared: "workspace:*" + typescript: "npm:^5.9.2" uuid: "npm:^13.0.0" vite: "npm:^7.0.0" vite-plugin-dts: "npm:^4.5.4"