diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 3a48c46cd8..d224127562 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -28,6 +28,7 @@ export const EXPECTED_MANIFEST: Manifest = { universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e', packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde', + apiClientChecksum: null, }, frontComponents: [ { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index a99b529c91..a03f5b5c6d 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -11,6 +11,7 @@ export const EXPECTED_MANIFEST: Manifest = { defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc', yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e', + apiClientChecksum: null, }, publicAssets: [], fields: [], diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts index daa0f595ad..a03d0a934d 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts @@ -8,6 +8,7 @@ import { type RestartableWatcher, type RestartableWatcherOptions, } from '@/cli/utilities/build/common/restartable-watcher-interface'; +import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin'; import * as esbuild from 'esbuild'; import path from 'path'; import { OUTPUT_DIR, NODE_ESM_CJS_BANNER } from 'twenty-shared/application'; @@ -214,7 +215,10 @@ export const createLogicFunctionsWatcher = ( externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES, fileFolder: FileFolder.BuiltLogicFunction, platform: 'node', - extraPlugins: [createSdkGeneratedResolverPlugin(options.appPath)], + extraPlugins: [ + createTypecheckPlugin(options.appPath), + createSdkGeneratedResolverPlugin(options.appPath), + ], banner: NODE_ESM_CJS_BANNER, }, }); @@ -229,6 +233,7 @@ export const createFrontComponentsWatcher = ( fileFolder: FileFolder.BuiltFrontComponent, jsx: 'automatic', extraPlugins: [ + createTypecheckPlugin(options.appPath), createSdkGeneratedResolverPlugin(options.appPath), ...getFrontComponentBuildPlugins(), ], diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/tsc-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/tsc-watcher.ts new file mode 100644 index 0000000000..dc33fc4101 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/common/tsc-watcher.ts @@ -0,0 +1,103 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import * as fs from 'fs-extra'; +import path from 'node:path'; + +import { + parseTscOutputLine, + type TypecheckError, +} from '@/cli/utilities/build/common/typecheck-plugin'; + +export type TscWatcherOptions = { + appPath: string; + onErrors: (errors: TypecheckError[]) => void; +}; + +export class TscWatcher { + private appPath: string; + private onErrors: (errors: TypecheckError[]) => void; + private process: ChildProcess | null = null; + private pendingErrors: TypecheckError[] = []; + private buffer = ''; + private hasErrors = false; + + constructor(options: TscWatcherOptions) { + this.appPath = options.appPath; + this.onErrors = options.onErrors; + } + + async start(): Promise { + const tscPath = path.join(this.appPath, 'node_modules', '.bin', 'tsc'); + + if (!(await fs.pathExists(tscPath))) { + return; + } + + const tsconfigPath = path.join(this.appPath, 'tsconfig.json'); + + this.process = spawn( + tscPath, + ['--watch', '--noEmit', '--pretty', 'false', '-p', tsconfigPath], + { cwd: this.appPath, stdio: ['ignore', 'pipe', 'pipe'] }, + ); + + this.process.on('error', () => { + this.process = null; + }); + + this.process.stdout?.on('data', (chunk: Buffer) => { + this.handleOutput(chunk.toString()); + }); + + this.process.stderr?.on('data', (chunk: Buffer) => { + this.handleOutput(chunk.toString()); + }); + } + + close(): void { + this.process?.kill(); + this.process = null; + } + + private handleOutput(data: string): void { + this.buffer += data; + + const lines = this.buffer.split('\n'); + + this.buffer = lines.pop() ?? ''; + + for (const line of lines) { + this.processLine(line); + } + } + + private processLine(line: string): void { + if ( + line.includes('Starting compilation in watch mode...') || + line.includes('Starting incremental compilation...') + ) { + this.pendingErrors = []; + + return; + } + + if (line.includes('Watching for file changes.')) { + const hadErrors = this.hasErrors; + + this.hasErrors = this.pendingErrors.length > 0; + + if (this.hasErrors || hadErrors) { + this.onErrors(this.pendingErrors); + } + + this.pendingErrors = []; + + return; + } + + const error = parseTscOutputLine(line); + + if (error) { + this.pendingErrors.push(error); + } + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/typecheck-plugin.ts b/packages/twenty-sdk/src/cli/utilities/build/common/typecheck-plugin.ts new file mode 100644 index 0000000000..c0913e213b --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/common/typecheck-plugin.ts @@ -0,0 +1,84 @@ +import { execFile } from 'node:child_process'; +import type * as esbuild from 'esbuild'; +import path from 'node:path'; + +export type TypecheckError = { + text: string; + file: string; + line: number; + column: number; +}; + +const TSC_ERROR_REGEX = /^(.+)\((\d+),(\d+)\): error TS\d+: (.+)$/; + +export const parseTscOutputLine = (line: string): TypecheckError | null => { + const match = line.match(TSC_ERROR_REGEX); + + if (!match) { + return null; + } + + const [, filePath, lineStr, columnStr, text] = match; + + return { + text, + file: filePath, + line: Number(lineStr), + column: Number(columnStr) - 1, + }; +}; + +const parseTscOutput = (output: string): TypecheckError[] => { + const errors: TypecheckError[] = []; + + for (const line of output.split('\n')) { + const error = parseTscOutputLine(line); + + if (error) { + errors.push(error); + } + } + + return errors; +}; + +export const runTypecheck = (appPath: string): Promise => { + const tsconfigPath = path.join(appPath, 'tsconfig.json'); + const tscPath = path.join(appPath, 'node_modules', '.bin', 'tsc'); + + return new Promise((resolve) => { + execFile( + tscPath, + ['--noEmit', '--pretty', 'false', '-p', tsconfigPath], + { cwd: appPath }, + (_error, stdout, stderr) => { + resolve(parseTscOutput(stderr || stdout)); + }, + ); + }); +}; + +const toEsbuildErrors = (errors: TypecheckError[]): esbuild.PartialMessage[] => + errors.map((error) => ({ + text: error.text, + location: { + file: error.file, + line: error.line, + column: error.column, + lineText: '', + length: 0, + namespace: '', + suggestion: '', + }, + })); + +export const createTypecheckPlugin = (appPath: string): esbuild.Plugin => ({ + name: 'typecheck', + setup: (build) => { + build.onStart(async () => { + const errors = await runTypecheck(appPath); + + return { errors: toEsbuildErrors(errors) }; + }); + }, +}); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts index 7eae3d95ef..04abe70ec5 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts @@ -13,6 +13,7 @@ const validApplication: ApplicationManifest = { defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', packageJsonChecksum: '98592af7-4be9-4655-b5c4-9bef307a996c', yarnLockChecksum: '580ee05f-15fe-4146-bac2-6c382483c94e', + apiClientChecksum: null, }; const validField: FieldManifest = { 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 6c800b6fe3..05c8bdfc40 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 @@ -101,6 +101,7 @@ export const buildManifest = async ( ...extract.config, yarnLockChecksum: null, packageJsonChecksum: null, + apiClientChecksum: null, }; errors.push(...extract.errors); applicationFilePaths.push(relativePath); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts index e9e525dad1..735e33d288 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-update-checksums.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import { relative } from 'path'; import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; @@ -88,5 +89,30 @@ export const manifestUpdateChecksums = ({ } } } + + const apiClientChecksums: string[] = []; + + for (const [builtPath, { fileFolder }] of builtFileInfos.entries()) { + const rootBuiltPath = relative(OUTPUT_DIR, builtPath); + + if ( + fileFolder === FileFolder.Dependencies && + rootBuiltPath.startsWith('api-client/') + ) { + const entry = builtFileInfos.get(builtPath); + + if (entry) { + apiClientChecksums.push(entry.checksum); + } + } + } + + if (apiClientChecksums.length > 0) { + result.application.apiClientChecksum = crypto + .createHash('md5') + .update(apiClientChecksums.sort().join('')) + .digest('hex'); + } + return result; }; diff --git a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts index f0b8489478..4c9eaec3a3 100644 --- a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts @@ -22,6 +22,7 @@ export class ClientService { authToken?: string; }): Promise { const outputPath = this.resolveGeneratedPath(appPath); + const tempPath = `${outputPath}.tmp`; const getSchemaResponse = await this.apiService.getSchema({ authToken }); @@ -33,12 +34,12 @@ export class ClientService { const { data: schema } = getSchemaResponse; - await fs.ensureDir(outputPath); - await fs.emptyDir(outputPath); + await fs.ensureDir(tempPath); + await fs.emptyDir(tempPath); await generate({ schema, - output: outputPath, + output: tempPath, scalarTypes: { DateTime: 'string', JSON: 'Record', @@ -46,7 +47,10 @@ export class ClientService { }, }); - await this.injectTwentyClient(outputPath); + await this.injectTwentyClient(tempPath); + + await fs.remove(outputPath); + await fs.move(tempPath, outputPath); } private resolveGeneratedPath(appPath: string): string { @@ -70,22 +74,16 @@ const defaultOptions: ClientOptions = { export default class Twenty { private client: Client; - private apiUrl: string; - private authorizationToken: string; constructor(options?: ClientOptions) { - const merged: ClientOptions = { + this.client = createClient({ ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...(options?.headers ?? {}), }, - }; - - this.client = createClient(merged); - this.apiUrl = merged.url; - this.authorizationToken = merged.headers.Authorization; + }); } query(request: R & { __name?: string }) { @@ -95,41 +93,6 @@ export default class Twenty { mutation(request: R & { __name?: string }) { return this.client.mutation(request); } - - async uploadFile( - fileBuffer: Buffer, - filename: string, - contentType: string = 'application/octet-stream', - fileFolder: string = 'Attachment', - ): Promise<{ path: string; token: string }> { - const form = new FormData(); - - form.append('operations', JSON.stringify({ - query: \`mutation UploadFile($file: Upload!, $fileFolder: FileFolder) { - uploadFile(file: $file, fileFolder: $fileFolder) { path token } - }\`, - variables: { file: null, fileFolder }, - })); - form.append('map', JSON.stringify({ '0': ['variables.file'] })); - form.append('0', new Blob([fileBuffer], { type: contentType }), filename); - - - const response = await fetch(\`\${this.apiUrl}/graphql\`, { - method: 'POST', - headers: { - Authorization: this.authorizationToken, - }, - body: form, - }); - - const result = await response.json(); - - if (result.errors) { - throw new GenqlError(result.errors, result.data); - } - - return result.data.uploadFile; - } } `; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts index 4dbfcaf5d3..20576c119c 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts @@ -7,7 +7,10 @@ import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/st import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step'; import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step'; import { ResolveApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step'; -import { StartWatchersOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step'; +import { + StartWatchersOrchestratorStep, + type FileBuiltEvent, +} from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step'; import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step'; import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; import * as fs from 'fs-extra'; @@ -69,7 +72,7 @@ export class DevModeOrchestrator { this.startWatchersStep = new StartWatchersOrchestratorStep({ ...stepDeps, scheduleSync: this.scheduleSync.bind(this), - uploadFilesStep: this.uploadFilesStep, + onFileBuilt: this.handleFileBuilt.bind(this), }); } @@ -90,6 +93,16 @@ export class DevModeOrchestrator { return this.state; } + private handleFileBuilt(event: FileBuiltEvent): void { + if (this.state.steps.uploadFiles.output.fileUploader) { + this.uploadFilesStep.uploadFile( + event.builtPath, + event.sourcePath, + event.fileFolder, + ); + } + } + private scheduleSync(): void { if (this.syncTimer) { clearTimeout(this.syncTimer); @@ -150,11 +163,9 @@ export class DevModeOrchestrator { } } - if (this.state.hasObjectsOrFieldsChanged(buildResult.manifest!)) { - await this.generateApiClientStep.execute({ - appPath: this.state.appPath, - }); - } + const objectsOrFieldsChanged = this.state.hasObjectsOrFieldsChanged( + buildResult.manifest!, + ); await this.uploadFilesStep.waitForUploads(); @@ -163,6 +174,16 @@ export class DevModeOrchestrator { builtFileInfos: this.state.steps.uploadFiles.output.builtFileInfos, appPath: this.state.appPath, }); + + if (objectsOrFieldsChanged) { + await this.generateApiClientStep.execute({ + appPath: this.state.appPath, + }); + + await this.uploadFilesStep.copyAndUploadApiClientFiles( + this.state.appPath, + ); + } } private async initializePipeline(manifest: Manifest): Promise { diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts index 0f13224b38..71b2f3739a 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts @@ -4,15 +4,23 @@ import { type EsbuildWatcher, } from '@/cli/utilities/build/common/esbuild-watcher'; import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher'; +import { TscWatcher } from '@/cli/utilities/build/common/tsc-watcher'; +import { type TypecheckError } from '@/cli/utilities/build/common/typecheck-plugin'; import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums'; import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher'; import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; -import { type UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; import type { Location } from 'esbuild'; import { type EventName } from 'chokidar/handler.js'; import { ASSETS_DIR } from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; +export type FileBuiltEvent = { + fileFolder: FileFolder; + builtPath: string; + sourcePath: string; + checksum: string; +}; + export type StartWatchersOrchestratorStepOutput = { watchersStarted: boolean; }; @@ -21,24 +29,25 @@ export class StartWatchersOrchestratorStep { private state: OrchestratorState; private scheduleSync: () => void; private notify: () => void; - private uploadFilesStep: UploadFilesOrchestratorStep; + private onFileBuilt: (event: FileBuiltEvent) => void; private manifestWatcher: ManifestWatcher | null = null; private logicFunctionsWatcher: EsbuildWatcher | null = null; private frontComponentsWatcher: EsbuildWatcher | null = null; private assetWatcher: FileUploadWatcher | null = null; private dependencyWatcher: FileUploadWatcher | null = null; + private tscWatcher: TscWatcher | null = null; constructor(options: { state: OrchestratorState; scheduleSync: () => void; notify: () => void; - uploadFilesStep: UploadFilesOrchestratorStep; + onFileBuilt: (event: FileBuiltEvent) => void; }) { this.state = options.state; this.scheduleSync = options.scheduleSync; this.notify = options.notify; - this.uploadFilesStep = options.uploadFilesStep; + this.onFileBuilt = options.onFileBuilt; } async start(): Promise { @@ -74,6 +83,8 @@ export class StartWatchersOrchestratorStep { } async close(): Promise { + this.tscWatcher?.close(); + await Promise.all([ this.manifestWatcher?.close(), this.logicFunctionsWatcher?.close(), @@ -117,32 +128,20 @@ export class StartWatchersOrchestratorStep { this.notify(); } - private handleFileBuilt({ - fileFolder, - builtPath, - sourcePath, - checksum, - }: { - fileFolder: FileFolder; - builtPath: string; - sourcePath: string; - checksum: string; - }): void { + private handleFileBuilt(event: FileBuiltEvent): void { this.state.addEvent({ - message: `Successfully built ${builtPath}`, + message: `Successfully built ${event.builtPath}`, status: 'success', }); - this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, { - checksum, - builtPath, - sourcePath, - fileFolder, + this.state.steps.uploadFiles.output.builtFileInfos.set(event.builtPath, { + checksum: event.checksum, + builtPath: event.builtPath, + sourcePath: event.sourcePath, + fileFolder: event.fileFolder, }); - if (this.state.steps.uploadFiles.output.fileUploader) { - this.uploadFilesStep.uploadFile(builtPath, sourcePath, fileFolder); - } + this.onFileBuilt(event); this.notify(); this.scheduleSync(); @@ -153,6 +152,7 @@ export class StartWatchersOrchestratorStep { frontComponents: string[], ): Promise { await Promise.all([ + this.startTscWatcher(), this.startLogicFunctionsWatcher(logicFunctions), this.startFrontComponentsWatcher(frontComponents), this.startAssetWatcher(), @@ -207,4 +207,31 @@ export class StartWatchersOrchestratorStep { this.dependencyWatcher.start(); } + + private async startTscWatcher(): Promise { + this.tscWatcher = new TscWatcher({ + appPath: this.state.appPath, + onErrors: this.handleTypecheckErrors.bind(this), + }); + + await this.tscWatcher.start(); + } + + private handleTypecheckErrors(errors: TypecheckError[]): void { + if (errors.length === 0) { + this.state.addEvent({ + message: 'Typecheck passed', + status: 'success', + }); + } else { + this.state.applyStepEvents( + errors.map((error) => ({ + message: `Type error in ${error.file}(${error.line},${error.column}): ${error.text}`, + status: 'error' as const, + })), + ); + } + + this.notify(); + } } diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts index 056efdffe3..d1cc0446ad 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts @@ -3,7 +3,13 @@ import { type OrchestratorStateBuiltFileInfo, } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; import { FileUploader } from '@/cli/utilities/file/file-uploader'; -import { type FileFolder } from 'twenty-shared/types'; +import crypto from 'crypto'; +import * as fs from 'fs-extra'; +import { join } from 'path'; +import { OUTPUT_DIR } from 'twenty-shared/application'; +import { FileFolder } from 'twenty-shared/types'; + +const API_CLIENT_FILES = ['types.ts', 'schema.ts']; export type UploadFilesOrchestratorStepOutput = { fileUploader: FileUploader | null; @@ -103,6 +109,48 @@ export class UploadFilesOrchestratorStep { this.notify(); } + async copyAndUploadApiClientFiles(appPath: string): Promise { + const generatedDir = join( + appPath, + 'node_modules', + 'twenty-sdk', + 'generated', + ); + + if (!(await fs.pathExists(generatedDir))) { + return; + } + + const outputDir = join(appPath, OUTPUT_DIR, 'api-client'); + + await fs.ensureDir(outputDir); + + for (const fileName of API_CLIENT_FILES) { + const absoluteSourcePath = join(generatedDir, fileName); + + if (!(await fs.pathExists(absoluteSourcePath))) { + continue; + } + + await fs.copy(absoluteSourcePath, join(outputDir, fileName)); + + const content = await fs.readFile(absoluteSourcePath); + const checksum = crypto.createHash('md5').update(content).digest('hex'); + + const builtPath = join(OUTPUT_DIR, 'api-client', fileName); + const sourcePath = join('api-client', fileName); + + this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, { + checksum, + builtPath, + sourcePath, + fileFolder: FileFolder.Dependencies, + }); + + this.uploadFile(builtPath, sourcePath, FileFolder.Dependencies); + } + } + private uploadPendingFiles(): void { for (const [ builtPath, diff --git a/packages/twenty-sdk/src/sdk/application/application-config.ts b/packages/twenty-sdk/src/sdk/application/application-config.ts index e7ba4c1a38..e7884ca638 100644 --- a/packages/twenty-sdk/src/sdk/application/application-config.ts +++ b/packages/twenty-sdk/src/sdk/application/application-config.ts @@ -2,5 +2,5 @@ import { type ApplicationManifest } from 'twenty-shared/application'; export type ApplicationConfig = Omit< ApplicationManifest, - 'packageJsonChecksum' | 'yarnLockChecksum' + 'packageJsonChecksum' | 'yarnLockChecksum' | 'apiClientChecksum' >; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts index b5daa8db08..f7a45d52cb 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts @@ -70,6 +70,7 @@ describe('syncApplication', () => { applicationVariables: {}, packageJsonChecksum: null, yarnLockChecksum: null, + apiClientChecksum: null, }, roles: [ { diff --git a/packages/twenty-shared/src/application/applicationType.ts b/packages/twenty-shared/src/application/applicationType.ts index 125f9105bf..0473def806 100644 --- a/packages/twenty-shared/src/application/applicationType.ts +++ b/packages/twenty-shared/src/application/applicationType.ts @@ -21,4 +21,5 @@ export type ApplicationManifest = SyncableEntityOptions & { marketplaceData?: ApplicationMarketplaceData; packageJsonChecksum: string | null; yarnLockChecksum: string | null; + apiClientChecksum: string | null; };