diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev-once/app-dev-once.integration.spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev-once/app-dev-once.integration.spec.ts index bd3b7450d7..86f04606be 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev-once/app-dev-once.integration.spec.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev-once/app-dev-once.integration.spec.ts @@ -94,7 +94,13 @@ describe('minimal-app dev-once', () => { ); expect( - files.filter((file) => file.includes('.function.')).sort(), + files + .filter( + (file) => + file.endsWith('.function.mjs') || + file.endsWith('.function.mjs.map'), + ) + .sort(), ).toEqual(['my.function.mjs', 'my.function.mjs.map']); }); @@ -104,8 +110,26 @@ describe('minimal-app dev-once', () => { ); expect( - files.filter((file) => file.includes('.front-component.')).sort(), + files + .filter( + (file) => + file.endsWith('.front-component.mjs') || + file.endsWith('.front-component.mjs.map'), + ) + .sort(), ).toEqual(['my.front-component.mjs', 'my.front-component.mjs.map']); }); }); + + describe('source files', () => { + it('should include the logic function source', async () => { + expect(await pathExists(join(OUTPUT_PATH, 'my.function.ts'))).toBe(true); + }); + + it('should include the front component source', async () => { + expect( + await pathExists(join(OUTPUT_PATH, 'my.front-component.tsx')), + ).toBe(true); + }); + }); }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/build-application.ts b/packages/twenty-sdk/src/cli/utilities/build/common/build-application.ts index b24a05e84d..aed19bf457 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/build-application.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/build-application.ts @@ -130,6 +130,13 @@ export const buildApplication = async ( onFileBuilt: collectFileBuilt, }); + await copyStaticFiles({ + appPath: options.appPath, + fileFolder: FileFolder.Source, + filePaths: [...new Set([...logicFunctions, ...frontComponents])], + collectFileBuilt, + }); + await copyStaticFiles({ appPath: options.appPath, fileFolder: FileFolder.PublicAsset, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts index adac1a5d65..441dc797a7 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts @@ -27,6 +27,7 @@ import { VERSION_PROGRESSION_REASON_TO_INSTALL_EXCEPTION_CODE, VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE, } from 'src/engine/core-modules/application/application-package/constants/version-reason-to-exception-code.constant'; +import { buildApplicationFileList } from 'src/engine/core-modules/application/application-install/utils/build-application-file-list.util'; import { ApplicationManifestApplyService } from 'src/engine/core-modules/application/application-manifest/application-manifest-apply.service'; import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service'; import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; @@ -592,9 +593,9 @@ export class ApplicationInstallService { applicationUniversalIdentifier: string, workspaceId: string, ): Promise { - const filesToWrite = this.buildFileList(manifest); + const filesToWrite = buildApplicationFileList(manifest); - for (const { relativePath, fileFolder } of filesToWrite) { + for (const { relativePath, fileFolder, isRequired } of filesToWrite) { const absolutePath = this.resolveWithinDirOrThrow( extractedDir, relativePath, @@ -604,7 +605,20 @@ export class ApplicationInstallService { try { content = await fs.readFile(absolutePath); - } catch { + } catch (error) { + if ( + !isRequired && + error instanceof Error && + 'code' in error && + error.code === 'ENOENT' + ) { + this.logger.warn( + `Source file not found in package: ${relativePath}; skipping for backward compatibility`, + ); + + continue; + } + throw new ApplicationException( `File not found in package: ${relativePath}`, ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED, @@ -677,40 +691,6 @@ export class ApplicationInstallService { return file.id; } - private buildFileList( - manifest: Manifest, - ): Array<{ relativePath: string; fileFolder: FileFolder }> { - const files: Array<{ relativePath: string; fileFolder: FileFolder }> = []; - - files.push( - { relativePath: 'package.json', fileFolder: FileFolder.Dependencies }, - { relativePath: 'manifest.json', fileFolder: FileFolder.Source }, - ); - - for (const logicFunction of manifest.logicFunctions ?? []) { - files.push({ - relativePath: logicFunction.builtHandlerPath, - fileFolder: FileFolder.BuiltLogicFunction, - }); - } - - for (const frontComponent of manifest.frontComponents ?? []) { - files.push({ - relativePath: frontComponent.builtComponentPath, - fileFolder: FileFolder.BuiltFrontComponent, - }); - } - - for (const publicAsset of manifest.publicAssets ?? []) { - files.push({ - relativePath: publicAsset.filePath, - fileFolder: FileFolder.PublicAsset, - }); - } - - return files; - } - private async ensureApplicationExists(params: { existingApplication: ApplicationEntity | null; universalIdentifier: string; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/build-application-file-list.util.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/build-application-file-list.util.spec.ts new file mode 100644 index 0000000000..bbf819883b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/build-application-file-list.util.spec.ts @@ -0,0 +1,62 @@ +import { type Manifest } from 'twenty-shared/application'; +import { FileFolder } from 'twenty-shared/types'; + +import { buildApplicationFileList } from 'src/engine/core-modules/application/application-install/utils/build-application-file-list.util'; + +describe('buildApplicationFileList', () => { + it('includes source and built files for logic functions and front components', () => { + const manifest = { + logicFunctions: [ + { + sourceHandlerPath: 'src/send-email.function.ts', + builtHandlerPath: 'src/send-email.function.mjs', + }, + ], + frontComponents: [ + { + sourceComponentPath: 'src/inbox.front-component.tsx', + builtComponentPath: 'src/inbox.front-component.mjs', + }, + ], + publicAssets: [{ filePath: 'assets/logo.svg' }], + } as Manifest; + + expect(buildApplicationFileList(manifest)).toEqual([ + { + relativePath: 'package.json', + fileFolder: FileFolder.Dependencies, + isRequired: true, + }, + { + relativePath: 'manifest.json', + fileFolder: FileFolder.Source, + isRequired: true, + }, + { + relativePath: 'src/send-email.function.ts', + fileFolder: FileFolder.Source, + isRequired: false, + }, + { + relativePath: 'src/send-email.function.mjs', + fileFolder: FileFolder.BuiltLogicFunction, + isRequired: true, + }, + { + relativePath: 'src/inbox.front-component.tsx', + fileFolder: FileFolder.Source, + isRequired: false, + }, + { + relativePath: 'src/inbox.front-component.mjs', + fileFolder: FileFolder.BuiltFrontComponent, + isRequired: true, + }, + { + relativePath: 'assets/logo.svg', + fileFolder: FileFolder.PublicAsset, + isRequired: true, + }, + ]); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/utils/build-application-file-list.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/build-application-file-list.util.ts new file mode 100644 index 0000000000..158924c99d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/build-application-file-list.util.ts @@ -0,0 +1,65 @@ +import { type Manifest } from 'twenty-shared/application'; +import { FileFolder } from 'twenty-shared/types'; + +export type ApplicationFileToStore = { + relativePath: string; + fileFolder: FileFolder; + isRequired: boolean; +}; + +export const buildApplicationFileList = ( + manifest: Manifest, +): ApplicationFileToStore[] => { + const files: ApplicationFileToStore[] = [ + { + relativePath: 'package.json', + fileFolder: FileFolder.Dependencies, + isRequired: true, + }, + { + relativePath: 'manifest.json', + fileFolder: FileFolder.Source, + isRequired: true, + }, + ]; + + for (const logicFunction of manifest.logicFunctions ?? []) { + files.push( + { + relativePath: logicFunction.sourceHandlerPath, + fileFolder: FileFolder.Source, + isRequired: false, + }, + { + relativePath: logicFunction.builtHandlerPath, + fileFolder: FileFolder.BuiltLogicFunction, + isRequired: true, + }, + ); + } + + for (const frontComponent of manifest.frontComponents ?? []) { + files.push( + { + relativePath: frontComponent.sourceComponentPath, + fileFolder: FileFolder.Source, + isRequired: false, + }, + { + relativePath: frontComponent.builtComponentPath, + fileFolder: FileFolder.BuiltFrontComponent, + isRequired: true, + }, + ); + } + + for (const publicAsset of manifest.publicAssets ?? []) { + files.push({ + relativePath: publicAsset.filePath, + fileFolder: FileFolder.PublicAsset, + isRequired: true, + }); + } + + return files; +};