Skip to content

Пакет shared ​

packages/shared містить контракти, спільні для backend і frontend: ArkType-схеми вводу, схеми та інтерфейси відповідей, коди помилок, branded-типи, enum-и мов і утиліти. Завдяки цьому сервер і клієнт імпортують ту саму схему чи тип, а не тримають дві копії.

package.json і збірка ​

Дані з packages/shared/package.json:

  • name: "shared", private: true, type: "module";
  • exports має кореневий вхід . і підшляхи ./brands, ./schemas, ./responses, ./errors, ./enums, ./utils, ./guards, ./limits, ./patterns, ./ws-client. Кожен вказує на dist/<розділ>/index.js і .d.ts;
  • build: tsc --build, dev: tsc --build --watch. tsconfig.json збирає src у dist (composite: true);
  • залежності в рантаймі: @arktype/type, @ark/util, libphonenumber-js.

Backend і frontend підключають пакет як "shared": "workspace:*" (backend/package.json, frontend/package.json).

Оскільки exports вказують лише на dist, без збірки пакета споживачі його не знайдуть. Тому кореневий pnpm dev спочатку запускає build:shared (pnpm --filter shared build), потім збирає backend і лише тоді паралельно стартує dev-сервери (package.json). Див. також «Скрипти».

Структура src/ ​

РозділВміст
brandsbranded-примітиви: у primitives.ts, напр., EmailSchema = type("string.email").brand("Email") і PositiveIntSchema, а в ids.ts лежить EntityIdSchema
enumsAPP_LOCALE_CODES (as const), SHIPPED_LOCALE_CODES (мови з перекладом інтерфейсу), мапи назв мов і функції resolveLanguageName, resolveLanguageCode, isAppLocaleCode тощо
errorsоб'єкт ErrorCode (BadRequest: "bad_request", ValidationFailed: "validation_failed", CsrfInvalid: "csrf_invalid"…) і список ErrorCodeValues, без runtime-залежності від ArkType (модуль імпортує фронтенд). ErrorCodeSchema — у responses
guardsws-channels.ts: whitelist WS-каналів WS_CHANNELS (admin), тип WsChannel і guard isWsChannel(value: unknown) без залежностей; його використовує broadcastToChannel, а фронтенд може імпортувати з shared/guards
responsesсхеми та інтерфейси відповідей по контролерах (auth-controller.ts, main-controller.ts, admin-user-controller.ts…). Канонічна помилка лежить в error-response.ts: CanonicalErrorResponseSchema з status: "'error'", code, message і необов'язковими reason/details/csrfToken
schemasArkType-схеми вводу за доменами (auth, main, support, push-subscription, query, ws, activity, common). Кожна схема експортується разом із типом, напр. LoginInputSchema / LoginInput у auth/login.ts. У ws — реєстр payload'ів push-подій BroadcastEventSchemas з типами BroadcastEventName і BroadcastPayload<E> (broadcast-events.ts)
utilsnormalizePhoneNumber(input, defaultCountry?) (на libphonenumber-js, повертає E.164 або кидає PhoneNormalizationError) і looksLikePhoneNumber

Про правила написання схем ('+': 'reject', query-морфи) див. «Валідація». Чому DTO трансформерів не беруться з shared/responses, пояснено в розділі «Трансформери».

  • Два підшляхи без жодної бібліотеки, які безпечно тягнути в браузер: limits (числові ліміти: довжина повідомлення чату, розмір сторінки історії, PASSWORD_MIN_LENGTH/PASSWORD_MAX_LENGTH) і patterns (ENTITY_ID_PATTERN, looksLikePhoneNumber; shared/utils тягне libphonenumber-js, тому браузер бере форму номера звідси). Схеми й клієнт мають одне джерело, а тест у contracts.test.js тримає їх разом. Третій такий підшлях, ws-client, — те, в чому згодні обидва WebSocket-клієнти (застосунок і playground): classifyWsMessage (куди маршрутизувати вхідне повідомлення) і normalizeApiPrefix. Самі з'єднання не спільні свідомо: у застосунку heartbeat, оновлення токена й back-off рукостискань, у playground — просте перепідключення.
  • src/index.ts реекспортує всі розділи, зокрема guards; підшляхи з exports (shared/guards тощо) працюють так само.
  • EntityIdSchema — справжній branded-тип: type(/^[1-9]\d*$/).brand('EntityId'). Рядок стає EntityId лише після перевірки схемою; присвоєння звичайного рядка — помилка typecheck (перевіряє shared-brands.spec.ts у backend).

Використання в backend ​

Маршрути беруть схеми вводу із shared/schemas, а схеми відповідей імпортують з shared/responses як простір імен (http-routes.ts):

ts
import * as ResponseSchemas from 'shared/responses'

defineRoute({
    url: '/login',
    method: 'post',
    handler: AuthController.login.bind(AuthController),
    validator: LoginInputSchema,
    ResponseSchema: ResponseSchemas.LoginResponseSchema,
})

ResponseSchema у рантаймі не перевіряється, а потрапляє лише в doc-серіалізацію маршрутів (див. «Валідація»). Контролери імпортують лише типи, напр. import type { SupportChatInput } from 'shared/schemas' у ws/support-controller.ts. Сервіси користуються утилітами, як-от resolveLanguageName з shared/enums у support-llm-service.ts.

Використання у frontend ​

  • API-клієнт utils/api.ts типізується через shared/responses, shared/schemas і import type { ErrorCode } from 'shared/errors';
  • response-normalizer.ts і компоненти (наприклад, AuthModals.vue) порівнюють коди помилок зі значеннями ErrorCode;
  • useBroadcastHandler.ts перевіряє payload broadcast-подій guard'ами з ws-payload-guards.ts, а не схемами: ArkType у рантаймі збільшив би бандл і порушив CSP (JIT через new Function). Типи guard'ів — BroadcastPayload<E> з shared/schemas; тест паритету проганяє кожен guard і схему реєстру BroadcastEventSchemas на тих самих фікстурах (ArkType імпортує лише тест);
  • plugins/i18n.ts і useLocale.ts беруть SHIPPED_LOCALE_CODES (мови з перекладом інтерфейсу: en, uk, pl) і resolveLanguageCode з shared/enums;
  • адмінські view імпортують типи AdminUserListItem, AdminOnlineUserDetail тощо з shared/responses.

Тести ​

Скрипт test виконує pnpm build && node --test test/*.test.js, тобто вбудований раннер Node проганяє JS-файли з test/ на зібраному dist. Кореневий pnpm test (pnpm -r test) запускає його разом з іншими пакетами.

Що покрито:

ФайлЩо перевіряє
languages.test.jsresolveLanguageCode (локаль браузера → код застосунку або en), isAppLocaleCode, SHIPPED_LOCALE_CODES / isShippedLocale, назви мов для кожного APP_LOCALE_CODES
phone.test.jsnormalizePhoneNumber → E.164 і відмова для порожніх/невалідних, looksLikePhoneNumber
ws-channels.test.jsisWsChannel
contracts.test.jsErrorCodeSchema приймає рівно значення ErrorCode, EntityIdSchema і ENTITY_ID_PATTERN, looksLikePhoneNumber, ліміти пароля й повідомлень та '+': 'reject' вибраних схем вводу, реекспорт guards з кореня

Як додати контракт ​

  1. Схему вводу покладіть у src/schemas/<домен>/<name>.ts, а схему чи інтерфейс відповіді у src/responses/<controller>.ts. Поруч зі схемою експортуйте тип (typeof X.infer).
  2. Додайте експорт в index.ts відповідного розділу (напр. schemas/index.ts). Новий розділ потрібно додати і в src/index.ts, і в exports у package.json.
  3. Перезберіть пакет: pnpm build:shared (або pnpm --filter shared dev у watch-режимі). Інакше backend і frontend побачать стару версію dist.
  4. Підключіть схему в маршруті (validator, ResponseSchema) і далі дійте за кроками з «Додавання фічі».

Автор: Олександр Блажейко · blazheiko@gmail.com