Move fixture apps from twenty-sdk to twenty-apps/fixtures (#18531)

## Summary

- Move 4 test fixture apps from `twenty-sdk/src/cli/__tests__/apps/` to
`twenty-apps/fixtures/` with meaningful names (`rich-app` →
`postcard-app`, `root-app` → `minimal-app`)
- Replace all `from '@/sdk'` imports with `from 'twenty-sdk'` so fixture
apps are proper, portable twenty-sdk apps
- Remove the fragile `"@/*": ["../../../../../src/*"]` tsconfig hack and
replace with standard `"src/*": ["./src/*"]` paths
- Create a centralized `fixture-paths.ts` utility in twenty-sdk tests
for clean app path resolution

## Why

The fixture apps were deeply nested in twenty-sdk's test directory and
tightly coupled to its internal source layout via a tsconfig path alias
hack. This made them:
- Impossible to reuse outside of SDK CLI tests (e.g., for server-side
dev seeding with `DevSeederService`)
- Fragile — moving any twenty-sdk source file could break the path alias
- Poorly discoverable — buried 5 directories deep in test infrastructure

Moving them to `twenty-apps/fixtures/` makes them first-class portable
apps that can be imported by `twenty-server` for seeding, used in E2E
testing, and serve as canonical examples alongside `hello-world`.

## Test plan

- [x] All 8 twenty-sdk integration tests pass (3 suites: postcard-app,
minimal-app, invalid-app)
- [x] Prettier formatting verified on all changed files
- [ ] CI should confirm E2E tests also pass (these require a running
server)

Made with [Cursor](https://cursor.com)
This commit is contained in:
Félix Malfait
2026-03-10 17:29:53 +01:00
committed by GitHub
parent 7b9939b43e
commit 621962e049
77 changed files with 204 additions and 99 deletions
+1
View File
@@ -1,2 +1,3 @@
generated
.twenty
@@ -1,4 +1,4 @@
import { defineApplication } from '@/sdk';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './my.role';
@@ -1,4 +1,4 @@
import { defineRole } from '@/sdk';
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'9f992b6c-17e9-4381-bab5-b6150b574304';
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
export const ADD_NUMBERS_UNIVERSAL_IDENTIFIER =
'f9e5589c-e951-4d99-85db-0a305ab53502';
@@ -3,13 +3,13 @@
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["../../../../../src/*"]
"src/*": ["./src/*"]
}
},
"include": ["**/*"],
@@ -1,4 +1,4 @@
import { defineApplication } from '@/sdk';
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: 'invalid-app-0000-0000-0000-000000000001',
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001';
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001';
@@ -9,7 +9,7 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["../../../../../src/*"]
"src/*": ["./src/*"]
}
},
"include": ["**/*"],
@@ -1,4 +1,4 @@
import { defineApplication } from '@/sdk';
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
@@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/sdk';
import { defineFrontComponent } from 'twenty-sdk';
export const MyComponent = () => {
return (
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
const myHandler = () => {
return 'my-function-result';
@@ -1,4 +1,4 @@
import { FieldType, defineObject } from '@/sdk';
import { FieldType, defineObject } from 'twenty-sdk';
export default defineObject({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030',
@@ -1,4 +1,4 @@
import { defineRole } from '@/sdk';
import { defineRole } from 'twenty-sdk';
export default defineRole({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
@@ -1,5 +1,5 @@
{
"name": "rich-app",
"name": "minimal-app",
"version": "0.1.0",
"license": "MIT",
"engines": {
@@ -9,8 +9,9 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["../../../../../src/*"]
"src/*": ["./src/*"]
}
},
"include": ["**/*"]
"include": ["**/*"],
"exclude": ["node_modules", "dist", ".twenty"]
}
@@ -1,4 +1,4 @@
import { defineApplication } from '@/sdk';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role';
export default defineApplication({
@@ -1,5 +1,5 @@
{
"name": "root-app",
"name": "postcard-app",
"version": "0.1.0",
"license": "MIT",
"engines": {

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 570 B

@@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/sdk';
import { defineFrontComponent } from 'twenty-sdk';
import { CardDisplay } from '../utils/card-display.component';
export default defineFrontComponent({
@@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/sdk';
import { defineFrontComponent } from 'twenty-sdk';
import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util';
const GreetingComponent = () => {
@@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/sdk';
import { defineFrontComponent } from 'twenty-sdk';
export const TestComponent = () => {
return (
@@ -1,10 +1,10 @@
import {
POST_CARD_ON_POST_CARD_RECIPIENT_ID,
POST_CARD_RECIPIENTS_ON_POST_CARD_ID,
} from '@/cli/__tests__/apps/rich-app/src/fields/post-card-recipients-on-post-card.field';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
import { defineField, FieldType, OnDeleteAction, RelationType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
} from './post-card-recipients-on-post-card.field';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineField({
universalIdentifier: POST_CARD_ON_POST_CARD_RECIPIENT_ID,
@@ -1,6 +1,6 @@
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
import { defineField, FieldType, RelationType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export const POST_CARD_RECIPIENTS_ON_POST_CARD_ID =
'a1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d';
@@ -1,6 +1,6 @@
import { defineField, FieldType, RelationType } from '@/sdk';
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
export const POST_CARD_RECIPIENTS_ON_RECIPIENT_ID =
'a1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d';
@@ -1,5 +1,5 @@
import { defineField, FieldType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
import { defineField, FieldType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineField({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -1,5 +1,5 @@
import { defineField, FieldType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
import { defineField, FieldType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineField({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -1,10 +1,10 @@
import { defineField, FieldType, RelationType, OnDeleteAction } from '@/sdk';
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
import {
POST_CARD_RECIPIENTS_ON_RECIPIENT_ID,
RECIPIENT_ON_POST_CARD_RECIPIENT_ID,
} from '@/cli/__tests__/apps/rich-app/src/fields/post-card-recipients-on-recipient.field';
} from './post-card-recipients-on-recipient.field';
export default defineField({
universalIdentifier: RECIPIENT_ON_POST_CARD_RECIPIENT_ID,
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util';
const greetingHandler = () => {
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
const handler = async (params: { recipientName: string }) => {
return { found: true, name: params.recipientName };
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
const handler = async () => {
return { processed: true };
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
import { testFunction2 } from '../utils/test-function-2.util';
export default defineLogicFunction({
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
import { formatFarewell } from '../utils/greeting.util';
const handler = () => {
@@ -1,5 +1,5 @@
import { defineNavigationMenuItem } from '@/sdk';
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-post-card-recipients.view';
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
@@ -1,5 +1,5 @@
import { defineNavigationMenuItem } from '@/sdk';
import { ALL_POST_CARDS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-post-cards.view';
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
@@ -1,5 +1,5 @@
import { defineNavigationMenuItem } from '@/sdk';
import { ALL_RECIPIENTS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-recipients.view';
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
export const POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER =
'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e';
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
export const RECIPIENT_UNIVERSAL_IDENTIFIER =
'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d';
@@ -1,8 +1,8 @@
import { PermissionFlag, defineRole } from '@/sdk';
import { PermissionFlag, defineRole } from 'twenty-sdk';
import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER,
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
} from '../objects/post-card.object';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
@@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/sdk';
import { defineFrontComponent } from 'twenty-sdk';
export const RootComponent = () => {
return (
@@ -1,4 +1,4 @@
import { defineLogicFunction } from '@/sdk';
import { defineLogicFunction } from 'twenty-sdk';
const rootHandler = () => {
return 'root-function-result';
@@ -1,4 +1,4 @@
import { defineObject, FieldType } from '@/sdk';
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001',
@@ -1,4 +1,4 @@
import { defineRole } from '@/sdk';
import { defineRole } from 'twenty-sdk';
export default defineRole({
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',
@@ -1,8 +1,8 @@
import {
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
import { defineView } from '@/sdk';
} from '../objects/post-card-recipient.object';
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
export const ALL_POST_CARD_RECIPIENTS_VIEW_ID =
@@ -1,8 +1,8 @@
import { defineView } from '@/sdk';
import { defineView } from 'twenty-sdk';
import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER,
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
} from '../objects/post-card.object';
import { ViewType } from 'twenty-shared/types';
export const ALL_POST_CARDS_VIEW_ID = 'b1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d';
@@ -1,8 +1,8 @@
import {
RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
RECIPIENT_UNIVERSAL_IDENTIFIER,
} from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
import { defineView } from '@/sdk';
} from '../objects/recipient.object';
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
export const ALL_RECIPIENTS_VIEW_ID = 'b1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d';
@@ -9,8 +9,9 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["../../../../../src/*"]
"src/*": ["./src/*"]
}
},
"include": ["**/*"]
"include": ["**/*"],
"exclude": ["node_modules", "dist", ".twenty"]
}
@@ -0,0 +1,11 @@
import { resolve } from 'path';
const FIXTURES_ROOT = resolve(__dirname, '../../../../../twenty-apps/fixtures');
export const POSTCARD_APP_PATH = resolve(FIXTURES_ROOT, 'postcard-app');
export const MINIMAL_APP_PATH = resolve(FIXTURES_ROOT, 'minimal-app');
export const INVALID_APP_PATH = resolve(FIXTURES_ROOT, 'invalid-app');
export const FUNCTION_EXECUTE_APP_PATH = resolve(
FIXTURES_ROOT,
'function-execute-app',
);
@@ -1,12 +1,13 @@
import { resolve } from 'path';
import { vi } from 'vitest';
import { appBuild } from '@/cli/public-operations/app-build';
import { appUninstall } from '@/cli/public-operations/app-uninstall';
import { functionExecute } from '@/cli/public-operations/function-execute';
import { ADD_NUMBERS_UNIVERSAL_IDENTIFIER } from '../src/logic-functions/add-numbers.function';
import { FUNCTION_EXECUTE_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
const APP_PATH = resolve(__dirname, '../');
const ADD_NUMBERS_UNIVERSAL_IDENTIFIER = 'f9e5589c-e951-4d99-85db-0a305ab53502';
const APP_PATH = FUNCTION_EXECUTE_APP_PATH;
describe('functionExecute E2E', () => {
beforeAll(async () => {
@@ -18,8 +19,6 @@ describe('functionExecute E2E', () => {
);
}
// The server may need a moment to make uploaded files readable
// by the execution engine. Retry until the handler becomes available.
await vi.waitFor(
async () => {
const result = await functionExecute({
@@ -3,14 +3,18 @@ import { OUTPUT_DIR } from 'twenty-shared/application';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { pathExists } from '@/cli/utilities/file/fs-utils';
import { INVALID_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
const APP_PATH = join(__dirname, '..');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, OUTPUT_DIR, 'manifest.json');
const MANIFEST_OUTPUT_PATH = join(
INVALID_APP_PATH,
OUTPUT_DIR,
'manifest.json',
);
describe('invalid-app manifest', () => {
it('should fail to build manifest due to duplicate universalIdentifier', async () => {
const result = await runAppDevInProcess({
appPath: APP_PATH,
appPath: INVALID_APP_PATH,
timeout: 10000,
});
@@ -1,15 +1,13 @@
import { join } from 'path';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { MINIMAL_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineLogicFunctionsTests } from './tests/logic-functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
describe('root-app app:dev', () => {
describe('minimal-app app:dev', () => {
beforeAll(async () => {
const result = await runAppDevInProcess({ appPath: APP_PATH });
const result = await runAppDevInProcess({ appPath: MINIMAL_APP_PATH });
if (!result.success) {
const diagnostics = JSON.stringify(
@@ -24,7 +22,7 @@ describe('root-app app:dev', () => {
}
}, 60000);
defineManifestTests(APP_PATH);
defineLogicFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
defineManifestTests(MINIMAL_APP_PATH);
defineLogicFunctionsTests(MINIMAL_APP_PATH);
defineFrontComponentsTests(MINIMAL_APP_PATH);
});
@@ -1,7 +1,7 @@
import { join } from 'path';
import { type Manifest } from 'twenty-shared/application';
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest';
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/minimal-app/__integration__/app-dev/expected-manifest';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { pathExists, readJson } from '@/cli/utilities/file/fs-utils';
@@ -1,14 +1,15 @@
import { OUTPUT_DIR } from 'twenty-shared/application';
import { existsSync } from 'fs';
import { join, resolve } from 'path';
import { join } from 'path';
import { inspect } from 'util';
import { runCliCommand } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { POSTCARD_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
inspect.defaultOptions.depth = 10;
describe('Application: install delete and reinstall rich-app', () => {
const applicationName = 'rich-app';
const appPath = resolve(__dirname, '../');
describe('Application: install delete and reinstall postcard-app', () => {
const applicationName = 'postcard-app';
const appPath = POSTCARD_APP_PATH;
beforeAll(async () => {
expect(existsSync(appPath)).toBe(true);
@@ -1,14 +1,12 @@
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { join } from 'path';
import { POSTCARD_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
import { defineEntitiesTests } from './tests/entities.tests';
import { defineManifestTests } from './tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
describe('rich-app app:dev', () => {
describe('postcard-app app:dev', () => {
beforeAll(async () => {
const result = await runAppDevInProcess({ appPath: APP_PATH });
const result = await runAppDevInProcess({ appPath: POSTCARD_APP_PATH });
if (!result.success) {
const diagnostics = JSON.stringify(
@@ -23,6 +21,6 @@ describe('rich-app app:dev', () => {
}
}, 60000);
defineManifestTests(APP_PATH);
defineEntitiesTests(APP_PATH);
defineManifestTests(POSTCARD_APP_PATH);
defineEntitiesTests(POSTCARD_APP_PATH);
});
@@ -1,5 +1,6 @@
import { appBuild } from '@/cli/public-operations/app-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
import chalk from 'chalk';
export type AppBuildCommandOptions = {
@@ -11,6 +12,8 @@ export class AppBuildCommand {
async execute(options: AppBuildCommandOptions): Promise<void> {
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
await checkSdkVersionCompatibility(appPath);
console.log(chalk.blue('Building application...'));
console.log(chalk.gray(`App path: ${appPath}`));
console.log('');
@@ -1,8 +1,9 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { renderDevUI } from '@/cli/utilities/dev/ui/components/dev-ui';
import { DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager';
import { DevModeOrchestrator } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator';
import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { renderDevUI } from '@/cli/utilities/dev/ui/components/dev-ui';
import { DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager';
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
export type AppDevOptions = {
appPath?: string;
@@ -25,6 +26,8 @@ export class AppDevCommand {
async execute(options: AppDevOptions): Promise<void> {
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
await checkSdkVersionCompatibility(appPath);
const orchestratorState = new OrchestratorState({
appPath,
frontendUrl: process.env.FRONTEND_URL,
@@ -1,5 +1,6 @@
import { appPublish } from '@/cli/public-operations/app-publish';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
import chalk from 'chalk';
export type AppPublishCommandOptions = {
@@ -12,6 +13,9 @@ export type AppPublishCommandOptions = {
export class AppPublishCommand {
async execute(options: AppPublishCommandOptions): Promise<void> {
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
await checkSdkVersionCompatibility(appPath);
const isServerPublish = !!options.server;
console.log(
@@ -0,0 +1,81 @@
import { readFile } from 'node:fs/promises';
import { join } from 'path';
import { pathExists } from '@/cli/utilities/file/fs-utils';
import chalk from 'chalk';
import sdkPackageJson from '../../../../package.json';
const getAppDeclaredSdkVersion = async (
appPath: string,
): Promise<string | undefined> => {
const pkgPath = join(appPath, 'package.json');
if (!(await pathExists(pkgPath))) {
return undefined;
}
try {
const content = await readFile(pkgPath, 'utf-8');
const pkg = JSON.parse(content) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
return (
pkg.dependencies?.['twenty-sdk'] ??
pkg.devDependencies?.['twenty-sdk'] ??
undefined
);
} catch {
return undefined;
}
};
// Extracts the major version number from a semver string or range.
// Handles: "4.2.0", "^4.2.0", "~4.2.0", ">=4.0.0", "4.x", "4"
// Returns null for unparseable values like "latest", "*", "workspace:*"
const parseMajorVersion = (versionOrRange: string): number | null => {
const match = versionOrRange.match(/(\d+)\./);
if (match) {
return parseInt(match[1], 10);
}
const exactMatch = versionOrRange.match(/^[~^>=<]*(\d+)$/);
if (exactMatch) {
return parseInt(exactMatch[1], 10);
}
return null;
};
export const checkSdkVersionCompatibility = async (
appPath: string,
): Promise<void> => {
const cliVersion = sdkPackageJson.version;
const appDeclaredVersion = await getAppDeclaredSdkVersion(appPath);
if (!cliVersion || !appDeclaredVersion) {
return;
}
const cliMajor = parseMajorVersion(cliVersion);
const appMajor = parseMajorVersion(appDeclaredVersion);
if (cliMajor === null || appMajor === null) {
return;
}
if (cliMajor !== appMajor) {
console.warn(
chalk.yellow(
`⚠ Version mismatch: your app requires twenty-sdk@${appMajor}.x ` +
`but the CLI running is v${cliVersion}. ` +
`Major version mismatches may cause unexpected behavior.\n` +
` Update with: npm install -g twenty-sdk@${appMajor}`,
),
);
console.warn('');
}
};