Чеклист создания Package
Руководство по созданию нового пакета в packages/.
Когда создавать Package
Создавайте package когда:
- ✅ Код используется в нескольких интеграциях
- ✅ Это обёртка над внешним API (CRM, LLM, Vector DB)
- ✅ Это абстракция инфраструктуры (DB, Cache, Creds)
- ✅ Код должен быть независимо версионируемым
Не создавайте package когда:
- ❌ Код используется только в одной интеграции
- ❌ Это бизнес-логика специфичная для интеграции
- ❌ Код слишком простой (можно просто скопировать)
Структура Package
packages/my-package/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── README.md # Документация (симлинк в docs/packages/)
└── src/
├── index.ts # Экспорты
├── client.ts # Основной клиент
└── types.ts # TypeScript типыЧеклист
1. Создание структуры
bash
# Создать папку
mkdir -p packages/my-package/src
# Создать файлы
touch packages/my-package/package.json
touch packages/my-package/tsconfig.json
touch packages/my-package/tsup.config.ts
touch packages/my-package/src/index.ts
touch packages/my-package/src/client.ts
touch packages/my-package/src/types.ts
touch packages/my-package/README.md2. package.json
json
{
"name": "@happ-integ/my-package",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit"
},
"dependencies": {
// ...
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}3. tsconfig.json
json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}4. tsup.config.ts
typescript
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: true,
});5. Код
src/types.ts:
typescript
export interface IMyClientConfig {
apiKey: string;
baseUrl?: string;
}
export interface IMyClientResult {
success: boolean;
data?: any;
}src/client.ts:
typescript
import type { IMyClientConfig, IMyClientResult } from "./types";
export class MyClient {
private apiKey: string;
private baseUrl: string;
constructor(config: IMyClientConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl ?? "https://api.example.com";
}
async doSomething(): Promise<IMyClientResult> {
// ...
}
async healthCheck(): Promise<boolean> {
try {
// Проверка соединения
return true;
} catch {
return false;
}
}
}src/index.ts:
typescript
export * from "./types";
export * from "./client";6. Документация (README.md)
markdown
# @happ-integ/my-package
Описание пакета.
## Установка
\`\`\`bash
pnpm add @happ-integ/my-package
\`\`\`
## Использование
\`\`\`typescript
import { MyClient } from "@happ-integ/my-package";
const client = new MyClient({
apiKey: env.MY_API_KEY,
});
const result = await client.doSomething();
\`\`\`
## API
### MyClient
\`\`\`typescript
new MyClient(config: IMyClientConfig)
\`\`\`
**Config:**
| Параметр | Тип | Описание |
| --------- | -------- | ---------------------- |
| `apiKey` | `string` | API ключ |
| `baseUrl` | `string` | Base URL (опционально) |
### Методы
#### `doSomething(): Promise<IMyClientResult>`
Описание метода.
#### `healthCheck(): Promise<boolean>`
Проверка соединения.7. Симлинк для документации
bash
# Создать симлинк
ln -sf ../../packages/my-package/README.md docs/packages/my-package.md8. Добавить в VitePress
Обновить docs/.vitepress/config.ts:
typescript
{
text: "Packages",
collapsed: false,
items: [
{ text: "LLM", link: "/packages/llm" },
{ text: "Vector", link: "/packages/vector" },
// ...
{ text: "My Package", link: "/packages/my-package" }, // Добавить
],
},9. Установить зависимости
bash
pnpm install10. Собрать
bash
pnpm --filter @happ-integ/my-package buildФинальный чеклист
- [ ] Структура файлов создана
- [ ]
package.jsonс правильным именем и экспортами - [ ]
tsconfig.jsonнаследуется от корневого - [ ]
tsup.config.tsнастроен - [ ] Код клиента реализован
- [ ] Типы вынесены в
types.ts - [ ]
healthCheck()метод добавлен - [ ] README.md с описанием и примерами
- [ ] Симлинк в
docs/packages/создан - [ ] VitePress конфиг обновлён
- [ ] Package собирается без ошибок
- [ ] Package можно использовать в интеграции
Соглашения по именованию
| Что | Формат | Пример |
|---|---|---|
| Имя пакета | @happ-integ/kebab-case | @happ-integ/google-drive |
| Класс клиента | PascalCaseClient | GoogleDriveClient |
| Типы/Интерфейсы | IPascalCase | IGoogleDriveConfig |
| Файлы | kebab-case.ts | google-drive.service.ts |
Паттерны
Абстракция с провайдерами (DB, Cache, LLM, Vector)
typescript
import { Logger } from "@happ-integ/logger";
const logger = new Logger("my-service");
export class MyService {
private primary: Provider;
private secondary?: Provider;
constructor(config: IConfig) {
this.primary = this.getProvider(config.primary);
if (config.secondary) {
this.secondary = this.getProvider(config.secondary);
}
}
async doSomething() {
try {
return await this.primary.doSomething();
} catch (error) {
if (!this.secondary) throw error;
logger.warn("Primary failed, falling back...");
return await this.secondary.doSomething();
}
}
}CRM/API клиент
typescript
export class MyCRMClient {
constructor(private apiKey: string) {}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async getRecord(id: string) {
return this.request<IRecord>(`/records/${id}`);
}
}Пакет с автоматическим разрешением секретов из env
Для пакетов, которые используют глобальные секреты из Doppler (API ключи, URL и т.д.), используйте паттерн с env:
typescript
// types.ts
export interface IMyServiceEnv {
MY_API_KEY?: string;
MY_API_URL?: string;
}
export interface IMyServiceConfig {
apiKey?: string;
apiUrl?: string;
env?: IMyServiceEnv;
}
// client.ts
export class MyService {
private apiKey: string;
private apiUrl: string;
constructor(config: IMyServiceConfig) {
// Приоритет: явный параметр > env binding
const apiKey = config.apiKey || config.env?.MY_API_KEY;
const apiUrl = config.apiUrl || config.env?.MY_API_URL;
if (!apiKey) {
throw new Error("MY_API_KEY is required");
}
this.apiKey = apiKey;
this.apiUrl = apiUrl || "https://api.example.com";
}
}Использование:
typescript
// В интеграции — просто передаём env
const service = new MyService({ env });
// Или явно передаём ключ (для тестов или особых случаев)
const service = new MyService({ apiKey: "sk-..." });Важно:
- НЕ используйте
process.env— оно не работает в Cloudflare Workers - Секреты из Doppler автоматически доступны через
envbinding