diff --git a/packages/twenty-sdk/jest.config.mjs b/packages/twenty-sdk/jest.config.mjs deleted file mode 100644 index 98ad3b46d1b..00000000000 --- a/packages/twenty-sdk/jest.config.mjs +++ /dev/null @@ -1,40 +0,0 @@ -const jestConfig = { - displayName: 'twenty-cli', - preset: '../../jest.preset.js', - testEnvironment: 'node', - transformIgnorePatterns: ['../../node_modules/'], - transform: { - '^.+\\.[tj]sx?$': [ - '@swc/jest', - { - jsc: { - parser: { syntax: 'typescript', tsx: false }, - }, - }, - ], - }, - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - moduleFileExtensions: ['ts', 'js'], - extensionsToTreatAsEsm: ['.ts'], - coverageDirectory: './coverage', - testMatch: [ - '/src/**/__tests__/**/*.(test|spec).{js,ts}', - '/src/**/?(*.)(test|spec).{js,ts}', - ], - collectCoverageFrom: [ - 'src/**/*.{ts,js}', - '!src/**/*.d.ts', - '!src/cli/cli.ts', - ], - coverageThreshold: { - global: { - statements: 1, - lines: 1, - functions: 1, - }, - }, -}; - -export default jestConfig; diff --git a/packages/twenty-sdk/jest.e2e.config.ts b/packages/twenty-sdk/jest.e2e.config.ts deleted file mode 100644 index b91fcfb3743..00000000000 --- a/packages/twenty-sdk/jest.e2e.config.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type JestConfigWithTsJest } from 'ts-jest'; - -const jestConfig: JestConfigWithTsJest = { - // For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string - // Prettier v3 should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1 - prettierPath: null, - displayName: 'twenty-sdk-e2e', - silent: false, - errorOnDeprecated: true, - maxConcurrency: 1, - moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: '.', - testEnvironment: 'node', - testRegex: '\\.e2e-spec\\.ts$', - modulePathIgnorePatterns: ['/dist'], - globalTeardown: '/src/cli/__tests__/e2e/teardown.ts', - setupFilesAfterEnv: ['/src/cli/__tests__/e2e/setupTest.ts'], - testTimeout: 30000, // 30 seconds timeout for e2e tests - maxWorkers: 1, - transform: { - '^.+\\.(t|j)s$': [ - '@swc/jest', - { - jsc: { - parser: { - syntax: 'typescript', - tsx: false, - decorators: true, - }, - transform: { - decoratorMetadata: true, - }, - baseUrl: '.', - paths: { - 'src/*': ['./src/*'], - }, - }, - }, - ], - }, - transformIgnorePatterns: [ - 'node_modules/(?!(chalk|inquirer|@inquirer|ansi-styles|strip-ansi|has-flag|supports-color|color-convert|color-name|wrap-ansi|string-width|is-fullwidth-code-point|emoji-regex|onetime|mimic-fn|signal-exit|yallist|lru-cache|p-limit|p-queue|p-timeout|p-finally|p-try|p-cancelable|p-locate|p-map|p-race|p-reduce|p-some|p-waterfall|p-defer|p-delay|p-retry|p-any|p-settle|p-all|p-map-series|p-map-concurrent|p-filter|p-reject|p-tap|p-log|p-debounce|p-throttle|p-forever|p-whilst|p-do-whilst|p-until|p-wait-for|p-min-delay)/)', - ], - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, -}; - -export default jestConfig; diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 1a622e404ac..2007a866cf2 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -35,7 +35,6 @@ "archiver": "^7.0.1", "axios": "^1.6.0", "chalk": "^5.3.0", - "chokidar": "^4.0.0", "commander": "^12.0.0", "dotenv": "^16.4.0", "fast-glob": "^3.3.0", @@ -43,7 +42,6 @@ "graphql": "^16.8.1", "graphql-sse": "^2.5.4", "inquirer": "^10.0.0", - "jiti": "^2.0.0", "jsonc-parser": "^3.2.0", "lodash.camelcase": "^4.3.0", "lodash.capitalize": "^4.2.1", @@ -58,14 +56,12 @@ "@types/archiver": "^6.0.0", "@types/fs-extra": "^11.0.0", "@types/inquirer": "^9.0.0", - "@types/jest": "^29.5.0", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.capitalize": "^4", "@types/lodash.kebabcase": "^4.1.7", "@types/lodash.startcase": "^4", "@types/node": "^24.0.0", "@types/react": "^19.0.2", - "jest": "^29.5.0", "tsx": "^4.7.0", "vite-plugin-dts": "^4.5.4", "wait-on": "^7.2.0" diff --git a/packages/twenty-sdk/project.json b/packages/twenty-sdk/project.json index 2fa328444f7..0c8e2ea2270 100644 --- a/packages/twenty-sdk/project.json +++ b/packages/twenty-sdk/project.json @@ -72,18 +72,17 @@ } }, "test": { - "executor": "@nx/jest:jest", + "executor": "@nx/vitest:test", "outputs": [ "{workspaceRoot}/coverage/{projectRoot}" ], "options": { - "jestConfig": "{projectRoot}/jest.config.mjs" + "config": "{projectRoot}/vitest.config.ts" }, "configurations": { "ci": { - "ci": true, "coverage": true, - "watchAll": false + "watch": false } } }, @@ -92,7 +91,7 @@ "options": { "cwd": "packages/twenty-sdk", "commands": [ - "npx wait-on http://localhost:3000/healthz --timeout 600000 --interval 1000 --log && NODE_ENV=test npx jest --config ./jest.e2e.config.ts" + "npx wait-on http://localhost:3000/healthz --timeout 600000 --interval 1000 --log && NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts" ] }, "parallel": false, diff --git a/packages/twenty-sdk/src/cli/__tests__/e2e/setupTest.ts b/packages/twenty-sdk/src/cli/__tests__/e2e/setupTest.ts index b4150d331f7..788a6336e49 100644 --- a/packages/twenty-sdk/src/cli/__tests__/e2e/setupTest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/e2e/setupTest.ts @@ -1,12 +1,11 @@ import { ConfigService } from '@/cli/utilities/config/services/config.service'; import { testConfig } from '@/cli/__tests__/e2e/constants/testConfig'; +import { vi, beforeAll, afterAll } from 'vitest'; beforeAll(() => { - jest - .spyOn(ConfigService.prototype, 'getConfig') - .mockResolvedValue(testConfig); + vi.spyOn(ConfigService.prototype, 'getConfig').mockResolvedValue(testConfig); }); afterAll(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json b/packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json new file mode 100644 index 00000000000..32ae947da35 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["../../../../src/*"] + } + }, + "include": ["src/**/*"] +} diff --git a/packages/twenty-sdk/src/cli/commands/app/app-sync.ts b/packages/twenty-sdk/src/cli/commands/app/app-sync.ts index 90b2018f421..103291f8529 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-sync.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-sync.ts @@ -1,8 +1,10 @@ import { ApiService } from '@/cli/utilities/api/services/api.service'; import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types'; -import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; +import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import path from 'path'; export class AppSyncCommand { private apiService = new ApiService(); @@ -20,8 +22,16 @@ export class AppSyncCommand { return { success: false, error: 'Build failed' }; } + const yarnLockPath = path.join(appPath, 'yarn.lock'); + let yarnLock = ''; + + if (await fs.pathExists(yarnLockPath)) { + yarnLock = await fs.readFile(yarnLockPath, 'utf8'); + } + const serverlessSyncResult = await this.apiService.syncApplication({ manifest, + yarnLock, }); if (serverlessSyncResult.success === false) { diff --git a/packages/twenty-sdk/src/cli/utilities/api/services/api.service.ts b/packages/twenty-sdk/src/cli/utilities/api/services/api.service.ts index bc62e3108a3..ec82e0961e2 100644 --- a/packages/twenty-sdk/src/cli/utilities/api/services/api.service.ts +++ b/packages/twenty-sdk/src/cli/utilities/api/services/api.service.ts @@ -89,18 +89,22 @@ export class ApiService { async syncApplication({ manifest, + yarnLock, }: { manifest: ApplicationManifest; + yarnLock: string; }): Promise { try { const mutation = ` - mutation SyncApplication($manifest: JSON!) { - syncApplication(manifest: $manifest) + mutation SyncApplication($manifest: JSON!, $packageJson: JSON!, $yarnLock: String!) { + syncApplication(manifest: $manifest, packageJson: $packageJson, yarnLock: $yarnLock) } `; const variables = { manifest, + packageJson: manifest.packageJson, + yarnLock, }; const response: AxiosResponse = await this.client.post( diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts similarity index 100% rename from packages/twenty-sdk/src/cli/utilities/build/common/watcher.ts rename to packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts diff --git a/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-watcher.ts index 8f1f2ce50f1..df37a7f433f 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-watcher.ts @@ -9,7 +9,7 @@ import { printWatchingMessage } from '../common/display'; import { type RestartableWatcher, type RestartableWatcherOptions, -} from '../common/watcher'; +} from '../common/restartable-watcher.interface'; import { FRONT_COMPONENTS_DIR } from './constants'; export const FRONT_COMPONENT_EXTERNAL_MODULES: (string | RegExp)[] = [ diff --git a/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts b/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts index 7e79d08a714..17b9b2cf0dd 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts @@ -1,6 +1,3 @@ -import { OUTPUT_DIR } from '../common/constants'; -import { FUNCTIONS_DIR } from './constants'; -import * as fs from 'fs-extra'; import path from 'path'; export const computeFunctionOutputPath = ( @@ -25,65 +22,3 @@ export const computeFunctionOutputPath = ( outputDir: normalizedOutputDir, }; }; - -export const buildFunctionEntries = ( - appPath: string, - handlerPaths: Array<{ handlerPath: string }>, -): Record => { - const entries: Record = {}; - - for (const fn of handlerPaths) { - const { relativePath } = computeFunctionOutputPath(fn.handlerPath); - const chunkName = relativePath.replace(/\.js$/, ''); - entries[chunkName] = path.join(appPath, fn.handlerPath); - } - - return entries; -}; - -export const cleanupOldFunctions = async ( - appPath: string, - currentEntryPoints: string[], -): Promise => { - const functionsDir = path.join(appPath, OUTPUT_DIR, FUNCTIONS_DIR); - - if (!(await fs.pathExists(functionsDir))) { - return; - } - - const expectedFiles = new Set( - currentEntryPoints.map((entryPoint) => { - const { relativePath } = computeFunctionOutputPath(entryPoint); - return relativePath; - }), - ); - - const expectedFilesWithMaps = new Set(); - for (const file of expectedFiles) { - expectedFilesWithMaps.add(file); - expectedFilesWithMaps.add(`${file}.map`); - } - - const removeOrphanedFiles = async (dir: string, relativeBase: string = ''): Promise => { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name; - - if (entry.isDirectory()) { - await removeOrphanedFiles(fullPath, relativePath); - const remaining = await fs.readdir(fullPath); - if (remaining.length === 0) { - await fs.remove(fullPath); - } - } else if (entry.isFile()) { - if (!expectedFilesWithMaps.has(relativePath)) { - await fs.remove(fullPath); - } - } - } - }; - - await removeOrphanedFiles(functionsDir); -}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/functions/function-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/functions/function-watcher.ts index cab82c4074e..2076aff98c3 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/functions/function-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/functions/function-watcher.ts @@ -9,9 +9,24 @@ import { printWatchingMessage } from '../common/display'; import { type RestartableWatcher, type RestartableWatcherOptions, -} from '../common/watcher'; +} from '../common/restartable-watcher.interface'; import { FUNCTIONS_DIR } from './constants'; -import { buildFunctionEntries } from './function-paths'; +import { computeFunctionOutputPath } from './function-paths'; + +const buildFunctionEntries = ( + appPath: string, + handlerPaths: Array<{ handlerPath: string }>, +): Record => { + const entries: Record = {}; + + for (const fn of handlerPaths) { + const { relativePath } = computeFunctionOutputPath(fn.handlerPath); + const chunkName = relativePath.replace(/\.js$/, ''); + entries[chunkName] = path.join(appPath, fn.handlerPath); + } + + return entries; +}; export const FUNCTION_EXTERNAL_MODULES: (string | RegExp)[] = [ 'path', 'fs', 'crypto', 'stream', 'util', 'os', 'url', 'http', 'https', diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts index 625490e3cf3..49b1fc8a248 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts @@ -1,11 +1,11 @@ import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/test-app/src/app/default-function.role'; import { - POST_CARD_EXTENSION_CATEGORY_FIELD_ID, - POST_CARD_EXTENSION_PRIORITY_FIELD_ID, + POST_CARD_EXTENSION_CATEGORY_FIELD_ID, + POST_CARD_EXTENSION_PRIORITY_FIELD_ID, } from '@/cli/__tests__/test-app/src/app/postCard.object-extension'; import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; -import { type ApplicationManifest } from 'twenty-shared/application'; import { join } from 'path'; +import { type ApplicationManifest } from 'twenty-shared/application'; const TEST_APP_PATH = join(__dirname, '../../../../__tests__/test-app'); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts index 48f9fe29764..022fb36e080 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts @@ -1,9 +1,9 @@ import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate'; -import { FieldMetadataType } from 'twenty-shared/types'; import { type Application, type ObjectExtensionManifest, } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; describe('validateManifest - objectExtensions', () => { const validApplication: Application = { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts index 40240514ef8..4326ae3ad59 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts @@ -1,62 +1,74 @@ import chalk from 'chalk'; import path from 'path'; -import { type Application, type ApplicationManifest } from 'twenty-shared/application'; +import { type Application } from 'twenty-shared/application'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -export const buildApplication = async (appPath: string): Promise => { - const applicationConfigPath = path.join(appPath, 'src', 'app', 'application.config.ts'); +export class ApplicationEntityBuilder + implements ManifestEntityBuilder +{ + async build(appPath: string): Promise { + const applicationConfigPath = path.join( + appPath, + 'src', + 'app', + 'application.config.ts', + ); - return extractManifestFromFile(applicationConfigPath, appPath); -}; - -export const validateApplication = ( - application: Application | undefined, - errors: ValidationError[], -): void => { - if (!application) { - errors.push({ - path: 'application', - message: 'Application config is required', - }); - return; + return extractManifestFromFile(applicationConfigPath, appPath); } - if (!application.universalIdentifier) { - errors.push({ - path: 'application', - message: 'Application must have a universalIdentifier', - }); - } -}; + validate(application: Application, errors: ValidationError[]): void { + if (!application) { + errors.push({ + path: 'application', + message: 'Application config is required', + }); + return; + } -export const displayApplication = (manifest: ApplicationManifest): void => { - const appName = manifest.application.displayName ?? 'Application'; - console.log(chalk.green(` ✓ Loaded "${appName}"`)); -}; - -export const collectApplicationIds = ( - application: Application | undefined, -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; - - if (application?.universalIdentifier) { - ids.push({ - id: application.universalIdentifier, - location: 'application', - }); - } - - if (application?.applicationVariables) { - for (const [name, variable] of Object.entries(application.applicationVariables)) { - if (variable.universalIdentifier) { - ids.push({ - id: variable.universalIdentifier, - location: `application.variables.${name}`, - }); - } + if (!application.universalIdentifier) { + errors.push({ + path: 'application', + message: 'Application must have a universalIdentifier', + }); } } - return ids; -}; + display(application: Application): void { + const appName = application.displayName ?? 'Application'; + console.log(chalk.green(` ✓ Loaded "${appName}"`)); + } + + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const seen = new Map(); + const application = manifest.application; + + if (application?.universalIdentifier) { + seen.set(application.universalIdentifier, ['application']); + } + + if (application?.applicationVariables) { + for (const [name, variable] of Object.entries( + application.applicationVariables, + )) { + if (variable.universalIdentifier) { + const locations = seen.get(variable.universalIdentifier) ?? []; + locations.push(`application.variables.${name}`); + seen.set(variable.universalIdentifier, locations); + } + } + } + + return Array.from(seen.entries()) + .filter(([_, locations]) => locations.length > 1) + .map(([id, locations]) => ({ id, locations })); + } +} + +export const applicationEntityBuilder = new ApplicationEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity.interface.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity.interface.ts new file mode 100644 index 00000000000..893ccfe3d50 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity.interface.ts @@ -0,0 +1,19 @@ +import { type ApplicationManifest } from 'twenty-shared/application'; +import { type ValidationError } from '../manifest.types'; + +export type EntityIdWithLocation = { + id: string; + locations: string[]; +}; + +export type ManifestWithoutSources = Omit< + ApplicationManifest, + 'sources' | 'packageJson' +>; + +export type ManifestEntityBuilder = { + build(appPath: string): Promise; + validate(data: EntityManifest, errors: ValidationError[]): void; + display(data: EntityManifest): void; + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[]; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts index 3e8d700b928..d1c2d20a015 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts @@ -1,87 +1,92 @@ +import { toPosixRelative } from '@/cli/utilities/file/utils/file-path'; import chalk from 'chalk'; import { glob } from 'fast-glob'; -import { posix, relative, sep } from 'path'; import { type FrontComponentManifest } from 'twenty-shared/application'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -const toPosixRelative = (filepath: string, appPath: string): string => { - const rel = relative(appPath, filepath); - return rel.split(sep).join(posix.sep); -}; +export class FrontComponentEntityBuilder + implements ManifestEntityBuilder +{ + async build(appPath: string): Promise { + const componentFiles = await glob(['src/app/**/*.front-component.tsx'], { + cwd: appPath, + absolute: true, + ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], + }); -export const buildFrontComponents = async ( - appPath: string, -): Promise => { - const componentFiles = await glob(['src/app/**/*.front-component.tsx'], { - cwd: appPath, - absolute: true, - ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], - }); + const frontComponentManifests: FrontComponentManifest[] = []; - const frontComponentManifests: FrontComponentManifest[] = []; - - for (const filepath of componentFiles) { - try { - frontComponentManifests.push( - await extractManifestFromFile( - filepath, - appPath, - { entryProperty: 'component', jsx: true }, - ), - ); - } catch (error) { - const relPath = toPosixRelative(filepath, appPath); - throw new Error( - `Failed to load front component from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, - ); + for (const filepath of componentFiles) { + try { + frontComponentManifests.push( + await extractManifestFromFile( + filepath, + appPath, + { entryProperty: 'component', jsx: true }, + ), + ); + } catch (error) { + const relPath = toPosixRelative(filepath, appPath); + throw new Error( + `Failed to load front component from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } } + + return frontComponentManifests; } - return frontComponentManifests; -}; - -export const validateFrontComponents = ( - components: FrontComponentManifest[], - errors: ValidationError[], -): void => { - for (const component of components) { - const componentPath = `front-components/${component.name ?? component.componentName ?? 'unknown'}`; - - if (!component.universalIdentifier) { - errors.push({ - path: componentPath, - message: 'Front component must have a universalIdentifier', - }); - } - } -}; - -export const displayFrontComponents = (components: FrontComponentManifest[]): void => { - console.log(chalk.green(` ✓ Found ${components.length} front component(s)`)); - - if (components.length > 0) { - console.log(chalk.gray(` 📍 Front component entry points:`)); + validate( + components: FrontComponentManifest[], + errors: ValidationError[], + ): void { for (const component of components) { - const name = component.name || component.universalIdentifier; - console.log(chalk.gray(` - ${name} (${component.componentPath})`)); - } - } -}; + const componentPath = `front-components/${component.name ?? component.componentName ?? 'unknown'}`; -export const collectFrontComponentIds = ( - components: FrontComponentManifest[], -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; - - for (const component of components) { - if (component.universalIdentifier) { - ids.push({ - id: component.universalIdentifier, - location: `front-components/${component.name ?? component.componentName}`, - }); + if (!component.universalIdentifier) { + errors.push({ + path: componentPath, + message: 'Front component must have a universalIdentifier', + }); + } } } - return ids; -}; + display(components: FrontComponentManifest[]): void { + console.log(chalk.green(` ✓ Found ${components.length} front component(s)`)); + + if (components.length > 0) { + console.log(chalk.gray(` 📍 Front component entry points:`)); + for (const component of components) { + const name = component.name || component.universalIdentifier; + console.log(chalk.gray(` - ${name} (${component.componentPath})`)); + } + } + } + + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const seen = new Map(); + const components = manifest.frontComponents ?? []; + + for (const component of components) { + if (component.universalIdentifier) { + const location = `front-components/${component.name ?? component.componentName}`; + const locations = seen.get(component.universalIdentifier) ?? []; + locations.push(location); + seen.set(component.universalIdentifier, locations); + } + } + + return Array.from(seen.entries()) + .filter(([_, locations]) => locations.length > 1) + .map(([id, locations]) => ({ id, locations })); + } +} + +export const frontComponentEntityBuilder = new FrontComponentEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts index 2bf64cb3ce8..1906374a28b 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts @@ -1,149 +1,154 @@ +import { toPosixRelative } from '@/cli/utilities/file/utils/file-path'; import chalk from 'chalk'; import { glob } from 'fast-glob'; -import { posix, relative, sep } from 'path'; import { type ServerlessFunctionManifest } from 'twenty-shared/application'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -const toPosixRelative = (filepath: string, appPath: string): string => { - const rel = relative(appPath, filepath); - return rel.split(sep).join(posix.sep); -}; +export class FunctionEntityBuilder + implements ManifestEntityBuilder +{ + async build(appPath: string): Promise { + const functionFiles = await glob(['src/app/**/*.function.ts'], { + cwd: appPath, + absolute: true, + ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], + }); -export const buildFunctions = async ( - appPath: string, -): Promise => { - const functionFiles = await glob(['src/app/**/*.function.ts'], { - cwd: appPath, - absolute: true, - ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], - }); + const functionManifests: ServerlessFunctionManifest[] = []; - const functionManifests: ServerlessFunctionManifest[] = []; - - for (const filepath of functionFiles) { - try { - functionManifests.push( - await extractManifestFromFile( - filepath, - appPath, - { entryProperty: 'handler' }, - ), - ); - } catch (error) { - const relPath = toPosixRelative(filepath, appPath); - throw new Error( - `Failed to load function from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, - ); + for (const filepath of functionFiles) { + try { + functionManifests.push( + await extractManifestFromFile( + filepath, + appPath, + { entryProperty: 'handler' }, + ), + ); + } catch (error) { + const relPath = toPosixRelative(filepath, appPath); + throw new Error( + `Failed to load function from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } } + + return functionManifests; } - return functionManifests; -}; - -export const validateFunctions = ( - functions: ServerlessFunctionManifest[], - errors: ValidationError[], -): void => { - for (const fn of functions) { - const fnPath = `functions/${fn.name ?? fn.handlerName ?? 'unknown'}`; - - if (!fn.universalIdentifier) { - errors.push({ - path: fnPath, - message: 'Function must have a universalIdentifier', - }); - } - - for (const trigger of fn.triggers ?? []) { - const triggerPath = `${fnPath}.triggers.${trigger.type ?? 'unknown'}`; - - if (!trigger.universalIdentifier) { - errors.push({ - path: triggerPath, - message: 'Trigger must have a universalIdentifier', - }); - } - - if (!trigger.type) { - errors.push({ - path: triggerPath, - message: 'Trigger must have a type', - }); - continue; - } - - switch (trigger.type) { - case 'route': - if (!trigger.path) { - errors.push({ - path: triggerPath, - message: 'Route trigger must have a path', - }); - } - if (!trigger.httpMethod) { - errors.push({ - path: triggerPath, - message: 'Route trigger must have an httpMethod', - }); - } - break; - - case 'cron': - if (!trigger.pattern) { - errors.push({ - path: triggerPath, - message: 'Cron trigger must have a pattern', - }); - } - break; - - case 'databaseEvent': - if (!trigger.eventName) { - errors.push({ - path: triggerPath, - message: 'Database event trigger must have an eventName', - }); - } - break; - } - } - } -}; - -export const displayFunctions = (functions: ServerlessFunctionManifest[]): void => { - console.log(chalk.green(` ✓ Found ${functions.length} function(s)`)); - - if (functions.length > 0) { - console.log(chalk.gray(` 📍 Function entry points:`)); + validate( + functions: ServerlessFunctionManifest[], + errors: ValidationError[], + ): void { for (const fn of functions) { - const name = fn.name || fn.universalIdentifier; - console.log(chalk.gray(` - ${name} (${fn.handlerPath})`)); - } - } -}; + const fnPath = `functions/${fn.name ?? fn.handlerName ?? 'unknown'}`; -export const collectFunctionIds = ( - functions: ServerlessFunctionManifest[], -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; - - for (const fn of functions) { - if (fn.universalIdentifier) { - ids.push({ - id: fn.universalIdentifier, - location: `functions/${fn.name ?? fn.handlerName}`, - }); - } - for (const trigger of fn.triggers ?? []) { - if (trigger.universalIdentifier) { - ids.push({ - id: trigger.universalIdentifier, - location: `functions/${fn.name ?? fn.handlerName}.triggers.${trigger.type}`, + if (!fn.universalIdentifier) { + errors.push({ + path: fnPath, + message: 'Function must have a universalIdentifier', }); } + + for (const trigger of fn.triggers ?? []) { + const triggerPath = `${fnPath}.triggers.${trigger.type ?? 'unknown'}`; + + if (!trigger.universalIdentifier) { + errors.push({ + path: triggerPath, + message: 'Trigger must have a universalIdentifier', + }); + } + + if (!trigger.type) { + errors.push({ + path: triggerPath, + message: 'Trigger must have a type', + }); + continue; + } + + switch (trigger.type) { + case 'route': + if (!trigger.path) { + errors.push({ + path: triggerPath, + message: 'Route trigger must have a path', + }); + } + if (!trigger.httpMethod) { + errors.push({ + path: triggerPath, + message: 'Route trigger must have an httpMethod', + }); + } + break; + + case 'cron': + if (!trigger.pattern) { + errors.push({ + path: triggerPath, + message: 'Cron trigger must have a pattern', + }); + } + break; + + case 'databaseEvent': + if (!trigger.eventName) { + errors.push({ + path: triggerPath, + message: 'Database event trigger must have an eventName', + }); + } + break; + } + } } } - return ids; -}; + display(functions: ServerlessFunctionManifest[]): void { + console.log(chalk.green(` ✓ Found ${functions.length} function(s)`)); + + if (functions.length > 0) { + console.log(chalk.gray(` 📍 Function entry points:`)); + for (const fn of functions) { + const name = fn.name || fn.universalIdentifier; + console.log(chalk.gray(` - ${name} (${fn.handlerPath})`)); + } + } + } + + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const seen = new Map(); + const functions = manifest.serverlessFunctions ?? []; + + for (const fn of functions) { + if (fn.universalIdentifier) { + const location = `functions/${fn.name ?? fn.handlerName}`; + const locations = seen.get(fn.universalIdentifier) ?? []; + locations.push(location); + seen.set(fn.universalIdentifier, locations); + } + for (const trigger of fn.triggers ?? []) { + if (trigger.universalIdentifier) { + const location = `functions/${fn.name ?? fn.handlerName}.triggers.${trigger.type}`; + const locations = seen.get(trigger.universalIdentifier) ?? []; + locations.push(location); + seen.set(trigger.universalIdentifier, locations); + } + } + } + + return Array.from(seen.entries()) + .filter(([_, locations]) => locations.length > 1) + .map(([id, locations]) => ({ id, locations })); + } +} + +export const functionEntityBuilder = new FunctionEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts index 1e0dfc97ea9..ea53b97a22e 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts @@ -1,143 +1,178 @@ +import { toPosixRelative } from '@/cli/utilities/file/utils/file-path'; import { glob } from 'fast-glob'; -import { posix, relative, sep } from 'path'; import { type ObjectExtensionManifest } from 'twenty-shared/application'; import { FieldMetadataType } from 'twenty-shared/types'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -const toPosixRelative = (filepath: string, appPath: string): string => { - const rel = relative(appPath, filepath); - return rel.split(sep).join(posix.sep); -}; +export class ObjectExtensionEntityBuilder + implements ManifestEntityBuilder +{ + async build(appPath: string): Promise { + const extensionFiles = await glob(['src/app/**/*.object-extension.ts'], { + cwd: appPath, + absolute: true, + ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], + }); -export const buildObjectExtensions = async ( - appPath: string, -): Promise => { - const extensionFiles = await glob(['src/app/**/*.object-extension.ts'], { - cwd: appPath, - absolute: true, - ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], - }); + const objectExtensionManifests: ObjectExtensionManifest[] = []; - const objectExtensionManifests: ObjectExtensionManifest[] = []; - - for (const filepath of extensionFiles) { - try { - objectExtensionManifests.push( - await extractManifestFromFile(filepath, appPath), - ); - } catch (error) { - const relPath = toPosixRelative(filepath, appPath); - throw new Error( - `Failed to load object extension from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, - ); + for (const filepath of extensionFiles) { + try { + objectExtensionManifests.push( + await extractManifestFromFile( + filepath, + appPath, + ), + ); + } catch (error) { + const relPath = toPosixRelative(filepath, appPath); + throw new Error( + `Failed to load object extension from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } } + + return objectExtensionManifests; } - return objectExtensionManifests; -}; + validate( + extensions: ObjectExtensionManifest[], + errors: ValidationError[], + ): void { + for (const ext of extensions) { + const targetName = + ext.targetObject?.nameSingular ?? + ext.targetObject?.universalIdentifier ?? + 'unknown'; + const extPath = `object-extensions/${targetName}`; -export const validateObjectExtensions = ( - extensions: ObjectExtensionManifest[], - errors: ValidationError[], -): void => { - for (const ext of extensions) { - const targetName = - ext.targetObject?.nameSingular ?? - ext.targetObject?.universalIdentifier ?? - 'unknown'; - const extPath = `object-extensions/${targetName}`; - - if (!ext.targetObject) { - errors.push({ - path: extPath, - message: 'Object extension must have a targetObject', - }); - continue; - } - - const { nameSingular, universalIdentifier } = ext.targetObject; - - if (!nameSingular && !universalIdentifier) { - errors.push({ - path: extPath, - message: - 'Object extension targetObject must have either nameSingular or universalIdentifier', - }); - } - - if (nameSingular && universalIdentifier) { - errors.push({ - path: extPath, - message: - 'Object extension targetObject cannot have both nameSingular and universalIdentifier', - }); - } - - if (!ext.fields || ext.fields.length === 0) { - errors.push({ - path: extPath, - message: 'Object extension must have at least one field', - }); - } - - for (const field of ext.fields ?? []) { - const fieldPath = `${extPath}.fields.${field.label ?? 'unknown'}`; - - if (!field.universalIdentifier) { + if (!ext.targetObject) { errors.push({ - path: fieldPath, - message: 'Field must have a universalIdentifier', + path: extPath, + message: 'Object extension must have a targetObject', + }); + continue; + } + + const { nameSingular, universalIdentifier } = ext.targetObject; + + if (!nameSingular && !universalIdentifier) { + errors.push({ + path: extPath, + message: + 'Object extension targetObject must have either nameSingular or universalIdentifier', }); } - if (!field.type) { + if (nameSingular && universalIdentifier) { errors.push({ - path: fieldPath, - message: 'Field must have a type', + path: extPath, + message: + 'Object extension targetObject cannot have both nameSingular and universalIdentifier', }); } - if (!field.label) { + if (!ext.fields || ext.fields.length === 0) { errors.push({ - path: fieldPath, - message: 'Field must have a label', + path: extPath, + message: 'Object extension must have at least one field', }); } - if ( - (field.type === FieldMetadataType.SELECT || - field.type === FieldMetadataType.MULTI_SELECT) && - (!Array.isArray(field.options) || field.options.length === 0) - ) { - errors.push({ - path: fieldPath, - message: 'SELECT/MULTI_SELECT field must have options', - }); - } - } - } -}; + for (const field of ext.fields ?? []) { + const fieldPath = `${extPath}.fields.${field.label ?? 'unknown'}`; -export const collectObjectExtensionIds = ( - extensions: ObjectExtensionManifest[], -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; + if (!field.universalIdentifier) { + errors.push({ + path: fieldPath, + message: 'Field must have a universalIdentifier', + }); + } - for (const ext of extensions) { - const targetName = - ext.targetObject?.nameSingular ?? - ext.targetObject?.universalIdentifier ?? - 'unknown'; - for (const field of ext.fields ?? []) { - if (field.universalIdentifier) { - ids.push({ - id: field.universalIdentifier, - location: `object-extensions/${targetName}.fields.${field.label}`, - }); + if (!field.type) { + errors.push({ + path: fieldPath, + message: 'Field must have a type', + }); + } + + if (!field.label) { + errors.push({ + path: fieldPath, + message: 'Field must have a label', + }); + } + + if ( + (field.type === FieldMetadataType.SELECT || + field.type === FieldMetadataType.MULTI_SELECT) && + (!Array.isArray(field.options) || field.options.length === 0) + ) { + errors.push({ + path: fieldPath, + message: 'SELECT/MULTI_SELECT field must have options', + }); + } } } } - return ids; -}; + display(_extensions: ObjectExtensionManifest[]): void { + // Object extensions don't have a dedicated display - they're part of the manifest + } + + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const extensions = manifest.objectExtensions ?? []; + const objects = manifest.objects ?? []; + + const objectFieldIds = new Map(); + for (const obj of objects) { + for (const field of obj.fields ?? []) { + if (field.universalIdentifier) { + const location = `objects/${obj.nameSingular}.fields.${field.label}`; + objectFieldIds.set(field.universalIdentifier, location); + } + } + } + + const extensionFieldLocations = new Map(); + for (const ext of extensions) { + const targetName = + ext.targetObject?.nameSingular ?? + ext.targetObject?.universalIdentifier ?? + 'unknown'; + for (const field of ext.fields ?? []) { + if (field.universalIdentifier) { + const location = `object-extensions/${targetName}.fields.${field.label}`; + const locations = + extensionFieldLocations.get(field.universalIdentifier) ?? []; + locations.push(location); + extensionFieldLocations.set(field.universalIdentifier, locations); + } + } + } + + const duplicates: EntityIdWithLocation[] = []; + + for (const [id, extLocations] of extensionFieldLocations.entries()) { + const objectLocation = objectFieldIds.get(id); + + if (extLocations.length > 1 || objectLocation) { + const allLocations = objectLocation + ? [objectLocation, ...extLocations] + : extLocations; + duplicates.push({ id, locations: allLocations }); + } + } + + return duplicates; + } +} + +export const objectExtensionEntityBuilder = new ObjectExtensionEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts index 3e73a37323a..8da4548eb53 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts @@ -1,132 +1,136 @@ +import { toPosixRelative } from '@/cli/utilities/file/utils/file-path'; import chalk from 'chalk'; import { glob } from 'fast-glob'; -import { posix, relative, sep } from 'path'; import { type ObjectManifest } from 'twenty-shared/application'; import { FieldMetadataType } from 'twenty-shared/types'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -const toPosixRelative = (filepath: string, appPath: string): string => { - const rel = relative(appPath, filepath); - return rel.split(sep).join(posix.sep); -}; +export class ObjectEntityBuilder + implements ManifestEntityBuilder +{ + async build(appPath: string): Promise { + const objectFiles = await glob(['src/app/**/*.object.ts'], { + cwd: appPath, + absolute: true, + ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], + }); -export const buildObjects = async (appPath: string): Promise => { - const objectFiles = await glob(['src/app/**/*.object.ts'], { - cwd: appPath, - absolute: true, - ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], - }); + const objectManifests: ObjectManifest[] = []; - const objectManifests: ObjectManifest[] = []; - - for (const filepath of objectFiles) { - try { - objectManifests.push( - await extractManifestFromFile(filepath, appPath), - ); - } catch (error) { - const relPath = toPosixRelative(filepath, appPath); - throw new Error( - `Failed to load object from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, - ); + for (const filepath of objectFiles) { + try { + objectManifests.push( + await extractManifestFromFile(filepath, appPath), + ); + } catch (error) { + const relPath = toPosixRelative(filepath, appPath); + throw new Error( + `Failed to load object from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } } + + return objectManifests; } - return objectManifests; -}; + validate(objects: ObjectManifest[], errors: ValidationError[]): void { + for (const obj of objects) { + const objPath = `objects/${obj.nameSingular ?? 'unknown'}`; -export const validateObjects = ( - objects: ObjectManifest[], - errors: ValidationError[], -): void => { - for (const obj of objects) { - const objPath = `objects/${obj.nameSingular ?? 'unknown'}`; - - if (!obj.universalIdentifier) { - errors.push({ - path: objPath, - message: 'Object must have a universalIdentifier', - }); - } - - if (!obj.nameSingular) { - errors.push({ - path: objPath, - message: 'Object must have a nameSingular', - }); - } - - if (!obj.namePlural) { - errors.push({ - path: objPath, - message: 'Object must have a namePlural', - }); - } - - for (const field of obj.fields ?? []) { - const fieldPath = `${objPath}.fields.${field.label ?? 'unknown'}`; - - if (!field.universalIdentifier) { + if (!obj.universalIdentifier) { errors.push({ - path: fieldPath, - message: 'Field must have a universalIdentifier', + path: objPath, + message: 'Object must have a universalIdentifier', }); } - if (!field.type) { + if (!obj.nameSingular) { errors.push({ - path: fieldPath, - message: 'Field must have a type', + path: objPath, + message: 'Object must have a nameSingular', }); } - if (!field.label) { + if (!obj.namePlural) { errors.push({ - path: fieldPath, - message: 'Field must have a label', + path: objPath, + message: 'Object must have a namePlural', }); } - if ( - (field.type === FieldMetadataType.SELECT || - field.type === FieldMetadataType.MULTI_SELECT) && - (!Array.isArray(field.options) || field.options.length === 0) - ) { - errors.push({ - path: fieldPath, - message: 'SELECT/MULTI_SELECT field must have options', - }); - } - } - } -}; + for (const field of obj.fields ?? []) { + const fieldPath = `${objPath}.fields.${field.label ?? 'unknown'}`; -export const displayObjects = (objects: ObjectManifest[]): void => { - console.log(chalk.green(` ✓ Found ${objects.length} object(s)`)); -}; + if (!field.universalIdentifier) { + errors.push({ + path: fieldPath, + message: 'Field must have a universalIdentifier', + }); + } -export const collectObjectIds = ( - objects: ObjectManifest[], -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; + if (!field.type) { + errors.push({ + path: fieldPath, + message: 'Field must have a type', + }); + } - for (const obj of objects) { - if (obj.universalIdentifier) { - ids.push({ - id: obj.universalIdentifier, - location: `objects/${obj.nameSingular}`, - }); - } - for (const field of obj.fields ?? []) { - if (field.universalIdentifier) { - ids.push({ - id: field.universalIdentifier, - location: `objects/${obj.nameSingular}.fields.${field.label}`, - }); + if (!field.label) { + errors.push({ + path: fieldPath, + message: 'Field must have a label', + }); + } + + if ( + (field.type === FieldMetadataType.SELECT || + field.type === FieldMetadataType.MULTI_SELECT) && + (!Array.isArray(field.options) || field.options.length === 0) + ) { + errors.push({ + path: fieldPath, + message: 'SELECT/MULTI_SELECT field must have options', + }); + } } } } - return ids; -}; + display(objects: ObjectManifest[]): void { + console.log(chalk.green(` ✓ Found ${objects.length} object(s)`)); + } + + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const seen = new Map(); + const objects = manifest.objects ?? []; + + for (const obj of objects) { + if (obj.universalIdentifier) { + const location = `objects/${obj.nameSingular}`; + const locations = seen.get(obj.universalIdentifier) ?? []; + locations.push(location); + seen.set(obj.universalIdentifier, locations); + } + for (const field of obj.fields ?? []) { + if (field.universalIdentifier) { + const location = `objects/${obj.nameSingular}.fields.${field.label}`; + const locations = seen.get(field.universalIdentifier) ?? []; + locations.push(location); + seen.set(field.universalIdentifier, locations); + } + } + } + + return Array.from(seen.entries()) + .filter(([_, locations]) => locations.length > 1) + .map(([id, locations]) => ({ id, locations })); + } +} + +export const objectEntityBuilder = new ObjectEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts index 62f18ed023a..f67164f7814 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts @@ -1,80 +1,82 @@ +import { toPosixRelative } from '@/cli/utilities/file/utils/file-path'; import chalk from 'chalk'; import { glob } from 'fast-glob'; -import { posix, relative, sep } from 'path'; import { type RoleManifest } from 'twenty-shared/application'; import { extractManifestFromFile } from '../manifest-file-extractor'; import { type ValidationError } from '../manifest.types'; +import { + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, +} from './entity.interface'; -const toPosixRelative = (filepath: string, appPath: string): string => { - const rel = relative(appPath, filepath); - return rel.split(sep).join(posix.sep); -}; +export class RoleEntityBuilder implements ManifestEntityBuilder { + async build(appPath: string): Promise { + const roleFiles = await glob(['src/app/**/*.role.ts'], { + cwd: appPath, + absolute: true, + ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], + }); -export const buildRoles = async (appPath: string): Promise => { - const roleFiles = await glob(['src/app/**/*.role.ts'], { - cwd: appPath, - absolute: true, - ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], - }); + const roleManifests: RoleManifest[] = []; - const roleManifests: RoleManifest[] = []; + for (const filepath of roleFiles) { + try { + roleManifests.push( + await extractManifestFromFile(filepath, appPath), + ); + } catch (error) { + const relPath = toPosixRelative(filepath, appPath); + throw new Error( + `Failed to load role from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } - for (const filepath of roleFiles) { - try { - roleManifests.push( - await extractManifestFromFile(filepath, appPath), - ); - } catch (error) { - const relPath = toPosixRelative(filepath, appPath); - throw new Error( - `Failed to load role from ${relPath}: ${error instanceof Error ? error.message : String(error)}`, - ); + return roleManifests; + } + + validate(roles: RoleManifest[], errors: ValidationError[]): void { + for (const role of roles) { + const rolePath = `roles/${role.label ?? 'unknown'}`; + + if (!role.universalIdentifier) { + errors.push({ + path: rolePath, + message: 'Role must have a universalIdentifier', + }); + } + + if (!role.label) { + errors.push({ + path: rolePath, + message: 'Role must have a label', + }); + } } } - return roleManifests; -}; - -export const validateRoles = ( - roles: RoleManifest[], - errors: ValidationError[], -): void => { - for (const role of roles) { - const rolePath = `roles/${role.label ?? 'unknown'}`; - - if (!role.universalIdentifier) { - errors.push({ - path: rolePath, - message: 'Role must have a universalIdentifier', - }); - } - - if (!role.label) { - errors.push({ - path: rolePath, - message: 'Role must have a label', - }); - } - } -}; - -export const displayRoles = (roles: RoleManifest[]): void => { - console.log(chalk.green(` ✓ Found ${roles?.length ?? 'no'} role(s)`)); -}; - -export const collectRoleIds = ( - roles: RoleManifest[], -): Array<{ id: string; location: string }> => { - const ids: Array<{ id: string; location: string }> = []; - - for (const role of roles) { - if (role.universalIdentifier) { - ids.push({ - id: role.universalIdentifier, - location: `roles/${role.label}`, - }); - } + display(roles: RoleManifest[]): void { + console.log(chalk.green(` ✓ Found ${roles?.length ?? 'no'} role(s)`)); } - return ids; -}; + findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { + const seen = new Map(); + const roles = manifest.roles ?? []; + + for (const role of roles) { + if (role.universalIdentifier) { + const location = `roles/${role.label}`; + const locations = seen.get(role.universalIdentifier) ?? []; + locations.push(location); + seen.set(role.universalIdentifier, locations); + } + } + + return Array.from(seen.entries()) + .filter(([_, locations]) => locations.length > 1) + .map(([id, locations]) => ({ id, locations })); + } +} + +export const roleEntityBuilder = new RoleEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts index 770e292a361..b7c8846eb2d 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts @@ -7,12 +7,12 @@ import path, { relative, sep } from 'path'; import { type ApplicationManifest } from 'twenty-shared/application'; import { type Sources } from 'twenty-shared/types'; import { OUTPUT_DIR } from '../common/constants'; -import { buildApplication } from './entities/application'; -import { buildFrontComponents } from './entities/front-component'; -import { buildFunctions } from './entities/function'; -import { buildObjects } from './entities/object'; -import { buildObjectExtensions } from './entities/object-extension'; -import { buildRoles } from './entities/role'; +import { applicationEntityBuilder } from './entities/application'; +import { frontComponentEntityBuilder } from './entities/front-component'; +import { functionEntityBuilder } from './entities/function'; +import { objectEntityBuilder } from './entities/object'; +import { objectExtensionEntityBuilder } from './entities/object-extension'; +import { roleEntityBuilder } from './entities/role'; import { displayEntitySummary, displayErrors, displayWarnings } from './manifest-display'; import { validateManifest } from './manifest-validate'; import { ManifestValidationError } from './manifest.types'; @@ -113,12 +113,12 @@ export const runManifestBuild = async ( roleManifests, sources, ] = await Promise.all([ - buildApplication(appPath), - buildObjects(appPath), - buildObjectExtensions(appPath), - buildFunctions(appPath), - buildFrontComponents(appPath), - buildRoles(appPath), + applicationEntityBuilder.build(appPath), + objectEntityBuilder.build(appPath), + objectExtensionEntityBuilder.build(appPath), + functionEntityBuilder.build(appPath), + frontComponentEntityBuilder.build(appPath), + roleEntityBuilder.build(appPath), loadSources(appPath), ]); @@ -142,7 +142,6 @@ export const runManifestBuild = async ( serverlessFunctions: functionManifests, frontComponents: frontComponentManifests, roles: roleManifests, - packageJson, }); if (!validation.isValid) { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-display.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-display.ts index cefb0d71b3c..4559c9bb475 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-display.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-display.ts @@ -1,18 +1,18 @@ import chalk from 'chalk'; import { type ApplicationManifest } from 'twenty-shared/application'; -import { displayApplication } from './entities/application'; -import { displayFrontComponents } from './entities/front-component'; -import { displayFunctions } from './entities/function'; -import { displayObjects } from './entities/object'; -import { displayRoles } from './entities/role'; +import { applicationEntityBuilder } from './entities/application'; +import { frontComponentEntityBuilder } from './entities/front-component'; +import { functionEntityBuilder } from './entities/function'; +import { objectEntityBuilder } from './entities/object'; +import { roleEntityBuilder } from './entities/role'; import { type ManifestValidationError, type ValidationWarning } from './manifest.types'; export const displayEntitySummary = (manifest: ApplicationManifest): void => { - displayApplication(manifest); - displayObjects(manifest.objects); - displayFunctions(manifest.serverlessFunctions); - displayFrontComponents(manifest.frontComponents ?? []); - displayRoles(manifest.roles ?? []); + applicationEntityBuilder.display(manifest.application); + objectEntityBuilder.display(manifest.objects); + functionEntityBuilder.display(manifest.serverlessFunctions); + frontComponentEntityBuilder.display(manifest.frontComponents ?? []); + roleEntityBuilder.display(manifest.roles ?? []); }; export const displayErrors = (error: ManifestValidationError): void => { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-file-extractor.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-file-extractor.ts index f44cbe80e63..9cfe4314bd3 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-file-extractor.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-file-extractor.ts @@ -1,74 +1,16 @@ -import * as fs from 'fs-extra'; -import { createJiti } from 'jiti'; -import { type JitiOptions } from 'jiti/lib/types'; import path from 'path'; -import { parseJsoncFile } from '../../file/utils/file-jsonc'; +import { + closeViteServer, + findImportSource, + getViteServer, + loadModule, +} from './vite-module-loader'; export type ExtractManifestOptions = { jsx?: boolean; entryProperty?: string; }; -const getTsconfigAliases = async ( - appPath: string, -): Promise> => { - const tsconfigPath = path.join(appPath, 'tsconfig.json'); - - if (!(await fs.pathExists(tsconfigPath))) { - return {}; - } - - try { - const tsconfig = await parseJsoncFile(tsconfigPath); - const paths = tsconfig?.compilerOptions?.paths as - | Record - | undefined; - const baseUrl = (tsconfig?.compilerOptions?.baseUrl as string) || '.'; - - if (!paths) { - return {}; - } - - const aliases: Record = {}; - - for (const [pattern, targets] of Object.entries(paths)) { - if (targets.length === 0) continue; - - const aliasKey = pattern.replace(/\/\*$/, ''); - const targetPath = targets[0].replace(/\/\*$/, ''); - const resolvedTarget = path.resolve(appPath, baseUrl, targetPath); - aliases[aliasKey] = resolvedTarget; - } - - return aliases; - } catch { - return {}; - } -}; - -const createModuleLoader = async ( - appPath: string, - options: { jsx?: boolean } = {}, -) => { - const jitiOptions: JitiOptions = { - moduleCache: false, - fsCache: false, - interopDefault: true, - }; - - if (options.jsx) { - jitiOptions.jsx = { runtime: 'automatic' }; - } - - const aliases = await getTsconfigAliases(appPath); - - if (Object.keys(aliases).length > 0) { - jitiOptions.alias = aliases; - } - - return createJiti(appPath, jitiOptions); -}; - const findConfigInModule = ( module: Record, validator?: (value: unknown) => boolean, @@ -93,57 +35,18 @@ const findConfigInModule = ( return undefined; }; -const escapeRegExp = (string: string): string => { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -}; - -const extractImportPath = ( - source: string, - identifier: string, - filepath: string, - appPath: string, -): string | null => { - const escapedIdentifier = escapeRegExp(identifier); - - const patterns = [ - new RegExp( - `import\\s*\\{[^}]*\\b${escapedIdentifier}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`, - ), - new RegExp( - `import\\s*\\{[^}]*\\w+\\s+as\\s+${escapedIdentifier}[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`, - ), - new RegExp(`import\\s+${escapedIdentifier}\\s+from\\s*['"]([^'"]+)['"]`), - ]; - - for (const pattern of patterns) { - const match = source.match(pattern); - - if (match) { - const importPath = match[1]; - const fileDir = path.dirname(filepath); - const absolutePath = path.resolve(fileDir, importPath); - const relativePath = path.relative(appPath, absolutePath); - - const resultPath = relativePath.endsWith('.ts') - ? relativePath - : `${relativePath}.ts`; - - return resultPath.replace(/\\/g, '/'); - } - } - - return null; -}; - export const extractManifestFromFile = async ( filepath: string, appPath: string, options: ExtractManifestOptions = {}, ): Promise => { - const { jsx, entryProperty } = options; - const jiti = await createModuleLoader(appPath, { jsx }); + const { entryProperty } = options; - const module = (await jiti.import(filepath)) as Record; + // Get or create the Vite server for this appPath + const server = await getViteServer(appPath); + + // Load the module using Vite's SSR loader + const module = await loadModule(server, filepath); const configValidator = entryProperty ? (value: unknown): boolean => @@ -178,10 +81,10 @@ export const extractManifestFromFile = async ( ); } - const source = await fs.readFile(filepath, 'utf8'); - const importPath = extractImportPath(source, entryName, filepath, appPath); + // Use Vite to resolve where the function was imported from + const importSource = await findImportSource(server, filepath, entryName, appPath); const entryPath = - importPath ?? path.relative(appPath, filepath).replace(/\\/g, '/'); + importSource ?? path.relative(appPath, filepath).replace(/\\/g, '/'); const { [entryProperty]: _, ...configWithoutEntry } = config; @@ -193,3 +96,6 @@ export const extractManifestFromFile = async ( return manifest as TManifest; }; + +// Re-export for cleanup +export { closeViteServer }; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts index 4d89941fc71..f7524162816 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts @@ -1,60 +1,46 @@ -import { type ApplicationManifest } from 'twenty-shared/application'; -import { collectApplicationIds, validateApplication } from './entities/application'; -import { collectFrontComponentIds, validateFrontComponents } from './entities/front-component'; -import { collectFunctionIds, validateFunctions } from './entities/function'; -import { collectObjectExtensionIds, validateObjectExtensions } from './entities/object-extension'; -import { collectObjectIds, validateObjects } from './entities/object'; -import { collectRoleIds, validateRoles } from './entities/role'; +import { applicationEntityBuilder } from './entities/application'; +import { + type EntityIdWithLocation, + type ManifestWithoutSources, +} from './entities/entity.interface'; +import { frontComponentEntityBuilder } from './entities/front-component'; +import { functionEntityBuilder } from './entities/function'; +import { objectEntityBuilder } from './entities/object'; +import { objectExtensionEntityBuilder } from './entities/object-extension'; +import { roleEntityBuilder } from './entities/role'; import { type ValidationError, type ValidationResult, type ValidationWarning, } from './manifest.types'; -const collectAllIds = ( - manifest: Omit, -): Array<{ id: string; location: string }> => { +const collectAllDuplicates = ( + manifest: ManifestWithoutSources, +): EntityIdWithLocation[] => { return [ - ...collectApplicationIds(manifest.application), - ...collectObjectIds(manifest.objects ?? []), - ...collectObjectExtensionIds(manifest.objectExtensions ?? []), - ...collectFunctionIds(manifest.serverlessFunctions ?? []), - ...collectRoleIds(manifest.roles ?? []), - ...collectFrontComponentIds(manifest.frontComponents ?? []), + ...applicationEntityBuilder.findDuplicates(manifest), + ...objectEntityBuilder.findDuplicates(manifest), + ...objectExtensionEntityBuilder.findDuplicates(manifest), + ...functionEntityBuilder.findDuplicates(manifest), + ...roleEntityBuilder.findDuplicates(manifest), + ...frontComponentEntityBuilder.findDuplicates(manifest), ]; }; -const findDuplicates = ( - ids: Array<{ id: string; location: string }>, -): Array<{ id: string; locations: string[] }> => { - const seen = new Map(); - - for (const { id, location } of ids) { - const locations = seen.get(id) ?? []; - locations.push(location); - seen.set(id, locations); - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); -}; - export const validateManifest = ( - manifest: Omit, + manifest: ManifestWithoutSources, ): ValidationResult => { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; - validateApplication(manifest.application, errors); - validateObjects(manifest.objects ?? [], errors); - validateObjectExtensions(manifest.objectExtensions ?? [], errors); - validateFunctions(manifest.serverlessFunctions ?? [], errors); - validateRoles(manifest.roles ?? [], errors); - validateFrontComponents(manifest.frontComponents ?? [], errors); + applicationEntityBuilder.validate(manifest.application, errors); + objectEntityBuilder.validate(manifest.objects ?? [], errors); + objectExtensionEntityBuilder.validate(manifest.objectExtensions ?? [], errors); + functionEntityBuilder.validate(manifest.serverlessFunctions ?? [], errors); + roleEntityBuilder.validate(manifest.roles ?? [], errors); + frontComponentEntityBuilder.validate(manifest.frontComponents ?? [], errors); - const allIds = collectAllIds(manifest); - const duplicates = findDuplicates(allIds); + const duplicates = collectAllDuplicates(manifest); for (const dup of duplicates) { errors.push({ path: dup.locations.join(', '), diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts index f1272596000..c8cda7cf028 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts @@ -6,7 +6,7 @@ import { build, type InlineConfig, type Plugin, type Rollup } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import { OUTPUT_DIR } from '../common/constants'; import { printWatchingMessage } from '../common/display'; -import { type RestartableWatcher } from '../common/watcher'; +import { type RestartableWatcher } from '../common/restartable-watcher.interface'; import { runManifestBuild } from './manifest-build'; export type ManifestWatcherCallbacks = { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/vite-module-loader.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/vite-module-loader.ts new file mode 100644 index 00000000000..e23fd1d34f1 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/vite-module-loader.ts @@ -0,0 +1,111 @@ +import path from 'path'; +import { createServer, type ViteDevServer } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +// Singleton Vite dev server per appPath +const servers = new Map(); + +export const getViteServer = async (appPath: string): Promise => { + const existing = servers.get(appPath); + if (existing) { + return existing; + } + + const server = await createServer({ + root: appPath, + plugins: [tsconfigPaths({ root: appPath })], + server: { middlewareMode: true }, + optimizeDeps: { disabled: true }, + logLevel: 'silent', + configFile: false, + esbuild: { + jsx: 'automatic', + }, + }); + + servers.set(appPath, server); + return server; +}; + +export const closeViteServer = async (appPath?: string): Promise => { + if (appPath) { + const server = servers.get(appPath); + if (server) { + await server.close(); + servers.delete(appPath); + } + } else { + // Close all servers + for (const [key, server] of servers) { + await server.close(); + servers.delete(key); + } + } +}; + +// Load a module using Vite's SSR loader +export const loadModule = async ( + server: ViteDevServer, + filepath: string, +): Promise> => { + return (await server.ssrLoadModule(filepath)) as Record; +}; + +// Find where an identifier was imported from by parsing the source +// and using Vite's module graph to resolve the import path +export const findImportSource = async ( + server: ViteDevServer, + filepath: string, + identifier: string, + appPath: string, +): Promise => { + // Read the source and find import statements + const fs = await import('fs-extra'); + const source = await fs.default.readFile(filepath, 'utf8'); + + // Find the import statement that imports the identifier + const importRegexes = [ + // Named import: import { identifier } from 'path' + new RegExp( + `import\\s*\\{[^}]*\\b${identifier}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`, + ), + // Aliased import: import { something as identifier } from 'path' + new RegExp( + `import\\s*\\{[^}]*\\w+\\s+as\\s+${identifier}[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`, + ), + // Default import: import identifier from 'path' + new RegExp(`import\\s+${identifier}\\s+from\\s*['"]([^'"]+)['"]`), + ]; + + let importSpecifier: string | null = null; + for (const regex of importRegexes) { + const match = source.match(regex); + if (match) { + importSpecifier = match[1]; + break; + } + } + + if (!importSpecifier) { + // Not imported, must be defined in the same file + return null; + } + + // Use Vite to resolve the import path + const resolved = await server.pluginContainer.resolveId(importSpecifier, filepath); + if (resolved?.id) { + return path.relative(appPath, resolved.id).replace(/\\/g, '/'); + } + + // Fallback to simple relative path resolution + if (importSpecifier.startsWith('.')) { + const fileDir = path.dirname(filepath); + const absolutePath = path.resolve(fileDir, importSpecifier); + const relativePath = path.relative(appPath, absolutePath); + return ( + relativePath.endsWith('.ts') ? relativePath : `${relativePath}.ts` + ).replace(/\\/g, '/'); + } + + return null; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/file/utils/file-path.ts b/packages/twenty-sdk/src/cli/utilities/file/utils/file-path.ts index c0b30aa031f..2f45c53f871 100644 --- a/packages/twenty-sdk/src/cli/utilities/file/utils/file-path.ts +++ b/packages/twenty-sdk/src/cli/utilities/file/utils/file-path.ts @@ -1,8 +1,13 @@ -import { join } from 'path'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; +import { join, posix, relative, sep } from 'path'; export const formatPath = (appPath?: string) => { return appPath && !appPath?.startsWith('/') ? join(CURRENT_EXECUTION_DIRECTORY, appPath) : appPath; }; + +export const toPosixRelative = (filepath: string, basePath: string): string => { + const rel = relative(basePath, filepath); + return rel.split(sep).join(posix.sep); +}; diff --git a/packages/twenty-sdk/vitest.config.ts b/packages/twenty-sdk/vitest.config.ts new file mode 100644 index 00000000000..f5c2dd16b9a --- /dev/null +++ b/packages/twenty-sdk/vitest.config.ts @@ -0,0 +1,30 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + root: __dirname, + ignoreConfigErrors: true, + }), + ], + test: { + name: 'twenty-sdk', + environment: 'node', + include: [ + 'src/**/__tests__/**/*.{test,spec}.{ts,tsx}', + 'src/**/*.{test,spec}.{ts,tsx}', + ], + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.d.ts', 'src/cli/cli.ts'], + thresholds: { + statements: 1, + lines: 1, + functions: 1, + }, + }, + globals: true, + }, +}); diff --git a/packages/twenty-sdk/vitest.e2e.config.ts b/packages/twenty-sdk/vitest.e2e.config.ts new file mode 100644 index 00000000000..062b9f69ffb --- /dev/null +++ b/packages/twenty-sdk/vitest.e2e.config.ts @@ -0,0 +1,31 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + root: __dirname, + ignoreConfigErrors: true, + }), + ], + test: { + name: 'twenty-sdk-e2e', + environment: 'node', + include: ['src/**/*.e2e-spec.ts'], + globals: true, + testTimeout: 30000, + hookTimeout: 30000, + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, + }, + }, + sequence: { + concurrent: false, + }, + setupFiles: ['src/cli/__tests__/e2e/setupTest.ts'], + globalSetup: undefined, + onConsoleLog: () => true, + }, +}); diff --git a/yarn.lock b/yarn.lock index 785580b3c8b..becc872a876 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23524,16 +23524,6 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.0": - version: 29.5.14 - resolution: "@types/jest@npm:29.5.14" - dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 10c0/18e0712d818890db8a8dab3d91e9ea9f7f19e3f83c2e50b312f557017dc81466207a71f3ed79cf4428e813ba939954fa26ffa0a9a7f153181ba174581b1c2aed - languageName: node - linkType: hard - "@types/jest@npm:^30.0.0": version: 30.0.0 resolution: "@types/jest@npm:30.0.0" @@ -29697,7 +29687,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.1, chokidar@npm:^4.0.3": +"chokidar@npm:4.0.3, chokidar@npm:^4.0.1, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" dependencies: @@ -35264,7 +35254,7 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.7.0": +"expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" dependencies: @@ -41775,7 +41765,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:29.7.0, jest@npm:^29.5.0": +"jest@npm:29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -41849,15 +41839,6 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.0.0": - version: 2.6.1 - resolution: "jiti@npm:2.6.1" - bin: - jiti: lib/jiti-cli.mjs - checksum: 10c0/79b2e96a8e623f66c1b703b98ec1b8be4500e1d217e09b09e343471bbb9c105381b83edbb979d01cef18318cc45ce6e153571b6c83122170eefa531c64b6789b - languageName: node - linkType: hard - "jju@npm:~1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -49918,7 +49899,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: @@ -57001,7 +56982,6 @@ __metadata: "@types/archiver": "npm:^6.0.0" "@types/fs-extra": "npm:^11.0.0" "@types/inquirer": "npm:^9.0.0" - "@types/jest": "npm:^29.5.0" "@types/lodash.camelcase": "npm:^4.3.7" "@types/lodash.capitalize": "npm:^4" "@types/lodash.kebabcase": "npm:^4.1.7" @@ -57011,7 +56991,6 @@ __metadata: archiver: "npm:^7.0.1" axios: "npm:^1.6.0" chalk: "npm:^5.3.0" - chokidar: "npm:^4.0.0" commander: "npm:^12.0.0" dotenv: "npm:^16.4.0" fast-glob: "npm:^3.3.0" @@ -57019,8 +56998,6 @@ __metadata: graphql: "npm:^16.8.1" graphql-sse: "npm:^2.5.4" inquirer: "npm:^10.0.0" - jest: "npm:^29.5.0" - jiti: "npm:^2.0.0" jsonc-parser: "npm:^3.2.0" lodash.camelcase: "npm:^4.3.0" lodash.capitalize: "npm:^4.2.1"