Пакет 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/
| Розділ | Вміст |
|---|---|
| brands | branded-примітиви: у primitives.ts, напр., EmailSchema = type("string.email").brand("Email") і PositiveIntSchema, а в ids.ts лежить EntityIdSchema |
| enums | APP_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 |
| guards | ws-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 |
| schemas | ArkType-схеми вводу за доменами (auth, main, support, push-subscription, query, ws, activity, common). Кожна схема експортується разом із типом, напр. LoginInputSchema / LoginInput у auth/login.ts. У ws — реєстр payload'ів push-подій BroadcastEventSchemas з типами BroadcastEventName і BroadcastPayload<E> (broadcast-events.ts) |
| utils | normalizePhoneNumber(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):
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.js | resolveLanguageCode (локаль браузера → код застосунку або en), isAppLocaleCode, SHIPPED_LOCALE_CODES / isShippedLocale, назви мов для кожного APP_LOCALE_CODES |
| phone.test.js | normalizePhoneNumber → E.164 і відмова для порожніх/невалідних, looksLikePhoneNumber |
| ws-channels.test.js | isWsChannel |
| contracts.test.js | ErrorCodeSchema приймає рівно значення ErrorCode, EntityIdSchema і ENTITY_ID_PATTERN, looksLikePhoneNumber, ліміти пароля й повідомлень та '+': 'reject' вибраних схем вводу, реекспорт guards з кореня |
Як додати контракт
- Схему вводу покладіть у
src/schemas/<домен>/<name>.ts, а схему чи інтерфейс відповіді уsrc/responses/<controller>.ts. Поруч зі схемою експортуйте тип (typeof X.infer). - Додайте експорт в
index.tsвідповідного розділу (напр. schemas/index.ts). Новий розділ потрібно додати і вsrc/index.ts, і вexportsуpackage.json. - Перезберіть пакет:
pnpm build:shared(абоpnpm --filter shared devу watch-режимі). Інакше backend і frontend побачать стару версіюdist. - Підключіть схему в маршруті (
validator,ResponseSchema) і далі дійте за кроками з «Додавання фічі».