Перейти к основному содержанию

Публичные ресурсы (папка public/)

Папка public/ в корне вашего приложения содержит статические файлы — изображения, значки, шрифты и любые другие ресурсы, необходимые вашему приложению во время выполнения. Эти файлы автоматически включаются в сборки, синхронизируются в режиме разработки и загружаются на сервер. Файлы, размещённые в public/, являются:
  • Публично доступными — после синхронизации с сервером ресурсы доступны по публичному URL. Для доступа к ним аутентификация не требуется.
  • Доступными в компонентах фронтенда — используйте URL ресурсов для отображения изображений, значков или любого медиа внутри ваших компонентов React.
  • Доступными в логических функциях — используйте URL ресурсов в письмах, ответах API или любой серверной логике.
  • Используются для метаданных маркетплейса — поля logoUrl и screenshots в defineApplication() ссылаются на файлы из этой папки (например, public/logo.png). Они отображаются в маркетплейсе при публикации вашего приложения.
  • Автосинхронизация в режиме разработки — когда вы добавляете, обновляете или удаляете файл в public/, он автоматически синхронизируется с сервером. Перезапуск не требуется.
  • Включены в сборкиyarn twenty build упаковывает все публичные ресурсы в выходной дистрибутив.

Доступ к публичным ресурсам с помощью getPublicAssetUrl

Используйте хелпер getPublicAssetUrl из twenty-sdk, чтобы получить полный URL файла в каталоге public/ вашего приложения. Он работает как в логических функциях, так и в компонентах фронтенда. В логической функции:
src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';

const handler = async (): Promise<any> => {
  const logoUrl = getPublicAssetUrl('logo.png');
  const invoiceUrl = getPublicAssetUrl('templates/invoice.png');

  // Fetch the file content (no auth required — public endpoint)
  const response = await fetch(invoiceUrl);
  const buffer = await response.arrayBuffer();

  return { logoUrl, size: buffer.byteLength };
};

export default defineLogicFunction({
  universalIdentifier: 'a1b2c3d4-...',
  name: 'send-invoice',
  description: 'Sends an invoice with the app logo',
  timeoutSeconds: 10,
  handler,
});
В компоненте фронтенда:
src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';

export default defineFrontComponent(() => {
  const logoUrl = getPublicAssetUrl('logo.png');

  return <img src={logoUrl} alt="App logo" />;
});
Аргумент path задаётся относительно папки public/ вашего приложения. И getPublicAssetUrl('logo.png'), и getPublicAssetUrl('public/logo.png') приводят к одному и тому же URL — префикс public/, если он есть, удаляется автоматически.

Использование пакетов npm

Вы можете устанавливать и использовать любые пакеты npm в своём приложении. И логические функции, и компоненты фронтенда собираются с помощью esbuild, который встраивает все зависимости в выходной файл — каталоги node_modules во время выполнения не нужны.

Установка пакета

yarn add axios
Затем импортируйте его в своём коде:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';

const handler = async (): Promise<any> => {
  const { data } = await axios.get('https://api.example.com/data');

  return { data };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-data',
  description: 'Fetches data from an external API',
  timeoutSeconds: 10,
  handler,
});
То же самое работает для компонентов фронтенда:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';

const DateWidget = () => {
  return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'date-widget',
  component: DateWidget,
});

Как работает бандлинг

Этап сборки использует esbuild для создания одного самодостаточного файла на каждую логическую функцию и на каждый компонент фронтенда. Все импортированные пакеты встроены в бандл. Логические функции выполняются в среде Node.js. Встроенные модули Node (fs, path, crypto, http и т. д.) доступны и не требуют установки. Компоненты фронтенда выполняются в Web Worker. Встроенные модули Node недоступны — доступны только браузерные API и пакеты npm, работающие в браузерной среде. В обеих средах доступны как предварительно предоставленные модули twenty-client-sdk/core и twenty-client-sdk/metadata — они не включаются в бандл, а подставляются сервером во время выполнения.

Тестирование вашего приложения

SDK предоставляет программные API, которые позволяют собирать, разворачивать, устанавливать и удалять ваше приложение из тестового кода. В сочетании с Vitest и типизированными клиентами API вы можете писать интеграционные тесты, которые проверяют, что ваше приложение работает сквозным образом на реальном сервере Twenty.

Настройка

Приложение, созданное скэффолдером, уже включает Vitest. Если вы настраиваете его вручную, установите зависимости:
yarn add -D vitest vite-tsconfig-paths
Создайте vitest.config.ts в корне вашего приложения:
vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['tsconfig.spec.json'],
      ignoreConfigErrors: true,
    }),
  ],
  test: {
    testTimeout: 120_000,
    hookTimeout: 120_000,
    include: ['src/**/*.integration-test.ts'],
    setupFiles: ['src/__tests__/setup-test.ts'],
    env: {
      TWENTY_API_URL: 'http://localhost:2020',
      TWENTY_API_KEY: 'your-api-key',
    },
  },
});
Создайте файл инициализации, который проверяет доступность сервера перед запуском тестов:
src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';

const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');

beforeAll(async () => {
  // Verify the server is running
  const response = await fetch(`${TWENTY_API_URL}/healthz`);

  if (!response.ok) {
    throw new Error(
      `Twenty server is not reachable at ${TWENTY_API_URL}. ` +
        'Start the server before running integration tests.',
    );
  }

  // Write a temporary config for the SDK
  fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });

  fs.writeFileSync(
    path.join(TEST_CONFIG_DIR, 'config.json'),
    JSON.stringify({
      remotes: {
        local: {
          apiUrl: process.env.TWENTY_API_URL,
          apiKey: process.env.TWENTY_API_KEY,
        },
      },
      defaultRemote: 'local',
    }, null, 2),
  );
});

Программные API SDK

Подпуть twenty-sdk/cli экспортирует функции, которые можно вызывать напрямую из тестового кода:
ФункцияОписание
appBuildСобрать приложение и при необходимости упаковать tar-архив
appDeployЗагрузить tar-архив на сервер
appInstallУстановить приложение в активное рабочее пространство
appUninstallУдалить приложение из активного рабочего пространства
Каждая функция возвращает объект результата с success: boolean и либо data, либо error.

Написание интеграционного теста

Полный пример, который собирает, разворачивает и устанавливает приложение, а затем проверяет, что оно появляется в рабочем пространстве:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const APP_PATH = process.cwd();

describe('App installation', () => {
  beforeAll(async () => {
    const buildResult = await appBuild({
      appPath: APP_PATH,
      tarball: true,
      onProgress: (message: string) => console.log(`[build] ${message}`),
    });

    if (!buildResult.success) {
      throw new Error(`Build failed: ${buildResult.error?.message}`);
    }

    const deployResult = await appDeploy({
      tarballPath: buildResult.data.tarballPath!,
      onProgress: (message: string) => console.log(`[deploy] ${message}`),
    });

    if (!deployResult.success) {
      throw new Error(`Deploy failed: ${deployResult.error?.message}`);
    }

    const installResult = await appInstall({ appPath: APP_PATH });

    if (!installResult.success) {
      throw new Error(`Install failed: ${installResult.error?.message}`);
    }
  });

  afterAll(async () => {
    await appUninstall({ appPath: APP_PATH });
  });

  it('should find the installed app in the workspace', async () => {
    const metadataClient = new MetadataApiClient();

    const result = await metadataClient.query({
      findManyApplications: {
        id: true,
        name: true,
        universalIdentifier: true,
      },
    });

    const installedApp = result.findManyApplications.find(
      (app: { universalIdentifier: string }) =>
        app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
    );

    expect(installedApp).toBeDefined();
  });
});

Запуск тестов

Убедитесь, что ваш локальный сервер Twenty запущен, затем:
yarn test
Или в режиме наблюдения во время разработки:
yarn test:watch

Проверка типов

Вы также можете запустить проверку типов для своего приложения без запуска тестов:
yarn twenty typecheck
Это запускает tsc --noEmit и сообщает о любых ошибках типов.

Справочник по CLI

Помимо dev, build, add и typecheck, CLI предоставляет команды для выполнения функций, просмотра логов и управления установками приложений.

Выполнение функций (yarn twenty exec)

Запустите логическую функцию вручную, не вызывая её через HTTP, cron или событие базы данных:
# Execute by function name
yarn twenty exec -n create-new-post-card

# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf

# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'

# Execute the post-install function
yarn twenty exec --postInstall

Просмотр логов функций (yarn twenty logs)

Потоковая передача журналов выполнения логических функций вашего приложения:
# Stream all function logs
yarn twenty logs

# Filter by function name
yarn twenty logs -n create-new-post-card

# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Это отличается от yarn twenty server logs, который показывает логи контейнера Docker. yarn twenty logs показывает журналы выполнения функций вашего приложения с сервера Twenty.

Удаление приложения (yarn twenty uninstall)

Удалите свое приложение из активного рабочего пространства:
yarn twenty uninstall

# Skip the confirmation prompt
yarn twenty uninstall --yes

Управление удалёнными серверами

Remote — это сервер Twenty, к которому подключается ваше приложение. Во время настройки скэффолдер автоматически создаст его для вас. Вы можете в любой момент добавлять новые удалённые серверы или переключаться между ними.
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add

# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local

# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote

# List all configured remotes
yarn twenty remote list

# Switch the active remote
yarn twenty remote switch <name>
Ваши учётные данные хранятся в ~/.twenty/config.json.

CI с GitHub Actions

Скэффолдер генерирует готовый к использованию рабочий процесс GitHub Actions в .github/workflows/ci.yml. Он автоматически запускает ваши интеграционные тесты при каждом пуше в main и в pull request’ах. Рабочий процесс:
  1. Извлекает ваш код
  2. Поднимает временный сервер Twenty с помощью экшена twentyhq/twenty/.github/actions/spawn-twenty-docker-image
  3. Устанавливает зависимости с помощью yarn install --immutable
  4. Запускает yarn test с TWENTY_API_URL и TWENTY_API_KEY, переданными из выходных данных экшена
.github/workflows/ci.yml
name: CI

on:
  push:
    branches:
      - main
  pull_request: {}

env:
  TWENTY_VERSION: latest

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Spawn Twenty instance
        id: twenty
        uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
        with:
          twenty-version: ${{ env.TWENTY_VERSION }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Enable Corepack
        run: corepack enable

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --immutable

      - name: Run integration tests
        run: yarn test
        env:
          TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
          TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
Вам не нужно настраивать секреты — экшен spawn-twenty-docker-image запускает эфемерный сервер Twenty прямо в раннере и выводит данные для подключения. Секрет GITHUB_TOKEN предоставляется GitHub автоматически. Чтобы закрепить конкретную версию Twenty вместо latest, измените переменную окружения TWENTY_VERSION в начале рабочего процесса.