Sync built files (#17379)

as title, upload built files to local storage

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-01-23 13:43:53 +01:00
committed by GitHub
parent 30620c79fd
commit 0091ef5f6c
134 changed files with 1051 additions and 416 deletions
@@ -10,9 +10,8 @@ import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getR
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, pascalCase } from 'twenty-shared/utils';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { pascalCase } from '~/utils/string/pascalCase';
export const getRecordNodeFromRecord = <T extends ObjectRecord>({
objectMetadataItems,
@@ -4,8 +4,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue';
import { isDefined } from 'twenty-shared/utils';
import { pascalCase } from '~/utils/string/pascalCase';
import { isDefined, pascalCase } from 'twenty-shared/utils';
type UsePreviewRecordParams = {
objectNameSingular: string;
+6
View File
@@ -5,6 +5,12 @@ export default [
{
ignores: ['**/dist/**'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
'prettier/prettier': 'error',
},
},
{
rules: {
'no-console': 'off',
@@ -1,4 +1,4 @@
import { defineApp } from '../define-app';
import { defineApp } from '@/application';
describe('defineApp', () => {
it('should return the config when valid', () => {
@@ -1,4 +1,4 @@
import { defineFrontComponent } from '../front-components/define-front-component';
import { defineFrontComponent } from '@/application';
// Mock component for testing
const MockComponent = () => null;
@@ -1,4 +1,4 @@
import { defineFunction } from '../functions/define-function';
import { defineFunction } from '@/application';
// Mock handler for testing
const mockHandler = async () => ({ success: true });
@@ -1,4 +1,4 @@
import { defineObject } from '../objects/define-object';
import { defineObject } from '@/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectManifest } from 'twenty-shared/application';
@@ -1,4 +1,4 @@
import { defineRole } from '../roles/define-role';
import { defineRole } from '@/application';
describe('defineRole', () => {
const validConfig = {
@@ -6,6 +6,5 @@ export type {
FullNameMetadata as FullNameField,
LinksMetadata as LinksField,
PhonesMetadata as PhonesField,
RichTextV2Metadata as RichTextField
RichTextV2Metadata as RichTextField,
} from 'twenty-shared/types';
@@ -1,4 +1,4 @@
import { type FrontComponentConfig } from './front-component-config';
import { type FrontComponentConfig } from '@/application/front-components/front-component-config';
/**
* Define a front component configuration with validation.
@@ -4,7 +4,10 @@ export type FrontComponentType = React.ComponentType<any>;
export type FrontComponentConfig = Omit<
FrontComponentManifest,
'sourceComponentPath' | 'builtComponentPath' | 'builtComponentChecksum' | 'componentName'
| 'sourceComponentPath'
| 'builtComponentPath'
| 'builtComponentChecksum'
| 'componentName'
> & {
name?: string;
description?: string;
@@ -1,4 +1,4 @@
import { type FunctionConfig } from './function-config';
import { type FunctionConfig } from '@/application/functions/function-config';
/**
* Define a serverless function configuration with validation.
@@ -68,7 +68,9 @@ export const defineFunction = <T extends FunctionConfig>(config: T): T => {
break;
default:
throw new Error(`Unknown trigger type: ${(trigger as { type: string }).type}`);
throw new Error(
`Unknown trigger type: ${(trigger as { type: string }).type}`,
);
}
}
@@ -7,7 +7,10 @@ export type FunctionHandler = (...args: any[]) => any | Promise<any>;
export type FunctionConfig = Omit<
ServerlessFunctionManifest,
'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' | 'handlerName'
| 'sourceHandlerPath'
| 'builtHandlerPath'
| 'builtHandlerChecksum'
| 'handlerName'
> & {
name?: string;
description?: string;
@@ -1,4 +1,4 @@
import { type RoleConfig } from '../role-config';
import { type RoleConfig } from '@/application/role-config';
/**
* Define a role configuration with validation.
@@ -36,7 +36,10 @@ export const defineRole = <T extends RoleConfig>(config: T): T => {
// Validate object permissions if provided
if (config.objectPermissions) {
for (const permission of config.objectPermissions) {
if (!permission.objectNameSingular && !permission.objectUniversalIdentifier) {
if (
!permission.objectNameSingular &&
!permission.objectUniversalIdentifier
) {
throw new Error(
'Object permission must have either objectNameSingular or objectUniversalIdentifier',
);
@@ -47,7 +50,10 @@ export const defineRole = <T extends RoleConfig>(config: T): T => {
// Validate field permissions if provided
if (config.fieldPermissions) {
for (const permission of config.fieldPermissions) {
if (!permission.objectNameSingular && !permission.objectUniversalIdentifier) {
if (
!permission.objectNameSingular &&
!permission.objectUniversalIdentifier
) {
throw new Error(
'Field permission must have either objectNameSingular or objectUniversalIdentifier',
);
@@ -1,11 +1,11 @@
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { join } from 'path';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineConsoleOutputTests } from './tests/console-output.tests';
const APP_PATH = join(__dirname, '../..');
@@ -33,7 +33,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -3,9 +3,9 @@ import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-
import { join } from 'path';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineFunctionsTests } from './tests/functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
const APP_PATH = join(__dirname, '../..');
@@ -0,0 +1,376 @@
import { FieldType } from '@/application';
import type { ApplicationManifest } from 'twenty-shared/application';
import { PermissionFlagType } from 'twenty-shared/constants';
export const EXPECTED_MANIFEST: ApplicationManifest = {
sources: {},
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
description: 'Default recipient name for postcards',
isSecret: false,
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
value: 'Alex Karp',
},
},
description: 'A simple hello world app',
displayName: 'Hello World',
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
icon: 'IconWorld',
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
},
frontComponents: [
{
builtComponentPath: 'front-components/src/root.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'RootComponent',
description: 'A root-level front component',
name: 'root-component',
sourceComponentPath: 'src/root.front-component.tsx',
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/card.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'CardDisplay',
description: 'A component using an external component file',
name: 'card-component',
sourceComponentPath: 'src/components/card.front-component.tsx',
universalIdentifier: 'i0i1i2i3-i4i5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/greeting.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'GreetingComponent',
description: 'A component that uses greeting utility',
name: 'greeting-component',
sourceComponentPath: 'src/components/greeting.front-component.tsx',
universalIdentifier: 'h0h1h2h3-h4h5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/test.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'TestComponent',
description: 'A test front component',
name: 'test-component',
sourceComponentPath: 'src/components/test.front-component.tsx',
universalIdentifier: 'f1234567-abcd-4000-8000-000000000001',
},
],
objectExtensions: [
{
fields: [
{
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
},
{
description: 'Post card category',
label: 'Category',
name: 'category',
options: [
{
color: 'blue',
label: 'Personal',
position: 0,
value: 'PERSONAL',
},
{
color: 'green',
label: 'Business',
position: 1,
value: 'BUSINESS',
},
{
color: 'orange',
label: 'Promotional',
position: 2,
value: 'PROMOTIONAL',
},
],
type: FieldType.SELECT,
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
},
],
targetObject: {
nameSingular: 'postCard',
},
},
],
objects: [
{
description: 'A simple root-level object',
fields: [
{
label: 'Title',
name: 'title',
type: FieldType.TEXT,
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000002',
},
{
label: 'Body',
name: 'body',
type: FieldType.TEXT,
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000003',
},
],
icon: 'IconNote',
labelPlural: 'Root notes',
labelSingular: 'Root note',
namePlural: 'rootNotes',
nameSingular: 'rootNote',
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001',
},
{
description: 'A post card object',
fields: [
{
description: "Postcard's content",
icon: 'IconAbc',
label: 'Content',
name: 'content',
type: FieldType.TEXT,
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
},
{
icon: 'IconUser',
label: 'Recipient name',
name: 'recipientName',
type: FieldType.FULL_NAME,
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
},
{
icon: 'IconHome',
label: 'Recipient address',
name: 'recipientAddress',
type: FieldType.ADDRESS,
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
},
{
defaultValue: "'DRAFT'",
icon: 'IconSend',
label: 'Status',
name: 'status',
options: [
{
color: 'gray',
label: 'Draft',
position: 0,
value: 'DRAFT',
},
{
color: 'orange',
label: 'Sent',
position: 1,
value: 'SENT',
},
{
color: 'green',
label: 'Delivered',
position: 2,
value: 'DELIVERED',
},
{
color: 'orange',
label: 'Returned',
position: 3,
value: 'RETURNED',
},
],
type: FieldType.SELECT,
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
},
{
defaultValue: null,
icon: 'IconCheck',
isNullable: true,
label: 'Delivered at',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
},
],
icon: 'IconMail',
labelPlural: 'Post cards',
labelSingular: 'Post card',
namePlural: 'postCards',
nameSingular: 'postCard',
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
},
],
packageJson: {
name: 'rich-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
roles: [
{
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
canBeAssignedToUsers: true,
canDestroyAllObjectRecords: false,
canReadAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canUpdateAllSettings: false,
description: 'A simple root-level role',
label: 'Root role',
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',
},
{
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
canBeAssignedToUsers: false,
canDestroyAllObjectRecords: false,
canReadAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canUpdateAllSettings: false,
description: 'Default role for function Twenty client',
fieldPermissions: [
{
canReadFieldValue: false,
canUpdateFieldValue: false,
fieldName: 'content',
objectNameSingular: 'postCard',
},
],
label: 'Default function role',
objectPermissions: [
{
canDestroyObjectRecords: false,
canReadObjectRecords: true,
canSoftDeleteObjectRecords: false,
canUpdateObjectRecords: true,
objectNameSingular: 'postCard',
},
],
permissionFlags: [PermissionFlagType.APPLICATIONS],
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
},
],
functions: [
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/root.function.mjs',
handlerName: 'rootHandler',
name: 'root-function',
sourceHandlerPath: 'src/root.function.ts',
timeoutSeconds: 5,
triggers: [
{
httpMethod: 'GET',
isAuthRequired: false,
path: '/root',
type: 'route',
universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000002',
},
],
universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000001',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/greeting.function.mjs',
handlerName: 'greetingHandler',
name: 'greeting-function',
sourceHandlerPath: 'src/functions/greeting.function.ts',
timeoutSeconds: 5,
triggers: [
{
httpMethod: 'GET',
isAuthRequired: false,
path: '/greet',
type: 'route',
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000002',
},
],
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000001',
},
{
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',
timeoutSeconds: 2,
triggers: [
{
pattern: '0 0 1 1 *',
type: 'cron',
universalIdentifier: '9fd0dda9-4664-4fbc-9656-509f4477b9ff',
},
],
universalIdentifier: 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/test-function.function.mjs',
handlerName: 'handler',
name: 'test-function',
sourceHandlerPath: 'src/functions/test-function.function.ts',
timeoutSeconds: 2,
triggers: [
{
forwardedRequestHeaders: ['signature'],
httpMethod: 'GET',
isAuthRequired: false,
path: '/post-card/create',
type: 'route',
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
},
{
pattern: '0 0 1 1 *',
type: 'cron',
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
},
{
eventName: 'person.created',
type: 'databaseEvent',
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
},
],
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
},
],
};
@@ -8,7 +8,9 @@ export const defineConsoleOutputTests = (
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Starting Twenty Application Development Mode');
expect(output).toContain(
'[init] 🚀 Starting Twenty Application Development Mode',
);
expect(output).toContain('[init] 📁 App Path:');
});
@@ -33,7 +35,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -4,7 +4,10 @@ import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components preserving source path structure', async () => {
const frontComponentsDir = join(appPath, '.twenty/output/front-components');
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
@@ -2,7 +2,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';
import { EXPECTED_MANIFEST } from '../expected-manifest';
export const defineManifestTests = (appPath: string): void => {
const manifestOutputPath = join(appPath, '.twenty/output/manifest.json');
@@ -16,7 +16,7 @@ export const defineManifestTests = (appPath: string): void => {
const { sources: _sources, ...sanitizedManifest } = manifest;
expect(normalizeManifestForComparison(sanitizedManifest)).toEqual(
normalizeManifestForComparison(expectedManifest),
normalizeManifestForComparison(EXPECTED_MANIFEST),
);
for (const fn of manifest.functions) {
@@ -36,7 +36,9 @@ export const defineManifestTests = (appPath: string): void => {
const manifest = await fs.readJson(manifestOutputPath);
expect(manifest?.application.displayName).toBe('Hello World');
expect(manifest?.application.description).toBe('A simple hello world app');
expect(manifest?.application.description).toBe(
'A simple hello world app',
);
});
it('should load all entity types', async () => {
@@ -1,7 +1,7 @@
import { join } from 'path';
import { runAppBuild } from '../../../../integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '../../../../integration/utils/run-cli-command.util';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
@@ -33,7 +33,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -1,7 +1,7 @@
import { join } from 'path';
import { runAppDev } from '../../../../integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '../../../../integration/utils/run-cli-command.util';
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineFunctionsTests } from './tests/functions.tests';
@@ -8,7 +8,9 @@ export const defineConsoleOutputTests = (
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Starting Twenty Application Development Mode');
expect(output).toContain(
'[init] 🚀 Starting Twenty Application Development Mode',
);
expect(output).toContain('[init] 📁 App Path:');
});
@@ -33,7 +35,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -4,7 +4,10 @@ import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components at root level', async () => {
const frontComponentsDir = join(appPath, '.twenty/output/front-components');
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
@@ -8,10 +8,7 @@ export const defineFunctionsTests = (appPath: string): void => {
const files = await fs.readdir(functionsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
expect(sortedFiles).toEqual([
'my.function.mjs',
'my.function.mjs.map',
]);
expect(sortedFiles).toEqual(['my.function.mjs', 'my.function.mjs.map']);
});
});
};
@@ -16,14 +16,21 @@ export const defineManifestTests = (appPath: string): void => {
it('should have correct manifest content', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const manifest: ApplicationManifest = await fs.readJSON(manifestPath);
const expectedPath = join(appPath, '__integration__/app-dev/manifest.expected.json');
const expectedPath = join(
appPath,
'__integration__/app-dev/manifest.expected.json',
);
const expected: ApplicationManifest = await fs.readJSON(expectedPath);
expect(manifest.application).toEqual(expected.application);
expect(manifest.objects).toEqual(expected.objects);
expect(normalizeManifestForComparison({ functions: manifest.functions }).functions).toEqual(
normalizeManifestForComparison({ functions: expected.functions }).functions,
expect(
normalizeManifestForComparison({ functions: manifest.functions })
.functions,
).toEqual(
normalizeManifestForComparison({ functions: expected.functions })
.functions,
);
for (const fn of manifest.functions) {
@@ -33,9 +40,13 @@ export const defineManifestTests = (appPath: string): void => {
}
expect(
normalizeManifestForComparison({ frontComponents: manifest.frontComponents }).frontComponents,
normalizeManifestForComparison({
frontComponents: manifest.frontComponents,
}).frontComponents,
).toEqual(
normalizeManifestForComparison({ frontComponents: expected.frontComponents }).frontComponents,
normalizeManifestForComparison({
frontComponents: expected.frontComponents,
}).frontComponents,
);
for (const component of manifest.frontComponents ?? []) {
@@ -1,4 +1,4 @@
import { type TwentyConfig } from '@/cli/utilities/config/services/config.service';
import { type TwentyConfig } from '@/cli/utilities/config/config-service';
export const testConfig: TwentyConfig = {
apiUrl: 'http://localhost:3000',
@@ -1,4 +1,4 @@
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { testConfig } from '@/cli/__tests__/e2e/constants/testConfig';
import { vi, beforeAll, afterAll } from 'vitest';
@@ -1,6 +1,13 @@
export type LogPrefix = 'init' | 'manifest-watch' | 'functions-watch' | 'front-components-watch';
export type LogPrefix =
| 'init'
| 'manifest-watch'
| 'functions-watch'
| 'front-components-watch';
export const getOutputByPrefix = (output: string, prefix: LogPrefix): string => {
export const getOutputByPrefix = (
output: string,
prefix: LogPrefix,
): string => {
const prefixPattern = `[${prefix}]`;
const lines = output.split('\n');
@@ -1,7 +1,13 @@
// Loose type for JSON manifest imports where enum values are inferred as strings
type JsonManifestInput = {
functions?: Array<{ builtHandlerChecksum?: string | null; [key: string]: unknown }>;
frontComponents?: Array<{ builtComponentChecksum?: string | null; [key: string]: unknown }>;
functions?: Array<{
builtHandlerChecksum?: string | null;
[key: string]: unknown;
}>;
frontComponents?: Array<{
builtComponentChecksum?: string | null;
[key: string]: unknown;
}>;
[key: string]: unknown;
};
@@ -20,4 +26,5 @@ export const normalizeManifestForComparison = <T extends JsonManifestInput>(
? '[checksum]'
: null,
})),
sources: {}, // removing sources for now, waiting compressed file implementation
});
@@ -1,11 +1,16 @@
import { runCliCommand, type RunCliCommandResult } from './run-cli-command.util';
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppBuildOptions = {
appPath: string;
timeout?: number;
};
export const runAppBuild = (options: RunAppBuildOptions): Promise<RunCliCommandResult> => {
export const runAppBuild = (
options: RunAppBuildOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 30000 } = options;
// app:build runs once and exits, so we don't wait for specific output
@@ -1,14 +1,18 @@
import { runCliCommand, type RunCliCommandResult } from './run-cli-command.util';
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppDevOptions = {
appPath: string;
timeout?: number;
};
export const runAppDev = (options: RunAppDevOptions): Promise<RunCliCommandResult> => {
export const runAppDev = (
options: RunAppDevOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 30000 } = options;
return runCliCommand({
command: 'app:dev',
args: [appPath],
@@ -23,12 +23,7 @@ export type RunCliCommandResult = {
export const runCliCommand = (
options: RunCliCommandOptions,
): Promise<RunCliCommandResult> => {
const {
command,
args = [],
waitForOutput,
timeout = 30000,
} = options;
const { command, args = [], waitForOutput, timeout = 30000 } = options;
return new Promise((resolve) => {
// Run from CLI directory to use twenty-sdk's tsconfig paths
@@ -68,7 +63,7 @@ export const runCliCommand = (
setTimeout(() => {
child.kill();
resolve({ success: true, output });
}, 500);
}, 1500);
}
});
@@ -1,16 +1,18 @@
export const sanitizeOutput = (output: string): string => {
return output
// Remove ANSI color codes
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
// Normalize file paths (replace any absolute path to a test app)
.replace(/\/[^\s]+\/__tests__\/apps\/[^/]+/g, '<APP_PATH>')
// Normalize timestamps
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>')
// Normalize durations
.replace(/\d+ms/g, '<DURATION>')
// Trim trailing whitespace from each line
.split('\n')
.map((line) => line.trimEnd())
.join('\n')
.trim();
return (
output
// Remove ANSI color codes
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
// Normalize file paths (replace any absolute path to a test app)
.replace(/\/[^\s]+\/__tests__\/apps\/[^/]+/g, '<APP_PATH>')
// Normalize timestamps
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>')
// Normalize durations
.replace(/\d+ms/g, '<DURATION>')
// Trim trailing whitespace from each line
.split('\n')
.map((line) => line.trimEnd())
.join('\n')
.trim()
);
};
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { registerCommands } from '@/cli/commands/app.command';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { registerCommands } from '@/cli/commands/app-command';
import { ConfigService } from '@/cli/utilities/config/config-service';
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { inspect } from 'util';
@@ -1,4 +1,4 @@
import { formatPath } from '@/cli/utilities/file/utils/file-path';
import { formatPath } from '@/cli/utilities/file/file-path';
import chalk from 'chalk';
import type { Command } from 'commander';
import { AppBuildCommand } from './app/app-build';
@@ -106,9 +106,7 @@ export const registerCommands = (program: Command): void => {
.action(async (appPath?: string) => {
try {
const result = await syncCommand.execute(formatPath(appPath));
if (!result.success) {
process.exit(1);
}
process.exit(result.success ? 0 : 1);
} catch {
process.exit(1);
}
@@ -123,9 +121,7 @@ export const registerCommands = (program: Command): void => {
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
process.exit(result.success ? 0 : 1);
} catch {
process.exit(1);
}
@@ -1,15 +1,15 @@
import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
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 {
type ManifestBuildResult,
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';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
const initLogger = createLogger('init');
@@ -23,7 +23,9 @@ export class AppBuildCommand {
private appPath: string = '';
async execute(options: AppBuildOptions): Promise<ApiResponse<null>> {
async execute(
options: AppBuildOptions,
): Promise<ApiResponse<ManifestBuildResult>> {
this.appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
initLogger.log('🚀 Building Twenty Application');
@@ -38,7 +40,7 @@ export class AppBuildCommand {
initLogger.success('✅ Build completed successfully');
return { success: true, data: null };
return { success: true, data: buildResult };
}
private async runBuild(): Promise<ManifestBuildResult | null> {
@@ -56,7 +58,9 @@ export class AppBuildCommand {
return buildResult;
}
private async buildFunctions(buildResult: ManifestBuildResult): Promise<void> {
private async buildFunctions(
buildResult: ManifestBuildResult,
): Promise<void> {
this.functionsBuilder = new FunctionsWatcher({
appPath: this.appPath,
sourcePaths: buildResult.filePaths.functions,
@@ -69,6 +73,7 @@ export class AppBuildCommand {
builtPath,
checksum,
});
if (updatedManifest) {
buildResult.manifest = updatedManifest;
}
@@ -79,7 +84,9 @@ export class AppBuildCommand {
await this.functionsBuilder.start();
}
private async buildFrontComponents(buildResult: ManifestBuildResult): Promise<void> {
private async buildFrontComponents(
buildResult: ManifestBuildResult,
): Promise<void> {
this.frontComponentsBuilder = new FrontComponentsWatcher({
appPath: this.appPath,
sourcePaths: buildResult.filePaths.frontComponents,
@@ -1,10 +1,13 @@
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, updateManifestChecksum } 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 { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { type ApplicationManifest } from 'twenty-shared/application';
const initLogger = createLogger('init');
@@ -69,10 +72,14 @@ export class AppDevCommand {
await this.startManifestWatcher();
await this.startFunctionsWatcher(buildResult.filePaths.functions);
await this.startFrontComponentsWatcher(buildResult.filePaths.frontComponents);
await this.startFrontComponentsWatcher(
buildResult.filePaths.frontComponents,
);
}
private initializeFunctionsFileUploadStatus(manifest: ApplicationManifest): void {
private initializeFunctionsFileUploadStatus(
manifest: ApplicationManifest,
): void {
this.state.fileStatusMaps.functions.clear();
for (const fn of manifest.functions ?? []) {
@@ -85,16 +92,21 @@ export class AppDevCommand {
}
}
private initializeFrontComponentsFileUploadStatus(manifest: ApplicationManifest): void {
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,
});
this.state.fileStatusMaps.frontComponents.set(
component.universalIdentifier,
{
sourcePath: component.sourceComponentPath,
builtPath: component.builtComponentPath,
checksum: null,
isUploaded: false,
},
);
}
}
@@ -106,7 +118,8 @@ export class AppDevCommand {
this.state.manifest = result.manifest;
const functionSourcePaths = result.filePaths.functions;
const shouldRestartFunctions = this.functionsWatcher?.shouldRestart(functionSourcePaths);
const shouldRestartFunctions =
this.functionsWatcher?.shouldRestart(functionSourcePaths);
if (shouldRestartFunctions) {
if (result.manifest) {
this.initializeFunctionsFileUploadStatus(result.manifest);
@@ -115,7 +128,8 @@ export class AppDevCommand {
}
const componentSourcePaths = result.filePaths.frontComponents;
const shouldRestartFrontComponents = this.frontComponentsWatcher?.shouldRestart(componentSourcePaths);
const shouldRestartFrontComponents =
this.frontComponentsWatcher?.shouldRestart(componentSourcePaths);
if (shouldRestartFrontComponents) {
if (result.manifest) {
this.initializeFrontComponentsFileUploadStatus(result.manifest);
@@ -133,34 +147,37 @@ export class AppDevCommand {
this.functionsWatcher = new FunctionsWatcher({
appPath: this.appPath,
sourcePaths,
onFileBuilt: (builtPath, checksum) => {
this.updateFileStatus('function', builtPath, checksum);
onFileBuilt: async (builtPath, checksum) => {
await this.updateFileStatus('function', builtPath, checksum);
},
});
await this.functionsWatcher.start();
}
private async startFrontComponentsWatcher(sourcePaths: string[]): Promise<void> {
private async startFrontComponentsWatcher(
sourcePaths: string[],
): Promise<void> {
this.frontComponentsWatcher = new FrontComponentsWatcher({
appPath: this.appPath,
sourcePaths,
onFileBuilt: (builtPath, checksum) => {
this.updateFileStatus('frontComponent', builtPath, checksum);
onFileBuilt: async (builtPath, checksum) => {
await this.updateFileStatus('frontComponent', builtPath, checksum);
},
});
await this.frontComponentsWatcher.start();
}
private updateFileStatus(
private async updateFileStatus(
entityType: 'function' | 'frontComponent',
builtPath: string,
checksum: string,
): void {
const statusMap = entityType === 'function'
? this.state.fileStatusMaps.functions
: this.state.fileStatusMaps.frontComponents;
): Promise<void> {
const statusMap =
entityType === 'function'
? this.state.fileStatusMaps.functions
: this.state.fileStatusMaps.frontComponents;
for (const [_id, status] of statusMap) {
if (status.builtPath === builtPath) {
@@ -172,10 +189,33 @@ export class AppDevCommand {
const manifest = this.state.manifest;
if (manifest) {
const updatedManifest = updateManifestChecksum({ manifest, entityType, builtPath, checksum });
const updatedManifest = updateManifestChecksum({
manifest,
entityType,
builtPath,
checksum,
});
if (updatedManifest) {
this.state.manifest = updatedManifest;
writeManifestToOutput(this.appPath, updatedManifest);
await writeManifestToOutput(this.appPath, updatedManifest);
}
}
}
private markFileAsUploaded(
entityType: 'function' | 'frontComponent',
builtPath: string,
success: boolean,
): void {
const statusMap =
entityType === 'function'
? this.state.fileStatusMaps.functions
: this.state.fileStatusMaps.frontComponents;
for (const [_id, status] of statusMap) {
if (status.builtPath === builtPath) {
status.isUploaded = success;
break;
}
}
}
@@ -1,13 +1,13 @@
import chalk from 'chalk';
import { GenerateService } from '@/cli/utilities/generate/services/generate.service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { ClientService } from '@/cli/utilities/client/client-service';
export class AppGenerateCommand {
private generateService = new GenerateService();
private clientService = new ClientService();
async execute(appPath: string = CURRENT_EXECUTION_DIRECTORY) {
try {
await this.generateService.generateClient(appPath);
await this.clientService.generate(appPath);
} catch (error) {
console.error(
chalk.red('Generate Twenty client failed:'),
@@ -1,27 +1,43 @@
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import path from 'path';
import { AppBuildCommand } from '@/cli/commands/app/app-build';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
import { ApiService } from '@/cli/utilities/api/api-service';
export class AppSyncCommand {
private apiService = new ApiService();
private buildCommand = new AppBuildCommand();
async execute(
appPath: string = CURRENT_EXECUTION_DIRECTORY,
): Promise<ApiResponse<any>> {
console.log(chalk.blue('🚀 Syncing Twenty Application'));
console.log(chalk.gray(`📁 App Path: ${appPath}`));
console.log('');
const { manifest } = await runManifestBuild(appPath, { writeOutput: false });
const result = await this.buildCommand.execute({
appPath,
});
if (!result.success) {
return result;
}
const manifest = result.data.manifest;
if (!manifest) {
return { success: false, error: 'Build failed' };
return { success: false, error: 'No manifest found. Build failed?' };
}
const uploadService = new FileUploader({
applicationUniversalIdentifier: manifest.application.universalIdentifier,
appPath,
});
await uploadService.uploadManifestBuiltFiles(manifest);
const yarnLockPath = path.join(appPath, 'yarn.lock');
let yarnLock = '';
@@ -29,20 +45,17 @@ export class AppSyncCommand {
yarnLock = await fs.readFile(yarnLockPath, 'utf8');
}
const serverlessSyncResult = await this.apiService.syncApplication({
const syncResult = await this.apiService.syncApplication({
manifest,
yarnLock,
});
if (serverlessSyncResult.success === false) {
console.error(
chalk.red('❌ Application Sync failed:'),
serverlessSyncResult.error,
);
if (!syncResult.success) {
console.error(chalk.red('❌ Application Sync failed:'), syncResult.error);
} else {
console.log(chalk.green('✅ Application synced successfully'));
}
return serverlessSyncResult;
return syncResult;
}
}
@@ -1,7 +1,7 @@
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types';
import { ApiService } from '@/cli/utilities/api/api-service';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
@@ -25,7 +25,10 @@ export class AppUninstallCommand {
process.exit(1);
}
const { manifest } = await runManifestBuild(appPath, { display: false, writeOutput: false });
const { manifest } = await runManifestBuild(appPath, {
display: false,
writeOutput: false,
});
if (!manifest) {
return { success: false, error: 'Build failed' };
@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ConfigService } from '@/cli/utilities/config/config-service';
export class AuthListCommand {
private configService = new ConfigService();
@@ -22,7 +22,8 @@ export class AuthListCommand {
console.log(chalk.blue('Available workspaces:\n'));
for (const workspace of availableWorkspaces) {
const config = await this.configService.getConfigForWorkspace(workspace);
const config =
await this.configService.getConfigForWorkspace(workspace);
const hasCredentials = !!config.apiKey;
const isDefault = workspace === currentDefault;
@@ -1,16 +1,13 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
export class AuthLoginCommand {
private configService = new ConfigService();
private apiService = new ApiService();
async execute(options: {
apiKey?: string;
apiUrl?: string;
}): Promise<void> {
async execute(options: { apiKey?: string; apiUrl?: string }): Promise<void> {
try {
let { apiKey, apiUrl } = options;
@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ConfigService } from '@/cli/utilities/config/config-service';
export class AuthLogoutCommand {
private configService = new ConfigService();
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
export class AuthStatusCommand {
private configService = new ConfigService();
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
export class AuthSwitchCommand {
private configService = new ConfigService();
@@ -1,9 +1,9 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/utils/entity-front-component-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/utils/entity-function-template';
import { convertToLabel } from '@/cli/utilities/entity/utils/entity-label';
import { getNewObjectFileContent } from '@/cli/utilities/entity/utils/entity-object-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/utils/entity-role-template';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getNewObjectFileContent } from '@/cli/utilities/entity/entity-object-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
@@ -1,6 +1,6 @@
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { type ApplicationManifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
@@ -1,6 +1,6 @@
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
export class FunctionLogsCommand {
@@ -16,7 +16,10 @@ export class FunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const { manifest } = await runManifestBuild(appPath, { display: false, writeOutput: false });
const { manifest } = await runManifestBuild(appPath, {
display: false,
writeOutput: false,
});
if (!manifest) {
process.exit(1);
@@ -1,8 +0,0 @@
// Placeholder for sdk generate command
// TODO: Implement SDK generation functionality
export class SdkGenerateCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -1,5 +0,0 @@
/**
* Directory name for static assets in Twenty applications.
* Assets are copied from this folder at the root of the app to the build output.
*/
export const ASSETS_DIR = 'assets';
@@ -1,4 +1,4 @@
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
import chalk from 'chalk';
import * as fs from 'fs';
@@ -11,7 +11,8 @@ import {
import * as path from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type FileFolder } from 'twenty-shared/types';
import { type ApiResponse } from '../types/api-response.types';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { pascalCase } from 'twenty-shared/utils';
export class ApiService {
private client: AxiosInstance;
@@ -271,7 +272,8 @@ export class ApiService {
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0]?.message || 'Failed to fetch functions',
error:
response.data.errors[0]?.message || 'Failed to fetch functions',
};
}
@@ -415,10 +417,12 @@ export class ApiService {
async uploadFile({
filePath,
builtHandlerPath,
fileFolder,
applicationUniversalIdentifier,
}: {
filePath: string;
builtHandlerPath: string;
fileFolder: FileFolder;
applicationUniversalIdentifier: string;
}): Promise<ApiResponse<boolean>> {
@@ -443,13 +447,15 @@ export class ApiService {
}
`;
const graphqlEnumFileFolder = pascalCase(fileFolder);
const operations = JSON.stringify({
query: mutation,
variables: {
file: null,
applicationUniversalIdentifier,
filePath,
fileFolder,
filePath: builtHandlerPath,
fileFolder: graphqlEnumFileFolder,
},
});
@@ -2,7 +2,7 @@ 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';
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
export type ProcessEsbuildResultParams = {
result: esbuild.BuildResult;
@@ -25,8 +25,9 @@ export const processEsbuildResult = async ({
onFileBuilt,
onSuccess,
}: ProcessEsbuildResultParams): Promise<ProcessEsbuildResultOutput> => {
const outputFiles = Object.keys(result.metafile?.outputs ?? {})
.filter((file) => file.endsWith('.mjs'));
const outputFiles = Object.keys(result.metafile?.outputs ?? {}).filter(
(file) => file.endsWith('.mjs'),
);
let hasChanges = false;
@@ -50,7 +51,7 @@ export const processEsbuildResult = async ({
onSuccess(relativePath);
if (onFileBuilt) {
onFileBuilt(builtPath, checksum);
await onFileBuilt(builtPath, checksum);
}
}
@@ -4,7 +4,8 @@ export type LoggerContext =
| 'init'
| 'manifest-watch'
| 'functions-watch'
| 'front-components-watch';
| 'front-components-watch'
| 'file-upload';
type LoggerConfig = {
prefix: string;
@@ -28,6 +29,10 @@ const LOGGER_CONFIGS: Record<LoggerContext, LoggerConfig> = {
prefix: '[front-components-watch]',
color: chalk.green,
},
'file-upload': {
prefix: '[file-upload]',
color: chalk.blue,
},
};
export type Logger = {
@@ -43,8 +48,11 @@ export const createLogger = (context: LoggerContext): Logger => {
return {
log: (message: string) => console.log(`${prefix} ${message}`),
success: (message: string) => console.log(`${prefix} ${chalk.green(message)}`),
error: (message: string) => console.error(`${prefix} ${chalk.red(message)}`),
warn: (message: string) => console.log(`${prefix} ${chalk.yellow(message)}`),
success: (message: string) =>
console.log(`${prefix} ${chalk.green(message)}`),
error: (message: string) =>
console.error(`${prefix} ${chalk.red(message)}`),
warn: (message: string) =>
console.log(`${prefix} ${chalk.yellow(message)}`),
};
};
@@ -5,7 +5,10 @@ export interface RestartableWatcher {
shouldRestart(sourcePaths: string[]): boolean;
}
export type OnFileBuiltCallback = (builtPath: string, checksum: string) => void;
export type OnFileBuiltCallback = (
builtPath: string,
checksum: string,
) => void | Promise<void>;
export type RestartableWatcherOptions = {
appPath: string;
@@ -1,16 +1,16 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs-extra';
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 { cleanupRemovedFiles } from '@/cli/utilities/build/common/cleanup-removed-files';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
import { processEsbuildResult } from '@/cli/utilities/build/common/esbuild-result-processor';
import { createLogger } from '@/cli/utilities/build/common/logger';
import {
type OnFileBuiltCallback,
type RestartableWatcher,
type RestartableWatcherOptions,
} from '../common/restartable-watcher.interface';
import { FRONT_COMPONENTS_DIR } from './constants';
} from '@/cli/utilities/build/common/restartable-watcher-interface';
import { FRONT_COMPONENTS_DIR } from '@/cli/utilities/build/front-components/constants';
const logger = createLogger('front-components-watch');
@@ -78,7 +78,11 @@ export class FrontComponentsWatcher implements RestartableWatcher {
logger.warn('🔄 Restarting...');
await this.close();
const outputDir = path.join(this.appPath, OUTPUT_DIR, FRONT_COMPONENTS_DIR);
const outputDir = path.join(
this.appPath,
OUTPUT_DIR,
FRONT_COMPONENTS_DIR,
);
await cleanupRemovedFiles(outputDir, this.componentPaths, sourcePaths);
this.componentPaths = sourcePaths;
this.lastChecksums.clear();
@@ -142,7 +146,8 @@ export class FrontComponentsWatcher implements RestartableWatcher {
builtDir: FRONT_COMPONENTS_DIR,
lastChecksums: watcher.lastChecksums,
onFileBuilt: watcher.onFileBuilt,
onSuccess: (relativePath) => logger.success(`✓ Built ${relativePath}`),
onSuccess: (relativePath) =>
logger.success(`✓ Built ${relativePath}`),
});
if (hasChanges && watchMode) {
@@ -1,16 +1,16 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs-extra';
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 { cleanupRemovedFiles } from '@/cli/utilities/build/common/cleanup-removed-files';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
import { processEsbuildResult } from '@/cli/utilities/build/common/esbuild-result-processor';
import { createLogger } from '@/cli/utilities/build/common/logger';
import {
type OnFileBuiltCallback,
type RestartableWatcher,
type RestartableWatcherOptions,
} from '../common/restartable-watcher.interface';
import { FUNCTIONS_DIR } from './constants';
} from '@/cli/utilities/build/common/restartable-watcher-interface';
import { FUNCTIONS_DIR } from '@/cli/utilities/build/functions/constants';
const logger = createLogger('functions-watch');
@@ -140,10 +140,13 @@ export class FunctionsWatcher implements RestartableWatcher {
{
name: 'external-patterns',
setup: (build) => {
build.onResolve({ filter: /(?:^|\/)generated(?:\/|$)/ }, (args) => ({
path: args.path,
external: true,
}));
build.onResolve(
{ filter: /(?:^|\/)generated(?:\/|$)/ },
(args) => ({
path: args.path,
external: true,
}),
);
},
},
{
@@ -165,7 +168,8 @@ export class FunctionsWatcher implements RestartableWatcher {
builtDir: FUNCTIONS_DIR,
lastChecksums: watcher.lastChecksums,
onFileBuilt: watcher.onFileBuilt,
onSuccess: (relativePath) => logger.success(`✓ Built ${relativePath}`),
onSuccess: (relativePath) =>
logger.success(`✓ Built ${relativePath}`),
});
if (hasChanges && watchMode) {
@@ -31,6 +31,7 @@ describe('validateManifest - objectExtensions', () => {
const result = validateManifest({
application: validApplication,
objects: [],
frontComponents: [],
objectExtensions: [validObjectExtension],
functions: [],
roles: [],
@@ -60,6 +61,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [extensionByUuid],
functions: [],
frontComponents: [],
roles: [],
});
@@ -87,6 +89,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [validObjectExtension, anotherExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -123,6 +126,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [extensionWithSelect],
functions: [],
frontComponents: [],
roles: [],
});
@@ -142,6 +146,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -164,6 +169,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -190,6 +196,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -217,6 +224,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -247,6 +255,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -277,6 +286,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -307,6 +317,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -338,6 +349,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -370,6 +382,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
@@ -411,6 +424,7 @@ describe('validateManifest - objectExtensions', () => {
objects: [],
objectExtensions: [extensionWithDuplicates],
functions: [],
frontComponents: [],
roles: [],
});
@@ -458,6 +472,7 @@ describe('validateManifest - objectExtensions', () => {
},
],
functions: [],
frontComponents: [],
roles: [],
});
@@ -6,13 +6,13 @@ import {
} from 'twenty-shared/application';
import { createLogger } from '../../common/logger';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
const logger = createLogger('manifest-watch');
@@ -1,5 +1,5 @@
import { type ApplicationManifest } from 'twenty-shared/application';
import { type ValidationError } from '../manifest.types';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
export type EntityIdWithLocation = {
id: string;
@@ -1,21 +1,25 @@
import { glob } from 'fast-glob';
import { type FrontComponentManifest } from 'twenty-shared/application';
import { createLogger } from '../../common/logger';
import { FRONT_COMPONENTS_DIR } from '../../front-components/constants';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { FRONT_COMPONENTS_DIR } from '@/cli/utilities/build/front-components/constants';
const logger = createLogger('manifest-watch');
type FrontComponentConfig = Omit<
FrontComponentManifest,
'sourceComponentPath' | 'builtComponentPath' | 'builtComponentChecksum' | 'componentName'
| 'sourceComponentPath'
| 'builtComponentPath'
| 'builtComponentChecksum'
| 'componentName'
> & {
component: { name: string };
};
@@ -23,10 +27,17 @@ type FrontComponentConfig = Omit<
export class FrontComponentEntityBuilder
implements ManifestEntityBuilder<FrontComponentManifest>
{
async build(appPath: string): Promise<EntityBuildResult<FrontComponentManifest>> {
async build(
appPath: string,
): Promise<EntityBuildResult<FrontComponentManifest>> {
const componentFiles = await glob(['**/*.front-component.tsx'], {
cwd: appPath,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: FrontComponentManifest[] = [];
@@ -1,15 +1,16 @@
import { glob } from 'fast-glob';
import { type ServerlessFunctionManifest } from 'twenty-shared/application';
import { createLogger } from '../../common/logger';
import { FUNCTIONS_DIR } from '../../functions/constants';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { FUNCTIONS_DIR } from '@/cli/utilities/build/functions/constants';
const logger = createLogger('manifest-watch');
@@ -23,10 +24,17 @@ type ExtractedFunctionManifest = Omit<
export class FunctionEntityBuilder
implements ManifestEntityBuilder<ServerlessFunctionManifest>
{
async build(appPath: string): Promise<EntityBuildResult<ServerlessFunctionManifest>> {
async build(
appPath: string,
): Promise<EntityBuildResult<ServerlessFunctionManifest>> {
const functionFiles = await glob(['**/*.function.ts'], {
cwd: appPath,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: ServerlessFunctionManifest[] = [];
@@ -2,22 +2,29 @@ import { glob } from 'fast-glob';
import { type ObjectExtensionManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
export class ObjectExtensionEntityBuilder
implements ManifestEntityBuilder<ObjectExtensionManifest>
{
async build(appPath: string): Promise<EntityBuildResult<ObjectExtensionManifest>> {
async build(
appPath: string,
): Promise<EntityBuildResult<ObjectExtensionManifest>> {
const extensionFiles = await glob(['**/*.object-extension.ts'], {
cwd: appPath,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: ObjectExtensionManifest[] = [];
@@ -2,15 +2,15 @@ import { glob } from 'fast-glob';
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { createLogger } from '../../common/logger';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
const logger = createLogger('manifest-watch');
@@ -20,7 +20,12 @@ export class ObjectEntityBuilder
async build(appPath: string): Promise<EntityBuildResult<ObjectManifest>> {
const objectFiles = await glob(['**/*.object.ts'], {
cwd: appPath,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: ObjectManifest[] = [];
@@ -30,7 +35,9 @@ export class ObjectEntityBuilder
const absolutePath = `${appPath}/${filePath}`;
manifests.push(
await manifestExtractFromFileServer.extractManifestFromFile<ObjectManifest>(absolutePath),
await manifestExtractFromFileServer.extractManifestFromFile<ObjectManifest>(
absolutePath,
),
);
} catch (error) {
throw new Error(
@@ -1,14 +1,14 @@
import { glob } from 'fast-glob';
import { type RoleManifest } from 'twenty-shared/application';
import { createLogger } from '../../common/logger';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from './entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { createLogger } from '@/cli/utilities/build/common/logger';
const logger = createLogger('manifest-watch');
@@ -16,7 +16,12 @@ export class RoleEntityBuilder implements ManifestEntityBuilder<RoleManifest> {
async build(appPath: string): Promise<EntityBuildResult<RoleManifest>> {
const roleFiles = await glob(['**/*.role.ts'], {
cwd: appPath,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: RoleManifest[] = [];
@@ -26,7 +31,9 @@ export class RoleEntityBuilder implements ManifestEntityBuilder<RoleManifest> {
const absolutePath = `${appPath}/${filePath}`;
manifests.push(
await manifestExtractFromFileServer.extractManifestFromFile<RoleManifest>(absolutePath),
await manifestExtractFromFileServer.extractManifestFromFile<RoleManifest>(
absolutePath,
),
);
} catch (error) {
throw new Error(
@@ -1,22 +1,26 @@
import { findPathFile } from '@/cli/utilities/file/utils/file-find';
import { parseJsoncFile } from '@/cli/utilities/file/utils/file-jsonc';
import { findPathFile } from '@/cli/utilities/file/file-find';
import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc';
import { glob } from 'fast-glob';
import * as fs from 'fs-extra';
import { relative, sep } from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type Sources } from 'twenty-shared/types';
import { createLogger } from '../common/logger';
import { applicationEntityBuilder } from './entities/application';
import { frontComponentEntityBuilder } from './entities/front-component';
import { functionEntityBuilder } from './entities/function';
import { objectEntityBuilder } from './entities/object';
import { objectExtensionEntityBuilder } from './entities/object-extension';
import { roleEntityBuilder } from './entities/role';
import { displayEntitySummary, displayErrors, displayWarnings } from './manifest-display';
import { applicationEntityBuilder } from '@/cli/utilities/build/manifest/entities/application';
import { frontComponentEntityBuilder } from '@/cli/utilities/build/manifest/entities/front-component';
import { functionEntityBuilder } from '@/cli/utilities/build/manifest/entities/function';
import { objectEntityBuilder } from '@/cli/utilities/build/manifest/entities/object';
import { objectExtensionEntityBuilder } from '@/cli/utilities/build/manifest/entities/object-extension';
import { roleEntityBuilder } from '@/cli/utilities/build/manifest/entities/role';
import {
displayEntitySummary,
displayErrors,
displayWarnings,
} from '@/cli/utilities/build/manifest/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';
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
import { ManifestValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate';
const logger = createLogger('manifest-watch');
@@ -93,7 +97,9 @@ export const updateManifestChecksum = ({
checksum,
}: UpdateManifestChecksumParams): ApplicationManifest | null => {
if (entityType === 'function') {
const fnIndex = manifest.functions.findIndex((f) => f.builtHandlerPath === builtPath);
const fnIndex = manifest.functions.findIndex(
(f) => f.builtHandlerPath === builtPath,
);
if (fnIndex === -1) {
return null;
}
@@ -105,16 +111,19 @@ export const updateManifestChecksum = ({
};
}
const componentIndex = manifest.frontComponents?.findIndex(
(c) => c.builtComponentPath === builtPath,
) ?? -1;
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,
frontComponents: manifest.frontComponents.map((component, index) =>
index === componentIndex
? { ...component, builtComponentChecksum: checksum }
: component,
),
};
};
@@ -173,11 +182,9 @@ export const runManifestBuild = async (
const manifest: ApplicationManifest = {
application,
objects: objectManifests,
objectExtensions:
objectExtensionManifests.length > 0 ? objectExtensionManifests : undefined,
objectExtensions: objectExtensionManifests,
functions: functionManifests,
frontComponents:
frontComponentManifests.length > 0 ? frontComponentManifests : undefined,
frontComponents: frontComponentManifests,
roles: roleManifests,
sources,
packageJson,
@@ -1,11 +1,14 @@
import { type ApplicationManifest } from 'twenty-shared/application';
import { createLogger } from '../common/logger';
import { applicationEntityBuilder } from './entities/application';
import { frontComponentEntityBuilder } from './entities/front-component';
import { functionEntityBuilder } from './entities/function';
import { objectEntityBuilder } from './entities/object';
import { roleEntityBuilder } from './entities/role';
import { type ManifestValidationError, type ValidationWarning } from './manifest.types';
import { frontComponentEntityBuilder } from '@/cli/utilities/build/manifest/entities/front-component';
import { functionEntityBuilder } from '@/cli/utilities/build/manifest/entities/function';
import { objectEntityBuilder } from '@/cli/utilities/build/manifest/entities/object';
import { roleEntityBuilder } from '@/cli/utilities/build/manifest/entities/role';
import {
type ManifestValidationError,
type ValidationWarning,
} from '@/cli/utilities/build/manifest/manifest-types';
import { applicationEntityBuilder } from '@/cli/utilities/build/manifest/entities/application';
const logger = createLogger('manifest-watch');
@@ -24,14 +24,22 @@ export class ManifestExtractFromFileServer {
options: ExtractManifestOptions = {},
): Promise<TManifest> {
if (!this.appPath) {
throw new Error('ManifestExtractFromFileServer not initialized. Call init(appPath) first.');
throw new Error(
'ManifestExtractFromFileServer not initialized. Call init(appPath) first.',
);
}
const { entryProperty } = options;
const server = await this.getServer();
const module = (await server.ssrLoadModule(filepath)) as Record<string, unknown>;
const module = (await server.ssrLoadModule(filepath)) as Record<
string,
unknown
>;
const config = this.extractConfigFromModule<Record<string, unknown>>(module, entryProperty);
const config = this.extractConfigFromModule<Record<string, unknown>>(
module,
entryProperty,
);
if (!config) {
const expectedExport = entryProperty
@@ -48,11 +56,14 @@ export class ManifestExtractFromFileServer {
const entryName = entryFunction.name;
if (!entryName) {
throw new Error(`${entryProperty} function in ${filepath} must be a named function`);
throw new Error(
`${entryProperty} function in ${filepath} must be a named function`,
);
}
const importSource = await this.resolveEntryPath(filepath, entryName);
const entryPath = importSource ?? path.relative(this.appPath, filepath).replace(/\\/g, '/');
const entryPath =
importSource ?? path.relative(this.appPath, filepath).replace(/\\/g, '/');
const { [entryProperty]: _, ...configWithoutEntry } = config;
@@ -72,7 +83,9 @@ export class ManifestExtractFromFileServer {
private async getServer(): Promise<ViteDevServer> {
if (!this.appPath) {
throw new Error('ManifestExtractFromFileServer not initialized. Call init(appPath) first.');
throw new Error(
'ManifestExtractFromFileServer not initialized. Call init(appPath) first.',
);
}
if (this.server) {
@@ -100,7 +113,10 @@ export class ManifestExtractFromFileServer {
isPlainObject(value) &&
typeof (value as Record<string, unknown>)[entryProperty!] === 'function';
if (isDefined(module.default) && (!entryProperty || hasValidEntry(module.default))) {
if (
isDefined(module.default) &&
(!entryProperty || hasValidEntry(module.default))
) {
return module.default as T;
}
@@ -124,7 +140,9 @@ export class ManifestExtractFromFileServer {
const source = await fs.readFile(filepath, 'utf8');
const patterns = [
new RegExp(`import\\s*\\{[^}]*\\b${entryName}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`),
new RegExp(
`import\\s*\\{[^}]*\\b${entryName}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`,
),
new RegExp(`import\\s+${entryName}\\s+from\\s*['"]([^'"]+)['"]`),
];
@@ -142,19 +160,28 @@ export class ManifestExtractFromFileServer {
}
const server = await this.getServer();
const resolved = await server.pluginContainer.resolveId(importSpecifier, filepath);
const resolved = await server.pluginContainer.resolveId(
importSpecifier,
filepath,
);
if (resolved?.id) {
return path.relative(this.appPath, resolved.id).replace(/\\/g, '/');
}
if (importSpecifier.startsWith('.')) {
const absolutePath = path.resolve(path.dirname(filepath), importSpecifier);
const absolutePath = path.resolve(
path.dirname(filepath),
importSpecifier,
);
const relativePath = path.relative(this.appPath, absolutePath);
return (relativePath.endsWith('.ts') ? relativePath : `${relativePath}.ts`).replace(/\\/g, '/');
return (
relativePath.endsWith('.ts') ? relativePath : `${relativePath}.ts`
).replace(/\\/g, '/');
}
return null;
}
}
export const manifestExtractFromFileServer = new ManifestExtractFromFileServer();
export const manifestExtractFromFileServer =
new ManifestExtractFromFileServer();
@@ -3,7 +3,7 @@ import { applicationEntityBuilder } from './entities/application';
import {
type EntityIdWithLocation,
type ManifestWithoutSources,
} from './entities/entity.interface';
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { frontComponentEntityBuilder } from './entities/front-component';
import { functionEntityBuilder } from './entities/function';
import { objectEntityBuilder } from './entities/object';
@@ -13,7 +13,7 @@ import {
type ValidationError,
type ValidationResult,
type ValidationWarning,
} from './manifest.types';
} from '@/cli/utilities/build/manifest/manifest-types';
const collectAllDuplicates = (
manifest: ManifestWithoutSources,
@@ -39,7 +39,10 @@ export const validateManifest = (
errors,
);
objectEntityBuilder.validate(manifest.objects ?? [], errors);
objectExtensionEntityBuilder.validate(manifest.objectExtensions ?? [], errors);
objectExtensionEntityBuilder.validate(
manifest.objectExtensions ?? [],
errors,
);
functionEntityBuilder.validate(manifest.functions ?? [], errors);
roleEntityBuilder.validate(manifest.roles ?? [], errors);
frontComponentEntityBuilder.validate(manifest.frontComponents ?? [], errors);
@@ -1,7 +1,10 @@
import chokidar, { type FSWatcher } from 'chokidar';
import path from 'path';
import { createLogger } from '../common/logger';
import { runManifestBuild, type ManifestBuildResult } from './manifest-build';
import { createLogger } from '@/cli/utilities/build/common/logger';
import {
type ManifestBuildResult,
runManifestBuild,
} from '@/cli/utilities/build/manifest/manifest-build';
const logger = createLogger('manifest-watch');
@@ -30,7 +33,8 @@ export class ManifestWatcher {
'**/node_modules/**',
'**/.twenty/**',
'**/dist/**',
(filePath: string) => filePath.includes('/.twenty/') || filePath.includes('\\.twenty\\'),
(filePath: string) =>
filePath.includes('/.twenty/') || filePath.includes('\\.twenty\\'),
],
ignoreInitial: true,
awaitWriteFinish: {
@@ -1,8 +1,7 @@
import * as fs from 'fs-extra';
import path from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { OUTPUT_DIR } from '../common/constants';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
export const writeManifestToOutput = async (
appPath: string,
@@ -1,5 +1,5 @@
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { generate } from '@genql/cli';
import chalk from 'chalk';
import * as fs from 'fs-extra';
@@ -11,7 +11,7 @@ import {
export const GENERATED_FOLDER_NAME = 'generated';
export class GenerateService {
export class ClientService {
private configService: ConfigService;
private apiService: ApiService;
@@ -20,7 +20,7 @@ export class GenerateService {
this.apiService = new ApiService();
}
async generateClient(appPath: string): Promise<void> {
async generate(appPath: string): Promise<void> {
const outputPath = join(appPath, GENERATED_FOLDER_NAME);
console.log(chalk.blue('📦 Generating Twenty client...'));
@@ -1,4 +1,4 @@
import { convertToLabel } from '@/cli/utilities/entity/utils/entity-label';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
describe('convertToLabel', () => {
it('should convert to label', () => {
@@ -1,4 +1,4 @@
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/utils/entity-front-component-template';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
describe('getFrontComponentBaseFile', () => {
it('should render proper file using defineFrontComponent', () => {
@@ -1,4 +1,4 @@
import { getFunctionBaseFile } from '@/cli/utilities/entity/utils/entity-function-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
describe('getFunctionBaseFile', () => {
it('should render proper file using defineFunction', () => {
@@ -1,4 +1,4 @@
import { getNewObjectFileContent } from '@/cli/utilities/entity/utils/entity-object-template';
import { getNewObjectFileContent } from '@/cli/utilities/entity/entity-object-template';
describe('getNewObjectFileContent', () => {
it('should return proper object file using defineObject', () => {
@@ -1,4 +1,4 @@
import { getRoleBaseFile } from '@/cli/utilities/entity/utils/entity-role-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
describe('getRoleBaseFile', () => {
it('should render proper file using defineRole', () => {
@@ -11,11 +11,9 @@ describe('getRoleBaseFile', () => {
expect(result).toContain('export default defineRole({');
expect(result).toContain(
"universalIdentifier: MY_ROLE_ROLE_UNIVERSAL_IDENTIFIER",
);
expect(result).toContain(
"'71e45a58-41da-4ae4-8b73-a543c0a9d3d4'",
'universalIdentifier: MY_ROLE_ROLE_UNIVERSAL_IDENTIFIER',
);
expect(result).toContain("'71e45a58-41da-4ae4-8b73-a543c0a9d3d4'");
expect(result).toContain("label: 'my-role'");
expect(result).toContain("description: 'Add a description for your role'");
@@ -48,7 +46,9 @@ describe('getRoleBaseFile', () => {
name: 'admin-access',
});
expect(result).toContain('export const ADMIN_ACCESS_ROLE_UNIVERSAL_IDENTIFIER');
expect(result).toContain(
'export const ADMIN_ACCESS_ROLE_UNIVERSAL_IDENTIFIER',
);
expect(result).toContain(
'universalIdentifier: ADMIN_ACCESS_ROLE_UNIVERSAL_IDENTIFIER',
);
@@ -43,10 +43,6 @@ export const parseJsoncString = (
return result;
};
export const parseTextFile = async (filePath: string) => {
return await fs.readFile(filePath, 'utf8');
};
export const parseJsoncFile = async (
filePath: string,
options: JsoncParseOptions = {},
@@ -61,12 +57,3 @@ export const parseJsoncFile = async (
throw new Error(`Failed to read file ${filePath}: ${error}`);
}
};
export const writeJsoncFile = async (
filePath: string,
data: any,
options: { spaces?: number } = {},
): Promise<void> => {
const content = JSON.stringify(data, null, options.spaces ?? 2);
await fs.writeFile(filePath, content, 'utf8');
};
@@ -0,0 +1,8 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { join } from 'path';
export const formatPath = (appPath?: string) => {
return appPath && !appPath?.startsWith('/')
? join(CURRENT_EXECUTION_DIRECTORY, appPath)
: appPath;
};
@@ -0,0 +1,64 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import path from 'path';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
import { FileFolder } from 'twenty-shared/types';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { type ApplicationManifest } from 'twenty-shared/application';
export class FileUploader {
private apiService = new ApiService();
private applicationUniversalIdentifier: string;
private appPath: string;
private logger = createLogger('file-upload');
constructor(options: {
applicationUniversalIdentifier: string;
appPath: string;
}) {
this.applicationUniversalIdentifier =
options.applicationUniversalIdentifier;
this.appPath = options.appPath;
}
async uploadFile({
builtPath,
fileFolder,
}: {
builtPath: string;
fileFolder: FileFolder;
}) {
const uploadResult = await this.apiService.uploadFile({
filePath: path.join(this.appPath, OUTPUT_DIR, builtPath),
builtHandlerPath: builtPath,
fileFolder,
applicationUniversalIdentifier: this.applicationUniversalIdentifier,
});
if (uploadResult.success) {
this.logger.success(`☁️ Uploaded ${builtPath}`);
} else {
this.logger.error(
`Failed to upload ${builtPath} -- ${uploadResult.error}`,
);
}
}
async uploadManifestBuiltFiles(manifest: ApplicationManifest) {
const uploadPromises = [
...manifest.functions.map((builtFile) =>
this.uploadFile({
builtPath: builtFile.builtHandlerPath,
fileFolder: FileFolder.BuiltFunction,
}),
),
...manifest.frontComponents.map((builtFile) =>
this.uploadFile({
builtPath: builtFile.builtComponentPath,
fileFolder: FileFolder.BuiltFrontComponent,
}),
),
];
await Promise.all(uploadPromises);
}
}
@@ -1,17 +0,0 @@
import * as fs from 'fs-extra';
import dotenv from 'dotenv';
import { findPathFile } from './file-find';
export const loadEnvVariables = async (appPath: string) => {
let envFile = '';
try {
const envFilePath = await findPathFile(appPath, '.env');
envFile = await fs.readFile(envFilePath, 'utf8');
} catch {
// Allow missing .env
}
return dotenv.parse(envFile);
};
@@ -1,13 +0,0 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { join, posix, relative, sep } from 'path';
export const formatPath = (appPath?: string) => {
return appPath && !appPath?.startsWith('/')
? join(CURRENT_EXECUTION_DIRECTORY, appPath)
: appPath;
};
export const toPosixRelative = (filepath: string, basePath: string): string => {
const rel = relative(basePath, filepath);
return rel.split(sep).join(posix.sep);
};
@@ -1,7 +1,7 @@
import {
type Diagnostic,
formatDiagnosticsWithColorAndContext,
sys,
type Diagnostic,
formatDiagnosticsWithColorAndContext,
sys,
} from 'typescript';
export const formatAndWarnTsDiagnostics = ({
@@ -17,5 +17,6 @@ export default defineConfig({
diff: {
truncateThreshold: 0,
},
fileParallelism: false,
},
});
@@ -6,6 +6,7 @@ import {
type CompositeProperty,
type CompositeType,
} from 'twenty-shared/types';
import { pascalCase } from 'twenty-shared/utils';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { computeCompositeFieldEnumTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-enum-type-key.util';
@@ -14,7 +15,6 @@ import {
type FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { pascalCase } from 'src/utils/pascal-case';
@Injectable()
export class CompositeFieldMetadataGqlEnumTypeGenerator {
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLEnumType } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, pascalCase } from 'twenty-shared/utils';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
@@ -13,7 +13,6 @@ import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metad
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { transformEnumValue } from 'src/engine/utils/transform-enum-value';
import { pascalCase } from 'src/utils/pascal-case';
@Injectable()
export class EnumFieldMetadataGqlEnumTypeGenerator {

Some files were not shown because too many files have changed in this diff Show More