Add checksum to manifest (#17368)
## 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`
This commit is contained in:
@@ -4,7 +4,7 @@ export type FrontComponentType = React.ComponentType<any>;
|
||||
|
||||
export type FrontComponentConfig = Omit<
|
||||
FrontComponentManifest,
|
||||
'sourceComponentPath' | 'builtComponentPath' | 'componentName'
|
||||
'sourceComponentPath' | 'builtComponentPath' | 'builtComponentChecksum' | 'componentName'
|
||||
> & {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ export type FunctionHandler = (...args: any[]) => any | Promise<any>;
|
||||
|
||||
export type FunctionConfig = Omit<
|
||||
ServerlessFunctionManifest,
|
||||
'sourceHandlerPath' | 'builtHandlerPath' | 'handlerName'
|
||||
'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' | 'handlerName'
|
||||
> & {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
+10
-2
@@ -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",
|
||||
|
||||
+17
-2
@@ -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);
|
||||
|
||||
+5
-3
@@ -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": [
|
||||
|
||||
+24
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<ApplicationManifest>,
|
||||
>(
|
||||
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,
|
||||
})),
|
||||
});
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
|
||||
@@ -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<string, FileStatus>;
|
||||
frontComponents: Map<string, FileStatus>;
|
||||
};
|
||||
|
||||
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<void> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
private async startFunctionsWatcher(sourcePaths: string[]): Promise<void> {
|
||||
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<void> {
|
||||
private async startFrontComponentsWatcher(sourcePaths: string[]): Promise<void> {
|
||||
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('');
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, string>;
|
||||
onFileBuilt?: OnFileBuiltCallback;
|
||||
onSuccess: (relativePath: string) => void;
|
||||
};
|
||||
|
||||
export type ProcessEsbuildResultOutput = {
|
||||
hasChanges: boolean;
|
||||
};
|
||||
|
||||
export const processEsbuildResult = async ({
|
||||
result,
|
||||
outputDir,
|
||||
builtDir,
|
||||
lastChecksums,
|
||||
onFileBuilt,
|
||||
onSuccess,
|
||||
}: ProcessEsbuildResultParams): Promise<ProcessEsbuildResultOutput> => {
|
||||
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 };
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
import { type ManifestBuildResult } from '../manifest/manifest-build';
|
||||
|
||||
export interface RestartableWatcher {
|
||||
restart(result: ManifestBuildResult): Promise<void>;
|
||||
restart(sourcePaths: string[]): Promise<void>;
|
||||
start(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
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;
|
||||
};
|
||||
|
||||
+40
-34
@@ -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<string, string> = new Map();
|
||||
private onFileBuilt?: OnFileBuiltCallback;
|
||||
private buildCompletePromise: Promise<void> = 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<void> {
|
||||
async restart(sourcePaths: string[]): Promise<void> {
|
||||
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<void>((resolve) => {
|
||||
this.resolveBuildComplete = resolve;
|
||||
});
|
||||
|
||||
await this.esBuildContext.rebuild();
|
||||
await this.buildCompletePromise;
|
||||
|
||||
if (this.watchMode) {
|
||||
await this.esBuildContext.watch();
|
||||
|
||||
@@ -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<string, string> = new Map();
|
||||
private onFileBuilt?: OnFileBuiltCallback;
|
||||
private buildCompletePromise: Promise<void> = 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<void> {
|
||||
async restart(sourcePaths: string[]): Promise<void> {
|
||||
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<void>((resolve) => {
|
||||
this.resolveBuildComplete = resolve;
|
||||
});
|
||||
|
||||
await this.esBuildContext.rebuild();
|
||||
await this.buildCompletePromise;
|
||||
|
||||
if (this.watchMode) {
|
||||
await this.esBuildContext.watch();
|
||||
|
||||
+15
-15
@@ -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: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string, string[]>();
|
||||
const functions = manifest.serverlessFunctions ?? [];
|
||||
const functions = manifest.functions ?? [];
|
||||
|
||||
for (const fn of functions) {
|
||||
if (fn.universalIdentifier) {
|
||||
|
||||
@@ -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<Sources> => {
|
||||
return sources;
|
||||
};
|
||||
|
||||
const writeManifestToOutput = async (
|
||||
appPath: string,
|
||||
manifest: ApplicationManifest,
|
||||
): Promise<void> => {
|
||||
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 };
|
||||
|
||||
@@ -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 ?? []);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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<string> => {
|
||||
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;
|
||||
};
|
||||
+3
-3
@@ -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(
|
||||
|
||||
@@ -13,7 +13,7 @@ export type ApplicationManifest = {
|
||||
application: Application;
|
||||
objects: ObjectManifest[];
|
||||
objectExtensions?: ObjectExtensionManifest[];
|
||||
serverlessFunctions: ServerlessFunctionManifest[];
|
||||
functions: ServerlessFunctionManifest[];
|
||||
frontComponents?: FrontComponentManifest[];
|
||||
roles?: RoleManifest[];
|
||||
sources: Sources;
|
||||
|
||||
@@ -4,5 +4,6 @@ export type FrontComponentManifest = {
|
||||
description?: string;
|
||||
sourceComponentPath: string;
|
||||
builtComponentPath: string;
|
||||
builtComponentChecksum: string | null;
|
||||
componentName: string;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ export type ServerlessFunctionManifest = SyncableEntityOptions & {
|
||||
triggers: ServerlessFunctionTriggerManifest[];
|
||||
sourceHandlerPath: string;
|
||||
builtHandlerPath: string;
|
||||
builtHandlerChecksum: string | null;
|
||||
handlerName: string;
|
||||
toolInputSchema?: InputJsonSchema;
|
||||
isTool?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user