Copy application source files during build and install (#22991)
## Summary This is a requirement for the 2-way sync feature - Copy logic function and front component source files into the build output during SDK app builds. - Share application file list construction between install and build paths so source and built artifacts stay aligned. - Allow app installs to skip missing optional source files for backward compatibility while still requiring built artifacts. - Extend watcher handling to track source-file uploads and restart when the source set changes. - Update integration coverage to assert built outputs and copied source files for minimal apps. <img width="940" height="165" alt="Screenshot 2026-07-17 at 14 53 40" src="https://github.com/user-attachments/assets/b9306990-5fc0-4866-a390-3bb8c21a88ad" /> <img width="579" height="181" alt="Screenshot 2026-07-17 at 14 53 57" src="https://github.com/user-attachments/assets/af282318-76c3-49a9-b62a-833a0aadc688" />
This commit is contained in:
+17
-37
@@ -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<void> {
|
||||
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;
|
||||
|
||||
+62
@@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+65
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user