From 210c66b5dd61e8f502f19ff48a37b60cbb295edc Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 22 Jan 2026 18:16:55 +0100 Subject: [PATCH] Add checksum to manifest (#17368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Refactor app-dev state management and build utilities - Store only `manifest` in `AppDevState` instead of full `ManifestBuildResult`; add `sourcePath` to `FileStatus` - Pass `sourcePaths` directly to watchers instead of `ManifestBuildResult` - Only reset `fileUploadStatus` for functions/components when their source paths change - Add pure `updateManifestChecksum` utility that returns a new manifest without side effects - Extract `processEsbuildResult` to deduplicate build result processing between watchers - Rename `serverlessFunctions` → `functions` in SDK code (API unchanged) - Extract `writeManifestToOutput` to shared `manifest-writer.ts` --- .../front-component-config.ts | 2 +- .../application/functions/function-config.ts | 2 +- .../app-dev/manifest.expected.json | 12 +- .../app-dev/tests/manifest.tests.ts | 19 ++- .../app-dev/manifest.expected.json | 8 +- .../app-dev/tests/manifest.tests.ts | 26 +++- .../utils/normalize-manifest.util.ts | 20 +++ .../src/cli/commands/app/app-build.ts | 39 +++++- .../src/cli/commands/app/app-dev.ts | 120 +++++++++++++++--- .../cli/commands/function/function-execute.ts | 2 +- .../build/common/esbuild-result-processor.ts | 56 ++++++++ .../common/restartable-watcher.interface.ts | 11 +- .../front-component-watcher.ts | 74 ++++++----- .../build/functions/function-watcher.ts | 75 ++++++----- .../__tests__/validate-manifest.spec.ts | 30 ++--- .../manifest/entities/front-component.ts | 3 +- .../build/manifest/entities/function.ts | 9 +- .../build/manifest/manifest-build.ts | 72 +++++++---- .../build/manifest/manifest-display.ts | 2 +- .../build/manifest/manifest-validate.ts | 4 +- .../build/manifest/manifest-writer.ts | 18 +++ .../application/application-sync.service.ts | 6 +- .../application/applicationManifestType.ts | 2 +- .../application/frontComponentManifestType.ts | 1 + .../serverlessFunctionManifestType.ts | 1 + 25 files changed, 459 insertions(+), 155 deletions(-) create mode 100644 packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/build/common/esbuild-result-processor.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts diff --git a/packages/twenty-sdk/src/application/front-components/front-component-config.ts b/packages/twenty-sdk/src/application/front-components/front-component-config.ts index 9e567381209..98c83290b58 100644 --- a/packages/twenty-sdk/src/application/front-components/front-component-config.ts +++ b/packages/twenty-sdk/src/application/front-components/front-component-config.ts @@ -4,7 +4,7 @@ export type FrontComponentType = React.ComponentType; export type FrontComponentConfig = Omit< FrontComponentManifest, - 'sourceComponentPath' | 'builtComponentPath' | 'componentName' + 'sourceComponentPath' | 'builtComponentPath' | 'builtComponentChecksum' | 'componentName' > & { name?: string; description?: string; diff --git a/packages/twenty-sdk/src/application/functions/function-config.ts b/packages/twenty-sdk/src/application/functions/function-config.ts index a646636dab0..6e518ddfcc8 100644 --- a/packages/twenty-sdk/src/application/functions/function-config.ts +++ b/packages/twenty-sdk/src/application/functions/function-config.ts @@ -7,7 +7,7 @@ export type FunctionHandler = (...args: any[]) => any | Promise; export type FunctionConfig = Omit< ServerlessFunctionManifest, - 'sourceHandlerPath' | 'builtHandlerPath' | 'handlerName' + 'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' | 'handlerName' > & { name?: string; description?: string; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/manifest.expected.json b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/manifest.expected.json index ad76bcd8a71..08ccfc67df2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/manifest.expected.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/manifest.expected.json @@ -17,6 +17,7 @@ "frontComponents": [ { "builtComponentPath": "front-components/src/root.front-component.mjs", + "builtComponentChecksum": "[checksum]", "componentName": "RootComponent", "description": "A root-level front component", "name": "root-component", @@ -25,6 +26,7 @@ }, { "builtComponentPath": "front-components/src/components/card.front-component.mjs", + "builtComponentChecksum": "[checksum]", "componentName": "CardDisplay", "description": "A component using an external component file", "name": "card-component", @@ -33,6 +35,7 @@ }, { "builtComponentPath": "front-components/src/components/greeting.front-component.mjs", + "builtComponentChecksum": "[checksum]", "componentName": "GreetingComponent", "description": "A component that uses greeting utility", "name": "greeting-component", @@ -41,6 +44,7 @@ }, { "builtComponentPath": "front-components/src/components/test.front-component.mjs", + "builtComponentChecksum": "[checksum]", "componentName": "TestComponent", "description": "A test front component", "name": "test-component", @@ -280,8 +284,9 @@ "universalIdentifier": "b648f87b-1d26-4961-b974-0908fd991061" } ], - "serverlessFunctions": [ + "functions": [ { + "builtHandlerChecksum": "[checksum]", "builtHandlerPath": "functions/src/root.function.mjs", "handlerName": "rootHandler", "name": "root-function", @@ -299,6 +304,7 @@ "universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000001" }, { + "builtHandlerChecksum": "[checksum]", "builtHandlerPath": "functions/src/functions/greeting.function.mjs", "handlerName": "greetingHandler", "name": "greeting-function", @@ -316,7 +322,8 @@ "universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000001" }, { - "builtHandlerPath": "functions/src/utils/test-function-2.util.mjs", + "builtHandlerChecksum": "[checksum]", + "builtHandlerPath": "functions/src/functions/test-function-2.function.mjs", "handlerName": "testFunction2", "name": "test-function-2", "sourceHandlerPath": "src/utils/test-function-2.util.ts", @@ -331,6 +338,7 @@ "universalIdentifier": "eb3ffc98-88ec-45d4-9b4a-56833b219ccb" }, { + "builtHandlerChecksum": "[checksum]", "builtHandlerPath": "functions/src/functions/test-function.function.mjs", "handlerName": "handler", "name": "test-function", diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts index ee5e19c007a..e8e19bf91da 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts @@ -1,6 +1,7 @@ import * as fs from 'fs-extra'; import { join } from 'path'; +import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util'; import expectedManifest from '../manifest.expected.json'; export const defineManifestTests = (appPath: string): void => { @@ -14,7 +15,21 @@ export const defineManifestTests = (appPath: string): void => { const { sources: _sources, ...sanitizedManifest } = manifest; - expect(sanitizedManifest).toEqual(expectedManifest); + expect(normalizeManifestForComparison(sanitizedManifest)).toEqual( + normalizeManifestForComparison(expectedManifest), + ); + + for (const fn of manifest.functions) { + expect(fn.builtHandlerChecksum).toBeDefined(); + expect(fn.builtHandlerChecksum).not.toBeNull(); + expect(typeof fn.builtHandlerChecksum).toBe('string'); + } + + for (const component of manifest.frontComponents ?? []) { + expect(component.builtComponentChecksum).toBeDefined(); + expect(component.builtComponentChecksum).not.toBeNull(); + expect(typeof component.builtComponentChecksum).toBe('string'); + } }); it('should have correct application config', async () => { @@ -28,7 +43,7 @@ export const defineManifestTests = (appPath: string): void => { const manifest = await fs.readJson(manifestOutputPath); expect(manifest?.objects).toHaveLength(2); - expect(manifest?.serverlessFunctions).toHaveLength(4); + expect(manifest?.functions).toHaveLength(4); expect(manifest?.frontComponents).toHaveLength(4); expect(manifest?.roles).toHaveLength(2); expect(manifest?.objectExtensions).toHaveLength(1); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/manifest.expected.json b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/manifest.expected.json index 7a5a3ac63bd..615da3609bf 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/manifest.expected.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/manifest.expected.json @@ -25,7 +25,7 @@ ] } ], - "serverlessFunctions": [ + "functions": [ { "universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000010", "name": "my-function", @@ -41,7 +41,8 @@ ], "handlerName": "myHandler", "sourceHandlerPath": "my.function.ts", - "builtHandlerPath": "functions/my.function.mjs" + "builtHandlerPath": "functions/my.function.mjs", + "builtHandlerChecksum": "[checksum]" } ], "frontComponents": [ @@ -51,7 +52,8 @@ "description": "A root-level front component", "componentName": "MyComponent", "sourceComponentPath": "my.front-component.tsx", - "builtComponentPath": "front-components/my.front-component.mjs" + "builtComponentPath": "front-components/my.front-component.mjs", + "builtComponentChecksum": "[checksum]" } ], "roles": [ diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts index 505cca9f1e9..5ca1f51b7b5 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts @@ -2,6 +2,8 @@ import * as fs from 'fs-extra'; import { join } from 'path'; import { type ApplicationManifest } from 'twenty-shared/application'; +import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util'; + export const defineManifestTests = (appPath: string): void => { describe('manifest', () => { it('should have generated manifest.json', async () => { @@ -19,8 +21,28 @@ export const defineManifestTests = (appPath: string): void => { expect(manifest.application).toEqual(expected.application); expect(manifest.objects).toEqual(expected.objects); - expect(manifest.serverlessFunctions).toEqual(expected.serverlessFunctions); - expect(manifest.frontComponents).toEqual(expected.frontComponents); + + expect(normalizeManifestForComparison({ functions: manifest.functions }).functions).toEqual( + normalizeManifestForComparison({ functions: expected.functions }).functions, + ); + + for (const fn of manifest.functions) { + expect(fn.builtHandlerChecksum).toBeDefined(); + expect(fn.builtHandlerChecksum).not.toBeNull(); + expect(typeof fn.builtHandlerChecksum).toBe('string'); + } + + expect( + normalizeManifestForComparison({ frontComponents: manifest.frontComponents }).frontComponents, + ).toEqual( + normalizeManifestForComparison({ frontComponents: expected.frontComponents }).frontComponents, + ); + + for (const component of manifest.frontComponents ?? []) { + expect(component.builtComponentChecksum).toBeDefined(); + expect(component.builtComponentChecksum).not.toBeNull(); + expect(typeof component.builtComponentChecksum).toBe('string'); + } expect(manifest.roles).toEqual(expected.roles); }); }); diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts new file mode 100644 index 00000000000..e06fe3fccf7 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts @@ -0,0 +1,20 @@ +import { type ApplicationManifest } from 'twenty-shared/application'; + +// Replace dynamic checksum values with a placeholder for consistent comparisons +export const normalizeManifestForComparison = < + T extends Partial, +>( + manifest: T, +): T => ({ + ...manifest, + functions: manifest.functions?.map((fn) => ({ + ...fn, + builtHandlerChecksum: fn.builtHandlerChecksum ? '[checksum]' : null, + })), + frontComponents: manifest.frontComponents?.map((component) => ({ + ...component, + builtComponentChecksum: component.builtComponentChecksum + ? '[checksum]' + : null, + })), +}); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-build.ts b/packages/twenty-sdk/src/cli/commands/app/app-build.ts index 54d023e3fb9..6163668d104 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-build.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-build.ts @@ -2,8 +2,13 @@ import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types'; import { createLogger } from '@/cli/utilities/build/common/logger'; import { FrontComponentsWatcher } from '@/cli/utilities/build/front-components/front-component-watcher'; import { FunctionsWatcher } from '@/cli/utilities/build/functions/function-watcher'; -import { runManifestBuild, type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build'; +import { + runManifestBuild, + updateManifestChecksum, + type ManifestBuildResult, +} from '@/cli/utilities/build/manifest/manifest-build'; import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; +import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; const initLogger = createLogger('init'); @@ -45,7 +50,7 @@ export class AppBuildCommand { await this.buildFunctions(buildResult); await this.buildFrontComponents(buildResult); - + await writeManifestToOutput(this.appPath, buildResult.manifest); await this.cleanup(); return buildResult; @@ -54,8 +59,21 @@ export class AppBuildCommand { private async buildFunctions(buildResult: ManifestBuildResult): Promise { this.functionsBuilder = new FunctionsWatcher({ appPath: this.appPath, - buildResult, + sourcePaths: buildResult.filePaths.functions, watch: false, + onFileBuilt: (builtPath, checksum) => { + if (buildResult.manifest) { + const updatedManifest = updateManifestChecksum({ + manifest: buildResult.manifest, + entityType: 'function', + builtPath, + checksum, + }); + if (updatedManifest) { + buildResult.manifest = updatedManifest; + } + } + }, }); await this.functionsBuilder.start(); @@ -64,8 +82,21 @@ export class AppBuildCommand { private async buildFrontComponents(buildResult: ManifestBuildResult): Promise { this.frontComponentsBuilder = new FrontComponentsWatcher({ appPath: this.appPath, - buildResult, + sourcePaths: buildResult.filePaths.frontComponents, watch: false, + onFileBuilt: (builtPath, checksum) => { + if (buildResult.manifest) { + const updatedManifest = updateManifestChecksum({ + manifest: buildResult.manifest, + entityType: 'frontComponent', + builtPath, + checksum, + }); + if (updatedManifest) { + buildResult.manifest = updatedManifest; + } + } + }, }); await this.frontComponentsBuilder.start(); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts index ee6f208e330..ff02e05e35b 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts @@ -1,9 +1,11 @@ import { createLogger } from '@/cli/utilities/build/common/logger'; import { FrontComponentsWatcher } from '@/cli/utilities/build/front-components/front-component-watcher'; import { FunctionsWatcher } from '@/cli/utilities/build/functions/function-watcher'; -import { runManifestBuild, type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build'; +import { runManifestBuild, updateManifestChecksum } from '@/cli/utilities/build/manifest/manifest-build'; import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher'; +import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; +import { type ApplicationManifest } from 'twenty-shared/application'; const initLogger = createLogger('init'); @@ -11,8 +13,21 @@ export type AppDevOptions = { appPath?: string; }; +export type FileStatus = { + sourcePath: string; + builtPath: string; + checksum: string | null; + isUploaded: boolean; +}; + +export type FileStatusMaps = { + functions: Map; + frontComponents: Map; +}; + type AppDevState = { - buildResult: ManifestBuildResult | null; + manifest: ApplicationManifest | null; + fileStatusMaps: FileStatusMaps; }; export class AppDevCommand { @@ -22,7 +37,11 @@ export class AppDevCommand { private appPath: string = ''; private state: AppDevState = { - buildResult: null, + manifest: null, + fileStatusMaps: { + functions: new Map(), + frontComponents: new Map(), + }, }; async execute(options: AppDevOptions): Promise { @@ -44,11 +63,39 @@ export class AppDevCommand { return; } - this.state.buildResult = buildResult; + this.state.manifest = buildResult.manifest; + this.initializeFunctionsFileUploadStatus(buildResult.manifest); + this.initializeFrontComponentsFileUploadStatus(buildResult.manifest); await this.startManifestWatcher(); - await this.startFunctionsWatcher(buildResult); - await this.startFrontComponentsWatcher(buildResult); + await this.startFunctionsWatcher(buildResult.filePaths.functions); + await this.startFrontComponentsWatcher(buildResult.filePaths.frontComponents); + } + + private initializeFunctionsFileUploadStatus(manifest: ApplicationManifest): void { + this.state.fileStatusMaps.functions.clear(); + + for (const fn of manifest.functions ?? []) { + this.state.fileStatusMaps.functions.set(fn.universalIdentifier, { + sourcePath: fn.sourceHandlerPath, + builtPath: fn.builtHandlerPath, + checksum: null, + isUploaded: false, + }); + } + } + + private initializeFrontComponentsFileUploadStatus(manifest: ApplicationManifest): void { + this.state.fileStatusMaps.frontComponents.clear(); + + for (const component of manifest.frontComponents ?? []) { + this.state.fileStatusMaps.frontComponents.set(component.universalIdentifier, { + sourcePath: component.sourceComponentPath, + builtPath: component.builtComponentPath, + checksum: null, + isUploaded: false, + }); + } } private async startManifestWatcher(): Promise { @@ -56,14 +103,24 @@ export class AppDevCommand { appPath: this.appPath, callbacks: { onBuildSuccess: (result) => { - this.state.buildResult = result; + this.state.manifest = result.manifest; - if (this.functionsWatcher?.shouldRestart(result)) { - this.functionsWatcher.restart(result); + const functionSourcePaths = result.filePaths.functions; + const shouldRestartFunctions = this.functionsWatcher?.shouldRestart(functionSourcePaths); + if (shouldRestartFunctions) { + if (result.manifest) { + this.initializeFunctionsFileUploadStatus(result.manifest); + } + this.functionsWatcher?.restart(functionSourcePaths); } - if (this.frontComponentsWatcher?.shouldRestart(result)) { - this.frontComponentsWatcher.restart(result); + const componentSourcePaths = result.filePaths.frontComponents; + const shouldRestartFrontComponents = this.frontComponentsWatcher?.shouldRestart(componentSourcePaths); + if (shouldRestartFrontComponents) { + if (result.manifest) { + this.initializeFrontComponentsFileUploadStatus(result.manifest); + } + this.frontComponentsWatcher?.restart(componentSourcePaths); } }, }, @@ -72,24 +129,57 @@ export class AppDevCommand { await this.manifestWatcher.start(); } - private async startFunctionsWatcher(buildResult: ManifestBuildResult): Promise { + private async startFunctionsWatcher(sourcePaths: string[]): Promise { this.functionsWatcher = new FunctionsWatcher({ appPath: this.appPath, - buildResult, + sourcePaths, + onFileBuilt: (builtPath, checksum) => { + this.updateFileStatus('function', builtPath, checksum); + }, }); await this.functionsWatcher.start(); } - private async startFrontComponentsWatcher(buildResult: ManifestBuildResult): Promise { + private async startFrontComponentsWatcher(sourcePaths: string[]): Promise { this.frontComponentsWatcher = new FrontComponentsWatcher({ appPath: this.appPath, - buildResult, + sourcePaths, + onFileBuilt: (builtPath, checksum) => { + this.updateFileStatus('frontComponent', builtPath, checksum); + }, }); await this.frontComponentsWatcher.start(); } + private updateFileStatus( + entityType: 'function' | 'frontComponent', + builtPath: string, + checksum: string, + ): void { + const statusMap = entityType === 'function' + ? this.state.fileStatusMaps.functions + : this.state.fileStatusMaps.frontComponents; + + for (const [_id, status] of statusMap) { + if (status.builtPath === builtPath) { + status.checksum = checksum; + status.isUploaded = false; + break; + } + } + + const manifest = this.state.manifest; + if (manifest) { + const updatedManifest = updateManifestChecksum({ manifest, entityType, builtPath, checksum }); + if (updatedManifest) { + this.state.manifest = updatedManifest; + writeManifestToOutput(this.appPath, updatedManifest); + } + } + } + private setupGracefulShutdown(): void { const shutdown = () => { console.log(''); diff --git a/packages/twenty-sdk/src/cli/commands/function/function-execute.ts b/packages/twenty-sdk/src/cli/commands/function/function-execute.ts index 605de012d77..c36425df2ed 100644 --- a/packages/twenty-sdk/src/cli/commands/function/function-execute.ts +++ b/packages/twenty-sdk/src/cli/commands/function/function-execute.ts @@ -164,7 +164,7 @@ export class FunctionExecuteCommand { fn: { universalIdentifier: string; applicationId: string | null }, manifest: ApplicationManifest, ): boolean { - return manifest.serverlessFunctions.some( + return manifest.functions.some( (manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier, ); } diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-result-processor.ts b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-result-processor.ts new file mode 100644 index 00000000000..ade5411ce5e --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-result-processor.ts @@ -0,0 +1,56 @@ +import crypto from 'crypto'; +import type * as esbuild from 'esbuild'; +import * as fs from 'fs-extra'; +import path from 'path'; +import { type OnFileBuiltCallback } from './restartable-watcher.interface'; + +export type ProcessEsbuildResultParams = { + result: esbuild.BuildResult; + outputDir: string; + builtDir: string; + lastChecksums: Map; + onFileBuilt?: OnFileBuiltCallback; + onSuccess: (relativePath: string) => void; +}; + +export type ProcessEsbuildResultOutput = { + hasChanges: boolean; +}; + +export const processEsbuildResult = async ({ + result, + outputDir, + builtDir, + lastChecksums, + onFileBuilt, + onSuccess, +}: ProcessEsbuildResultParams): Promise => { + const outputFiles = Object.keys(result.metafile?.outputs ?? {}) + .filter((file) => file.endsWith('.mjs')); + + let hasChanges = false; + + for (const outputFile of outputFiles) { + const absoluteOutputFile = path.resolve(outputFile); + const relativePath = path.relative(outputDir, absoluteOutputFile); + const builtPath = `${builtDir}/${relativePath}`; + + const content = await fs.readFile(absoluteOutputFile); + const checksum = crypto.createHash('md5').update(content).digest('hex'); + + const lastChecksum = lastChecksums.get(builtPath); + if (lastChecksum === checksum) { + continue; + } + + hasChanges = true; + lastChecksums.set(builtPath, checksum); + onSuccess(relativePath); + + if (onFileBuilt) { + onFileBuilt(builtPath, checksum); + } + } + + return { hasChanges }; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts b/packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts index e2e88959e32..fe0d5f1a777 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/restartable-watcher.interface.ts @@ -1,14 +1,15 @@ -import { type ManifestBuildResult } from '../manifest/manifest-build'; - export interface RestartableWatcher { - restart(result: ManifestBuildResult): Promise; + restart(sourcePaths: string[]): Promise; start(): Promise; close(): Promise; - shouldRestart(result: ManifestBuildResult): boolean; + shouldRestart(sourcePaths: string[]): boolean; } +export type OnFileBuiltCallback = (builtPath: string, checksum: string) => void; + export type RestartableWatcherOptions = { appPath: string; - buildResult: ManifestBuildResult | null; + sourcePaths: string[]; watch?: boolean; + onFileBuilt?: OnFileBuiltCallback; }; 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 9e83844cbf9..d23bb6df4f6 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 @@ -4,11 +4,12 @@ import path from 'path'; import { cleanupRemovedFiles } from '../common/cleanup-removed-files'; import { OUTPUT_DIR } from '../common/constants'; import { createLogger } from '../common/logger'; +import { processEsbuildResult } from '../common/esbuild-result-processor'; import { + type OnFileBuiltCallback, type RestartableWatcher, type RestartableWatcherOptions, } from '../common/restartable-watcher.interface'; -import { type ManifestBuildResult } from '../manifest/manifest-build'; import { FRONT_COMPONENTS_DIR } from './constants'; const logger = createLogger('front-components-watch'); @@ -30,17 +31,21 @@ export class FrontComponentsWatcher implements RestartableWatcher { private esBuildContext: esbuild.BuildContext | null = null; private isRestarting = false; private watchMode: boolean; - private lastInputsSignature: string | null = null; + private lastChecksums: Map = new Map(); + private onFileBuilt?: OnFileBuiltCallback; + private buildCompletePromise: Promise = Promise.resolve(); + private resolveBuildComplete: (() => void) | null = null; constructor(options: RestartableWatcherOptions) { this.appPath = options.appPath; - this.componentPaths = options.buildResult?.filePaths.frontComponents ?? []; + this.componentPaths = options.sourcePaths; this.watchMode = options.watch ?? true; + this.onFileBuilt = options.onFileBuilt; } - shouldRestart(result: ManifestBuildResult): boolean { + shouldRestart(sourcePaths: string[]): boolean { const currentPaths = this.componentPaths.sort().join(','); - const newPaths = result.filePaths.frontComponents.sort().join(','); + const newPaths = [...sourcePaths].sort().join(','); return currentPaths !== newPaths; } @@ -65,7 +70,7 @@ export class FrontComponentsWatcher implements RestartableWatcher { this.esBuildContext = null; } - async restart(result: ManifestBuildResult): Promise { + async restart(sourcePaths: string[]): Promise { if (this.isRestarting) return; this.isRestarting = true; @@ -74,9 +79,9 @@ export class FrontComponentsWatcher implements RestartableWatcher { await this.close(); const outputDir = path.join(this.appPath, OUTPUT_DIR, FRONT_COMPONENTS_DIR); - const newPaths = result.filePaths.frontComponents; - await cleanupRemovedFiles(outputDir, this.componentPaths, newPaths); - this.componentPaths = newPaths; + await cleanupRemovedFiles(outputDir, this.componentPaths, sourcePaths); + this.componentPaths = sourcePaths; + this.lastChecksums.clear(); if (this.componentPaths.length > 0) { logger.log('🎨 Building...'); @@ -102,8 +107,6 @@ export class FrontComponentsWatcher implements RestartableWatcher { } const watchMode = this.watchMode; - - // Capture reference for use in plugin callbacks const watcher = this; this.esBuildContext = await esbuild.context({ @@ -123,32 +126,30 @@ export class FrontComponentsWatcher implements RestartableWatcher { { name: 'build-notifications', setup: (build) => { - build.onEnd((result) => { - if (result.errors.length > 0) { - logger.error('✗ Build error:'); - for (const error of result.errors) { - logger.error(` ${error.text}`); + build.onEnd(async (result) => { + try { + if (result.errors.length > 0) { + logger.error('✗ Build error:'); + for (const error of result.errors) { + logger.error(` ${error.text}`); + } + return; } - return; - } - const inputs = Object.keys(result.metafile?.inputs ?? {}).sort(); - const inputsSignature = inputs.join(','); + const { hasChanges } = await processEsbuildResult({ + result, + outputDir, + builtDir: FRONT_COMPONENTS_DIR, + lastChecksums: watcher.lastChecksums, + onFileBuilt: watcher.onFileBuilt, + onSuccess: (relativePath) => logger.success(`✓ Built ${relativePath}`), + }); - if (watcher.lastInputsSignature === inputsSignature) { - return; - } - watcher.lastInputsSignature = inputsSignature; - - const outputs = Object.keys(result.metafile?.outputs ?? {}) - .filter((file) => file.endsWith('.mjs')) - .map((file) => path.relative(outputDir, file)); - - for (const output of outputs) { - logger.success(`✓ Built ${output}`); - } - if (watchMode) { - logger.log('👀 Watching for changes...'); + if (hasChanges && watchMode) { + logger.log('👀 Watching for changes...'); + } + } finally { + watcher.resolveBuildComplete?.(); } }); }, @@ -156,7 +157,12 @@ export class FrontComponentsWatcher implements RestartableWatcher { ], }); + this.buildCompletePromise = new Promise((resolve) => { + this.resolveBuildComplete = resolve; + }); + await this.esBuildContext.rebuild(); + await this.buildCompletePromise; if (this.watchMode) { await this.esBuildContext.watch(); 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 1d0ccad21ed..211f3a6b9eb 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 @@ -4,11 +4,12 @@ import path from 'path'; import { cleanupRemovedFiles } from '../common/cleanup-removed-files'; import { OUTPUT_DIR } from '../common/constants'; import { createLogger } from '../common/logger'; +import { processEsbuildResult } from '../common/esbuild-result-processor'; import { + type OnFileBuiltCallback, type RestartableWatcher, type RestartableWatcherOptions, } from '../common/restartable-watcher.interface'; -import { type ManifestBuildResult } from '../manifest/manifest-build'; import { FUNCTIONS_DIR } from './constants'; const logger = createLogger('functions-watch'); @@ -44,17 +45,21 @@ export class FunctionsWatcher implements RestartableWatcher { private esBuildContext: esbuild.BuildContext | null = null; private isRestarting = false; private watchMode: boolean; - private lastInputsSignature: string | null = null; + private lastChecksums: Map = new Map(); + private onFileBuilt?: OnFileBuiltCallback; + private buildCompletePromise: Promise = Promise.resolve(); + private resolveBuildComplete: (() => void) | null = null; constructor(options: RestartableWatcherOptions) { this.appPath = options.appPath; - this.functionPaths = options.buildResult?.filePaths.functions ?? []; + this.functionPaths = options.sourcePaths; this.watchMode = options.watch ?? true; + this.onFileBuilt = options.onFileBuilt; } - shouldRestart(result: ManifestBuildResult): boolean { + shouldRestart(sourcePaths: string[]): boolean { const currentPaths = this.functionPaths.sort().join(','); - const newPaths = result.filePaths.functions.sort().join(','); + const newPaths = [...sourcePaths].sort().join(','); return currentPaths !== newPaths; } @@ -79,7 +84,7 @@ export class FunctionsWatcher implements RestartableWatcher { this.esBuildContext = null; } - async restart(result: ManifestBuildResult): Promise { + async restart(sourcePaths: string[]): Promise { if (this.isRestarting) return; this.isRestarting = true; @@ -88,9 +93,9 @@ export class FunctionsWatcher implements RestartableWatcher { await this.close(); const outputDir = path.join(this.appPath, OUTPUT_DIR, FUNCTIONS_DIR); - const newPaths = result.filePaths.functions; - await cleanupRemovedFiles(outputDir, this.functionPaths, newPaths); - this.functionPaths = newPaths; + await cleanupRemovedFiles(outputDir, this.functionPaths, sourcePaths); + this.functionPaths = sourcePaths; + this.lastChecksums.clear(); if (this.functionPaths.length > 0) { logger.log('📦 Building...'); @@ -116,8 +121,6 @@ export class FunctionsWatcher implements RestartableWatcher { } const watchMode = this.watchMode; - - // Capture reference for use in plugin callbacks const watcher = this; this.esBuildContext = await esbuild.context({ @@ -137,7 +140,6 @@ export class FunctionsWatcher implements RestartableWatcher { { name: 'external-patterns', setup: (build) => { - // Externalize paths containing "generated" (matches /(?:^|\/)generated(?:\/|$)/) build.onResolve({ filter: /(?:^|\/)generated(?:\/|$)/ }, (args) => ({ path: args.path, external: true, @@ -147,32 +149,30 @@ export class FunctionsWatcher implements RestartableWatcher { { name: 'build-notifications', setup: (build) => { - build.onEnd((result) => { - if (result.errors.length > 0) { - logger.error('✗ Build error:'); - for (const error of result.errors) { - logger.error(` ${error.text}`); + build.onEnd(async (result) => { + try { + if (result.errors.length > 0) { + logger.error('✗ Build error:'); + for (const error of result.errors) { + logger.error(` ${error.text}`); + } + return; } - return; - } - const inputs = Object.keys(result.metafile?.inputs ?? {}).sort(); - const inputsSignature = inputs.join(','); + const { hasChanges } = await processEsbuildResult({ + result, + outputDir, + builtDir: FUNCTIONS_DIR, + lastChecksums: watcher.lastChecksums, + onFileBuilt: watcher.onFileBuilt, + onSuccess: (relativePath) => logger.success(`✓ Built ${relativePath}`), + }); - if (watcher.lastInputsSignature === inputsSignature) { - return; - } - watcher.lastInputsSignature = inputsSignature; - - const outputs = Object.keys(result.metafile?.outputs ?? {}) - .filter((file) => file.endsWith('.mjs')) - .map((file) => path.relative(outputDir, file)); - - for (const output of outputs) { - logger.success(`✓ Built ${output}`); - } - if (watchMode) { - logger.log('👀 Watching for changes...'); + if (hasChanges && watchMode) { + logger.log('👀 Watching for changes...'); + } + } finally { + watcher.resolveBuildComplete?.(); } }); }, @@ -180,7 +180,12 @@ export class FunctionsWatcher implements RestartableWatcher { ], }); + this.buildCompletePromise = new Promise((resolve) => { + this.resolveBuildComplete = resolve; + }); + await this.esBuildContext.rebuild(); + await this.buildCompletePromise; if (this.watchMode) { await this.esBuildContext.watch(); 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 022fb36e080..48fb14bbb93 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 @@ -32,7 +32,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [validObjectExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -59,7 +59,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [extensionByUuid], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -86,7 +86,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [validObjectExtension, anotherExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -122,7 +122,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [extensionWithSelect], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -141,7 +141,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -163,7 +163,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -189,7 +189,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -216,7 +216,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -246,7 +246,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -276,7 +276,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -306,7 +306,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -337,7 +337,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -369,7 +369,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [invalidExtension], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -410,7 +410,7 @@ describe('validateManifest - objectExtensions', () => { application: validApplication, objects: [], objectExtensions: [extensionWithDuplicates], - serverlessFunctions: [], + functions: [], roles: [], }); @@ -457,7 +457,7 @@ describe('validateManifest - objectExtensions', () => { ], }, ], - serverlessFunctions: [], + functions: [], roles: [], }); 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 882353fa167..deee9d88fe3 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 @@ -15,7 +15,7 @@ const logger = createLogger('manifest-watch'); type FrontComponentConfig = Omit< FrontComponentManifest, - 'sourceComponentPath' | 'builtComponentPath' | 'componentName' + 'sourceComponentPath' | 'builtComponentPath' | 'builtComponentChecksum' | 'componentName' > & { component: { name: string }; }; @@ -47,6 +47,7 @@ export class FrontComponentEntityBuilder componentName: component.name, sourceComponentPath: filePath, builtComponentPath, + builtComponentChecksum: null, }); } catch (error) { throw new Error( 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 859b99b8ab7..959210b3cd0 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 @@ -15,7 +15,7 @@ const logger = createLogger('manifest-watch'); type ExtractedFunctionManifest = Omit< ServerlessFunctionManifest, - 'sourceHandlerPath' | 'builtHandlerPath' + 'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' > & { handlerPath: string; }; @@ -42,12 +42,15 @@ export class FunctionEntityBuilder ); const { handlerPath, ...rest } = extracted; - const builtHandlerPath = this.computeBuiltHandlerPath(handlerPath); + // builtHandlerPath is computed from filePath (the .function.ts file) + // since that's what esbuild actually builds, not handlerPath + const builtHandlerPath = this.computeBuiltHandlerPath(filePath); manifests.push({ ...rest, sourceHandlerPath: handlerPath, builtHandlerPath, + builtHandlerChecksum: null, }); } catch (error) { throw new Error( @@ -149,7 +152,7 @@ export class FunctionEntityBuilder findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { const seen = new Map(); - const functions = manifest.serverlessFunctions ?? []; + const functions = manifest.functions ?? []; for (const fn of functions) { if (fn.universalIdentifier) { 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 331fdf80490..3c196ac9b9b 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 @@ -2,10 +2,9 @@ import { findPathFile } from '@/cli/utilities/file/utils/file-find'; import { parseJsoncFile } from '@/cli/utilities/file/utils/file-jsonc'; import { glob } from 'fast-glob'; import * as fs from 'fs-extra'; -import path, { relative, sep } from 'path'; +import { 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 { createLogger } from '../common/logger'; import { applicationEntityBuilder } from './entities/application'; import { frontComponentEntityBuilder } from './entities/front-component'; @@ -16,6 +15,7 @@ import { roleEntityBuilder } from './entities/role'; import { displayEntitySummary, displayErrors, displayWarnings } from './manifest-display'; import { manifestExtractFromFileServer } from './manifest-extract-from-file-server'; import { validateManifest } from './manifest-validate'; +import { writeManifestToOutput } from './manifest-writer'; import { ManifestValidationError } from './manifest.types'; const logger = createLogger('manifest-watch'); @@ -58,25 +58,6 @@ const loadSources = async (appPath: string): Promise => { return sources; }; -const writeManifestToOutput = async ( - appPath: string, - manifest: ApplicationManifest, -): Promise => { - try { - const outputDir = path.join(appPath, OUTPUT_DIR); - await fs.ensureDir(outputDir); - - const manifestPath = path.join(outputDir, 'manifest.json'); - await fs.writeJSON(manifestPath, manifest, { spaces: 2 }); - - logger.success(`✓ Written to ${manifestPath}`); - } catch (error) { - logger.error( - `✗ Failed to write: ${error instanceof Error ? error.message : error}`, - ); - } -}; - export type RunManifestBuildOptions = { display?: boolean; writeOutput?: boolean; @@ -96,6 +77,48 @@ export type ManifestBuildResult = { filePaths: EntityFilePaths; }; +export type ManifestEntityType = 'function' | 'frontComponent'; + +export type UpdateManifestChecksumParams = { + manifest: ApplicationManifest; + entityType: ManifestEntityType; + builtPath: string; + checksum: string; +}; + +export const updateManifestChecksum = ({ + manifest, + entityType, + builtPath, + checksum, +}: UpdateManifestChecksumParams): ApplicationManifest | null => { + if (entityType === 'function') { + const fnIndex = manifest.functions.findIndex((f) => f.builtHandlerPath === builtPath); + if (fnIndex === -1) { + return null; + } + return { + ...manifest, + functions: manifest.functions.map((fn, index) => + index === fnIndex ? { ...fn, builtHandlerChecksum: checksum } : fn, + ), + }; + } + + const componentIndex = manifest.frontComponents?.findIndex( + (c) => c.builtComponentPath === builtPath, + ) ?? -1; + if (componentIndex === -1) { + return null; + } + return { + ...manifest, + frontComponents: manifest.frontComponents?.map((component, index) => + index === componentIndex ? { ...component, builtComponentChecksum: checksum } : component, + ), + }; +}; + export const runManifestBuild = async ( appPath: string, options: RunManifestBuildOptions = {}, @@ -152,7 +175,7 @@ export const runManifestBuild = async ( objects: objectManifests, objectExtensions: objectExtensionManifests.length > 0 ? objectExtensionManifests : undefined, - serverlessFunctions: functionManifests, + functions: functionManifests, frontComponents: frontComponentManifests.length > 0 ? frontComponentManifests : undefined, roles: roleManifests, @@ -164,7 +187,7 @@ export const runManifestBuild = async ( application, objects: objectManifests, objectExtensions: objectExtensionManifests, - serverlessFunctions: functionManifests, + functions: functionManifests, frontComponents: frontComponentManifests, roles: roleManifests, }); @@ -181,7 +204,8 @@ export const runManifestBuild = async ( } if (writeOutput) { - await writeManifestToOutput(appPath, manifest); + const manifestPath = await writeManifestToOutput(appPath, manifest); + logger.success(`✓ Written to ${manifestPath}`); } return { manifest, filePaths }; 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 c610227b390..d43608b7281 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 @@ -14,7 +14,7 @@ export const displayEntitySummary = (manifest: ApplicationManifest): void => { manifest.application ? [manifest.application] : [], ); objectEntityBuilder.display(manifest.objects ?? []); - functionEntityBuilder.display(manifest.serverlessFunctions ?? []); + functionEntityBuilder.display(manifest.functions ?? []); frontComponentEntityBuilder.display(manifest.frontComponents ?? []); roleEntityBuilder.display(manifest.roles ?? []); }; 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 87eb91f42fd..7646b592993 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 @@ -40,7 +40,7 @@ export const validateManifest = ( ); objectEntityBuilder.validate(manifest.objects ?? [], errors); objectExtensionEntityBuilder.validate(manifest.objectExtensions ?? [], errors); - functionEntityBuilder.validate(manifest.serverlessFunctions ?? [], errors); + functionEntityBuilder.validate(manifest.functions ?? [], errors); roleEntityBuilder.validate(manifest.roles ?? [], errors); frontComponentEntityBuilder.validate(manifest.frontComponents ?? [], errors); @@ -58,7 +58,7 @@ export const validateManifest = ( }); } - if (!isNonEmptyArray(manifest.serverlessFunctions)) { + if (!isNonEmptyArray(manifest.functions)) { warnings.push({ message: 'No functions defined', }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts new file mode 100644 index 00000000000..bdfac2c23ef --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs-extra'; +import path from 'path'; +import { type ApplicationManifest } from 'twenty-shared/application'; + +import { OUTPUT_DIR } from '../common/constants'; + +export const writeManifestToOutput = async ( + appPath: string, + manifest: ApplicationManifest, +): Promise => { + const outputDir = path.join(appPath, OUTPUT_DIR); + await fs.ensureDir(outputDir); + + const manifestPath = path.join(outputDir, 'manifest.json'); + await fs.writeJSON(manifestPath, manifest, { spaces: 2 }); + + return manifestPath; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index e45f2afc413..d0e8318e523 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -105,7 +105,7 @@ export class ApplicationSyncService { }); } - if (manifest.serverlessFunctions.length > 0) { + if (manifest.functions.length > 0) { if (!isDefined(application.serverlessFunctionLayerId)) { throw new ApplicationException( `Failed to sync serverless function, could not find a serverless function layer.`, @@ -114,7 +114,7 @@ export class ApplicationSyncService { } await this.syncServerlessFunctions({ - serverlessFunctionsToSync: manifest.serverlessFunctions, + serverlessFunctionsToSync: manifest.functions, code: manifest.sources, workspaceId, applicationId: application.id, @@ -158,7 +158,7 @@ export class ApplicationSyncService { let serverlessFunctionLayerId = application.serverlessFunctionLayerId; - if (manifest.serverlessFunctions.length > 0) { + if (manifest.functions.length > 0) { if (!isDefined(serverlessFunctionLayerId)) { serverlessFunctionLayerId = ( await this.serverlessFunctionLayerService.create( diff --git a/packages/twenty-shared/src/application/applicationManifestType.ts b/packages/twenty-shared/src/application/applicationManifestType.ts index 6779416eed2..adf238aaa57 100644 --- a/packages/twenty-shared/src/application/applicationManifestType.ts +++ b/packages/twenty-shared/src/application/applicationManifestType.ts @@ -13,7 +13,7 @@ export type ApplicationManifest = { application: Application; objects: ObjectManifest[]; objectExtensions?: ObjectExtensionManifest[]; - serverlessFunctions: ServerlessFunctionManifest[]; + functions: ServerlessFunctionManifest[]; frontComponents?: FrontComponentManifest[]; roles?: RoleManifest[]; sources: Sources; diff --git a/packages/twenty-shared/src/application/frontComponentManifestType.ts b/packages/twenty-shared/src/application/frontComponentManifestType.ts index 49b1f56140c..5097520a5dc 100644 --- a/packages/twenty-shared/src/application/frontComponentManifestType.ts +++ b/packages/twenty-shared/src/application/frontComponentManifestType.ts @@ -4,5 +4,6 @@ export type FrontComponentManifest = { description?: string; sourceComponentPath: string; builtComponentPath: string; + builtComponentChecksum: string | null; componentName: string; }; diff --git a/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts b/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts index 41dc828ee67..8d9272bac31 100644 --- a/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts +++ b/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts @@ -26,6 +26,7 @@ export type ServerlessFunctionManifest = SyncableEntityOptions & { triggers: ServerlessFunctionTriggerManifest[]; sourceHandlerPath: string; builtHandlerPath: string; + builtHandlerChecksum: string | null; handlerName: string; toolInputSchema?: InputJsonSchema; isTool?: boolean;